GNU GCC(g ++):为什么会生成多个dtor?


89

开发环境: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测试),然后查看生成了以下哪种符号后,

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
罗伯

2
您的基类是否有意使用非虚拟析构函数?
Kerrek SB 2011年

2
一个小的观察;您犯了罪,并且没有使BaseClass析构函数虚拟化。
Lyke 2011年

对不起,我的样品不完整。是的,BaseClass应该具有虚拟析构函数,以便可以多态使用这些类对象。
SMG

1
@Lyke:好吧,如果您知道不打算通过指向基础的指针删除派生对象,那我只是确定...有趣的是,如果您确实将基本成员设为虚拟,您甚至更多的破坏者。
Kerrek SB 2011年

Answers:


73

首先,在Itanium C ++ ABI中描述了这些功能的目的;请参见“基础对象析构函数”,“完整对象析构函数”和“删除析构函数”下的定义。5.1.4中给出了到整齐的名称的映射。

基本上:

  • D2是“基础对象析构函数”。它破坏对象本身,以及数据成员和非虚拟基类。
  • D1是“完整对象析构函数”。此外,它还会破坏虚拟基类。
  • D0是“删除对象析构函数”。它完成了完整的对象析构函数所做的所有事情,并且调用operator delete了它以实际释放内存。

如果没有虚拟基类,则D2和D1相同;在足够的优化级别上,GCC实际上会将两个符号的别名别名为相同的代码。


谢谢您的明确答复。现在我可以与之建立联系,尽管我还需要学习更多,因为我对虚拟继承之类的东西不太熟悉。
Smg

@Smg:在虚拟继承中,“虚拟”继承的类是最衍生对象的唯一责任。就是说,如果您拥有struct B: virtual Aand then struct C: B,那么在销毁一个B调用时,它依次B::D1调用A::D2;在销毁一个C您调用时C::D1,那个调用B::D2A::D2(注意如何B::D2不调用析构函数)。在此细分中,真正令人惊奇的是,实际上能够使用3个析构函数的简单线性层次结构来管理所有情况。
Matthieu M.

嗯,我可能不清楚这一点……我认为在第一种情况(销毁B对象)中,将调用A :: D1而不是A :: D2。同样在第二种情况下(销毁C对象),将调用A :: D1而不是A :: D2。我错了吗?
Smg

A :: D1不会被调用,因为A在这里不是顶级类。销毁A的虚拟基类(可能存在或可能不存在)的责任不属于A,而是属于顶级类的D1或D0。
bdonlan

37

构造函数通常有两种变体(非负责人 / in-charge)和三个析构函数(非负责人 / in-charge / 负责人删除)。

未在充电构造函数和析构函数正在处理从另一个类继承使用的类的对象时使用的virtual关键字,当对象是不完整的对象(因此当前对象是“不充电”构造或自毁的虚拟基础对象)。该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);

结果:

  • 的每个vtable中的dtor条目foobazquux指向相应的负责删除 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() 在充电,它调用虚拟析构函数~baz() 主管删除,它调用~foo() 在充电operator delete
  • b1~baz() 负责人破坏,这称为~foo() 负责人

派生的任何人都quux将使用其非负责人的 ctor和dtor,并承担创建foo对象的责任。

原则上,对于没有虚拟基础的类,不需要使用负责人变量。在那种情况下,有时将充电中的变型称为统一的,和/或将充电中充电中的符号都别名为一个实现。


感谢您结合清晰易懂的示例进行清晰的解释。在涉及虚拟继承的情况下,创建虚拟基类对象是最派生类的责任。至于除最派生类之外的其他类,应该由非负责任的构造函数解释它们,因此它们不会涉及虚拟基类。
Smg

感谢您明确的解释。我想澄清更多事情,如果我们不使用auto_ptr而是在构造函数中分配内存并在析构函数中删除,该怎么办。在那种情况下,我们将只有两个销毁者不负责/负责删除吗?
nonenone

1
@bhavin,不,设置保持不变。为析构函数生成的代码始终会破坏对象本身和任何子对象,因此您可以将delete表达式的代码作为自己的析构函数的一部分或子对象析构函数调用的一部分来获取。该delete表达式是实现为通过对象的虚函数表的调用,如果它有一个虚析构函数(在这里我们找到主管删除,或者直接调用该对象的主管析构函数。
西蒙·里希特

一个delete表达式从不调用未主管变种,而摧毁一个使用虚拟继承的对象,唯一使用的其他析构函数。
西蒙·里希特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.