为什么析构函数会被调用三次?

3

我正在处理RVO(返回值优化)/拷贝构造函数/析构函数相关的代码,并进行随机检查。这里我有点困惑,为什么析构函数会被调用三次..??

#include <iostream>
using namespace std;
class A{
    public:
        A(){
            cout << "Simple Constructor" << endl;
        }
        A(const A& obj){
            cout << "Copy Constructor " << endl;
        }
        A operator =(A obj){
            cout << "Assignment Operator" << endl;
        }
        ~A(){
            cout << "Destructor " << endl;
        }       
};
A fun(A &obj){
    cout << "Fun" << endl;
    return obj;
}
int main(){
    A obj;
    obj=fun(obj);
    cout << "End" << endl;
    return 0;
}

输出:

Simple Constructor // ok
Fun // ok
Copy Constructor // ok for =
Assignment Operator // ok
Destructor // ok for =
Destructor // why here destructor called?
End // ok
Destructor // ok for main

我原本期望 Destructor 被调用两次。

一次是针对 (=) 运算符的 对象。

第二次是针对 int main() 函数的对象。

为什么它会被第三次调用?以及如何发生的?


7
A operator =(A obj){ 这个操作符会拿一个临时的拷贝,所以这个拷贝的析构函数也会被调用。这里应该改成 A& operator =(const A& obj){},同时还需要补充上 return *this; - πάντα ῥεῖ
5
紧急链接:C ++书籍指南和列表 - Ivan Aksamentov - Drop
1
@Drop 我正在软件工程的第二学期,我们的老师没有深入地教授我们。我只是在思考、工作,无法与任何人讨论。 :) 这就是为什么我来到这里的原因。 :) 书籍并不能提供清晰的一切。 :) 不管怎样,谢谢。 - user5028722
1
  1. 它没有被VS2015编译。
  2. 修复错误后再次检查,并仔细计算真实输出和您发布的构造函数数量。
- Ivan Aksamentov - Drop
1
它不应该与GCC(Dev C++背后的编译器)有任何区别。区别在于你以某种方式运行了不同的代码(出于错误)。 - Ivan Aksamentov - Drop
显示剩余7条评论
5个回答

3
     A operator =(A obj){
        cout << "Assignment Operator" << endl;
    }

你声明了函数A::operator=的返回类型(在这种情况下是一个A实例),但实现中没有返回语句。这是未定义行为
你实际上在问关于未定义行为的响应。答案是“任何事情都有可能发生”。在你的编译器和系统中,你比构造函数多调用了一次析构函数。你很幸运,程序没有创建nasal demons或者清除你的硬盘。
解决方案很简单:不要触发未定义行为。编写复制赋值运算符的规范方法是将返回类型设为类的引用并返回*this:
     A& operator =(A obj){
        cout << "Assignment Operator" << endl;
        return *this;
    }

通过这个修正,您将看到对各种构造函数的调用和析构函数的调用平衡。 假设您改为使用非标准的复制分配操作符(但具有正确编写的返回语句):
     A operator =(A obj){
        cout << "Assignment Operator" << endl;
        return *this;
    }

再一次,你会看到构造函数和析构函数的调用是平衡的。你会看到多少次调用取决于你的编译器和优化级别。


2
您正在以下函数中通过值传递obj并返回值:

A operator =(A obj){
    cout << "Assignment Operator" << endl;
    return *this;
}

为了避免额外的复制,您应该将您的函数更改为:
A& operator =(const A& obj){
    cout << "Assignment Operator" << endl;
    return *this;
}

2

[stmt.return]/p2引自N4527

函数结束时不需要返回值相当于使用不带返回值的return语句;在一个有返回值的函数中,这会导致未定义的行为。

额外的析构函数被调用以释放未初始化的对象-未定义的行为

这也是为什么clang / MSVC在第一时间就不接受你的代码的原因。Gcc虽然接受了,但仍然会有警告。


详细解释:

虽然您说operator =返回一个对象,但您没有返回任何对象。这意味着跳过初始化并将其视为完全成型的对象。

以下是gcc应该按照的方式(这是唯一接受您的代码并产生输出的编译器):

Simple Constructor // Construct obj in main
Fun // Calls the function
Copy Constructor // To construct operator='s argument directly (RVO elision here)
Assignment Operator // Assignment operator
(assignment doesn't return anything and gcc accepts it - so nop here)
Destructor // Destroy operator='s temporary (the parameter)
Destructor // Destroy the UNINITIALIZED object allocated for the result of operator=
End
Destructor // obj in main

0

看起来是因为你没有从赋值运算符返回对象,但调用者必须销毁所谓的返回值。令我惊讶的是编译器会允许这种情况(或者当返回的对象应该被构造时,语言允许它)。

因此,被销毁的对象包括:

  1. 赋值运算符中所谓的临时返回对象
  2. 发送到赋值运算符的临时对象
  3. main 中定义的原始对象

被构造的对象包括:

  1. 发送到赋值运算符的临时对象
  2. main 中定义的原始对象

将赋值运算符定义为返回 void 或实际返回 obj*this 将消除这种差异。


我认为编译器在这种情况下只显示警告。您需要使用参数“-Wall”来让g++显示警告。 - Rodolfo

0
可能导致三次析构函数调用的原因有以下几种:
  1. 您将本地对象传递给赋值运算符,因此当函数执行停止时,它会被删除。
  2. obj = fun(obj),在这里,您正在将一个新对象分配给“obj”,因此旧对象必须被删除。
  3. 最后一种情况是当主函数结束时,“obj”本身被删除。
希望这可以解决问题!

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