C ++에서 추상 클래스에 대한 가상 소멸자를 선언해야하는 이유는 무엇입니까?


165

C ++에서 기본 클래스에 대한 가상 소멸자를 선언하는 것이 좋지만 virtual인터페이스로 작동하는 추상 클래스에 대해서도 소멸자 를 선언하는 것이 항상 중요 합니까? 이유와 예를 알려주세요.

답변:


196

인터페이스에는 더욱 중요합니다. 클래스의 모든 사용자는 구체적인 구현에 대한 포인터가 아닌 인터페이스에 대한 포인터를 가질 것입니다. 소멸자가 비 가상적 인 경우 삭제하면 파생 클래스의 소멸자가 아닌 인터페이스의 소멸자 (또는 컴파일러가 제공 한 기본값을 지정하지 않은 경우)를 호출합니다. 즉각적인 메모리 누출.

예를 들어

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete p정의되지 않은 동작을 호출합니다. 전화를 걸 수있는 것은 아닙니다 Interface::~Interface.
Mankarse 2012 년

@ Mankarse : 정의되지 않은 원인을 설명 할 수 있습니까? Derived가 자체 소멸자를 구현하지 않은 경우 여전히 정의되지 않은 동작입니까?
Ponkadoodle

14
@Wallacoloo : 그것은 때문에 정의되지 않는다 [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Derived가 내재적으로 생성 된 소멸자를 사용한 경우 여전히 정의되지 않습니다.
Mankarse

37

귀하의 질문에 대한 답변은 종종 있지만 항상 그런 것은 아닙니다. 추상 클래스가 클라이언트가 포인터에 대해 delete를 호출하는 것을 금지하거나 문서에서 그렇게 말하는 경우 가상 소멸자를 자유롭게 선언 할 수 없습니다.

소멸자를 보호하여 클라이언트가 포인터에 대해 delete를 호출하는 것을 금지 할 수 있습니다. 이와 같이 작동하면 가상 소멸자를 생략하는 것이 완벽하고 안전합니다.

결국 가상 메소드 테이블이 없어지고 클라이언트에 대한 포인터를 통해 삭제할 수 없도록하려는 의사를 알리기 때문에 실제로는 가상으로 선언하지 않는 이유가 있습니다.

[이 기사의 항목 4 참조 : http://www.gotw.ca/publications/mill18.htm ]


귀하의 답변이 작동하도록하는 열쇠는 "삭제를 요구하지 않는 것"입니다. 일반적으로 인터페이스로 설계된 추상 기본 클래스가있는 경우 인터페이스 클래스에서 삭제가 호출됩니다.
John Dibling

위의 요한이 지적했듯이, 당신이 제안하는 것은 꽤 위험합니다. 인터페이스의 클라이언트가 기본 유형 만 알고있는 객체를 절대로 파괴하지 않는다는 가정에 의존합니다. 비 가상적 일 경우 추상 클래스의 dtor를 보호하는 것만 보장 할 수있는 유일한 방법입니다.
Michel

Michel, 나는 그렇게 말했다 :) "당신이 그렇게하면 소멸자를 보호합니다. 그렇게하면 클라이언트는 해당 인터페이스에 대한 포인터를 사용하여 삭제할 수 없습니다." 실제로 클라이언트에 의존하지는 않지만 클라이언트에게 "당신은 할 수 없습니다 ..."라고 말해야합니다. 나는 어떤 위험도 보지 못했다
Johannes Schaub-litb

나는 지금 내 대답의 가난한 말을 고쳤다. 클라이언트에 의존하지 않는다고 명시 적으로 설명합니다. 실제로 나는 그것이 무언가를하는 클라이언트에 의존하는 것이 어쨌든 나가는 것이 분명하다고 생각했습니다. 고마워 :)
Johannes Schaub-litb

2
기본 클래스에 대한 포인터를 삭제할 때 실수로 잘못된 소멸자를 호출하는 문제의 다른 "방법"인 보호 된 소멸자를 언급하면 ​​+1입니다.
j_random_hacker

23

나는 약간의 연구를하고 당신의 답을 요약하려고 노력했습니다. 다음 질문은 어떤 종류의 소멸자가 필요한지 결정하는 데 도움이됩니다.

  1. 수업이 기본 수업으로 사용됩니까?
    • 번호 : 클래스의 각 개체에 피하기 V-포인터 공공 비 가상 소멸자를 선언 * .
    • 예 : 다음 질문을 읽으십시오.
  2. 당신의 기본 수업은 추상적입니까? (즉, 가상의 순수한 방법?)
    • 아니요 : 클래스 계층을 다시 디자인하여 기본 클래스를 추상화하십시오.
    • 예 : 다음 질문을 읽으십시오.
  3. 기본 포인터를 통해 다형성 삭제를 허용 하시겠습니까?
    • 아니요 : 원하지 않는 사용을 방지하기 위해 보호 된 가상 소멸자를 선언하십시오.
    • 예 : 퍼블릭 가상 소멸자를 선언합니다 (이 경우 오버 헤드 없음).

이게 도움이 되길 바란다.

* C ++에는 클래스를 최종 클래스로 표시 할 수있는 방법이 없다는 점에 유의해야합니다 (즉, 하위 클래스를 지정할 수 없음) 소멸자를 비가 상 및 공개로 선언하기로 결정한 경우 동료 프로그래머에게 수업에서 파생됩니다.

참고 문헌 :


11
이 답변은 부분적으로 구식이며 C ++에는 최종 키워드가 있습니다.
Étienne

10

예, 항상 중요합니다. 파생 클래스는 메모리가 할당되거나 객체가 파괴 될 때 정리해야하는 다른 리소스에 대한 참조를 보유 할 수 있습니다. 인터페이스 / 추상 클래스에 가상 소멸자를 제공하지 않으면 기본 클래스 핸들을 통해 파생 클래스 인스턴스를 삭제할 때마다 파생 클래스의 소멸자가 호출되지 않습니다.

따라서 메모리 누수 가능성을 열어 가고 있습니다.

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

사실, 그 예 사실, 그냥 메모리 누수가되지 않을 수 있습니다, 그러나 가능하게 충돌 : - /
에반 테란

7

항상 필요한 것은 아니지만 좋은 습관임을 알 수 있습니다. 기본 유형의 포인터를 통해 파생 객체를 안전하게 삭제할 수 있습니다.

예를 들어 :

Base *p = new Derived;
// use p as you see fit
delete p;

Base가상 소멸자가없는 경우 객체가 마치 것처럼 삭제하려고 시도하기 때문에 잘못된 형식 Base *입니다.


boost :: shared_pointer p (new Derived)를 boost :: shared_pointer <Base> p (new Derived)와 같이 수정하지 않으려면 ? 아마 ppl은 당신의 대답을 이해하고 투표 할 것입니다
Johannes Schaub-litb

편집 : litb가 제안한 것처럼 꺾쇠 괄호가 보이도록 두 부분을 "코딩"했습니다.
j_random_hacker

@EvanTeran : 답변이 원래 게시 된 이후로 이것이 변경되었는지 확실하지 않습니다 (boost 문서는 boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm에서 제안합니다).하지만 사실이 아닙니다. 요즘 shared_ptr객체는 마치 마치 객체를 삭제하려고 시도합니다. 객체로 만든 객체 Base *의 유형을 기억합니다. 참조 된 링크, 특히 "T는 가상 소멸자가 없거나 비어있는 경우에도 소멸자가 동일한 포인터를 사용하여 delete를 호출하고 원래 유형으로 완료합니다."라는 비트를 참조하십시오.
Stuart Golodetz

@StuartGolodetz : 흠, 당신 말이 맞을지 모르겠지만 솔직히 확실하지 않습니다. 그것은 여전히 병으로 형성 될 수있다 때문에 가상 소멸자 부족 상황. 살펴볼 가치가 있습니다.
Evan Teran


5

좋은 습관 일뿐만 아니라 모든 클래스 계층에 대한 규칙 # 1입니다.

  1. C ++에서 계층의 기본 클래스에는 가상 소멸자가 있어야합니다.

왜 그럴까. 전형적인 동물 계층 구조를 취하십시오. 가상 소멸자는 다른 메소드 호출과 마찬가지로 가상 디스패치를 ​​거치게됩니다. 다음 예제를 보자.

Animal* pAnimal = GetAnimal();
delete pAnimal;

동물이 추상 클래스라고 가정하십시오. C ++이 호출 할 적절한 소멸자를 아는 유일한 방법은 가상 메소드 디스패치를 ​​통하는 것입니다. 소멸자가 가상이 아닌 경우 Animal 소멸자를 호출하고 파생 클래스의 객체를 파괴하지 않습니다.

기본 클래스에서 소멸자를 가상으로 만드는 이유는 파생 클래스에서 선택 항목을 단순히 제거하기 때문입니다. 그들의 소멸자는 기본적으로 가상이됩니다.


2
나는 주로 하기 때문에, 당신과 동의 보통 당신이 기본 클래스 포인터 / 참조를 사용하여 파생 된 개체를 참조 할 수 있도록하려면 계층 구조를 정의 할 때. 그러나 항상 그런 것은 아니며 다른 경우에는 기본 클래스 dtor를 대신 보호하는 것으로 충분할 수 있습니다.
j_random_hacker

@j_random_hacker 그것을 보호하는 것은 잘못된 내부 삭제로부터 당신을 보호하지 않습니다
JaredPar

1
@JaredPar : 그렇습니다. 그러나 적어도 당신은 자신의 코드를 책임질 수 있습니다. 어려운 것은 클라이언트 코드 가 코드를 폭발시키지 않도록하는 것입니다 . (마찬가지로, 데이터 멤버 개인을하면 해당 멤버 바보 같은 일을하고부터 내부 코드를 방지하지 않습니다.)
j_random_hacker

@j_random_hacker, 블로그 게시물로 응답하여 죄송하지만이 시나리오에 실제로 적합합니다. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar

@JaredPar : 우수 게시물, 특히 소매 코드 계약 확인에 대해 100 % 동의합니다. 가상 dtor가 필요 없다는 것을 알고 있는 경우가 있습니다 . 예 : 템플릿 발송을위한 태그 클래스. 크기가 0이며 상속을 사용하여 전문화를 나타냅니다.
j_random_hacker

3

대답은 간단합니다. 그렇지 않으면 가상이어야합니다. 그렇지 않으면 기본 클래스가 완전한 다형성 클래스가 아닙니다.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

위의 삭제를 선호하지만 기본 클래스의 소멸자가 가상이 아닌 경우 기본 클래스의 소멸자 만 호출되고 파생 클래스의 모든 데이터는 삭제되지 않은 상태로 유지됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.