【问题标题】:How does the compiler knows that a second destructor has to be called, for an object constructed twice, at the same address?编译器如何知道必须调用第二个析构函数,对于在同一地址构造两次的对象?
【发布时间】:2012-02-12 13:03:27
【问题描述】:

在下面的代码中,C 类中的对象sub 被构造了两次。第一个构造调用默认 ctor Sub(),第二个构造使用 placement new 在同一地址重建此对象。

因此,析构函数也被调用了两次。第一次调用使用对~C() 中的Sub dtor 的直接调用,第二次调用在main() 结束后调用,我相信,由atexit() 函数调用。

鉴于sub对象在同一个地址被重构,编译器怎么知道第二个析构函数必须在main()之后调用?他将这些信息保存在哪里?

#include <iostream>
using namespace std;

struct Table
{
    int i;
    Table(int j) : i(j) {}
};

struct Sub
{
    Table* pTable;
    Sub(int j) { cout << "ctor placement new" << endl; pTable = new Table(j); }
    Sub() { cout << "ctor default" << endl; pTable = 0; }
    ~Sub() { if( pTable ) cout << "dtor placement new" << endl;
             else         cout << "dtor default" << endl;
             delete pTable; pTable = 0; }
};

class C
{
    Sub sub;

    public:
    C() { new (&sub) Sub(10); }
    ~C() { (&sub)->~Sub(); }
};

int main()
{
    C c;
}

【问题讨论】:

    标签: c++ constructor destructor placement-new


    【解决方案1】:

    您对atexit() 的假设不正确。当对象c 超出main() 的范围时,C 的析构函数将调用sub 的析构函数。

    C++ 析构函数总是为其所有子对象调用析构函数。

    无论如何,您的代码都是无效的,因为您在已经构造成对象的一块内存 (sub) 上调用了placement new 运算符。与析构函数类似,C++ 构造函数总是为其所有子对象调用构造函数。

    【讨论】:

    • 为什么placement new的调用无效?哪里说一个对象不能在同一个地址构造两次?
    • 也许你不知道使用参数调用子对象构造函数的正确语法:C(): sub(10) { } 将在构造 C 时调用 Sub::Sub(int) 构造函数。
    • 请注意,在第一个构造函数调用中没有创建表。所以没有内存泄漏。
    • 好吧,这无效的主要原因是C::~C()析构函数在已经被销毁的内存上调用了Sub::~Sub()析构函数。也许它不会对您的编译器造成问题,但请考虑更复杂的情况,例如使用虚拟方法甚至虚拟继承。
    • "哪里说一个对象不能在同一个地址构造两次?" ——你需要被告知这是个坏主意吗? (至少,在没有中间破坏的情况下构建它两次)
    【解决方案2】:

    当 c 超出范围时,它的析构函数被调用。 C 析构函数将显式调用子析构函数。当 C 的析构函数完成后,子析构函数也会(再次)被调用,因为所有 C++ 析构函数都会自动调用其所有内部对象的析构函数。

    本质上是代码

    (&sub)->~Sub();
    

    是不必要的,也是不正确的。永远不要显式调用托管对象的析构函数。

    编辑: 在通过placement new 构造的对象上显式调用析构函数是有效的。然而,这只是对象不是托管的情况。例如:

    class C
    {
        Sub sub[1];
    
        public:
        C() { new (sub) Sub(10); }
        ~C() { sub->~Sub(); }
    };
    

    这不仅有效,而且是必要的,因为 C 的成员是 Sub[1] 类型(或更一般地是 Sub*),所以当 C 被销毁时,不会显式调用 Sub 的析构函数。

    【讨论】:

    • 由于对象 sub 是通过调用 placement new expression 重构的,因此显式的析构函数调用是有效的。
    • 当c超出范围时,对象sub仍然会被自动销毁
    【解决方案3】:

    虽然这显然是未定义的行为,但如果你推理出正在发生的事情,那就很明显了。

    你创建了一个 C 类的对象。在这个过程中,Sub 的默认构造函数被隐式调用。 pTable 为 0。然后,显式调用 int 构造函数,该构造函数初始化 pTable。然后,在析构函数中,显式调用 Sub 的析构函数。 pTable 再次设置为 0。然后,在 C 的析构函数结束时,再次隐式调用 Sub 的析构函数。

    它不是在 main 的末尾发生的。它发生在 C 的析构函数的末尾。

    【讨论】:

    • 未定义的行为在哪里?
    • @user1042389:未定义的行为是在已经销毁(即不再有效)的对象上调用析构函数(在这种情况下是隐式的)。
    • 这里没有未定义的行为,因为对构造函数的第二次调用是使用placement new 进行的。事实上,在这些情况下,您需要显式调用析构函数。
    • @user1042389:未定义的行为不在您对 Sub 的析构函数的调用中。它在 C 的析构函数末尾隐式调用 Sub 的析构函数。因此,从技术上讲,编译器导致了它。但是你强迫了它,因为它需要在所有子对象上调用析构函数。
    • C++2003, §12.4 [class.dtor], ¶14:“一旦为对象调用析构函数,该对象就不再存在;如果调用析构函数,则行为未定义生命周期已结束的对象 (3.8)。[例如:如果自动对象的析构函数被显式调用,并且随后以通常会调用对象的隐式销毁的方式留下块,则行为未定义。]"
    【解决方案4】:

    其他答案对未定义的行为提出了有效的观点,但我很惊讶没有人提到应该如何纠正这个程序。

    Sub的析构函数确实这里需要手动调用,但是需要在放置new之前调用,而不是在销毁时调用:

    class C
    {
        Sub sub;
    
        public:
        C() { 
            (&sub)->~Sub();
            new (&sub) Sub(10); 
        }
    };
    

    在当前代码中,您创建了一个C 的实例,它默认构造一个Sub 的实例,然后立即 将另一个Sub 实例放置在旧实例之上而不调用它的析构函数。在 main 的末尾,c 的析构函数被调用,它显式调用sub 的析构函数,然后sub 的析构函数在已经被破坏的对象上再次被调用。这不仅是未定义的行为,如果分配了Sub 的默认构造函数,您也会出现内存泄漏。

    在更正的代码中,sub 是默认构造的,手动销毁,通过放置 new 构造,然后当 c 在 main 结束时超出范围时隐式销毁。

    所以答案是编译器知道当你在同一地址重新创建对象时需要调用析构函数,因为placement new不接受对象 作为它的第一个参数,它需要一个void*。它无法确定该指针是否指向现有对象,因此您必须手动调用析构函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-14
      • 2016-12-21
      • 2011-12-15
      • 2016-09-06
      • 2015-10-09
      • 1970-01-01
      • 2021-09-03
      • 1970-01-01
      相关资源
      最近更新 更多