C++에서 상속 관계를 다룰 때 가장 중요한 생성자/소멸자의 호출 순서와 멤버 변수에 대한 접근 권한, 그리고 객체지향의 꽃인 다형성(Polymorphism)에 대해 예제 코드를 통해 정리합니다.
상속 관계에서 자식 클래스의 객체가 생성될 때, 부모 클래스의 생성자는 반드시 호출됩니다. 또한, 클래스 내부에 멤버 객체(Composition)가 있다면 해당 객체의 생성자도 순서에 따라 호출됩니다.
/* - 부모 클래스 생성자 -> 자식 클래스 멤버 객체 생성자 -> 자식 클래스 생성자 순으로 실행
- 소멸자는 생성자의 역순으로 실행
*/
#include <iostream>
using namespace std;
class Ice {
public:
Ice() { cout << "Ice() 생성" << endl; }
~Ice() { cout << "~Ice() 소멸" << endl; }
};
class Pat {
public:
Pat() { cout << "Pat() 생성" << endl; }
~Pat() { cout << "~Pat() 소멸" << endl; }
};
class Bingsoo {
public:
Bingsoo() { cout << "Bingsoo() 부모 생성" << endl; }
~Bingsoo() { cout << "~Bingsoo() 부모 소멸" << endl; }
private:
Ice ice; // HAS-A 관계(포함관계 != 상속관계)
};
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() { cout << "PatBingsoo() 자식 생성" << endl; }
~PatBingsoo() { cout << "~PatBingsoo() 자식 소멸" << endl; }
private:
Pat pat; // HAS-A 관계
};
int main() {
cout << "== 객체 생성 시작 ==" << endl;
PatBingsoo* p = new PatBingsoo();
cout << "== 객체 소멸 시작 ==" << endl;
delete p;
return 0;
}
//출력 결과
== 객체 생성 시작 ==
Ice() 생성 // 1. 부모 클래스의 멤버 객체 생성
Bingsoo() 부모 생성 // 2. 부모 클래스 생성자 실행
Pat() 생성 // 3. 자식 클래스의 멤버 객체 생성
PatBingsoo() 자식 생성 // 4. 자식 클래스 생성자 실행
== 객체 소멸 시작 ==
~PatBingsoo() 자식 소멸 // 5. 자식 클래스 소멸자 실행 (생성의 역순 시작)
~Pat() 소멸 // 6. 자식 클래스의 멤버 객체 소멸
~Bingsoo() 부모 소멸 // 7. 부모 클래스 소멸자 실행
~Ice() 소멸 // 8. 부모 클래스의 멤버 객체 소멸
C++에서 상속(IS-A)과 포함(HAS-A) 관계가 섞여 있을 때, 객체가 생성되는 논리적인 단계를 뜯어보겠습니다.
객체가 생성될 때 컴파일러는 내부적으로 다음의 순서를 엄격히 지킵니다.
{})를 실행한다.{})를 실행한다.질문하신 PatBingsoo 객체를 생성할 때의 내부 동작을 단계별로 보겠습니다.
PatBingsoo는 Bingsoo를 상속받았으므로, 자식의 몸체를 실행하기 전에 부모인 Bingsoo를 먼저 만들어야 합니다. 그래서 제어권이 Bingsoo로 넘어갑니다.
Bingsoo 클래스 안을 보니 Ice ice;라는 멤버 변수가 있습니다.
Bingsoo의 생성자 {}가 실행되기 직전에, 멤버 변수인 Ice의 생성자가 먼저 호출됩니다.Ice() 생성재료(Ice)가 준비되었으니 이제 Bingsoo의 생성자 몸체를 실행합니다.
Bingsoo() 부모 생성부모 영역이 완전히 만들어졌으니 다시 자식인 PatBingsoo로 돌아옵니다. 자식 클래스 안을 보니 Pat pat;이라는 멤버가 있네요.
Pat() 생성이제 모든 재료가 준비되었으니 최종적으로 PatBingsoo의 생성자 몸체를 실행합니다.
PatBingsoo() 자식 생성"부모의 멤버 -> 부모의 본체 -> 자식의 멤버 -> 자식의 본체"
소멸자는 이 과정을 정확히 거울 보듯 반대(역순)로 진행합니다.
가장 마지막에 완성된 자식의 본체부터 부수기 시작해서, 가장 먼저 만들어졌던 부모의 재료(Ice)를 가장 나중에 치우는 것이죠.
만약 부모의 본체가 실행되기 전에 부모의 멤버(Ice)가 생성되지 않는다면, 부모 생성자 안에서 ice 변수를 사용하려고 할 때 에러가 날 것입니다. C++은 "사용하기 전에 반드시 완벽하게 준비시킨다"는 철학을 가지고 있기 때문에 이런 순서를 가집니다.
## 1. 포함 관계(Composition)란?
포함 관계는 한 클래스가 다른 클래스의 객체를 자신의 멤버 변수로 가지는 상태를 말합니다.
상속이 "A는 B이다(IS-A)"라면, 포함은 "A는 B를 가지고 있다(HAS-A)"는 논리입니다.
* 예제에서의 포함 관계:
- Bingsoo 클래스는 Ice 객체를 멤버로 가짐 (빙수는 얼음을 가짐)
- PatBingsoo 클래스는 Pat 객체를 멤버로 가짐 (팥빙수는 팥을 가짐)
## 2. 포함 관계의 특징
- 생명 주기 종속: 호스트 객체(Bingsoo)가 생성될 때 부품 객체(Ice)도 함께 생성되고, 호스트가 소멸할 때 부품도 운명을 같이합니다.
- 부품의 우선순위: 객체를 조립할 때 부품이 먼저 준비되어야 본체를 완성할 수 있듯, 생성자 호출 시 멤버 객체의 생성자가 본체 생성자보다 먼저 실행됩니다.
## 3. 전체 코드 및 실행 순서 상세
#include <iostream>
using namespace std;
class Ice {
public:
Ice() { cout << "Ice() 부품 생성" << endl; }
~Ice() { cout << "~Ice() 부품 소멸" << endl; }
};
class Pat {
public:
Pat() { cout << "Pat() 부품 생성" << endl; }
~Pat() { cout << "~Pat() 부품 소멸" << endl; }
};
class Bingsoo {
public:
Bingsoo() { cout << "Bingsoo() 부모 본체 생성" << endl; }
~Bingsoo() { cout << "~Bingsoo() 부모 본체 소멸" << endl; }
private:
Ice ice; // [포함 관계] 빙수의 부품
};
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() { cout << "PatBingsoo() 자식 본체 생성" << endl; }
~PatBingsoo() { cout << "~PatBingsoo() 자식 본체 소멸" << endl; }
private:
Pat pat; // [포함 관계] 팥빙수의 부품
};
int main() {
cout << "== 1. 객체 생성 과정 ==" << endl;
PatBingsoo* p = new PatBingsoo();
// 생성 순서 설명:
// 1단계 [부모 생성 시작]: 부모의 부품(Ice) 생성 -> 부모 본체(Bingsoo) 생성
// 2단계 [자식 생성 시작]: 자식의 부품(Pat) 생성 -> 자식 본체(PatBingsoo) 생성
cout << endl << "== 2. 객체 소멸 과정 ==" << endl;
delete p;
// 소멸 순서 설명 (생성의 역순):
// 1단계 [자식 소멸]: 자식 본체 소멸 -> 자식 부품(Pat) 소멸
// 2단계 [부모 소멸]: 부모 본체 소멸 -> 부모 부품(Ice) 소멸
return 0;
}
## 4. 요약 및 주의사항
- 포함된 멤버 변수는 '조립품'과 같아서 본체 클래스의 생성자 몸체({ })가 실행되기 전에 이미 메모리에 준비를 마쳐야 합니다.
- 위 예제에서 Ice ice;는 private으로 선언되어 있으므로, PatBingsoo(자식)는 부모의 부품인 ice에 직접 접근할 수 없습니다. 이것이 상속과 포함의 결정적인 차이 중 하나입니다.
부모 클래스의 private 멤버는 자식 클래스에서 직접 접근할 수 없습니다. 따라서 자식 클래스의 생성자에서 부모 클래스의 생성자를 명시적으로 호출하여 초기화해야 합니다.
#include <iostream>
#include <string>
using namespace std;
class Image {
public:
// 형변환 연산자 오버로딩: 객체를 string으로 취급할 때 호출
operator string() { return "사진 데이터"; }
};
class Message {
public:
Message(int sendTime, string sendName)
: sendTime(sendTime), sendName(sendName) {}
int getSendTime() const { return sendTime; }
string getSendName() const { return sendName; }
private:
int sendTime; // 자식에서 직접 접근 불가
string sendName;
};
class TextMessage : public Message {
public:
TextMessage(int sendTime, string sendName, string text)
: Message(sendTime, sendName) { // 부모 생성자 호출
this->text = text;
}
string getText() const { return text; }
private:
string text;
};
class ImageMessage : public Message {
public:
ImageMessage(int sendTime, string sendName, Image* image)
: Message(sendTime, sendName) {
this->image = image;
}
Image* getImage() const { return image; }
private:
Image* image;
};
부모 타입의 포인터로 자식 객체를 가리킬 때(Upcasting), 가상 함수(virtual)를 사용하면 실행 시점에 실제 객체의 타입에 맞는 함수가 호출됩니다.
class Weapon {
public:
Weapon(int power) : power(power) {
cout << "Weapon 생성자 호출" << endl;
}
// 가상 함수 선언
virtual void use() {
cout << "무기를 사용합니다." << endl;
}
protected:
// protected: 자식 클래스에게는 접근을 허용
int power;
};
class Sword : public Weapon {
public:
Sword(int power) : Weapon(power) {}
// 부모의 use()를 재정의(Override)
void use() override {
cout << "검을 휘둘러 " << power << "의 데미지를 입힙니다!" << endl;
}
};
int main() {
// 다형성 활용: 부모 포인터로 자식 객체 관리
Weapon* myWeapon = new Sword(100);
// Sword의 use()가 호출됨 (동적 바인딩)
myWeapon->use();
delete myWeapon;
return 0;
}
private은 자식도 못 건드린다. protected는 자식에겐 오픈된다.virtual 키워드를 써야 부모 포인터로 자식을 가리켰을 때 자식의 함수가 제대로 호출된다.