[C++] 생성자/소멸자에서 virtual 함수 호출시 문제
생성자 또는 소멸자에서 가상(virtual) 함수 호출시 어떻게 될까요. 기술 면접에서 종종 나오는 질문이다. 얼핏 대답하기가 쉽지 않다. 컴파일 오류가 날 것 같지는 않은데, 왠지 실행시에 undefined behaviour가 날 것 같기도 하고, 상속 관계까지 생각하다 보면 머리가 복잡해진다.
코드 예제를 보면서 생각해보자.
class Base
{
public:
Base() {
cout << "Base Constructor" << endl;
}
virtual ~Base() {
cout << "~Base Destructor" << endl;
}
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "~Derived Destructor" << endl;
}
};
int main()
{
cout << "> Creat" << endl;
Base * base = new Derived;
cout << "> Delete" << endl;
delete base;
}
Base 클래스와 Derived 클래스를 만들고, Derived를 Base로 부터 상속받게 한다. 각 생성자와 소멸자에 로그를 심고, 실행을 해보면 다음과 같이 프린트 된다.
> Creat
Base Constructor
Derived Constructor
> Delete
~Derived Destructor
~Base Destructor
new를 통해 오브젝트를 생성하게 되면, Base 생성자가 먼저 호출되고 다음으로 Derived 생성자가 호출된다. 그리고 delete 키워드를 호출하면, Derived 소멸자가 호출되고 Base의 소멸자가 호출된다. 이것은 Derived 오브젝트가 생성되기 전에 Base 오브젝트가 생성된다는 의미이고, 반대로 소멸시에는 Derived 오브젝트가 먼저 소멸되고, 그 후에 Base 오브젝트가 소멸된다는 것을 의미한다.
이제 Do()라는 가상 함수를 추가하고, Base 생성자에서 Do()를 호출해보자.
class Base
{
public:
Base() {
cout << "Base Constructor" << endl;
Do();
}
virtual ~Base() {
cout << "~Base Destructor" << endl;
}
virtual void Do() { cout << "[Base] Do" << endl; }
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "~Derived Destructor" << endl;
}
virtual void Do() { cout << "[Derived] Do" << endl; }
};
int main()
{
cout << "> Creat" << endl;
Base * base = new Derived;
cout << "> Delete" << endl;
delete base;
}
실행을 해보면, 아래와 같이 Base::Do()가 호출되었다는 것을 알 수 있다. 즉, 생성자에서 가상 함수를 호출할 수는 있지만, 하위 클래스의 가상 함수가 호출되지는 않는다. 생성자에서는 virtual의 의미가 없어지는 것이고 이는 Derived 오브젝트가 아직 생성되지 않았기 때문이다.
> Creat
Base Constructor
[Base] Do
Derived Constructor
> Delete
~Derived Destructor
~Base Destructor
이제 생성자에서 Do()를 제거하고, 소멸자에 Do()를 추가해보자.
#include "pch.h"
#include
using namespace std;
class Base
{
public:
Base() {
cout << "Base Constructor" << endl;
}
virtual ~Base() {
cout << "~Base Destructor" << endl;
Do();
}
virtual void Do() { cout << "[Base] Do" << endl; }
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "~Derived Destructor" << endl;
}
virtual void Do() { cout << "[Derived] Do" << endl; }
};
int main()
{
cout << "> Creat" << endl;
Base * base = new Derived;
cout << "> Delete" << endl;
delete base;
}
> Creat
Base Constructor
Derived Constructor
> Delete
~Derived Destructor
~Base Destructor
[Base] Do
이번에도 Base::Do()가 호출되는 것을 알 수 있다. 즉, Derived 소멸자가 호출된 이후에 Base 소멸자가 호출되었고, Derived 오브젝트가 이미 소멸된 이후이기 때문에 Base::Do()가 호출이 된 것이다.
정리하면, 생성자와 소멸자에서 가상 함수를 호출할 수는 있지만, 가상 함수의 역할은 하지 않게 되고, 멤버 함수로서의 기능으로만 작동된다.
만약, 순수 가상 함수라면 어떻게 될까?
class Base
{
public:
Base() {
cout << "Base Constructor" << endl;
}
virtual ~Base() {
cout << "~Base Destructor" << endl;
Do();
}
virtual void Do() = 0;
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "~Derived Destructor" << endl;
}
virtual void Do() { cout << "[Derived] Do" << endl; }
};
int main()
{
cout << "> Creat" << endl;
Base * base = new Derived;
cout << "> Delete" << endl;
delete base;
}
Base 클래스에서 Do()를 순수 가상 함수로 바꾸었다. 지금까지 생성자와 소멸자에서 Do()를 호출하면 Base::Do()가 호출되었었는데, 이제 그 구현 부분이 없어지고, 하위 클래스 Derived에만 존재하고 있는 상황이 되었다.
하지만, 실행을 해보면 아래와 같이 링크 오류가 발생하기 때문에 이렇게 사용하는 것은 허용되지 않는다.
LNK2019 "public: virtual void __thiscall Base::Do(void)" (?Do@Base@@UAEXXZ) 외부 기호(참조 위치: "public: virtual __thiscall Base::~Base(void)" (??1Base@@UAE@XZ) 함수)에서 확인하지 못했습니다
면접에서 자주 나오는 문제이니 잘 알아두면 좋을 것 같다.