Effective C++/Chapter 2: 생성자, 소멸자 및 대입 연산자

[Effective C++] 항목 9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

yeoul0714 2025. 4. 21. 00:46

1. 개요


저자는 객체 생성, 소멸중에 가상 함수를 절대 호출하지 말라고 합니다.

 

그 이유는 무엇일까? 

 

지금부터 알아 보도록 합시다.


 

2. 문제


아래 코드를 보면 Transaction이 가장 최상위 클래스이고

 

하위로 BuyTransaction, SellTransaction이 각각 Transaction을 상속받고 있는 상태입니다.

 

main에서는 BuyTransaciton의 객체를 만들어 주고 있습니다.

 

BuyTransaction객체를 만들기 위해서는 부모 클래스의 생성자가 먼저 호출되야 합니다.

 

그렇게 부모클래스인 Transaction의 생성자가 호출되는데 여기서 문제가 발생합니다.

 

왜냐하면 Transaction의 생성자에서는 logTransaction이라는 가상함수를 호출중이기 때문입니다.

 

여기서 호출되는 logTransaction함수는 Transaction의 것이고, BuyTransaction것이 아닙니다.

 

부모 클래스의 생성자가 호출될때 실행되는 가상함수는 절대로 자식 클래스의 내려가지 않습니다.

 

객체 스스로가 부모 클래스가 된것처럼 행동하게 됩니다.

 

결국 순수 가상함수를 호출하게 되고 프로그램은 비정상적으로 종료됩니다.

(컴파일러에 따라 애초에 링커오류가 발생할 가능성도 있습니다.)

class Transaction { // 모든 거래의 기본 클래스
public:
    Transaction(); // 생성자
    virtual void logTransaction() const = 0; // 타입에 따른 로그 기록용 순수 가상 함수
    // ...
};

Transaction::Transaction() // 기본 클래스 생성자 구현
{
    // ... (생성자 코드)
    logTransaction(); // 마지막 작업으로 이 거래를 로그에 기록 - 문제 발생 지점!
}

class BuyTransaction: public Transaction { // 구매 거래 파생 클래스
public:
    virtual void logTransaction() const; // 이 타입의 거래를 로그에 기록하는 방법
    // ...
};

class SellTransaction: public Transaction { // 판매 거래 파생 클래스
public:
    virtual void logTransaction() const; // 이 타입의 거래를 로그에 기록하는 방법
    // ...
};


int main()
{
    BuyTransaction b;
}

 

3. 문제 원인


단순히 생각해서 부모 클래스의 생성자가 자식 클래스보다 먼저 실행되기 때문입니다.

 

더 자세히 말하자면 자식 클래스의 객체를 만들게 되면 부모 클래스의 생성자를 우선적으로 호출 하는데

 

부모 클래스의 생성자를 호출하고 있을 때는 아직 자식 클래스의 생성자가 호출되지 않은 상태이고

 

자식 클래스의 데이터들은 아직 초기화가 되지 않은 상태라는 것입니다.

 

만약 초기화 되지도 않은 자식 클래스의 멤버에 접근하게 된다면 예상치 못한 동작이 발생하게 될 것입니다.

 

결국 결론은

 

자식 클래스의 객체를 만들때 실행되는 부모 클래스의 생성자에서 

 

1. 객체의 타입은 기본 클래스입니다.

 

2. dynamic_cast, typeid도 부모 클래스 객체로 취급합니다.

 

위의 예시로 보자면 BuyTransaction객체를 만들때 부모 클스의 생성자가 호출되는데 그 시점엔 객체의 타입이

 

Transaction입니다.

 

아직 자식 클래스인 BuyTransaction은 초기화되지 않았으니 없었던 취급을 하는 것이죠.

 

자식 클래스의 기능을 쓰고 싶다면 Transaction생성자 호출 이후에 BuyTransaction생성자 까지 호출 되어야만합니다.

 

소멸자는?

사실 소멸자라고 해도 다르게 생각할것은 없습니다.

 

생성의 역순으로 호출되기 때문에 자식 클래스가 먼저 소멸하게 됩니다.

 

그렇게 되면 부모 클래스의 소멸자에서는 자식 클래스에 접근 할 수 없는 상태이고 (자식 클래스는 이미 소멸)

 

결국 부모 클래스의 소멸자에서도 객체는 부모클래스 취급을 받게 됩니다.


 

4. 해결책


우선 logTransaction이 가상함수에서 일반함수로 바뀌었습니다. (이제 안전합니다.)

 

그리고 자식 클래스의 생성자에서 필요한 정보를 부모 클래스의 생성자로 넘깁니다.

 

어차피 부모 클래스의 생성자에서는 자식 클래스로 내려갈 수 없음으로 필요한 정보를 

 

자식클래스의 생성자에서 올려주도록 만들었습니다.

 

 

 

class Transaction {
public:
   explicit Transaction(const std::string& logInfo); // 로그 정보를 받는 명시적 생성자
   void logTransaction(const std::string& logInfo) const; // 이제 비가상 함수입니다
   // ...
};

Transaction::Transaction(const std::string& logInfo)
{
   // ...
   logTransaction(logInfo); // 이제 비가상 함수 호출
}

class BuyTransaction: public Transaction {
public:
   BuyTransaction(parameters)
       : Transaction(createLogString(parameters)) // 로그 정보를 기본 클래스에 전달
   {
       // ...
   }
   // 생성자
   // ...

private:
   static std::string createLogString(parameters);
};​

 

static으로 선언된 createLogString함수는 기본 클래스 생성자로 넘길 기본값을 만들어주는 도우미입니다.

 

또한 정적함수이기 때문에 생성되지 않은 자식 클래스의 미초기화 멤버를 건드릴 위험도 없습니다.

(데이터 영역에 있어서 - 컴파일시 이미 생성됨)

 

그리고 초기화 리스트가 복잡할때 코드를 효율적으로 짤 수 있습니다.

 

만약 아래처럼 Transaction으로 넘겨야할 string이 복잡하다고 할 때 

BuyTransaction(int x, int y, const std::string& name, double price) 
    : Transaction(std::to_string(x) + "-" + std::to_string(y) + "-" + name + "-" + std::to_string(price)),
      m_x(x), 
      m_y(y), 
      m_name(name), 
      m_price(price)
{
    // ...
}

 

아래처럼 그 내용을 static으로 뺄 수 있습니다.

BuyTransaction(int x, int y, const std::string& name, double price) 
    : Transaction(createLogString(x, y, name, price)),
      m_x(x), 
      m_y(y), 
      m_name(name), 
      m_price(price)
{
    // ...
}

private:
    static std::string createLogString(int x, int y, const std::string& name, double price) {
        return std::to_string(x) + "-" + std::to_string(y) + "-" + name + "-" + std::to_string(price);
    }

 

이것만은 잊지 말자!

  • 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요 가상함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않습니다.

 

상속 관계에서의 생성자, 소멸자의 호출 순서에 대한 이해가 있다면 빠르게 이해하고 넘어갈 수 있는 파트였다.


항목 10

https://yeoul0714.tistory.com/38

 

[Effective C++] 항목 10: 대입 연산자는 *this의 참조자를 반환하게 하자

1. 개요C++의 대입 연산자는 여러개가 동시에 엮일 수 있습니다. 무슨말인가 하면 우리는 일반적으로 대입연산자를 a=3; 이런식으로 씁니다. 그러나 a=b=c=3; 이런식으로 쓰는것도 가능하다는 말입

yeoul0714.tistory.com