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

수민이슈·2023년 3월 16일
0

Effective C++

목록 보기
7/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 한다!
💡 가상 함수를 하나 이상 가지고 있는 클래스의 소멸자는 가상 소멸자여야 한다!!
💡 기본 클래스로 설계되지 않았거나, 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언해서는 안된다!


🖊️ 왜 가상소멸자를 붙여야 하나요?

팩토리 함수 : 새로 생성된 파생 클래스 객체에 대한 기본 클래스의 포인터를 반환하는 함수

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

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

TimeKeeper* getTimeKeeper();
// TimeKeeper에서 파생된 클래스를 통해 동적으로 할당된 객체의 포인터를 반환

TimeKeeper* ptk = getTimeKeeper();

...

delete ptk;

이렇게 되면 getTimeKeeper 함수에서 반환되는 객체는 동적할당된 객체이므로 에 있음
그러면 결국 메모리 누수를 막기 위해 이 객체를 적절히 delete 해야 한다.


문제가 있음

  1. getTimeKeeper 함수가 반환하는 포인터(ptk)파생클래스 객체(AtomicClock)에 대한 포인터
  2. 이 포인터가 가리키는 객체 (AtomicClock)가 삭제될 때 기본 클래스 포인터(TimeKeeper* 포인터)를 통해 삭제
  3. 기본 클래스(TimeKeeper)의 소멸자가 비가상 소멸자임

이게 왜 문젠데?

기본 클래스 포인터에 의해 파생 클래스 객체가 삭제될 때,
기본 클래스에 비가상 소멸자가 들어있으면 객체의 파생 클래스 부분이 소멸되지 않게 된다.

그니까,
getTimeKeeper 함수에서 포인터를 통해 반환받은 AtomicClock 객체는, TimeKeeper*를 통해 삭제될 때 AtomicClock에서 정의된 데이터 멤버들이 삭제되지 못하고, 소멸자도 실행되지 못함
근데 기본 클래스(TimeClock)는 소멸자가 호출되고 잘 소멸됨
그래서 반쪽만 .. 사라짐


해결 방법은?

❤️ 사랑하는 기본 클래스에게 가상 소멸자를 선물하세요 ❤️

책에 이렇게 적혀있음 진짜 개웃기다 ㅠㅠ

class TimeKeeper {
public:
	TimeKeeper();
    virtual ~TimeKeeper();			// 기본 클래스의 소멸자를 가상 소멸자로!
    ...
};

TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;

가상함수를 하나라도 가진 클래스는 가상 소멸자를 가져야 한다.


🖊️ 그렇다고 모든 클래스의 소멸자를 가상소멸자로 하면 안된다.

가상 함수의 동작 구조

가상함수를 구현하려면, 가 필요하다.

가상함수 테이블 포인터 (vptr)

: 프로그램 실행 중에 주어진 객체에 대해 어떤 가상함수를 호출해야 하는지 결정하는데 쓰이는 포인터 형태의 자료구조

가상함수 테이블 (vtbl)

: 가상함수들의 주소(포인터)를 담은 배열
가상함수를 하나라도 가지고 있는 클래스는 그와 관련된 vtbl을 갖는다.

vtpr는 vtbl을 가리킨다.

동작 원리

어떤 객체에 대해 어떤 가상함수가 호출되려고 하면,
호출되는 함수는 그 객체의 vptr가 가리키는 vtbl에 따라 결정된다.
(vtbl에 있는 함수 포인터들 중 적절한 것이 연결됨)

그래서 왜 싹 다 가상 소멸자로 하면 안되는건데?

클래스에 가상함수가 들어가면 클래스 객체의 크기가 커진다.

그리고 예시를 하나 들자면,

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

SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss;			// SpecialString*가 string*로 변환됨
...
delete ps;

std::string, 이를 비롯한 STL 기본 컨테이너 (vector, list 등)은 비가상 소멸자를 가진다.

위 코드에서도 string은 비가상 소멸자를 지원하기 때문에,
delete ps에서 나락간다
SpecialString의 소멸자가 호출되지 않는다.

가상함수가 하나라도 들어있는 경우에만 가상 소멸자로 선언하자.


🖊️ 순수 가상 소멸자

순수 가상 함수는 해당 클래스를 추상클래스로 만든다.

추상클래스
: 그 클래스 자체로는 인스턴스를 만들수 없는 (객체를 생성할 수 없는) 클래스
주로 기본 클래스로 사용할 목적으로 만든다.
가상 소멸자 사용!

  1. 추상 클래스는 주로 기본 클래스로 쓰인다.
  2. 기본 클래스는 가상 소멸자를 가져야 한다.
  3. 순수 가상함수가 있으면 추상 클래스가 된다.

따라서

추상클래스를 만들고 싶으면 순수 가상 소멸자를 선언하자!

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

AWOV::~AWOV() {}

하지만, 순수 가상 소멸자는 정의를 꼭 해줘야 한다.


🖊️ 정리

소멸자의 동작 순서

상속 계통 구조에서 가장 말단 파생 클래스의 소멸자부터 기본 클래스의 소멸자까지 차례로 호출됨

다형성을 가진 기본 클래스만 Virtual!

왜냐면
기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 것만.
모든 기본 클래스가 다형성을 갖도록 설계된 것은 아님.
-> Uncopyable을 봐봐


😊 소감
가상함수가 필수적이라고 하구 이 부분도 친구가 설명해줬어서 계속 기억에 남았는데
소멸자에는 무조건 virtual을 선언해줘야 하는 줄 알았따.. 큰일날뻔
개인적이지만,, 이번 장 너무 문장들이 웃겨서 재밌게 읽었당 ㅋㅋㅋ

0개의 댓글