다음은 주식 거래(매도 주문, 매수 주문, ...)를 본떠 만든 클래스들이다.
// 모든 거래에 대한 기본 클래스 (순수 가상 클래스)
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; // 매도 주문 거래의 거래내역 로깅 구현
..
};
아래의 코드가 실행되면 어떤 일이 발생할까?
BuyTransaction b;
TransAction
생성자가 호출된 뒤 BuyTransaction
생성자가 호출된다.
그런데 위의 코드를 보면, TransAction
생성자 본문 마지막에 logTransaction
을 호출한다.
아마 사용자가 의도한 것은 BuyTransaction
의 logTransAction
이겠지만, 실제로는 Transaction
의 logTransaction
가 호출될 것이다. 객체는 기본 클래스 생성 과정에서는 기본 클래스 타입인 것처럼 동작하기 때문이다. (그리고 logTransaction
의 정의가 존재하지 않으므로 링크 에러가 발생한다.)
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안, 객체 타입은 기본 클래스!
- 호출되는 가상함수는 모두 기본 클래스의 가상함수로 결정
- 런타임 타입 정보를 사용하는 언어 요소(
dynamic_cast
,typeid
, ...)를 사용하더라도 모두 기본 클래스 객체로 취급
즉, BuyTransaction
객체를 생성하기 위해 기본 클래스인 Transaction
의 생성자가 실행하는 동안은 객체 타입이 Transaction
이라는 것이다.
따라서 기본 클래스의 생성자가 호출되는 동안, 가상 함수는 절대 파생 클래스 쪽으로 내려가지 않는다.
이러한 일이 발생하게 되므로 C++은 사용자가 이런 실수조차 하지 못하도록 막은 것이다.
참고로 객체가 소멸될 때도 마찬가지이다.
파생 클래스의 소멸자가 호출되면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정한다.
따라서 C++은 파생 클래스만의 데이터 멤버들은 없는 것처럼 취급하고 진행한다.
파생 클래스 객체가 기본 클래스 소멸자에 진입할 당시, 객체 타입은 기본 클래스!
- 가상함수,
dynamic_cast
등 C++ 기능 모두 기본 클래스 객체의 자격으로 처리
하지만 생성자/소멸자 안에서 가상 함수가 호출되는지 확인하는 것은 쉽지 않다.
아마 대부분의 사용자들은 Transaction
의 생성자가 여러 개이고 각 생성자에서 하는 작업 중 공통 작업들은 하나의 초기화 코드로 만들어 둘 것이다.
만약 이러한 private
비가상 초기화 함수 내에서 가상 함수인 logTransaction
을 호출한다면??
class Transaction {
public:
Transaction() { init(); } // 비가상 멤버 함수 호출
virtual void logTransaction() const = 0;
...
private:
void init() { logTransaction(); } // 비가상 함수에서 가상 함수 호출
};
처음에 설명한 코드와 개념적으로는 동일하지만, 이전 코드와 달리 위의 코드는 컴파일에 이어 링크도 성공하여 프로그램이 정상적으로 작동할 것처럼 보인다.
그러나 이 코드는 더 큰 문제를 낳는다.
logTransaction
이 순수 가상함수abort
)logTransaction
이 일반 가상함수Transaction
의 logTransaction
을 호출. 즉, Transaction
의 멤버만 초기화되어 이후 프로그램 작동에 문제 발생logTransaction
을 Transaction
클래스의 비가상 멤버 함수로 변경Transaction
의 생성자로 넘김logTransaction
이 비가상 함수이므로 Transacton
의 생성자가 이 함수를 안전하게 호출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)) { ... } // 로그 정보를 Transaction 생성자로 넘김
private:
static std::string createLogString(parameters);
..
};
createLogString
static
이므로 생성이 끝나지 않은 BuyTransaction
객체의 미초기화 데이터 멤버를 건드릴 위험도 없음정리
객체의 생성 및 소멸 중에는 객체를 기본 클래스 타입으로 취급
-> 파생 클래스의 가상함수가 아닌 기본 클래스의 가상함수를 호출
-> 생성자/소멸자에서 가상 함수를 호출하지 말자!