본문 바로가기
프로그래밍/C\C++

[Effective C++ 정리 #7] 이걸 안 하면 메모리 누수! 소멸자는 왜 virtual이어야 할까?

by 허구의 2025. 6. 17.
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

[Effective C++ 정리 #7] 다형성(polymorphism)을 위한 기초, 소멸자는 반드시 virtual로 선언하라

C++에서 상속을 활용해 다양한 객체를 다루는 경우, 의외로 간과하기 쉬운 치명적인 문제가 있습니다. 바로 기반 클래스의 소멸자를 virtual로 선언하지 않은 경우, 파생 클래스 객체가 완전히 파괴되지 않고 자원 누수로 이어지는 문제입니다.

이번 아이템에서는 "왜 가상 소멸자(virtual destructor)가 필요한지", "언제 써야 하고 언제 쓰지 말아야 하는지"에 대해 실제 예제와 함께 자세히 설명합니다.


문제 상황: 기본 클래스의 소멸자가 virtual이 아닐 때

여러 시간 측정 장치를 관리하기 위한 추상 클래스 TimeKeeper와 이를 상속한 파생 클래스들을 생각해봅시다.

class TimeKeeper {
public:
    TimeKeeper();
    ~TimeKeeper();  // ❌ virtual 아님
};

class AtomicClock : public TimeKeeper { ... };
class WaterClock  : public TimeKeeper { ... };
class WristWatch  : public TimeKeeper { ... };

 

사용자는 다음과 같이 팩토리 함수를 통해 시간 측정기를 받아 사용하고, 끝나면 delete 합니다.

TimeKeeper* getTimeKeeper(); // 동적 생성된 파생 클래스 반환

TimeKeeper* ptk = getTimeKeeper();
...
delete ptk; // 문제 발생 가능

 

여기서 문제가 생깁니다. delete는 기본 클래스의 소멸자를 호출하고 끝나버립니다. 파생 클래스의 소멸자는 호출되지 않기 때문에, 파생 클래스가 할당한 자원은 전혀 해제되지 않습니다. 이는 리소스 누수와 데이터 손상으로 이어질 수 있습니다.


해결책: virtual 소멸자 선언

문제는 단순히 해결됩니다. 기본 클래스의 소멸자를 virtual로 선언하면 됩니다.

class TimeKeeper {
public:
    TimeKeeper();
    virtual ~TimeKeeper();  // ✅ 반드시 virtual
};

 

이렇게 하면 delete ptk와 같이 기본 클래스 포인터를 통해 삭제할 때도, 자동으로 파생 클래스의 소멸자가 먼저 호출되고, 이후 기본 클래스 소멸자가 호출됩니다.

따라서, 다형성을 사용하는 기반 클래스에는 반드시 virtual 소멸자가 있어야 합니다!!


소멸자만 virtual이면 될까? 가상 함수가 있으면 무조건 virtual 소멸자!

만약 TimeKeeper가 다양한 시계 유형(원자 시계, 손목 시계 등)을 통일된 방식으로 다루기 위해, 다음과 같은 가상 함수를 제공한다고 해봅시다:

class TimeKeeper {
public:
    virtual ~TimeKeeper();           // ✅ virtual 소멸자
    virtual std::time_t getCurrentTime() const = 0; // 다형적 인터페이스
};

 

이처럼 getCurrentTime() 같이 파생 클래스에서 오버라이딩되는 가상 함수가 있다면, TimeKeeper는 명백히 다형성을 의도한 클래스이며, 따라서 소멸자도 반드시 virtual로 선언되어야 합니다.

 

요약하면 다음과 같습니다:

  • 클래스에 가상 함수가 있다 → virtual 소멸자도 있어야 한다.
  • 가상 함수가 없다면? → 소멸자를 virtual로 만들 필요 없음.

주의: 불필요한 virtual 소멸자 선언은 오히려 성능 저하

예를 들어 다음과 같은 간단한 클래스 Point를 보겠습니다:

class Point {
public:
    Point(int xCoord, int yCoord);
    ~Point();  // 굳이 virtual 필요 없음

private:
    int x, y;
};

 

이 클래스는 파생될 가능성도 낮고, 다형적으로 사용할 이유도 없습니다. 여기서 소멸자를 virtual로 선언하면 객체에 vptr이라는 가상 함수 테이블 포인터가 추가되어, 객체 크기가 늘어나고 C와의 호환성도 깨질 수 있습니다.

즉, virtual 소멸자는 필요할 때만 사용해야 하며, 모든 클래스에 무작정 붙이는 것은 나쁜 습관입니다.


추상 클래스를 만들고 싶다면? → 순수 가상 소멸자

때로는 클래스에 가상 함수는 필요 없지만, 인스턴스를 만들지 못하도록 추상 클래스로 만들고 싶은 경우가 있습니다.

이럴 땐 순수 가상 소멸자를 선언하면 됩니다.

class AWOV { // Abstract Without Virtuals
public:
    virtual ~AWOV() = 0;  // 순수 가상 소멸자
};

AWOV::~AWOV() {} // 반드시 정의도 필요

 

이 방법은 다음 세 가지 목적을 동시에 달성합니다:

  1. 인스턴스를 생성할 수 없는 추상 클래스
  2. 다형성 지원
  3. 가상 소멸자 문제 해결

경고! STL 컨테이너는 가상 소멸자가 없다

std::string, std::vector 등 대부분의 표준 라이브러리 타입은 virtual 소멸자를 갖고 있지 않습니다. 그런데 이들을 상속받아 사용하는 것은 매우 위험합니다.

class SpecialString : public std::string { ... };

SpecialString* pss = new SpecialString("Oops");
std::string* ps = pss;
delete ps;  // ❌ undefined behavior!

 

이 경우 SpecialString의 소멸자는 호출되지 않고, 그로 인해 동적으로 할당된 자원이 해제되지 않을 수 있습니다. 결과는 예측할 수 없는 동작(= undefined behavior)입니다.

 

C++은 Java나 C#처럼 final, sealed 같은 상속 금지 기능이 없어 주의가 더 필요합니다.


핵심 요약

  • 다형적으로 사용할 클래스는 반드시 virtual 소멸자를 선언하라.
  • 클래스에 virtual 함수가 있으면 소멸자도 virtual이 되어야 한다.
  • 베이스 클래스나 다형성으로 사용하도록 설계되지 않은 클래스에 virtual 소멸자를 사용하면 성능과 구조에 악영향을 줄 수 있다.

감사합니다.

728x90