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

수민이슈·2023년 3월 17일
0

Effective C++

목록 보기
9/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

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


🖊️ 객체의 생성, 소멸 중에는 가상함수를 호출하지 마라

예시 상황

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, SellTransaction이 있다 치자.
객체가 생성될 때 logTransaction() 함수를 통해 로그를 남겨야 하는 상황이다.
그래서 Transaction의 생성자에 가상함수인 logTransaction을 넣었다.

기본 클래스 생성자가 호출되는 동안, 가상함수는 동작하지 않는다.

생성자

일단, BuyTransaction 클래스 b를 선언했기 때문에,
BuyTransaction 클래스의 생성자가 불려야 한다.
근데 BuyTransaction 클래스는 Transaction 클래스로부터 파생된 클래스이므로
Transaction의 생성자가 호출된 다음에 BuyTransaction 클래스의 생성자가 불린다.
생성자가 호출되어야 클래스 데이터 멤버들의 초기화가 진행되기 때문에,
Transaction의 생성자가 호출되는 동안에는 BuyTransaction 클래스는 초기화되어있지 않은 상태다.

Transaction의 생성자가 호출되는 동안에는 b의 타입은 Transaction이다.
기본 클래스의 생성자가 호출되는 동안 가상함수는 절대 파생 클래스에서 동작하지 않는다.
당연함.. 객체의 타입이 기본 클래스가 됨
그래서 호출되는 가상함수는 모두 기본 클래스의 것으로 결정되고
런타임 타입 정보 (dynamic_cast, typeid 등)도 다 기본 클래스 타입의 객체로 결정함.

이유는 위에서 말한 것처럼,,
기본 클래스의 생성자가 호출되는 동안 파생 클래스는 초기화되어 있지 않다.
근데 가상함수를 파생 클래스 쪽에서 호출하면, 당연히 가상 클래스의 데이터 멤버들을 건드릴거 아님?
너무 위험해서,.. 이런 일을 방지하는 거다.

따라서,
파생 클래스의 생성자 실행이 시작되어야 그 객체는 파생 클래스 객체가 된다.

소멸자

소멸자도 똑같다.
소멸자는 파생 클래스에서 먼저 호출되고, 기본 클래스가 소멸된다.
그래서 파생 클래스의 소멸자가 호출되고 나면, 파생 클래스의 데이터 멤버들은 정의되지 않은 값을 가진다.

그래서 또, 똑같이
기본 클래스의 소멸자에 진입할 당시의 객체는 기본 클래스 객체가 된다.

🔥 생성자, 소멸자 호출 순서
생성자 : 기본 클래스 -> 파생 클래스
소멸자 : 파생 클래스 -> 기본 클래스

그리고 또 문제

위 코드에서는 생성자에서 바로 가상함수를 호출해버렸다.

근데 Transaction 클래스에서 logTransaction() 함수가 순수 가상 함수로 선언되어 있다.
함수의 정의가 존재하지 않으면 링크 단계에서 에러가 발생한다.
(Transaction::logTransaction의 구현 코드를 찾아야 함)

이 상황에서 logTransaction은 순수 가상 함수이기 때문에,
순수 가상 함수가 호출될 때 프로그램을 바로 끝내버린다.

but

만약 logTransaction 함수가 순수 가상함수가 아닌 그냥 가상 함수이고,
Transaction의 멤버 버전이 구현되어 있을 경우에는,,,
Transaction 버전의 logTransaction이 호출될거다.

대박 문제 .. 박박 문제....

대처방법

logTransaction을 Transaction 클래스의 비가상 멤버 함수로 변경하자

파생 클래스의 생성자들에게, 필요한 로그 정보를 Transaction의 생성자로 넘긴다는 규칙을 만들자.
그러면 logTransaction은 가상함수가 아니기 때문에
Transaction의 생성자에서 안전하게 호출할 수 있다.

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

Transaction::Transaction(const string& logInfo) {
	...
    logTransaction(logInfo);			// 생성자에서 비가상 함수 호출
};

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

private:
	static string createLogString(parameters);
};

이렇게 되면,
기본 클래스 생성 부분에서 가상 함수를 호출해도 아~무 의미가 없기 때문에
필요한 초기화 정보파생 클래스 쪽에서 기본 클래스의 생성자로 올려주도록 하는거다.

createLogString 함수는 static으로 되어 있기 때문에,
생성이 끝나지 않은 BuyTransaction 객체의 초기화되지 않은 데이터 멤버를 건드릴 가능성이 없다.

휴~


😊 느낀점

아 재밌당 !!!!!!!!!!!!
문제는 없는데 안먹힌다는 걸로 이해했다.
마지막 해결 방법쪽에서
기본 클래스 생성자가 호출되고 파생클래스 생성자가 호출되는건데 머지? 했는데
아~ 호출되기 전에 불리는 거니까 기본 클래스 생성자 호출하면서 createLogString해서 로그정보를 주면 되겠구나~ 하고 이해했다
글고,, 책이 진짜 재밌다!

1개의 댓글

comment-user-thumbnail
2024년 4월 2일

대박 문제 .. 박박 문제.... 짱귀네영

답글 달기