C++에서 가상(virtual) 메커니즘은 기본 클래스(base class) 포인터가 파생 클래스(derived class) 객체를 가리킬 때에만 동작합니다. 즉, 다형성(polymorphism)을 활용하려면 기본 클래스 타입의 포인터나 참조를 통해 파생 클래스에서 오버라이딩한 함수를 호출해야 합니다.
생성자는 가상으로 만들 수 없다
C++에서는 생성자를 가상 함수로 선언하는 것이 불가능합니다. 그 이유는 다음과 같습니다.
- 클래스의 생성자가 실행되는 시점에는 객체의 가상 테이블(vtable)이 아직 메모리에 완성되지 않은 상태입니다.
- 가상 테이블이 없다는 것은 가상 포인터(vptr)도 아직 설정되지 않았음을 의미합니다.
- 가상 함수 호출은 vptr을 통해 vtable을 참조하는 방식으로 이루어지기 때문에, 생성자가 실행되는 동안에는 가상 메커니즘이 작동할 수 없습니다.
따라서 생성자는 항상 비가상(non-virtual) 함수여야 하며, 컴파일러 역시 가상 생성자 문법을 허용하지 않습니다.
가상 소멸자는 가능하다
반면 소멸자(destructor)는 가상으로 선언하는 것이 가능하고, 실제로 강력히 권장됩니다. 기본 클래스 포인터로 파생 클래스 객체를 삭제(delete)할 때 소멸자가 가상이 아니라면 파생 클래스의 소멸자가 호출되지 않아 리소스 누수나 미정의 동작(undefined behavior)이 발생할 수 있습니다. 가상 소멸자를 사용하면 delete 시점에 파생 클래스 소멸자부터 기본 클래스 소멸자까지 올바른 순서로 모두 실행됩니다.
예제 코드
#include<iostream>
using namespace std;
class b {
public:
b() {
cout<<"Constructing base \n";
}
virtual ~b() {
cout<<"Destructing base \n";
}
};
class d: public b {
public:
d() {
cout<<"Constructing derived \n";
}
~d() {
cout<<"Destructing derived \n";
}
};
int main(void) {
d *derived = new d();
b *bptr = derived;
delete bptr;
return 0;
}실행 결과
Constructing base Constructing derived Destructing derived Destructing base
코드 설명
new d()로 파생 클래스 객체를 생성하면 객체 생성 순서에 따라 기본 클래스 생성자가 먼저 호출된 뒤 파생 클래스 생성자가 호출됩니다. 그 결과 "Constructing base", "Constructing derived"가 순서대로 출력됩니다.
이후 기본 클래스 포인터 bptr로 delete를 수행하면, 소멸자가 virtual로 선언되어 있기 때문에 파생 클래스 소멸자가 먼저 호출되고 그다음 기본 클래스 소멸자가 호출됩니다. 만약 기본 클래스의 소멸자가 가상이 아니었다면 "Destructing derived"는 출력되지 않고 기본 클래스 소멸자만 호출되어 잘못된 자원 해제가 일어날 수 있습니다.
결론적으로, C++에서 생성자는 가상으로 만들 수 없지만, 기본 클래스 포인터를 통해 객체를 다룬다면 소멸자는 반드시 virtual로 선언하는 것이 안전한 객체 지향 설계의 기본 원칙입니다.