GNU GCC(g ++):为什么会生成多个析构函数?

100

开发环境: GNU GCC (g++) 4.1.2

当我尝试研究如何在单元测试中增加“代码覆盖率-特别是函数覆盖率”时,我发现一些类析构函数似乎被多次生成。请问有人知道为什么吗?

我通过使用以下代码进行了尝试和观察。

在“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 --demangle test

我可以看到以下输出结果。

==== 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)为什么会生成多个析构函数(BaseClass-2,DerivedClass-3)?
2)这些析构函数有什么区别?多个析构函数将如何被选择性地使用?
我现在有一种感觉,为了实现 C++ 项目的100%功能覆盖率,我们需要理解这一点,以便我可以在我的单元测试中调用所有这些析构函数。
如果有人能够回复我上面的问题,我将非常感激。

6
包含一个最小完整的示例程序加一分。(http://sscce.org) - Robᵩ
2
你的基类故意有一个非虚析构函数吗? - Kerrek SB
2
一个小观察:你犯了个错误,没有将你的BaseClass析构函数定义为虚函数。 - Lyke
1
@Lyke:如果你知道你不会通过指向基类的指针删除派生类,那就没问题了,我只是想确认一下...有趣的是,如果你将基类成员设置为虚拟的,你会得到更多的析构函数。 - Kerrek SB
我之前以不同的形式提出了这个问题,但在有人注意到它是重复的之前,我已经得到了两个非常好的答案。在此粘贴一个链接,以便人们可以找到其他答案:https://dev59.com/KVcP5IYBdhLWcg3wVok6 - Omnifarious
显示剩余3条评论
2个回答

84
首先,这些函数的目的在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没有调用A析构函数)。这个分区中真正令人惊奇的是,我们可以通过一个简单的线性层次结构管理所有情况,只需要3个析构函数即可。 - Matthieu M.
嗯,我可能没有清楚地理解重点...我认为在第一种情况下(析构B对象),将调用A::D1而不是A::D2。并且在第二种情况下(析构C对象),将调用A::D1而不是A::D2。我错了吗? - Smg
A::D1没有被调用,因为A在这里不是顶层类;销毁A的虚基类(可能存在,也可能不存在)的责任不属于A,而是属于顶层类的D1或D0。 - bdonlan

37

通常有两种构造函数的变体(不负责的/负责的)和三种析构函数的变体(不负责的/负责的/负责并删除的)。

当处理继承自另一个类且使用virtual关键字时,如果对象不是完整对象,则使用“不负责”构造函数和析构函数来处理该对象(因此当前对象“不负责”构造或销毁虚基础对象)。此构造函数接收指向虚基础对象的指针并将其存储。

负责的构造函数和析构函数适用于所有其他情况,即如果没有涉及虚继承;如果类具有虚析构函数,则“负责并删除”析构函数指针进入vtable槽,而知道对象的动态类型的作用域(即对于具有自动或静态存储期的对象)将使用“负责”的析构函数(因为这段内存不应该被释放)。

代码示例:

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);

结果:

  • foobazquux各自的虚函数表中的dtor条目指向相应的负责删除dtor。
  • b1b2baz()负责构造,它调用foo(1)负责构造
  • q1q2quux()负责构造,它依次调用foo(2)负责构造和baz()不负责构造,并带有一个指向先前创建的foo对象的指针
  • q2~auto_ptr()负责析构,它调用虚dtor~quux()负责删除,这又调用~baz()不负责~foo()负责operator delete
  • q1~quux()负责析构,它调用~baz()不负责~foo()负责
  • b2~auto_ptr()负责析构,它调用虚dtor~baz()负责删除,这又调用~foo()负责operator delete
  • b1~baz()负责析构,它调用~foo()负责

任何从quux派生的类都应使用其不负责的ctor和dtor,并承担创建foo对象的责任。

原则上,对于没有虚基类的类,永远不需要使用不负责的变体;在这种情况下,负责的变体有时被称为统一的,并且/或者负责不负责的符号别名为一个单一实现。


感谢您提供的清晰解释和易于理解的示例。如果涉及虚拟继承,那么最派生类负责创建虚拟基类对象。至于其他类,它们应该通过非负责构造函数来构建,以便不触及虚拟基类。 - Smg
感谢您清晰明了的解释。我想要获得更多澄清,如果我们不使用auto_ptr而是在构造函数中分配内存并在析构函数中删除,那么情况会怎样?在这种情况下,我们是否只会有两个析构函数,一个不负责删除,一个负责删除? - nonenone
1
@bhavin,不,设置保持完全相同。析构函数生成的代码总是销毁对象本身和任何子对象,因此您将在自己的析构函数或子对象析构函数调用中获得delete表达式的代码。如果对象具有虚拟析构函数,则delete表达式将作为通过对象的vtable调用实现(在这里我们找到in-charge deleting),否则将直接调用对象的in-charge析构函数。 - Simon Richter
delete表达式不会调用非负责变体,这仅由其他析构函数在销毁使用虚拟继承的对象时使用。 - Simon Richter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接