[따배C++] 8. 객체지향의 기초

Created: June 6, 2021 11:26 AM
Tag: access function, access specifier, anonymous, chaining member function, const, constructor, delegating constructor, destructor, encapsulation, friend, header, nested type, oop, static

8.1 객체지향 프로그래밍과 클래스 objective oriented programming


객체지향 기술을 통해 코딩 실수를 줄이고 코드의 가독성을 높일 수 있으며 유지보수가 용이한 코드를 작성할 수 있다. 데이터와 기능이 합쳐진 개념을a 오브젝트라고 한다. 오브젝트를 프로그래밍 언어로 구현하기 위한 수단을 클래스라고 한다. 데이터만 다룰때는 구조체를, 기능까지 넣고자 할때(더 많은 객체지향 기능)만 클래스를 사용하는 것이 일반적이다.

#include <iostream>
#include <string>
#include <vector>

using namespace std;

class Friend
{
public: // access specifier 접근지정자(public, private, protected)
	string m_name; 	// 대부분의 교과서에서 구조체 맴버에 m_를 붙인다
	string m_address;
	int m_age;
	double m_height;
	double m_weight;

	void print()
	{
		cout << m_name << " " << m_address << " " \
			<< m_age << " " << m_height << " " \
			<< m_weight << endl;
	}
};

int main(void)
{
	Friend jj; // instanciation, instance // Friend는 메모리에 존재 X
	jj.print(); // 객체지향을 통한 사람이 이해하기 쉬운 형태의 코드

	vector<Friend> my_friends;
	my_friends.resize(2); // 친구 2명 저장할 공간

	for (auto &ele : my_friends)
		ele.print();

	return (0);
}
💡 실제로 메모리를 차지하도록 선언하는것을 `instanciation`이라고 하며, 실제로 메모리를 차지하는 변수 등을 `instance`라고 한다.

8.2 캡슐화 encapsulation, 접근 지정자 access specifiers, 접근 함수 access functions


현대의 소프트웨어 개발은 오픈소스를 잘 정리하여 조합하는것이 관건이다. 이때 복잡한 개념을 캡슐화 하는것이 필요하다. 클래스의 접근지정자로 private을 사용하고, 접근함수를 활용하여 캡슐화를 극대화할 수 있다.

#include <iostream>
#include <string>
#include <vector>

using namespace std;

class Date
{
private:
// private이 기본
// public: 외부에서 접근 가능
	int m_month;
	int m_day;
	int m_year;

public: // 같은 클래스 내에서는 프라이빗에 접근할 수 있다 // 위의 변수에 접근하려면 나를 통해라!
	// access funtion
	void setDate(const int &month_input, const int &day_input, const int &year_input)
	 // setters	
	{
		m_month = month_input;
		m_day = day_input;
		m_year = year_input;
	}
	const int &getDay() // getters
	{
		return (m_day);
	}
	void copyFrom(const Date &original) // 자신의 type을 param으로
	{
		m_month = original.m_month;
		m_day = original.m_day;
		m_year = original.m_year;
	}
};

int main(void)
{
	Date today;
	today.setDate(8, 4, 2025);
	Date copy;
	copy.copyFrom(today);
	return (0);
}

8.3 생성자 Constructors


construct 시 호출되는 함수를 constructor라고 한다. class 안에 class명과 동일한 이름의 반환값이 없는 함수를 작성하면 constructor로 기능한다. 이를 통해 클래스의 기본값을 설정할 수 있는 동시에 instanciation할 수 있다. 생성자가 없는 경우 클래스의 내용에 따라 쓰레기값이 출력될 수 있다. 그 이유는 아무 내용이 없는 생성자가 숨어있기 때문이다.

#include <iostream>
using namespace std;

class Fraction
{
private: // encapsulation
	int m_numerator; // 분자 // 기본값을 설정할 수도 있다!
	int m_denominator; // 분모

public:
	Fraction()
	{
		m_numerator = 0;
		m_denominator = 1;
  }
	// Fraction() {} // 기본 생성자

	void print()
	{
		cout << m_numerator << " / " << m_denominator << endl;
	}
};

int main(void)
{
	Fraction frac; // instance
	// 생성자에 파라미터가 하나도 없는 경우에만 ()를 붙이지 않는점에 유의!
	// -> 함수와 구분하기 위함(문법적 한계)
	frac.print();
	return (0);
}
#include <iostream>
using namespace std;

class Fraction
{
private:
	int m_numerator; // 분자
	int m_denominator; // 분모 // 기본값을 넣을 수 있다

public:
// Fraction // 디폴트 생성자가 숨어있다
	Fraction(const int &num_in, const int &den_in = 1)
	{
		m_numerator = num_in;
		m_denominator = den_in;
	}

	void print()
	{
		cout << m_numerator << " / " << m_denominator << endl;
	}
};

int main(void)
{
	// Fraction frac; // 에러 // 파라미터가 없는 생성자(기본생성자)가 없기 때문!
	// -> 기본생성자를 만들어 주거나 함수 오버로딩을 활용 
	// -> 하지만 두 방법을 동시에 사용하는 경우 모호성 문제가 발생
	Fraction one_thirds(1);
	one_thirds.print();

	Fraction one_thirds{ 1, 3 }; // public인 경우 생성자 없이 uniform init 가능
	// Fraction one_thirds{ 1.0, 3 }; 
	// unform init의 경우 형변환을 허용하지 않는다 
	// 최근에는 좀더 엄격한 uniform int을 권장하는 추세
	Fraction one_thirds(1, 3);

	one_thirds.print();

	return (0);
}
💡 생성자에 파라미터가 없는 경우 선언 시 ()를 붙이지 않는다.

Fraction frac; (O)
Fraction frac(); (X)

클래스 안의 클래스 생성순서

#include <iostream>

using namespace std;

class Second
{
public:
	Second()
	{
		cout << "Class Second Constrctor()" << endl;
	}
};

class First
{
	Second sec;

public:
	First()
	{
		cout << "Class First Constrctor()" << endl;
	}
};

int main(void)
{
	First fir;

	return (0);
}

// result
Class Second Constrctor()
Class First Constrctor()

8.4 생성자 맴버 initializer list


생성자 맴버 초기화를 목록의 형식으로 작성하면 편리하고 가독성을 높일 수 있다.

#include <iostream>
using namespace std;

class B
{
private:
	int m_b;

public:
	B(const int &m_b_in)
		: m_b(m_b_in)
	{}
};

class Something
{
private:
	// int		m_i;
	int		m_i = 100; // 바로 초기화 할 수 도 있다 // 우선순위는 생성자 먼저!
	double	m_d;
	char	m_c;
	int		m_arr[5]; // C++ 11
	B		m_b;

public:
	Something()
		: m_i(1) // 자동 casting
		, m_d{3.14} // 자동 casting 없이 엄격히
		, m_c('a')
		, m_arr{ 1, 2, 3, 4, 5 } // 괄호 대신 uniform init도 가능하다 // 자동 casting X
		, m_b(m_i - 1) // 다른 클래스의 맴버도 초기화 할 수 있다
	{
		m_i = 1; // 이니셜라이저 리스트와 함께 사용하는 경우 두번 초기화 된다! // 이후에 대입
		m_d = 3.14;
		m_c = 'a';
	}

	void print()
	{
		cout << m_i << " " << m_d << " " << m_c << endl;
		for (auto &e : m_arr)
			cout << e << " ";
		cout << endl;
	}
};

int main(void)
{
	Something som;
	som.print();
}

C++ Chapter 6.16 : for-each 반복문

8.5 위임 생성자 delegating constructor


생성자가 생성자를 호출할 수 있다. 즉, 이미 생성자가 있는 경우 생성자가 생성자를 가져다 쓸 수 있다. 이를 위임 생성자라고 한다. C++ 11부터 지원한다.

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
	int		m_id;
	string	m_name;

public:
	Student(const string &name_in) // 위임 생성자
		: Student(0, name_in) // 기본값 설정 (name_in만 입력 받아서)
	{}

	Student(const int &id_in, const string &name_in)
		: m_id(id_in), m_name(name_in)
	{}

	void print()
	{
		cout << m_id << " " << m_name << endl;
	}
};

int main(void)
{
	Student st1(0, "Jack Jack");
	st1.print();
	return (0);
}

8.6 소멸자 destructor


소멸자는 변수가 영역을 벗어나며 사라질 때 호출되는 함수이다. delete를 위해 주로 사용된다. 소멸자는 instance가 메모리에서 해제될 때 내부에서 자동으로 호출된다. 동적할당으로 만들어진 경우에는 영역을 벗어나도 자동으로 메모리가 해제되지 않기 때문에 delete으로 메모리를 해제할 때에만 소멸자가 호출된다.

💡 소멸자를 프로그래머가 직접 호출하는 것은 대부분의 경우 권장하지 않는다.
#include <iostream>
using namespace std;

class Simple
{
private:
	int m_id;

public:
	Simple(const int &id_in)
		: m_id(id_in)
	{
		cout << "Constructor " << m_id << endl;
	}

	~Simple() // 소멸자는 파라미터가 없다
	{
		cout << "Destructor " << m_id << endl;
	}
};

int main(void)
{
	// Simple s1(0);
	Simple *s1 = new Simple(0);
	Simple s2(1);

	delete s1;
	return (0);
}

소멸자를 통한 동적할당 메모리 해제

#include <iostream>
using namespace std;

class IntArray
{
private:
	int *m_arr = nullptr; // vector를 사용해도 된다
	int m_length = 0;

public:
	IntArray(const int length_in)
	{
		m_length = length_in;
		m_arr = new int[m_length];

		cout << "Constructor " << endl;
	}

	~IntArray()
	{
		if (m_arr != nullptr) // nullptr을 지울 때도 문제가 발생하기 때문에 if문 사용
			delete[] m_arr;
	}

	int size() { return (m_length); }
};

int main()
{
	while (1)
		IntArray my_int_arr(10000); // leak
	return (0);
}

8.7 this 포인터와 연쇄호출 chaining member functions


클래스는 마치 붕어빵을 찍는 기계로 종종 비유된다. 그렇다면 만들어진 붕어빵은 어떻게 서로 구분할 수 있을까? 클래스 안에 숨어 있는 this pointer와 연쇄호출 기능에 대해 알아보자.

#include <iostream>
using namespace std;

class Simple
{
private:
	int m_id;

public:
	Simple(int id)
	{
		this->setID(id); // this-> 가 숨어 있다
		this->m_id;

		cout << this << endl; // 자기 자신의 주소
	}

	void setID(int id) { this->m_id = id; }
	int getID() { return (m_id); }
};

int main(void)
{
	Simple s1(1), s2(2);
	s1.setID(2);
	s2.setID(4);

	cout << &s1 << " " << &s2 << " " << endl;

	return (0);
}

연쇄호출

#include <iostream>
using namespace std;

class Calc
{
private:
	int m_value;

public:
	Calc(int init_value)
		: m_value(init_value)
	{}

	// 자기 자신의 래퍼런스를 반환
	Calc &add(int value) { m_value += value; return (*this); }
	Calc &sub(int value) { m_value -= value; return (*this); }
	Calc &mult(int value) { m_value *= value; return (*this); }

	void print()
	{
		cout << m_value << endl;
	}
};

int main(void)
{
	Calc cal(10);
	cal.add(10).sub(1).mult(2).print(); // chaining meber functions
	// cal.sub(1);
	// cal.mult(2);
	// cal.print();
}

8.8 클래스 코드와 헤더파일


클래스에는 선언만 남겨두고 복잡한 정의부는 전부 cpp 파일로 빼서 정리하는것이 원칙이다. 클래스에는 생성자 조차 프로토타입만 남겨둔다. 헤더의 파일의 이름은 보통 클래스의 이름과 동일하게 작성한다.

// Calc.h
#include <iostream>
// using namespace std; <- 헤더에서는 안하는것이 좋다 
// 인클루드 하는 모든 파일에서 영향을 받기 때문

class Calc
{
private:
	int m_value;

public:
	// only prototypes
	Calc(int init_value);
	Calc &add(int value);
	void print();
};

// ==========
// Calc.cpp
#include "Calc.h"

using namespace std; // cpp파일은 다른 파일에 영향을 주지 않는다

Calc(int init_value)
	: m_value(init_value)
{}

Calc &Calc::add(int value)
{
	m_value += value;
	return (*this);
}

void Calc::print()
{
	cout << m_value << endl;
}

// ==========
// main.cpp
#include "Calc.h"

using namespace std;

int main(void)
{
	Calc cal(10);
	cal.add(10);

	return (0);
}

8.9 클래스와 const


함수 파라미터 클래스

#include <iostream>
using namespace std;

class Something
{
public:
	int m_value = 0;

	void setValue(int value)
	{
		m_value = value;
	}

	int getValue() const // member function이 const
	{
		return (m_value);
	}
};

void print(const Something &st) // 함수에 클래스를 전달할때는 이렇게 하는것이 최상의 효율이다
{
	cout << st.getValue() << endl;
}

int main(void)
{
	const Something something;
	
	cout << something.getValue() << endl; // const member function만 사용 가능

	print(something);

	return (0);
}
💡 `const`를 사용할 수 있는 경우 최대한 많이 써주는 것이 좋다!

const를 통한 overloading

#include <iostream>
#include <string>
using namespace std;

class Something
{
public:
	string m_value = "default";

	string &getValue()
	{
		cout << "non-const version" << endl;
		return (m_value);
	}

	const string &getValue() const
	{
		cout << "const version" << endl;
		return (m_value);
	}
};

int main(void)
{
	// non const ver
	Something something;
	something.getValue() = 10;

	// const ver
	const Something something2;
	something2.getValue();

	return (0);
}

8.10 정적 맴버 변수


정적 맴버는 필히 cpp파일에서 정의해야 한다. 중복 선언의 문제를 방지하기 위함이다. const를 사용하는 경우 다중 초기화를 주의해야 한다. constexpr을 사용하면 컴파일 타임에 값이 결정돼야 하는 경우 즉, 리터럴이 할당되는 경우 실수를 방지할 수 있다.

#include <iostream>
using namespace std;

class Something
{
public:
	static constexpr int s_value = 1;
	// static 맴버인 경우 cpp 파일 안에서 정의해야 한다(중복선언 문제 방지)
	// static const인 경우 여기서 정의해줘야 한다
	// constexpr: 컴파일 타임에 값이 완전히 결정되어야 하는 경우 즉, 리터럴이 할당되는 경우 등
};
💡 정적 맴버 함수의 경우 `this` pointer의 사용이 불가능하다. 즉, `this` poiner로 접근할 수 있는모든 것에 접근이 불가능하다.

8.12 친구 함수와 클래스 friend


#include <iostream>
using namespace std;

class B; // 전방선언 해주어야 한다.

class A
{
private:
	int m_value = 1;

	friend void doSomething(A &a, B &b); // prototype
};

class B
{
private:
	int m_value = 2;

	friend void doSomething(A &a, B &b);
};

// private member 접근 가능
void doSomething(A &a, B &b) // friend of both A and B
{
	cout << a.m_value << " " << b.m_value << endl;
}

int main(void)
{
	A a;
	B b;

	doSomething(a, b);

	return (0);
}
💡 전방 선언은 코드를 읽거나 디버깅 할때 불편하기 때문에 사용을 지양해야 한다.

클래스의 특정 맴버의 친구 class member function

#include <iostream>
using namespace std;

class A;

class B
{
private:
	int m_value = 2;

public:
	void doSomething(A& a);
};

class A
{
private:
	int m_value = 1;

	friend void B::doSomething(A& a); // <-
};

void B::doSomething(A& a) // body를 따로
{
	cout << a.m_value << endl;
}

int main(void)
{
	A a;
	B b;
	b.doSomething(a);

	return (0);
}

8.13 익명 객체 anonymous


객체를 사용할 때 따로 변수를 선언하지 않는 익명객체에 대해 알아보자. 익명객체는 변수명이 없기 때문에 오직 한번만 사용되고 없어지는 특징이 있다.

#include <iostream>
using namespace std;

class A
{
public:
	int m_value;

	A(const int &input)
		: m_value(input)
	{
		cout << "Constructor" << endl;
	}

	void print()
	{
		cout << m_value << endl;
	}
};

int main(void)
{
	A a;
	a.print();
	a.print(); // 직전에 만들어진 a 객체와는 같은 객체이다

	A().print(); // 익명 객체
	A().print(); // 직전에 만들어진 A() 객체와는 다른 객체이다

	A a(1);
	a.print();

	A(1).print(); // 마치 R value 처럼 기능

	return (0);
}
#include <iostream>
using namespace std;

class Cents
{
private:
	int m_cents;

public:
	Cents(int cents) { m_cents = cents; }

	int getCents() const { return (m_cents); }
};

Cents add(const Cents &c1, const Cents &c2)
{
	return (Cents(c1.getCents() + c2.getCents()));
}

int main(void)
{
	cout << add(Cents(6), Cents(8)).getCents() << endl;
	cout << int(6) + int(8) << endl;

	return (0);
}

8.15 클래스 안에 포함된 자료형 nested types 중첩자료형


#include <iostream>

class Fruit
{
public:
	enum class FruitType
	{
		APPLE, BANANA, CHERRY,
	};

	class InnerClass
	{

	};

	struct InnerStruct
	{

	};

private:
	FruitType m_type;

public:
	Fruit(FruitType type) : m_type(type)
	{}

	FruitType getType() { return (m_type); }
};

int main(void)
{
	Fruit apple(Fruit::FruitType::APPLE);

	if (apple.getType == Fruit::FruitType::APPLE)
	{
		std::cout << "Apple" << std::endl;
	}
}

좋은 웹페이지 즐겨찾기