当前位置: 首页 > 知识库问答 >
问题:

C:析构函数在对象范围之外被调用?

东郭宏深
2023-03-14

更新1:根据建议添加了打印“this”。

更新2:拆分成几个文件,尝试阻止gcc优化。

更新3:记录复制构造函数并输入添加函数。

更新4:在main中添加了Clang和第二个cout的输出。

我希望参数析构函数作为函数中的最后一条语句被调用。从今以后,我希望下面的代码能够提供以下输出。

default constructor: 008DFCF8
other constructor: 008DFCEC
copy constructor: 008DFBC0
in member add
destroying: 008DFBC0
copy constructor: 008DFBB8
copy constructor: 008DFBB4
in function add
destroying: 008DFBB4
destroying: 008DFBB8
3 == 3
end of main
destroying: 008DFCEC
destroying: 008DFCF8

使用MSVC(Visual Studio)时,输出与预期的一样。但GCC(4.8.2-19ubuntu1)输出以下内容,表明函数参数的析构函数是在main()中的第一个cout语句之后但在最后一个之前调用的。

default constructor: 0x7fff2fcea510
other constructor: 0x7fff2fcea520
copy constructor: 0x7fff2fcea550
in member add
copy constructor: 0x7fff2fcea540
copy constructor: 0x7fff2fcea530
in function add
3 == 3
destroying: 0x7fff2fcea530
destroying: 0x7fff2fcea540
destroying: 0x7fff2fcea550
end of main
destroying: 0x7fff2fcea520
destroying: 0x7fff2fcea510

对于那些好奇clang(3.4-1ubuntu3)输出什么的人。

default constructor: 0x7fff52cf9878
other constructor: 0x7fff52cf9870
copy constructor: 0x7fff52cf9860
copy constructor: 0x7fff52cf9858
in function add
3 == copy constructor: 0x7fff52cf9850
in member add
3
destroying: 0x7fff52cf9850
destroying: 0x7fff52cf9858
destroying: 0x7fff52cf9860
end of main
destroying: 0x7fff52cf9870
destroying: 0x7fff52cf9878

问题:

  1. 我最初的怀疑是GCC正在内联函数?如果这是真的,有没有办法禁用这种优化
  2. C规范中的哪个部分允许在main中的cout之后调用析构函数?特别有趣的是内联规则(如果相关)以及何时安排析构函数。
// Test.h
#ifndef __TEST_H__

#include <iostream>

using namespace std;

class Test
{
public:
    int val;

    Test(Test const &a) : val(a.val)
    {
        cout << "copy constructor: " << this << endl;
    }

    Test() : val(1)
    {
        cout << "default constructor: " << this << endl;
    }

    Test(int val) : val(val)
    {
        cout << "other constructor: " << this << endl;
    }

    ~Test()
    {
        cout << "destroying: " << this << endl;
    }

    int add(Test b);
};

#endif
// Add.cpp
#include "Test.h"

int Test::add(Test b)
{
    cout << "in member add" << endl;
    return val + b.val;
}

int add(Test a, Test b)
{
    cout << "in function add" << endl;
    return a.val + b.val;
}
// Main.cpp
#include "Test.h"

int add(Test a, Test b);

int main()
{
    Test one, two(2);

    cout << add(one, two) << " == " << one.add(two) << endl;

    cout << "end of main" << endl;

    return 0;
}

为GCC编译,使用:

g++ -c Add.cpp -o Add.o ; g++ -c Main.cpp -o Main.o ; g++ Add.o Main.o -o test

共有3个答案

姜德容
2023-03-14

考虑一下这句话:

cout << add(one, two) << " == " << one.add(two) << endl;

写为:

cout << add(one, two);
cout << " == " << one.add(two) << endl;

这会改变GCC的打印输出吗?

或者那样:

auto i = add(one, two);
cout << i << " == ";
auto j = one.add(two)
cout << j << endl;

我认为这是关于副作用(而不是内联)。VC似乎更早地安排了副作用(临时对象的破坏),而GCC在语句末尾安排了它-

新增报价:

临时对象是在各种情况下创建的:将引用绑定到prvalue、从函数返回prvalue、强制转换为prvalue、引发异常、输入异常处理程序,以及在某些初始化中。在每一种情况下,所有临时词都会被销毁,作为评估完整表达的最后一步,该表达(从词汇上)包含它们被创建的点,如果创建了多个临时词,它们会按照与创建顺序相反的顺序被销毁。即使评估以抛出异常结束,这也是正确的。

在我看来,这是支持GCC和反对VC(尤其是关于销毁后打印“3==3”,这对我来说很奇怪)。

红存
2023-03-14

在调用“add(a,b)”和调用成员add(b)时创建临时对象。我认为在gcc的例子中,您看到的是add()函数(参数)中的局部变量在这些函数返回时被销毁。最后两行“完成”用于变量“一”和“二”。

VC是不同的——但这并不是错的,它只是表明这两个编译器以不同的方式优化代码。

不要只打印“完成”,还可以尝试打印“this”值。在构造函数中也打印“this”,然后可以看到构造函数和析构函数调用是如何配对的。

哦,我把VC和GCC搞混了。VC先打印“done”三次,可能是因为add()参数被破坏了,而GCC最后才打印,可能是因为它内联了add函数。

呼延修然
2023-03-14

似乎C标准对于何时必须调用函数参数析构函数可能有点模糊。C 03和C 11都在5.2.2/4“函数调用”中说(强调是后加的):

定义参数的函数返回时,参数的生存期结束。每个参数的初始化和销毁都发生在调用函数的上下文中。

因此,参数的析构函数在概念上不会出现在函数的右大括号中。这是我不知道的。

该标准给出了一个注释,解释了这意味着如果参数的析构函数抛出,则只考虑调用函数或“更高”的异常处理程序(特别是,即使被调用的函数有“函数-try块”,它也不被考虑)。

虽然我认为目的是为了MSVC行为,但我可以看到有人可能会如何解释允许GCC行为的读数。

再说一遍,也许这是GCC中的一个bug?

 类似资料:
  • 我有一个简单的Nunit测试代码。VS2019下的net core 2.0: 它总是失败,而我最终发现i=0,析构函数在Test()之后被调用。但是你可以看到“t”在一个代码块中,在代码块之后是无效的,我也打了电话 在我的“断言”之前。 为什么即使在GC之后也不调用析构函数?如何修复代码以使测试通过? 谢谢。

  • 我是C++的新手,我写了一个小程序来了解赋值如何处理对象。这个页面(http://www.cplusplus.com/doc/tutorial/classes2/)的cpp文档提示我这样做。在这一页上,它指出: 隐式版本[复制赋值操作符]执行浅层复制,这适用于许多类,但不适用于具有指向对象的指针的类,这些对象处理其存储。在这种情况下,不仅类会冒两次删除指向对象的风险,而且赋值会通过在赋值之前不删除

  • 类 类是对某一事物的抽象描述,具体地讲,类是C++中的一种构造的数据类型。它即可包含描述事物的数据,又可包含处理这些数据的函数,类在程序运行时是被用作样板来建立对象的。所以要建立对象,首先必须定义类。 定义类 定义一个类的一般格式为: class 类名{     private:         成员表1;     public:         成员表2;     protected:     

  • 问题内容: 如何从其父范围调用在子范围中定义的方法? http://jsfiddle.net/wUPdW/ 问题答案: 您可以从父母到孩子使用: 工作jsfiddle:http : //jsfiddle.net/wUPdW/2/ 更新 :还有另一个版本,耦合性更低,更易于测试: jsfiddle:http : //jsfiddle.net/uypo360u/

  • 与《 构造函数》类似,C# 中的析构函数(也被称作“终结器”)同样是类中的一个特殊成员函数,主要用于在垃圾回收器回收类实例时执行一些必要的清理操作。 C# 中的析构函数具有以下特点: 析构函数只能在类中定义,不能用于结构体; 一个类中只能定义一个析构函数; 析构函数不能继承或重载; 析构函数没有返回值; 析构函数是自动调用的,不能手动调用; 析构函数不能使用访问权限修饰符修饰,也不能包含参数。 析

  • 主要内容:析构函数的执行时机创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。 析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个 符号。 注意:析构函数没有参数,