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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 정리하였습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 2. 생성자, 소멸자 및 대입 연산자

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

1. 가상 소멸자

class TimeKeeper {
public:
	TimeKeeper() { ... };
	~TimeKeeper() { ... };
};

// TimeKeeper에서 파생된 클래스를 통해,
// 동적 할당 객체의 포인터를 반환하는 함수
TimeKeeper* getTimeKeeper() { ... };

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

// 팩토리 메서드 패턴
TimeKeeper* ptk = getTimeKeeper();
delete ptk;

이렇게 코드를 짠다면 TimeKeeper의 소멸자는 호출되지만, 파생된 클래스(e.g. AtomicClock)의 소멸자는 호출되지 않는다.
TimeKeeper의 소멸자가 비가상 소멸자로 구현되었기 때문에, 파생된 클래스는 올바르게 소멸하지 않는 것이다.
결국 컴퓨터의 자원이 줄줄 새는 경험을 하게 된다.

class TimeKeeper {
public:
	...
	virtual ~TimeKeeper() { ... };
};
...

이를 해결하기 위해선 위와 같이, 소멸자를 가상으로 만들어주면 된다.

1.1. 가상 소멸자 사용시 주의점

class Point {
public:
	Point(int xCoord, int yCoord);
	~Point();

private:
	int x, y;
};

int가 32비트를 차지한다고 가정하자.
그렇다면 이 Point 객체는 32*2인 64비트 레지스터에 들어갈 수 있다.

그런데, Point 클래스의 소멸자가 가상 소멸자로 만들어지면 상황이 달라진다.

가상 함수를 C++에서 구현하기 위해선 별도의 자료구조가 필요하다.
이 자료구조는 프로그램 실행 중 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지 결정하는데 쓰이는 정보이다.

  • vptr(가상함수 테이블 포인터): 가상함수의 주소, 즉 포인터들의 배열을 가리키고 있다.
  • vtbl(가상함수 테이블): 가상 함수 테이블 포인터들의 배열

그러므로, Point 클래스에 가상 소멸자가 있다면, vptr이 필요하므로 객체의 크기가 다음과 같이 될 것이다.

\[ Point\ 객체의\ 크기 = 32*2 + vptr의\ 크기 \]

이때 vptr의 크기는 아키텍처 환경에 따라 달라진다.
만약 프로그램의 실행환경이 32비트 아키텍처라면 96비트, 64비트 아키텍처라면 128비트가 될 것이다.

이렇게 된다면 생기는 문제가 다음과 같다.
C등의 다른 언어로 선언된 동일한 자료구조와 호환성이 사라진다.
즉, 이식성이 떨어진다.

그러므로, 소멸자를 전부 virtual로 선언하는 것은 좋지 않은 생각이다.

예외적으로 가상 소멸자가 전혀 없지만, 비가상 소멸자로 인해 문제가 생기는 경우가 있다.

그 예로는 string이 있으며, 아래와 같은 상황이 있다.

1.2. 가상 소멸자를 사용하는 경우

기본 클래스에 가상 소멸자를 쥐어주자는 규칙은 다형성(polymorphism)을 가진 기본 클래스에서 사용하여야 한다.
즉, 기본 클래스 인터페이스를 통해 파생 클래스 타입을 조작하도록 설계된 기본 클래스에만 적용된다.

SpecialString* pss = new SpecialString("Impending Doom");

string* ps
...
ps = pss;
...
// SpecialString의 소멸자 호출 안됨!
// SpecialString의 자원이 누출됨!
delete ps;

이러한 경우는 STL 컨테이너 타입(vector, set, map, list)을 상속받을 때 생긴다.
비가상 소멸자가 있는 표준 컨테이너를 활용하여 커스텀 클래스를 만들려고 하지 말도록 하자.

1.3 순수 가상 소멸자

class AWOV {
public:
  virtual ~AWOV() = 0;
  ...
}

AWOV::~AWOV() {}

경우에 따라서는 순수 가상 소멸자가 편리하게 쓰일때도 있다.

순수 가상 함수는 클래스를 추상(abstract) 클래스로 만든다.

이러한 클래스들은 그 자체로 인스턴스를 생성하지 못한다.
그러므로, 해당 추상 클래스를 상속받아서 사용하여야 한다.

1.4. 소멸자 호출 순서

소멸자가 동작하는 순서는 다음과 같다.
상속 계통 구조에서 가장 밑단의 소멸자부터 기본 클래스의 소멸자쪽으로 거슬러 올라가며 호출된다.

🧐 정리

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

cpp 카테고리 내 다른 글 보러가기

댓글남기기