为什么同一对象的析构函数会被调用两次?

7
在以下来自核心转储的回溯中,A2:~A2被调用了两次:
#0  0x086f5371 in B1::~B1 (this=0xe6d3a030, 
    __in_chrg=<value optimized out>)
    at /fullpath/b1.cpp:400
#1  0x086ffd43 in ~B2 (this=0xe6d3a030, 
    __in_chrg=<value optimized out>)
    at /fullpath/b2.h:21
#2  B2::~B2 (this=0xe6d3a030, 
    __in_chrg=<value optimized out>)
    at /fullpath/b2.h:21
#3  0x086ea516 in A1::~A1 (this=0xe3e93958, 
    __in_chrg=<value optimized out>)
    at /fullpath/a1.cpp:716
#4  0x0889b85d in A2::~A2 (this=0xe3e93958, 
    __in_chrg=<value optimized out>)
    at /fullpath/a2.cpp:216
#5  0x0889b893 in A2::~A2 (this=0xe3e93958, 
    __in_chrg=<value optimized out>)
    at /fullpath/a2.cpp:216
#6  0x0862c0f1 in E::Identify (this=0xe8083e20, t=PT_UNKNOWN)
    at /fullpath/e.cpp:713

A2是由A1派生而来,B2是由B1派生而来。只有B2有一个默认析构函数,所有基类的析构函数都是虚的。

代码看起来像这样:

e.cpp:

E::E(){
    //... some code ...
    myA1= new A2();
}

void E::Identify(){
    //...
    if(myA1){
        delete myA1; //line 713 of e.cpp
        myA1 = NULL;
    }

}

a2.cpp:

A2::~A2(){
    //...
    if (sd) //sd is not null here and also not made null after deletion
    {
        delete [] sd; //when called the second time shouldn't it crash here?
    }
    //...
} // line 216 of a2.cpp

a1.cpp

A1::A1(){
//...
   myB1 = new B2();
//...
}

A1::~A1(){
//...
    delete myB1; //line 716 of a1.cpp
//...
}

我无法理解为什么对于同一对象A2,A2::~A2会被调用两次(回溯中this指针在第4和第5个帧中具有相同的值)。

如果我进入第4个帧并进行反汇编,它会打印与第5个帧反汇编代码非常不同的结果(大约90行汇编代码与大约20行汇编代码)。


1
无关紧要:删除空指针不会有任何作用,所以这个if语句是无意义的。 - R. Martinho Fernandes
1
A1 *myA1 = new A2(); 是在构造函数中定义的局部变量,因此你无法在 void E::Identify() 中访问它,除非你将其作为参数传递(但你没有这样做)。你确定这是你的原始代码吗?你展示的代码和你提出的问题之间存在差异。 - Alok Save
4
你是否在 ~A2 中添加了一些代码(比如 cout),并观察到它被执行了两次?在堆栈跟踪中出现两次并不重要,因为编译器通常将析构函数分成多个“thunk”。您可以通过检查回溯中的地址或使用 nm 查找 ~A2 符号来查看这一点。 - PlasmaHH
1
你有遵循过“三大法则”吗?这明显是没有遵守的失败的味道。 - Alok Save
1
@PlasmaHH:这听起来应该是一个答案,而不是一个评论。 - Sander De Dycker
显示剩余3条评论
2个回答

7
我将示例简化为:
#include <cassert>
class A1 {
public:
    virtual ~A1() {
        assert(false);
    }
};

class A2 : public A1 {
};

int main() {
    A1* a = new A2;
    delete a;
    return 0;
}

使用assert以触发核心转储。

使用g++ 4.7.2编译,我们在gdb中得到双重析构函数的回溯。

#0  0x00007f16060e92c5 in raise () from /usr/lib/libc.so.6
#1  0x00007f16060ea748 in abort () from /usr/lib/libc.so.6
#2  0x00007f16060e2312 in __assert_fail_base () from /usr/lib/libc.so.6
#3  0x00007f16060e23c2 in __assert_fail () from /usr/lib/libc.so.6
#4  0x00000000004007c8 in A1::~A1 (this=0xf60010, __in_chrg=<optimized out>) at double.cpp:6
#5  0x000000000040084d in A2::~A2 (this=0xf60010, __in_chrg=<optimized out>) at double.cpp:10
#6  0x0000000000400880 in A2::~A2 (this=0xf60010, __in_chrg=<optimized out>) at double.cpp:10
#7  0x000000000040078c in main () at double.cpp:15

尽管使用g++ 4.7编译的代码的回溯看起来相似,但只有一个A2 ::〜A2帧。

两个回溯都是使用相同版本的gdb(7.5.1)提取的。

因此,这是由g++ 4.7生成的代码造成的结果,并不会影响编译后的二进制文件。这不是真正的双重调用析构函数。


2
这可能是您的情况(但您没有展示给我们代码的这部分)...
如果类E持有对A2的私有成员指针,并且它没有复制构造函数或运算符=....
那么,可能会出现这样一种情况,即将类型为E的对象复制到具有默认复制构造函数或运算符=的另一个类型为E的对象(变量)中。
这将导致成员的浅层复制,从而使两个对象现在都指向同一个对象A1。
当对象E被销毁时,它们都尝试删除相同的A2对象。

4
那么析构函数的调用将不会出现在同一堆栈跟踪中。 - Sander De Dycker

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