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

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

FistKi 2021. 4. 24. 16:10

시간 기록을 유지하는 방법은 활용에 따라 무궁무진 하다. 그래서 TimeKeeper 라는 기본 클래스를 만들어 놓은 적절한 용도에 따라 이것을 상속하도록 설계하면 알맞을 같다.

 

 

 

TimeKeeper 쓰는 사용자들은 시간 정보에 접근하고 싶은데, TimeKeeper에서 시간 계산을 어떻게 하는지는 관심이 없고, 그냥 시간 정보에 접근만 하고 싶다. => 이럴 어떤 시간기록 객체에 대한 포인터를 반환하는 용도로 팩토리 함수(factory function, 새로 생성된 derived class 객체에 대한 base class 포인터를 반환하는 함수) 만들어 두면 좋을 같다.(getTimeKeeper())   [C++] 팩토리 함수 :: IT's me (tistory.com) 참고

 

팩토리 함수의 특징이 derived class new 만들고, new 반환된 derived class* => base class* type conversion 리턴한다.

 

반환되는 객체는 new 인해 힙영역에 있음 => 적절한 타이밍에 delete를 해줘야 한다.

 

 

근데 객체 삭제를 사용자가 하는건 이상하다.  그리고 사용자가 하면 에러 발생의 소지가 있고, 항목 18 보면 팩토리 함수의 인터페이스를 수정하면 흔히 발생하는 사용자 에러를 막을 수는 있지만, 어쨌든 코드

약점이 많음

 

문제는

  1. getTimeKeeper 반환하는 포인터가 base class* type conversion 하긴 하지만 derived class 가르키는
  2. new derived class 했는데, 힙에 있는건 dervied class인데  delete base class 소멸자를 통해 된다는
  3. base class(TimeKeeper) 들어 있는 소멸자가 비가상 소멸자라는

이다.

 

C++ 규정 : base class pointer 의해 derived class 객체가 삭제될 base class 비가상 소멸자가 들어 있으면 프로그램 동작은 미정의 사항이다.

 

보통 이럴 derived class 안에 있던 base class delete 되지만, 나머지 부분은 소멸하지 않는다. 근데 미정의 사항이기 때문에 모름

 

=> derived class 소멸자가 호출 자체가 안되는 것도 모자라, 애매하게 메모리 공간이 남아있어 자원이 줄줄 세고, 자료구조가 오염되고, 보안에도 취약함

 

문제를 해결하는 가장 간단한 방법!

 

base class 소멸자를 가상 소멸자로 만든다.

 

 

 

 

 

 

virtual function 클래스에 virtual function table 가는 포인터를 하나 만들고,

 

virtual function table에는 overriding 함수들의 주소가 적혀 있다.

 

소멸자는 일반 함수들과 달리 base class에서 virtual 등록하면 derived class 소멸자도 자동으로 virtual , 그래서 derived class 소멸자에는 vitual 키워드를 생략할 있다.

 

 

 

delete ptk => 참조 type TimeKeeper 이다. => TimeKeeper 소멸자 호출 => 가상 소멸자임 =>

virtual function table에서 derived class 소멸자 호출 => 원래 자신의 소멸자 호출

 

 

 

c++ 생성자 소멸자/(virtual)가상 소멸자를 쓰는 이유 (tistory.com)

 

 

소멸자는 derived destructor -> base destructor 순으로 되는데

 

예제에서 base destructor virtual 선언이 없다면 delete ptk에서 ptk reference type TimeKeeper이기 때문에 base destructor 호출되고 뒤에 호출될 소멸자가 없으므로 그대로 끝나게 되는데,

 

base destructor virtual 등록해놓으면 delete ptk에서 base destructor 호출 virtual function table에서 overriding derived destructor 주소를 통해 호출되기 때문에 derived destructor 호출 되고, derived destructor 호출되었으므로 다음 순서인 base destructor 자동으로 호출되게 된다.

 

 

virtual function 하나라도 사용하는 클래스라면 virtual destructor 사용하는 것이 맞다.

 

why? virtual function 쓴다는 기본적으로 base class 것이다라는 의미임( why, virtual function 자체가 derived class 대한 polymorphism, 기본 인터페이스를 제공해주고 그걸 여러 목적에 맞게 사용할 있게 하기 위함이다.)

 

virtual destructor 갖고 있지 않는 클래스 => base class 생각이 없다.

바꿔 말하면 base class 생각이 없는 함수면 소멸자를 virtual 선언하는 것은 좋지 않다.

 

why?

 

이런 class 있다 치면 int 32bit Point class 64bit 레지스터에 맞게 들어갈 있겠다.

 

실제 sizeof(Point)해보면 8bytes 나온다.

 

하지만 소멸자를 가상 소멸자로 만들어버리면

나온다.

 

virtual function => virtual function table 만들어짐. 그리고 table 가르키는 pointer 객체에 포함된다. 그래서 +4bytes

 

virtual function table 가르키는 pointer vptr이라고 하고 table vtbl 이라고 . table 함수 포인터들의 배열로 구현되어있다.

 

가상함수를 하나라도 갖고 있는 클래스는 반드시 그와 관련된 vtbl 갖고 있다. 어떤 객체에 대해 어떤 가상함수가 호출 => 호출되는 실제 함수는 객체의 vptr 가르키는 vtbl 따라 결정된다. vtbl 있는 함수 포인터들 적절한 것이 연결되는 것이다.

 

 

그리고 cpp에서 vptr vtbl C++만의 특성이기 때문에, C 다른 언어로 선언된 동일한 자료구조와도 호환성이 없어진다.

 

virtual destructor 선언하는 것은 클래스에 가상 함수가 하나라도 들어 있는 경우에만 한정하자!!

(또는 base class 경우, 보통 base 클래스는 가상 함수가 많이 쓰임)

 

근데 가상함수가 전혀 없는데 통수맞는 경우가 있다. std::string 대표적인 예인데

 

 

경우이다. string에는 가상함수를 갖고 있지 않다. 따라서 당연히 가상 소멸자도 없다!!! => 그런 클래스를 상속받는다? 에바임

 

그냥 보기엔 어때 싶지만, 이것을 사용한 응용프로그램 어딘가에서 SpecialString 포인터를 string 포인터로 어떻게든 전환한 포인터를 delete 순간 '미정의 동작' 지옥열차 탑승이다;

 

또한 STL 컨테이너 타입(vector, list, set, trl::unorderd_map … 등등) 가상 소멸자가 없는 클래스이다. 이들 클래스를 상속받는 일은 없도록 하자

 

 

 

경우에 따라서는 순수 가상 소멸자(pure virtual destructor) 두면 편리하게 수도 있다. 순수 가상 함수는 해당 클래스를 abstract class 바꿔버린다. ( abstract class = 자체로는 인스턴스를 못만드는, 타입의 객체를 생성할 없는 클래스)

하지만 어떤 클래스가 abstract class 였으면 좋겠는데, 마땅히 넣을 순수 가상 함수가 없다면 어떻게 할까?

 

=> abstract class 원래 base class 목적으로 만드는 것이다. base class 가상 소멸자를 가져야 한다. 한편 순수 가상 함수가 있으면 바로 추상 클래스가 된다.

 

=> abstract class 만들고 싶은 클래스에 순수 가상 소멸자를 선언하면 된다.

 

 

이렇게 하면 되는데 하나 문제가 있다. 바로 순수 가상 소멸자의 정의룰 두지 않으면 된다.

 

소멸자가 동작하는 순서 : 상속 계통 구조에서 가장 말단의 derived class 소멸자 -> 다음 소멸자 -> .. base class 소멸자

 

컴파일러는 ~AWOV 호출 코드 (CALL ~AWOV) 만들 것인데 링커 에러가 안날려면 정의를 만들어줘야 한다.

 

 

 

기본 클래스에 가상 소멸자를 사용하는 규칙은 다형성(polymorphism) 위해 설계된 기본 클래스, 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에서만 적용된다!!

이번 항목의 TimeKeeper 그런 경우이다. AtomicClock, WaterClock 객체를 보면 TimeKeeper 포인터만 가지고도 이것들을 조작할 있을 거라는 생각을 갖게 하기 때문이다.

 

하나의 인터페이스처럼 사용하도록 설계된 기본클래스에서만 해당된다는 것이다.

 

모든 기본 클래스가 다형성을 갖도록 설계된 것이 아니기 때문에 저걸 잊으면 안된다. string type 그렇고, STL 컨테이너 등은 기본 클래스는 커녕 다형성의 흔적조차 없다. 한번 기본 클래스로 사용하되, 다형성은 갖지 않도록 설계된 클래스들, 예를 들어 항목6 Uncopyable이나 표준 라이브러리의 input_iterator_tag, 항목 47 참조) 등도 기본 클래스의 인터페이스를 통한 파생 클래스 객체의 조작이 허용되지 않는다. 이들에게서 가상 소멸자가 없는 이유가 바로 그것이다.

 

 

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