본문 바로가기
프로그래밍/C\C++

[Effective C++ 정리 #9] 생성자에서 가상 함수 호출 문제 및 해결 방법

by 허구의 2025. 6. 19.
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

[Effective C++ 정리 #9] 생성자나 소멸자에서 가상 함수를 호출하지 마라

Java나 C#처럼 가상 함수 기반 객체 지향 프로그래밍에 익숙한 사람이라면, C++에서 가상 함수를 사용할 때 뜻밖의 함정에 빠지기 쉽습니다. 바로 생성자와 소멸자에서는 가상 함수가 다형적으로 동작하지 않는다는 점입니다.


왜 문제인가? — 생성자에서 가상 함수를 호출하는 경우

다음과 같은 계층 구조를 가정해봅시다.

class Transaction {
public:
    Transaction(); // 생성자
    virtual void logTransaction() const = 0;
};

Transaction::Transaction() {
    // 로그 남기기
    logTransaction();
}

class BuyTransaction : public Transaction {
public:
    virtual void logTransaction() const override {
        std::cout << "BuyTransaction logged\n";
    }
};

 

이제 다음 코드를 실행해봅니다:

BuyTransaction b;

 

직관적으로는 BuyTransaction::logTransaction이 호출될 것 같지만, 실제로는 Transaction의 logTransaction이 호출됩니다. 더 정확히 말하면, 순수 가상 함수(pure virtual)이므로 호출 시점에 프로그램이 종료됩니다.

 

왜냐하면 C++에서는 객체가 아직 완전히 생성되지 않았기 때문입니다. BuyTransaction 객체를 만들기 위해 먼저 Transaction의 생성자가 실행되는데, 그 시점엔 아직 BuyTransaction의 멤버들이 초기화되지 않았고, 객체는 **물리적으로 Transaction 그 자체**로 간주됩니다.


왜 이런 동작을 하는가?

C++의 객체 생명 주기 규칙상, 기본 클래스의 생성자가 먼저 실행되며, 그 이후에야 파생 클래스의 생성자가 실행됩니다.

즉, 파생 클래스의 생성자 코드가 실행되기 전까지는 객체는 아직 파생 클래스가 아닌 것처럼 동작하는 것이죠.

이는 단순한 설계 결정이 아니라, **정상 작동을 위한 필연적인 제약**입니다.

 

즉, C++은 이런 위험을 방지하기 위해 생성자와 소멸자에서 가상 함수 호출 시 객체를 현재 실행 중인 클래스의 타입으로 취급합니다.


간접 호출도 위험하다!

직접 가상 함수를 호출하지 않더라도, 다음과 같은 경우도 문제입니다.

class Transaction {
public:
    Transaction() {
        init();  // 비가상 함수
    }

    virtual void logTransaction() const = 0;

private:
    void init() {
        logTransaction();  // 간접 호출
    }
};

 

init() 자체는 비가상이지만, 그 내부에서 가상 함수를 호출하고 있습니다. 이 경우에도 문제가 발생할 수 있으며, 런타임 오류로 이어질 수 있습니다.


해결 방안: 그럼 어떻게 해결할까?

C++에서는 가상 함수를 호출하지 않고도 유사한 설계를 구현할 수 있는 방법들이 있습니다.

class Transaction {
public:
    explicit Transaction(const std::string& logInfo) {
        logTransaction(logInfo);
    }

    void logTransaction(const std::string& logInfo) const {
        std::cout << "Logging: " << logInfo << '\n';
    }
};

class BuyTransaction : public Transaction {
public:
    BuyTransaction()
    : Transaction(createLogString()) {
        ...
    }

private:
    static std::string createLogString() {
        return "BuyTransaction log entry";
    }
};

 

파생 클래스가 필요한 정보를 생성자 인자로 넘겨주고, 기초 클래스는 **비가상 함수**로 이를 처리하게 만듦으로써 안전하게 초기화와 동작 분리할 수 있습니다.


소멸자도 마찬가지

소멸자에서도 동일한 원칙이 적용됩니다.

파생 클래스의 소멸자가 먼저 호출되고 나면, 객체는 다시 **기초 클래스 형태로 축소**되며, 그 상태에서 가상 함수를 호출하면 **파생 클래스 멤버는 이미 파괴된 상태**일 수 있습니다.

이 때문에 소멸자 안에서 가상 함수 호출도 피해야 합니다.


핵심 요약

  • 생성자와 소멸자에서는 가상 함수가 다형적으로 동작하지 않는다.

감사합니다.

728x90