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::D2A::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.
  • b1b2에 의해 구성되어 baz() 있는 충전 호출 foo(1) 에 충전
  • q1q2에 의해 구성되어 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-chargeoperator 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.