GNU GCC (g ++) : 왜 다중 dtor를 생성합니까?


90

개발 환경 : GNU GCC (g ++) 4.1.2

단위 테스트에서 '코드 커버리지-특히 함수 커버리지'를 늘리는 방법을 조사하는 동안 일부 클래스 dtor가 여러 번 생성되는 것 같습니다. 왜 그런지 아는 분이 있습니까?

다음 코드를 사용하여 위에서 언급 한 것을 시도하고 관찰했습니다.

"test.h"에서

class BaseClass
{
public:
    ~BaseClass();
    void someMethod();
};

class DerivedClass : public BaseClass
{
public:
    virtual ~DerivedClass();
    virtual void someMethod();
};

"test.cpp"에서

#include <iostream>
#include "test.h"

BaseClass::~BaseClass()
{
    std::cout << "BaseClass dtor invoked" << std::endl;
}

void BaseClass::someMethod()
{
    std::cout << "Base class method" << std::endl;
}

DerivedClass::~DerivedClass()
{
    std::cout << "DerivedClass dtor invoked" << std::endl;
}

void DerivedClass::someMethod()
{
    std::cout << "Derived class method" << std::endl;
}

int main()
{
    BaseClass* b_ptr = new BaseClass;
    b_ptr->someMethod();
    delete b_ptr;
}

위의 코드 (g ++ test.cpp -o test)를 빌드하고 다음과 같이 어떤 종류의 기호가 생성되었는지 확인하면

nm-파괴 테스트

다음 출력을 볼 수 있습니다.

==== following is partial output ====
08048816 T DerivedClass::someMethod()
08048922 T DerivedClass::~DerivedClass()
080489aa T DerivedClass::~DerivedClass()
08048a32 T DerivedClass::~DerivedClass()
08048842 T BaseClass::someMethod()
0804886e T BaseClass::~BaseClass()
080488f6 T BaseClass::~BaseClass()

내 질문은 다음과 같습니다.

1) 왜 여러 dtor가 생성 되었습니까 (BaseClass-2, DerivedClass-3)?

2)이 dtor의 차이점은 무엇입니까? 이러한 다중 dtor는 어떻게 선택적으로 사용됩니까?

이제 C ++ 프로젝트에 대해 100 % 함수 범위를 달성하려면이를 이해해야 단위 테스트에서 이러한 모든 dtor를 호출 할 수 있다는 느낌이 들었습니다.

누군가가 위의 답장을 줄 수 있다면 대단히 감사하겠습니다.


5
최소한의 완전한 샘플 프로그램을 포함하려면 +1. ( sscce.org )
— Robᵩ 2011-07-07

2
기본 클래스에 의도적으로 비가 상 소멸자가 있습니까?
— Kerrek SB 2011

2
작은 관찰; 당신은 죄를 지었고 BaseClass 소멸자를 가상으로 만들지 않았습니다.
— Lyke 2011-07-07

불완전한 샘플에 대해 죄송합니다. 예, BaseClass에는 가상 소멸자가 있어야 이러한 클래스 객체를 다형 적으로 사용할 수 있습니다.
— Smg

1
@Lyke : 당신이 확인을의 포인터 - 투 -베이스를 통해 파생 삭제하지 않을거야 알고 있다면 물론, 난 그냥 당신이 얻을, 당신은 기본 구성원이 가상하게 할 경우, 재미있게 ... 확인하고 있었다 도 더 많은 소멸자.
— Kerrek SB 2011

답변:


74

첫째, 이러한 함수의 목적은 Itanium C ++ ABI에 설명되어 있습니다 . "기본 개체 소멸자", "완전한 개체 소멸자"및 "소멸자 삭제"의 정의를 참조하십시오. 잘린 이름에 대한 매핑은 5.1.4에 나와 있습니다.

원래:

  • D2는 "기본 개체 소멸자"입니다. 객체 자체는 물론 데이터 멤버 및 비가 상 기본 클래스도 파괴합니다.
  • D1은 "완전한 개체 소멸자"입니다. 추가로 가상 기본 클래스를 파괴합니다.
  • D0은 "객체 소멸자 삭제"입니다. 완전한 객체 소멸자가하는 모든 일을하고 operator delete실제로 메모리를 해제하기 위해 호출 합니다.

가상 기본 클래스가없는 경우 D2와 D1은 동일합니다. GCC는 충분한 최적화 수준에서 실제로 두 기호에 대해 동일한 코드에 대한 별칭을 지정합니다.


명확한 답변에 감사드립니다. 이제 공감할 수 있지만, 가상 상속에 익숙하지 않기 때문에 더 많이 공부해야합니다.
— Smg

@Smg : 가상 상속에서 "가상"상속 된 클래스는 가장 많이 파생 된 개체의 단독 책임하에 있습니다. 그것은 당신이있는 경우이며, struct B: virtual A다음과 struct C: B파괴 할 때 다음 B호출을 B::D1하는 호출 회전에서 A::D2하고 파괴 할 때 C당신을 호출 C::D1하는 호출 B::D2및 A::D2(주 방법 B::D2이 아닌 소멸자 된 invoke 않습니다). 이 세분화에서 정말 놀라운 것은 3 개의 소멸자 의 단순한 선형 계층 구조로 모든 상황을 실제로 관리 할 수 ​​있다는 것 입니다.
— Matthieu M. 2011

음, 요점을 명확하게 이해하지 못했을 수도 있습니다. 첫 번째 경우 (B 객체 파괴)에서는 A :: D2 대신 A :: D1이 호출 될 것이라고 생각했습니다. 또한 두 번째 경우 (C 객체 파괴)에서 A :: D2 대신 A :: D1이 호출됩니다. 내가 잘못?
— SMG

A :: D1은 여기서 A가 최상위 클래스가 아니기 때문에 호출되지 않습니다. A의 가상 기본 클래스 (존재할 수도 있고 없을 수도 있음)를 파괴하는 책임은 A가 아니라 최상위 클래스의 D1 또는 D0에 속합니다.
— bdonlan 2011-07-08

37

일반적으로 생성자에는 두 가지 변형 ( 비 충전 / 충전 )과 세 가지 소멸자 ( 비 충전 / 충전 / 충전 삭제 )가 있습니다.

는 -에서 충전하지 다른 클래스에서 상속을 사용하는 클래스의 객체 처리 할 때의 ctor와 dtor이 사용되는 virtual개체 (전체 객체가 아닌 경우, 키워드를 현재 개체가 구성 또는 자폭의 "되지 담당"그래서 가상 기본 개체). 이 ctor는 가상 기본 개체에 대한 포인터를 받아 저장합니다.

에서 충전 의 ctor와 DTORS 다른 모든 경우, 즉 반군 가상 상속이없는 경우위한 것입니다; 클래스에 가상 소멸자가있는 경우 담당 삭제 dtor 포인터는 vtable 슬롯으로 이동하고 객체의 동적 유형을 알고있는 범위 (예 : 자동 또는 정적 저장 기간이있는 객체의 경우)는 담당 dtor를 사용합니다 . (이 메모리는 해제되어서는 안되기 때문에).

코드 예 :

struct foo {
    foo(int);
    virtual ~foo(void);
    int bar;
};

struct baz : virtual foo {
    baz(void);
    virtual ~baz(void);
};

struct quux : baz {
    quux(void);
    virtual ~quux(void);
};

foo::foo(int i) { bar = i; }
foo::~foo(void) { return; }

baz::baz(void) : foo(1) { return; }
baz::~baz(void) { return; }

quux::quux(void) : foo(2), baz() { return; }
quux::~quux(void) { return; }

baz b1;
std::auto_ptr<foo> b2(new baz);
quux q1;
std::auto_ptr<foo> q2(new quux);

결과 :

  • dtor에 대한 vtable을 각각의 항목 foo, baz그리고 quux각각의 시점 에서 충전 삭제의 dtor.
  • b1과 b2에 의해 구성되어 baz() 있는 충전 호출 foo(1) 에 충전
  • q1과 q2에 의해 구성되어 quux() 있는 충전 폭포, foo(2) 전하에 와 baz() 있지-에 충전 받는 포인터로 foo그 이전에 건설 개체
  • q2에 의해 파괴되어 ~auto_ptr() -담당 가상 dtor 호출 ~quux() 에 충전 삭제를 호출 ~baz() 하지-에 충전 , ~foo() 에 충전 하고 operator delete.
  • q1에 의해 파괴되어 ~quux() -담당 호출 ~baz() 하지 충전 된 과 ~foo() 에 충전
  • b2~auto_ptr() in-charge에 의해 파괴되고 , 가상 dtor ~baz() in-charge 를 호출하고 ~foo() in-charge 및operator delete
  • b1에 의해 파괴되어 ~baz() 에서 충전 호출 ~foo() 에 충전

에서 파생 된 모든 사람 quux은 담당하지 않는 ctor 및 dtor를 사용하고 foo개체 를 만드는 책임을집니다 .

원칙적으로 가상 기반이없는 클래스에는 비 충전 변형이 필요하지 않습니다. 이 경우 청구 된 변형은 통합 이라고도하며 / 또는 청구 중 및 청구 중이 아닌 기호 모두 단일 구현으로 별칭이 지정됩니다.


이해하기 쉬운 예와 함께 명확한 설명을 해주셔서 감사합니다. 가상 상속이 관련된 경우 가상 기본 클래스 개체를 만드는 것은 가장 파생 된 클래스의 책임입니다. 가장 많이 파생 된 클래스 이외의 다른 클래스는 비 담당 생성자에 의해 해석되므로 가상 기본 클래스를 건드리지 않습니다.
— SMG

명확한 설명에 감사드립니다. auto_ptr을 사용하지 않고 대신 생성자에 메모리를 할당하고 소멸자에서 삭제하면 더 많은 것에 대한 설명을 얻고 싶었습니다. 이 경우 담당 / 담당 삭제가 아닌 소멸자가 두 개만 있을까요?
— nonenone

1
@bhavin, 아니, 설정은 정확히 동일하게 유지됩니다. 소멸자에 대해 생성 된 코드는 항상 개체 자체와 모든 하위 개체를 파괴하므로 delete자신의 소멸자의 일부 또는 하위 개체 소멸자 호출의 일부로 표현식에 대한 코드를 가져옵니다 . 그만큼delete우리가 찾을 가상 소멸자 (있는 경우 표현식은 객체의 vtable을 통해 전화로하거나 구현 에 충전 삭제를 하거나, 객체에 직접 전화로의 에서 충전 . 소멸자
— 사이먼 리히터

delete표현식은 호출을 결코 하지-에서 충전 가상 상속을 사용하는 오브젝트를 파괴하면서 유일한 소멸자에 의해 사용되는 변형을.
— Simon Richter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.