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

[Effective C++] 항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

yeoul0714 2025. 4. 18. 02:25

1. 부모 클래스 소멸자를 virtual로 해야하는 이유


우리는 기본 클래스의 소멸자는 반드시 virtual로 선언해주어야 합니다.

 

아래 예시를 통해서 그 이유를 알아보도록 합시다.

// Effective C++ 항목 7: 다형성을 위한 기초 클래스에서는 소멸자를 virtual로 선언하자
#include <iostream>

class GameEntity {
public:
    // 문제: 가상 소멸자가 없음
    ~GameEntity() { std::cout << "GameEntity 소멸자\n"; }
    
    // 대신 이렇게 선언해야 함:
    // virtual ~GameEntity() { std::cout << "GameEntity 소멸자\n"; }
};

class PlayerCharacter : public GameEntity {
public:
    ~PlayerCharacter() { 
        std::cout << "Yeoul의 캐릭터 데이터 저장 중...\n"; 
    }
};

class Monster : public GameEntity {
public:
    ~Monster() { 
        std::cout << "몬스터 리소스 정리 중...\n"; 
    }
};

class GameItem : public GameEntity {
public:
    ~GameItem() { 
        std::cout << "아이템 메모리 해제 중...\n"; 
    }
};

GameEntity* createGameEntity() {
    return new PlayerCharacter;  // 팩토리 함수
}

int main() {
    GameEntity* ge = createGameEntity();
    
    delete ge;  // 문제 발생: GameEntity의 소멸자만 호출됨
                // PlayerCharacter의 소멸자는 호출되지 않음!
   
    return 0;
}

 

createGameEntity()에서 반환되는 값들은 전부 heap에 있게 됨으로 결국엔 delete를 통해 메모리를 해제해 주어야 합니다.

 

그러나 이렇게 되면 PlayerCharacter의 소멸자는 호출되지 않게 됩니다.

 

그이유는

 

1. createGameEntity가 반환하는 포인터가 파생 클래스의 포인터이며

 

2. 객체가 소멸될때 기본 클래스의 포인터로 삭제된다는점 

 

3. 결정적으로 기본 클래스의 소멸자가 non-virtual destructor이기 때문입니다.

 

기본 클래스의 포인터로 파생 클래스 객체가 삭제될 때 기본 클래에 non virtual destructor가 있으면 프로그램의 동작은

 

예측 불가합니다.

 

기본클래스의 포인터로 delete를 실행하면 파생 클래스 부분이 소멸되지 않습니다.

 

애초에 파생 클래스의 소멸자가 실행되지 않습니다.

 

그런데 기본 클래스의 소멸자는 실행되어서 partially destroyed 객체의 상태가 되어버립니다. (반쪽짜리 소멸)

 


2. 해결방안


매우 간단합니다.

 

그냥 기본 클래스의 소멸자에 virtual을 붙히면 됩니다.

class GameEntity {
public:
     virtual ~GameEntity() { std::cout << "GameEntity 소멸자\n"; }
};

 

만약 소멸자에 virtual이 없는 클래스를 보게 된다면 이렇게 생각하면 됩니다.

 

"저 클래스는 상속해줄 의도가 전혀 없는 클래스구나."

 


3. 그렇다고 virtual을 남발하지는 말자


class Point {
public:
    Point(int x, int y);
    ~Point();
    
private:
    int x, y;
};

 

이 클래스는 int가 32비트라고 하면 64비트짜리 클래스입니다.

 

64비트짜리 레지스터에도 딱 맞고 다른 언어로 작성된 함수에 넘길일이 생겨도 64비트크기의 자료형으로 넘어갑니다.

그러나!

소멸자에 virtual을 붙히게 되면 상황은 완전히 바뀌게 됩니다.

 

왜냐하면 가상함수를 구현하려면 새로운 자료구조가 필요하기 때문입니다.

 

어떤 가상함수를 호출해야 하는지를 결정하는 정보이고 포인터의 형태를 가지고 있습니다.

 

흔히 vptr(virtual table pointer) 가상 함수 테이블 포인터라고 불립니다.

 

포인터들의 배열을 가리키고 있고 그 가상함수 포인터들의 배열은 vtbl(virtual table) 가상함수 테이블이라고 합니다.

 

가상함수를 하나라도 가진 클래스는 모두 vtbl을 가지고 있고 가상함수가 호출 되려고 하면

 

호출되는 함수는 객체의 vptr이 가리키는 vtbl에 따라서 호출하게 됩니다.

 

다시 말하자면 vtbl에 있는 함수주소들을 보고 적절한 것을 호출하는 것입니다.

 

중요한 점은 Point Class의 소멸자에 virtual을 붙히게 되면 클래스 크기가 커지게 된다는 점입니다.

 

32비트 운영체제라면 64비트에서 96비트로 커지고

 

64비트 운영체제라면 64비트에서 128비트로 커지게 됩니다.

 

한마디로 vptr크기만큼 커집니다. (32비트일경우 pointer크기 32비트, 64비트일경우 64비트)

 

정말로 2배 커졌습니다. (제 pc는 64비트입니다.)

 

또한 C언어등 다른 언어와의 호환성도 없어집니다.

 

똑같은 데이터 배치를 사용해도 vptr은 어쩔수없거든요.

 

결론은 상속해줄거 아니면 virtual은 함부로 써서는 안된다는 것입니다.


 

 

3. 기본 클래스를 상속 받아서 생기는 문제


아래 코드처럼 string 클래스를 상속받아서 나만의 특별한 string클래스를 만들고 싶다고 가정해봅시다.

class SpecialString: public std::string
{

}

 

이렇게 하면 아주 큰 문제가 생기게 됩니다.

 

그 문제는 우리가 지금까지 계속 언급하던 문제인데 string클래스의 소멸자는 virtual로 되어있지 않다는 것입니다.

 

만약 SpecialString을 만들고 이 객체의 포인터를 string클래스를 이용해서 해제해주게 되면 문제가 생기게 됩니다.

 

이 현상은 virtual destructor가 없는 모든 기본 클래스에 적용됨으로 주의해야합니다.

 

예를 들면 vector, list, set, unordered_map등이 있습니다.

 

이것들을 상속받아서 나만의 클래스를 만들려는 생각은 접는 것이 좋습니다.

 


 

4. 순수 가상 소멸자(pure virtual destructor)를 이용한 방법


우리는 순수 가상 소멸자를 이용해서 이러한 문제에 대응할 수 있습니다.

 

순수 가상함수를 가진 클래스 즉 추상 클래스는 그 자체로는 객를 만들 수 없습니다.

 

그래서 사실상 기본클래스로 쓰일 목적으로 만들어진 함수이지요.

 

그런데 추상 클래스를 만들고 싶은데 마땅히 쓸 순수 가상함수가 없을 경우도 생기게 됩니다.

 

그런데 말했듯이 추상 클래스는 기본클래스가 될것을 상정하고 만들어진 클래스 입니다.

 

그래서 마땅히 쓸 순수 가상함수가 없을 때 소멸자를 순수 가상함수로 만들면 됩니다.

 

class AWOV   // AWOV = "Abstract w/o Virtuals"
{ 
public:
    virtual ~AWOV() =0;
};

 

그런데 주의할 점은 이 순수 가상 소멸자의 정의를 두지 않으며 안됩니다.

 

AWOV::~AWOV() {}

 

이렇게 정의 해주어야 합니다.

 

왜냐하면 

 

소멸자의 동작 순서는 상속 계통에서 가장 말단의 클래스의 소멸자를 시작으로 하나씩 상위 클래스로 올라가며

 

소멸자가 호출됩니다. 

 

그런데 그렇게 타고 올라가게 되면 최상위 클래스인 AWOV에 왔을때 소멸자가 정의되어 있지 않으면

 

링커오류가 발생하게 됩니다. 

 

그러므로 정의는 꼭 필요한 작업입니다.

 

마지막으로 기본 클래스의 소멸자에 virtual을 써주어야 하는 경우는 다형성을 가진 기본 클래스만 가능합니다.

 

즉 기본 클래스 인터페이스로 파생 클래스 타입을 조작을 허용하는 기본클래스만 적용된다는 의미입니다.

 

위에서 설명한 것 처럼 부모 클래스로 자식 클래스의 객체를 관리하는 경우에 해당합니다.

 

물론 모든 클래스가 다형성을 가지도록 설계되지는 않았습니다.

 

항목 6에서 만들었던 Uncopyable클래스가 하나의 예시입니다.

 

이런 클래스에는 virtual을 붙히면 안됩니다.

 

이것만은 잊지 말자!

  • 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.
  • 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.

항목8

https://yeoul0714.tistory.com/35

 

[Effective C++] 항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

1. 여러개의 예외는 처리가 곤란하다.만약 이러한 클래스가 있고 벡터에서 10개의 Yeoul객체를 가지고 있다고 생각해보자.class Yeoul{public: ~Yeoul(){}};void DoSomething(){ std::vectorv;} 벡터 v가 소멸될때에

yeoul0714.tistory.com