이 글은 『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";
}
};
파생 클래스가 필요한 정보를 생성자 인자로 넘겨주고, 기초 클래스는 **비가상 함수**로 이를 처리하게 만듦으로써 안전하게 초기화와 동작 분리할 수 있습니다.
소멸자도 마찬가지
소멸자에서도 동일한 원칙이 적용됩니다.
파생 클래스의 소멸자가 먼저 호출되고 나면, 객체는 다시 **기초 클래스 형태로 축소**되며, 그 상태에서 가상 함수를 호출하면 **파생 클래스 멤버는 이미 파괴된 상태**일 수 있습니다.
이 때문에 소멸자 안에서 가상 함수 호출도 피해야 합니다.
핵심 요약
- 생성자와 소멸자에서는 가상 함수가 다형적으로 동작하지 않는다.
감사합니다.
'프로그래밍 > C\C++' 카테고리의 다른 글
| [Effective C++ 정리 #10] 대입 연산자의 return *this의 의미?! (4) | 2025.06.20 |
|---|---|
| C/C++ 참조자(reference) 내용 정리 (4) | 2025.06.19 |
| C/C++ 포인터(pointer) 내용 정리 (0) | 2025.06.19 |
| [Effective C++ 정리 #8] 소멸자에서 예외 처리 시 발생할 수 있는 문제 및 해결 방안 (2) | 2025.06.18 |
| [Effective C++ 정리 #7] 이걸 안 하면 메모리 누수! 소멸자는 왜 virtual이어야 할까? (4) | 2025.06.17 |