【问题标题】:GNU GCC (g++): Why does it generate multiple dtors?GNU GCC (g++):为什么它会生成多个 dtor?
【发布时间】:2011-09-30 15:05:45
【问题描述】:

开发环境: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 --demangle 测试

我可以看到以下输出。

==== 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。

如果有人能就上述问题给我答复,我将不胜感激。

【问题讨论】:

  • +1 包含一个最小的、完整的示例程序。 (sscce.org)
  • 您的基类是否有意使用非虚拟析构函数?
  • 一个小观察;你犯了罪,并没有让你的 BaseClass 析构函数成为虚拟的。
  • 对不起,我的样本不完整。是的,BaseClass 应该有虚拟析构函数,所以这些类对象可以多态地使用。
  • @Lyke:好吧,如果你知道你不会通过指向基址的指针删除派生的,那没关系,我只是确保......有趣的是,如果你这样做基本成员是虚拟的,你会得到更多析构函数。

标签: c++ g++ destructor


【解决方案1】:

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

基本上:

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

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

【讨论】:

  • 感谢您的明确答复。现在我可以理解了,虽然我需要学习更多,因为我对虚拟继承类的东西不太熟悉。
  • @Smg:在虚拟继承中,“虚拟”继承的类由最派生的对象单独负责。也就是说,如果您有struct B: virtual A,然后是struct C: B,那么在销毁B 时调用B::D1,后者依次调用A::D2,而在销毁C 时,您调用C::D1,后者调用@987654330 @ 和 A::D2 (注​​意 B::D2 如何不调用 A 析构函数)。在这个细分中真正令人惊奇的是实际上能够使用 3 析构函数的简单线性层次结构来管理所有情况。
  • 嗯,我可能没有清楚地理解这一点...我认为在第一种情况下(破坏 B 对象),将调用 A::D1 而不是 A::D2。同样在第二种情况下(破坏 C 对象),将调用 A::D1 而不是 A::D2。我错了吗?
  • A::D1 没有被调用,因为 A 不是这里的顶级类;销毁 A 的虚拟基类(可能存在也可能不存在)的责任不属于 A,而是属于顶级类的 D1 或 D0。
【解决方案2】:

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

not-in-charge ctor 和 dtor 在使用 virtual 关键字处理从另一个类继承的类的对象时使用,当对象不是完整对象时(所以当前对象“不负责”构建或破坏虚拟基础对象)。这个ctor接收一个指向虚拟基础对象的指针并存储它。

负责 ctor 和 dtors 适用于所有其他情况,即如果不涉及虚拟继承;如果该类有一个虚拟析构函数,则负责删除 dtor 指针进入 vtable 槽,而一个知道对象动态类型的作用域(即对于具有自动或静态存储持续时间的对象)将使用 in-charge 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);

结果:

  • foobazquux 的每个 vtable 中的 dtor 条目指向相应的负责删除 dtor。
  • b1b2baz()in-charge 构造,它调用 foo(1)in-charge
  • q1q2quux()in-charge 构建,它们属于 foo(2)in-chargebaz()not-in-使用指向它之前构造的 foo 对象的指针来充电
  • q2~auto_ptr() 销毁 in-charge,它调用虚拟 dtor ~quux() in-charge delete,它调用 ~baz() not-负责人~foo()负责人operator delete
  • q1~quux()in-charge 破坏,它调用 ~baz()not-in-charge~foo()in-charge >
  • b2~auto_ptr() 销毁 in-charge,它调用虚拟 dtor ~baz() in-charge delete,它调用 ~foo() in-收费operator delete
  • b1~baz()in-charge 破坏,它调用 ~foo()in-charge

quux 派生的任何人都将使用它的not-in-charge ctor 和 dtor,并负责创建 foo 对象。

原则上,对于没有虚基的类,永远不需要 not-in-charge 变体;在这种情况下,in-charge 变体有时被称为 unified,和/或 in-charge 的符号not-in-charge 被别名为单个实现。

【讨论】:

  • 感谢您的清晰解释以及非常易于理解的示例。在涉及虚拟继承的情况下,创建虚拟基类对象是最派生类的责任。至于派生最大的类以外的其他类,应该是由不负责的构造函数来解释的,所以它们不会触及虚拟基类。
  • 感谢您清晰的解释。我想澄清更多事情,如果我们不使用 auto_ptr 而是在构造函数中分配内存并在析构函数中删除会怎样。在那种情况下,我们是否只有两个不负责/负责删除的析构函数?
  • @bhavin,不,设置完全一样。为析构函数生成的代码总是会销毁对象本身和任何子对象,因此您可以将delete 表达式的代码作为您自己的析构函数的一部分,或者作为子对象析构函数调用的一部分。 delete 表达式被实现为通过对象的 vtable 调用,如果它有一个虚拟析构函数(我们发现 in-charge 删除,或者直接调用对象的 in-充电析构函数。
  • delete 表达式从不调用 not-in-charge 变体,它仅在销毁使用虚拟继承的对象时由其他析构函数使用。
猜你喜欢
  • 2011-07-07
  • 2022-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多