상세 컨텐츠

본문 제목

항목 7) 가상 소멸자에 대해서

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 12:05

본문

다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자.

 

시간 기록을 유지하는 TimeKeeper 라는 클래스를 예시로 설명하겠다.

해당 클래스를 상속 받는 여러 시계 class 들이 시간 기록에 대한 접근을 하고 싶을 때,

어떤 TimeKeeper 객체에 대한 포인터를 얻기 위한 용도로 Factory Function 를 활용한다고 생각해보자.

더보기

팩토리 메소드란?

 

파생 클래스를 가리키는 기본 클래스 타입의 포인터를 반환하는 함수.

( 자식 클래스를 가리키는 부모 클래스 타입의 포인터를 반환하는 함수 )

// 자식을 가리키는 부모 포인터
Parent *p = new Child();

코드로 표현하자면 아래와 같을 것이다.

// 팩토리 함수 사용 예시
class TimeKeeper {
protected:
	TimeKeeper* getTimeKeeper(); // 팩토리 함수
public:
	TimeKeeper() { cout << "TimeKeeper 생성자 호출\n"; }
	~TimeKeeper() { cout << "TimeKeeper 소멸자 호출\n"; }
};
 
class AtomicClock : public TimeKeeper { /*...생략 */ };
class WaterClock : public TimeKeeper { /*...생략 */ };
class WristClock : public TimeKeeper { /*...생략 */ };

예시는 팩토리 함수로 설명했지만,

굳이 해당 상황이 아니더라도 부모 타입이면서 자식 객체를 가리키는 상황이라면 상관없다.

// 좀 더 일반적인 상황
using namespace std;
 
class Parent {
public:
	Parent() { cout << "Parent 생성자 호출\n"; }
	~Parent() { cout << "Parent 소멸자 호출\n"; }
};
 
class Child : public Parent {
public:
	Child() { cout << "Child 생성자 호출\n"; }
	~Child() { cout << "Child 소멸자 호출\n"; }
};
 
int main()
{
	Parent *parent = new Child();
	
	delete parent;
 
	return 0;
}

위 코드의 실행 결과는 어떻게 나올까?

보다시피 Child 소멸자가 호출되지도 않은 채로 프로그램이 종료된다.

그리고 그 원인은 부모 클래스의 소멸자가 가상화가 되지 않았다는 것이 가장 크다.

C++ 규정 자체에 명시되어있기로는,

기본 클래스 포인터를 통해 파생 클래스 객체가 삭제 될 때 그 기본 클래스에 비가상 소멸자가 들어있으면 프로그램은 미정의 동작을 한다.

생각해보면 당연한 말이다.

파생 클래스 객체를 선언하고 삭제시킬 때, 생성자와 소멸자의 호출 순서에 대해서 생각해보자.

int main()
{
	// Parent *parent = new Child();
	Child *child = new Child;
 
	delete child;
 
	return 0;
}

자식 객체만 생성하더라도,
부모 생성자가 먼저 호출 되고 → 자식 생성자가 호출 된다.

그리고 소멸의 과정은 반대로,
자식 소멸자가 호출 되고 → 부모 소멸자가 호출 된다.

 

이러한 방식이 정상적 순서인데,
예시의 자식을 가리키는 부모 타입의 포인터 (Upcasting) 를 생성하고 삭제하는 행위는 자식 소멸자를 호출하기 전에 부모 소멸자를 호출한다.

그리고 이러한 문제는 virtual 키워드를 통해서 해결이 가능하다.

// virtual 소멸자
using namespace std;
 
class Parent {
public:
	Parent() { cout << "Parent 생성자 호출\n"; }
	virtual ~Parent() { cout << "Parent 소멸자 호출\n"; }
};
 
class Child : public Parent {
public:
	Child() { cout << "Child 생성자 호출\n"; }
	~Child() { cout << "Child 소멸자 호출\n"; }
};
 
int main()
{
	Parent *parent = new Child();
	delete parent;
 
	return 0;
}

이러한 현상을 방지하기 위해서,

가상 함수를 하나라도 가지고 있는 모든 기본 클래스는 가상 소멸자를 가지는 것이 바람직하다.

더 말하자면, 다형성을 가지도록 설계된 부모 클래스는 모두 가상 소멸자를 가지는 것이 맞다.

"그렇다면 차라리 모든 클래스의 소멸자를 가상 소멸자로 만들어 버리면 되지 않나?"

→ 아니다, 가상 소멸자를 만드는 행위는 결코 공짜가 아니다.

→ 때문에 부모 클래스로서 설계되지 않은 클래스에 대해서 가상 소멸자를 선언하는 것은 어리석은 행위라고 할 수 있다.

아래의 예시를 보자.

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

C++에서 클래스의 멤버 중 어떤 하나라도 가상화 키워드가 붙어 있다면,

해당 객체를 생성함과 동시에 해당 객체에 해당하는 vtbl (virtual table) 및 해당 테이블 가리키는 vptr (virtual table pointer) 역시 생성된다.

( vptr 은 동적 바인딩을 통해서 가상 함수가 실행되는 시점에 실제로 어떤 함수가 실행 되는지를 가리키는 포인터를 모아둔 테이블(vtbl)에 대한 포인터 )

즉, 실제로 사용되지 않는 가상 함수일지라도, 선언함과 동시에 무조건 vptr이 객체 생성에 추가된다는 의미이다.

32비트 실행 환경이라면 4바이트가 늘어나는 것이고, 64비트 실행 환경이라면 8바이트가 늘어나는 것이다.

참고로 순수 가상 함수를 선언하게 되면, 인스턴스 생성이 불가능한 추상 클래스가 된다.

이러한 점을 이용해 순수 가상 소멸자를 통해서 추상 클래스를 만드는 것도 가능하다.

( 추상 클래스를 만든다는 목적 자체가 이미 다형성을 활용하는 기본 클래스를 설계했다는 의미이기 때문에, 가상 소멸자를 선언해도 무방하다. )

 

Things to Remember

  • 다형성을 가지도록 설계한 기본 클래스는 반드시 가상 소멸자를 선언해야 한다. 즉, 가상 함수를 하나라도 가지고 있다면 반드시 가상 소멸자를 선언하라는 의미이다.
  • 반대로 다형성에 대한 의도가 없는 클래스에 대해서는 가상 소멸자에 대한 선언이 무의미한 낭비라는 점을 기억해야 한다.

관련글 더보기

댓글 영역