KoreanFoodie's Study
C++ DevNote : 파생 클래스에서 기반 클래스 생성자 제대로 호출하기 본문
C++ 에 대해 공부한 것과, 개발하면서 알게 된 것들을 다룹니다
파생 클래스에서의 기반 클래스 생성자 호출
사실 파생 클래스가 생성될 때, 기반 클래스 생성자가 호출된다는 건, C++ 을 조금이라도 해 본 사람은 누구나 알고 있는 사실이다. 또한 함수의 생성자/소멸자의 호출 순서가 다음과 같다는 것도, 익히 알려진 사실이다. 즉, A 클래스가 기반 클래스이고, B 클래스가 파생 클래스라고 가정했을 때 B 클래스 변수를 선언하면 호출 순서가 다음과 같이 나올 것이다 :
- A 클래스 생성자
- B 클래스 생성자
- B 클래스 소멸자
- A 클래스 소멸자
하지만 한 가지 기억해야 할 것은, 파생 클래스를 만들 때 호출되는 기반 클래스 생성자는 '인자가 없는 기반 클래스의 생성자' 이다. 즉, 인자를 받는 형식의 기반 클래스 생성자를 호출해주고 싶으면, 추가적인 작업을 해 주어야 한다는 뜻이다!
다음과 같은 코드를 보자.
#include <iostream>
using namespace std;
class Test
{
public:
int a;
Test() : a(0) {}
Test(int i) : a(i) {}
void poo() { cout << "a is " << a << endl; }
};
class A
{
public:
Test* test;
virtual void foo() { test->poo(); }
A(int i) { test = new Test(i); }
A() { test = new Test(-1); }
virtual ~A() {
cout << "A Deconstructor" << endl;
}
};
class B : public A
{
public:
B(int i) { }
virtual ~B() {
cout << "B Deconstructor" << endl;
}
};
int main() {
B b(1);
b.foo();
return 0;
}
사용자는 B 클래스에 1 을 전달하고, "a is 1" 이라는 출력값을 기대했다고 가정하자. 왜냐하면 기반 클래스 생성자인 A(int i) 가 불릴 것이라고 생각했으므로.
하지만 실제로 불리는 것은 A(int i) 형식이 아닌 A() 형식이 된다.
원하는 목적을 얻기 위해서는, B 의 생성자에서 원하는 A 의 생성자를 잘 호출해 주어야 한다 :
B(int i) : A(i) { }
B 의 생성자에서 위와 같이 A 의 생성자를 잘 호출해 주면, 결과값이 이제 "A is 1" 이 잘 나오게 된다.
참고로, 다음과 같이 쓰는 것은 생성자 body 내에서 A 타입의 임시 변수를 만드는 것일 뿐, B 클래스의 생성자에는 아무런 영향을 끼치지 못한다!
B(int i) { A(i); }
그 외 예시들
그럼 이제 재미있는 예시들을 조금씩 살펴보도록 하자.
1. 다중상속 (여러 클래스로부터 상속받을 경우) 의 경우
다음과 같은 코드가 있다고 하면, 생성자와 호출자의 순서는 어떻게 될까?
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A 생성" << endl; }
virtual ~A() { cout << "A 소멸" << endl; }
};
class B {
public:
B() { cout << "B 생성" << endl; }
virtual ~B() { cout << "B 소멸" << endl; }
};
class C : B, A {
public:
C() { cout << "C 생성" << endl; }
virtual ~C() { cout << "C 소멸" << endl; }
};
int main()
{
C c;
}
C 클래스는 A, B 클래스를 상속받는데, 순서가 B, A 순이다. 이 경우, 상속받은 순서대로 생성자/소멸자가 호출된다(답은 더보기에).
B 생성
A 생성
C 생성
C 소멸
A 소멸
B 소멸
2. 파생 클래스의 생성자에 기반 클래스 변수가 있을 경우
다음과 같은 코드를 보자.
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A 생성" << endl; }
virtual ~A() { cout << "A 소멸" << endl; }
};
class B : A {
public:
A a;
B() {
cout << "B 생성" << endl;
}
virtual ~B() { cout << "B 소멸" << endl; }
};
int main()
{
B b;
}
이 경우는 출력 결과가 어떻게 될까? 답은 더보기에 있다.
A 생성
A 생성
B 생성
B 소멸
A 소멸
A 소멸
즉, 먼저 B 를 생성하기 위해 기반 클래스 생성자인 A 의 생성자가 호출되고, B 의 멤버 변수인 a 를 초기화하기 위해 A 의 생성자가 또 불리게 된다. 마지막으로 B 의 생성자가 불린다!
다음과 같이 초기화 리스트에서 멤버 변수를 초기화 하면 어떻게 될까?
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A 생성" << endl; }
virtual ~A() { cout << "A 소멸" << endl; }
};
class B : A {
public:
A a;
B(A _a) : a(_a) {
cout << "B 생성" << endl;
}
virtual ~B() { cout << "B 소멸" << endl; }
};
int main()
{
A a;
B b(a);
}
결과는 상당히 신기하게 나올 것이다(더보기 참고).
A 생성
A 생성
B 생성
A 소멸
B 소멸
A 소멸
A 소멸
A 소멸
출력 결과를 보면 생성자는 3번, 소멸자는 5번 호출이 되고 있다. 이유가 무엇일까?
사실 나머지 2 건은 복사 생성자 때문에 불린 케이스이다. 코드를 다음과 같이 고치면, 그 이유가 좀 더 명확해진다!
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A Con" << endl; }
A(A const&) { cout << "A Con (Copy)" << endl; }
virtual ~A() { cout << "A Dec" << endl; }
};
class B : A {
public:
A a;
B(A _a) : a(_a) {
cout << "B Con" << endl;
}
virtual ~B() { cout << "B Dec" << endl; }
};
int main()
{
A a;
B b(a);
}
정답은 접은글을 참고하자.
A Con -> main 함수에 있는 a 생성자 호출
A Con (Copy) -> main 함수에서 복사 생성자 호출. 임시 변수 생성
A Con -> main 함수에 있는 b 의 기반 클래스 생성자 호출
A Con (Copy) -> B 생성자에서 멤버 변수인 a 를 초기화 하는데 복사 생성자 호출
B Con -> main 함수의 b 생성자 호출
A Dec -> B 생성자에서 복사 생성자로 임시로 만든 _a 에 대한 소멸자 호출
B Dec -> main 함수에 있는 b 의 소멸자 호출
A Dec -> B 클래스의 내부에 있는 멤버 변수 a 의 소멸자 호출
A Dec -> main 함수에 있는 b 의 기반 클래스 소멸자 호출
A Dec -> main 함수에 있는 a 변수의 소멸자 호출
클래스 내부에 있는 멤버 변수의 소멸자는, 해당 클래스의 소멸자 이후에 불린다! B 에 선언된 'A a;` 변수의 소멸자의 경우, B 의 소멸자 호출 이후 - A 의 소멸자 호출 이전에 불린다!
혹여나 정말로 극한까지 테스트를 해 보고 싶다면. 아래 코드를 돌려보자!
#include <iostream>
using namespace std;
class C {
public:
virtual ~C() { cout << "C Dec" << endl; }
};
class D {
public:
virtual ~D() { cout << "D Dec" << endl; }
};
class A {
public:
C c;
A() { cout << "A Con" << endl; }
A(A const&) { cout << "A Con (Copy)" << endl; }
virtual ~A() { cout << "A Dec" << endl; }
};
class B : A {
public:
A a;
D d;
B(A _a) : a(_a) {
cout << "B Con" << endl;
}
virtual ~B() { cout << "B Dec" << endl; }
};
int main()
{
A a;
B b(a);
}
위 코드의 출력값을 보면 생성자와 소멸자의 호출 순서를 다시는 잊지 않을 수 있을 것이다!
참고