- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마라. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로 내려가지 않는다.
객체 생성 및 소멸 중 가상 함수 금지
객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하면 안된다.
class Transaction { // 기본 클래스
public:
Transaction();
virtual void logTransaction() const = 0; // 순수 가상함수(로깅)
...
};
Transaction::Transaction() // 기본 클래스 생성자
{
...
logTransaction();
}
class BuyTransaction: public Transaction { // Transaction의 파생 클래스
public:
virtual void logTransaction() const;
...
};
class SellTransaction: public Transaction { // Transaction의 파생 클래스
public:
virtual void logTransaction() const;
...
};
주식 거래를 본뜬 클래스 구조를 설계했다고 가정해 보자.
BuyTansaction b;
이렇게 매수 주문을 생성하면 BuyTransation 생성자가 호출되기 전에 Transation의 생성자가 호출된다.
파생 클래스 객체가 생성될 때는 그 객체의 기본 클래스 부분이 먼저 호출되기 때문이다.
Transaction 생성자의 마지막 부분에 가상 함수인 logTransaction을 호출하는 문장이 보인다.
여기서 호출되는 logTransaction 함수는 Transaction의 것이다.
BuyTransaction을 생성하였는데 기본 클래스인 Transaction의 함수가 실행이 돼버린다.
기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대로 파생클래스 쪽으로 내려가지 않는다.
이때, 기본 클래스 생성자에서 호출된 가상 함수가 파생 클래스 쪽으로 내려가지 않는 이유는 다음과 같다.
기본 클래스 생성자가 파생 클래스 생성자보다 앞서 실행되기 때문에 기본 클래스 생성자가 실행되고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니다.
파생 클래스 버전의 가상 함수는 파생 클래스만의 데이터 멤버를 건드릴 것인데 객체의 초기화되지 않은 영역을 건드리는 것은 치명적인 위험을 내포하기 때문에 C++에서 막아 놓았다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은 그 객체의 타입은 기본 클래스이다.
호출되는 가상 함수는 모두 기본 클래스의 것으로 결정될 뿐만 아니라, 런타임 타입 정보를 사용하는 언어 요소를 사용한다고 해도 모두 기본 클래스 타입으로 취급한다. (타입 정보 사용 언어 요소: dynamic_cast 등)
객체가 소멸될 때도 똑같다.
파생 클래스의 소멸자가 일단 호출되고 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정하기 때문에 이들을 없는 것처럼 취급한다. 기본 클래스 소멸자에 진입할 당시의 객체는 기본 클래스의 객체가 된다.
생성자 혹은 소멸자 안에서 가상 함수가 호출되는지를 잡아내는 일은 항상 쉬운 것이 아니다.
만약 생성자가 여러 개라고 가정해 보자. 그래서 공동의 초기화 코드로 만들어 관리한다고 했을 때 다음과 같은 상황이 발생한다.
class Transation {
public:
Transaction() { init(); } // 비가상 멤버 함수 호출
virtual void logTransaction() const = 0;
...
private:
void init() { logTransaction(); } // 비가상 함수에서 가상 함수 호출
};
이 코드는 문제없이 컴파일이 된다. 생성자에서 비가상 함수를 호출하기 때문이다.
그렇다면 이러한 문제를 해결하기 위해서는 logTransaction을 Transaction 클래스의 비가상 멤버 함수로 바꾸면 된다.
그리고 파생 클래스의 생성자들이 필요한 로그 정보를 Transaction의 생성자로 넘기면 된다.
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 );
};
기본 클래스 부분이 생성될 때는 가상 함수를 호출한다 해도 기본 클래스로 취급하기 때문에 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 올려주도록 만듦으로써 부족한 부분을 채울 수 있는 것이다.
BuyTransaction 클래스에서 선언된 createLogString이라는 정적 함수를 사용하는데 이 함수는 기본 클래스 생성자 쪽으로 넘길 값을 생성하는 용도로 쓰이는 함수이다. 정적 멤버로 되어 있기 때문에, 생성이 끝나지 않은 BuyTransaction 객체의 미초기화된 데이터 멤버를 실수로 건드릴 위험도 없다.
결과적으로 미초기화된 데이터 멤버는 정의되지 않은 상태에 있다는 사실 때문에 기본 클래스 부분의 생성과 소멸이 진행되는 동안에 호출되는 가상 함수가 파생 클래스 쪽으로 내려가지 않는 것이다.