12. 클래스
1. 클래스
클래스는 (C 스타일의)구조체와는 달리 클래스 내부에 멤버로 함수가 포함될 수 있다.
멤버함수의 호출은 반드시 객체가 필요하고, 함수 내 지역변수 'this'
에 호출한 객체의 주소가 할당된다.
- 구조체와 클래스는 C++에서 동일하게 기능한다. 앞서 언급했듯 클래스가 구조체보다 발전했다는 것은 C 에서의 구조체와 비교했을 때를 의미한다.
- C++ 에서 구조체와 클래스는 동일하게 기능하지만, 유일하게 다른 점이 하나 있는데, 기본 접근제한 지정자가 클래스는
private
, 구조체는public
이라는 것이다. - 구조체와 클래스가 구분 없이 사용은 가능하지만, 암묵적으로 복잡한 기능은 클래스로, 단순한 C 스타일 기능은 구조체로 구현하는 편이다.
2. 생성자와 소멸자
생성자와 소멸자는 클래스 내에 선언되는 특수한 멤버함수이다. 둘은 다음과 같은 특징을 가진다:
- 별도로 구현하지 않아도 컴파일러가 자동으로 생성한다.
- 각각의 객체가 생성 또는 소멸할 때 컴파일러가 자동으로 호출한다.
- 둘 다 반환타입이 지정되지 않으며, 클래스의 이름을 함수 이름으로 사용한다.
- 생성자 또한 함수이므로 입력 인자를 받을 수 있다. 이를 응용해 인자를 다르게 한 여러개의 생성자를 선언하면 생성자 오버로딩이 가능하다.
-
생성자의 초기화
선언과 동시에 객체에 값이 할당되는 것을 ‘초기화’ 라고 한다.
생성자의 함수 정의 안에서 변수에 값을 할당하는 경우, 이후에 클래스 객체를 선언할 때 생성자가 자동으로 호출되어 값이 할당되기에 초기화처럼 보이지만, 엄밀히 말하면 이는 초기화가 아니다.
생성자를 이용해서 초기화를 진행하는 경우, 별도로 ‘:’(이니셜라이저) 문법을 통해 초기화를 해 주어야 한다.
const
변수의 초기화는 선언 당시에만 값을 할당할 수 있으므로 이니셜라이저만을 통해서 변수 초기화가 가능하다.
class MyClass {
int i;
double d;
const float cf;
MyClass() :i(1), d(2.2), cf(3.3f) //생성자를 통한 클래스 멤버변수의 초기화.
{
i = 10;
d = 20.2;
cf = 30.3; //const 변수에 초기화가 아닌 할당을 시도했으므로 컴파일 오류가 발생한다.
}
};
또한, 원시 자료형이 아닌 사용자 정의 자료형일 경우 초기화는 어떻게 되는지 의문이 들 수 있다.
이 경우 소괄호 안을 비워두면 컴파일러가 해당 자료형의 생성자를 호출하게 되며, 만약 원시 자료형의 소괄호를 비워 둘 경우 0으로 초기화된다.
template<typename T>
class MyClass {
T Data
MyClass()
:Data() //T에 클래스가 들어갈 경우 클래스의 생성자가 호출됨, 원시 자료형이 들어갈 경우 0으로 초기화됨.
{}
}
-
생성자 오버로딩
생성자 오버로딩은 말 그대로 생성자라는 함수의 오버로딩을 말한다. 생성자는 입력값을 다르게 받아서 같은 이름으로 여러개 선언해 오버로딩할 수 있으며, 이를 통해 클래스 객체 선언 시 입력값에 따라 다른 생성자를 호출시키거나, 반드시 입력값을 받는 등의 응용이 가능하다.
class MyClass {
private:
int i;
double d;
const float cf;
public:
MyClass() : i(0), d(0.), cf(0.f) { //입력값이 없을 경우 모든 멤버변수를 0으로 초기화하는 생성자
}
MyClass(int _i, double _d, float _cf) : i(_i), d(_d), cf(_cf) { //입력값을 받아서 입력값으로 멤버변수를 초기화해주는 생성자
}
};
int main() {
MyClass c1; //입력값이 없으므로 0으로 초기화하는 생성자를 호출
MyClass c2(10, 3.14, 6.28f); //입력값에 해당하는 값으로 멤벼변수를 초기화하는 생성자를 호출
MyClass c3(); //이건 입력값이 없는 생성자의 호출이 아니다. 컴파일러는 이를 반환타입 MyClass, 함수명 c3()라는 함수의 전방선언으로 컴파일한다.
return 0;
}
주의깊게 봐야 할 것은 MyClass c3();
부분이다. 이는 자칫 입력값이 없는 생성자를 호출하는 문법으로 착각하기 쉽지만, 컴파일러는 이 코드를 “반환타입 MyClass, 함수명 c3()라는 함수의 전방선언” 으로 인식하므로 주의해야 한다.
또한 컴파일러가 생성자와 소멸자를 자동으로 생성하는 조건도 주의해야 한다. 생성자 및 소멸자의 자동생성 규칙은 다음과 같다:
- 생성자가 하나도 없으면 기본 생성자를 생성한다. (하나라도 있다면 자동으로 생성되지 않는다)
- 소멸자가 정의되어 있지 않으면 소멸자를 생성한다.
생성자는 하나라도 생성되어 있으면 생성되지 않으므로, 입력값을 받는 생성자 하나만 존자한다 하더라도 생성되지 않으며, 이 경우 클래스 선언 시 입력값을 반드시 필요로 하게 된다.
또한 멤버변수에 const 변수가 포함될 경우 기본 생성자는 이니셜라이저로 초기화가 이루어지지 않으므로 컴파일 오류가 발생한다.
3. 클래스의 동적할당
클래스는 기본적으로 객체 선언 시 생성자가 반드시 호출되어야 하며, 객제가 소멸할 때 반드시 소멸자가 호출되어야 한다.
하지만 기존의 malloc(n) 함수는 단순히 n바이트의 공간을 힙 메모리에 할당할 뿐이므로 해당 공간이 어떤 자료형인지 컴파일러가 알 수 없고, 컴파일러가 생성자 및 소멸자를 호출할 수 없게 된다.
이를 해결하기 위해 C++ 에서 동적 할당용 키워드 new
가 추가되었다.
클래스를 동적 할당할 경우, 반드시 new 와 delete 로 생성자와 소멸자 호출을 시키며 동적할당을 진행하자.
추가로, 메모리 해제 키워드 delete 는 선언 시 자료형을 배열 형태로 선언했을 경우 delete[] 키워드로 사용해야 한다.
4. 연산자 오버로딩
기본 연산자를 클래스에서 사용하고 싶은 경우, 클래스 내에 연산자에 대한 함수를 오버로딩해서 연산을 시킬 수 있다.
...
MyClass operator + (const MyClass& _other) const {
MyClass temp = {};
temp.i = i + _other.i;
temp.d = d + _other.d;
return temp;
}
...
operator
을 이용해 기본 연산자를 함수처럼 오버로딩해서 사용할 수 있다. 연산자 +
를 위와 같이 오버로딩할 경우 클래스 간 덧셈연산은 각 멤버변수의 요소별 덧셈으로 처리된다.
함수 정의 바로 앞의 const
키워드는 해당 연산자를 호출한 객체를 수정하지 않도록 하는 키워드로써, 해당 함수가 입력 변수를 참조만 하고 수정은 하지 않겠다는 것을 명시해 준다.
5. 내부 클래스
내부 클래스는 클래스 내부에 정의된 또하나의 클래스를 뜻하며, 다음과 같은 특징을 가진다:
-
내부 클래스와 외부 클래스는 각각 별도의 클래스로, 멤버를 공유하지는 않는다.
-
내부 클래스는 속한 외부 클래스가 다른 경우 이름이 중복될 수 있다.
-
내부 클래스는 외부 클래스의 private 멤버에 접근이 가능하지만, 외부 클래스는 내부 클래스의 private 멤버에 접근할 수 없다.
-
내부 클래스 선언을 위해서는 외부 클래스부터 스코프 연산자(::)를 이용해 접근해야 한다.
만약 외부 클래스에서 내부 클래스의 private 멤버에 접근하고 싶다면 내부 클래스에서 외부 클래스를 friend 선언을 해 주면 된다.
6. 복사 생성자와 대입 연산자
복사 생성자는 생성자의 종류 중 하나로, 다른 동일한 타입의 객체를 복사하여 초기화하는 생성자이다.
앞서 언급되었듯이 컴파일러는 프로그래머가 별도로 생성자를 선언하지 않았을 경우, 생성자를 자동 생성한다고 하였다.
이 때, 컴파일러는 일반 생성자 뿐만 아니라 복사 생성자도 생성하게 된다. 정리하면, 컴파일러는 복사 생성자가 없으면 복사 생성자를 스스로 선언하고, 생성자 선언이 없었다면 기본 생성자도 선언한다.
따라서, 복사 생성자만 프로그래머가 선언을 해 두었다면 클래스는 오직 복사 생성자로만 선언할 수 있게 된다(기본 생성자는 복사”생성자”가 선언되어 있으므로 선언이 안됨).
또한 대입 연산자도 선언되어 있지 않다면 컴파일러가 스스로 생성한다.
대입 연산자는 연산 결과만 보면 결국 복사 생성자와 같아 보이지만, 복사 생성자는 “초기화” 가 가능한 점이 다르다.
이후 코드 상에서 호출 시 다음과 같은 경우를 주의해야 한다:
MyClass t1(t0); //복사 생성자를 호출한다
t1 = t0; //대입 연산자를 호출한다
MyClass t1 = t0; //복사 생성자를 호출한다
3번째 케이스와 같이 대입 연산자가 사용되어 마치 대입 연산처럼 보이지만, 실제로는 디버그 과정을 살펴보면 복사 생성자를 호출함을 확인할 수 있다.
이 경우 컴파일 과정에서 최적화를 위해 컴파일러가 기본 생성자로 초기화 후 대입 연산을 복사 생성자로 초기화 로 단순화하며 일어나는 일이다.