Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++ 생성자·소멸자에서 가상 함수 호출이 위험한 이유와 동작 원리

C++에서 생성자나 소멸자 안에서 가상 함수를 호출하는 것은 위험한 동작이며, 가능한 한 피해야 합니다. 그 이유는 이때 호출되는 가상 함수가 파생(Derived) 클래스에서 오버라이딩된 버전이 아니라, 기반(Base) 클래스의 버전으로 실행되기 때문입니다.

왜 기반 클래스의 함수가 호출될까?

원인은 C++의 객체 생성 순서에 있습니다. C++에서는 슈퍼클래스(기반 클래스)가 파생 클래스보다 먼저 생성됩니다. 즉, 파생 클래스 D의 객체가 만들어지려면 반드시 그 전에 기반 클래스 B가 먼저 인스턴스화되어야 합니다.

B의 생성자가 실행되는 시점에는 객체가 아직 D로 완성되지 않은 상태입니다. 따라서 이 시점의 가상 함수 테이블(vtable)에는 여전히 B의 s()에 대한 항목만 존재하며, D에서 재정의한 s()는 아직 연결되지 않습니다.

소멸자도 마찬가지입니다. 파생 클래스의 소멸자가 먼저 실행되고, 기반 클래스의 소멸자가 실행되는 시점에는 이미 파생 클래스 부분이 소멸된 상태이므로 동일한 현상이 발생합니다.

예제 코드

#include<iostream>
using namespace std;

class B {
public:
    B() {
        s();
    }
    virtual void s() {
        cout << "Base" << endl;
    }
};

class D : public B {
public:
    D() : B() {}
    virtual void s() {
        cout << "Derived" << endl;
    }
};

int main() {
    D de;
}

실행 결과

Base

D 타입의 객체 de를 생성했음에도 출력 결과는 "Derived"가 아니라 "Base"입니다. 이는 D의 생성자가 호출되기 전에 B의 생성자가 먼저 실행되고, 그 안에서 호출된 s()가 B의 버전으로 바인딩되었기 때문입니다. 직관적으로는 파생 클래스의 함수가 호출될 것이라 기대하기 쉽지만, 실제 동작은 다르므로 주의해야 합니다.

해결 방법

이러한 문제를 피하려면 다음과 같은 설계 방식을 고려할 수 있습니다.

  • 초기화 단계 분리: 생성자에서 가상 함수를 호출하지 말고, 객체 생성 후 별도의 init() 같은 초기화 함수를 명시적으로 호출합니다.
  • 팩토리 함수 활용: 객체 생성을 담당하는 팩토리 함수에서 생성 직후 필요한 초기화 로직을 수행하면, 완성된 객체 상태에서 가상 함수를 안전하게 호출할 수 있습니다.
  • 순수 가상 함수 대체: 생성 시점에 필요한 동작이라면 비가상 멤버 함수나 템플릿 기법(CRTP 등)으로 대체하는 것도 방법입니다.

결론적으로, 생성자와 소멸자 내부에서는 객체가 완전히 구성(또는 유지)되지 않은 상태이므로 가상 함수 호출을 피하고, 위와 같은 대안적인 설계를 사용하는 것이 안전합니다.