【问题标题】:C++ destructor not being called, depending on the linking order未调用 C++ 析构函数,具体取决于链接顺序
【发布时间】:2012-09-01 15:04:11
【问题描述】:

在检查内存泄漏后,我在我的应用程序中遇到了这个问题,并发现我的一些类根本没有被销毁。

下面的代码被分成3个文件,它应该实现一个名为pimpl的模式。预期的情况是让Cimpl 构造函数和析构函数都打印它们的消息。然而,这不是我用 g++ 得到的。在我的应用程序中,只有构造函数被调用。

classes.h:

#include <memory>

class Cimpl;

class Cpimpl {
    std::auto_ptr<Cimpl> impl;
public:
    Cpimpl();
};

classes.cpp:

#include "classes.h"
#include <stdio.h>

class Cimpl {
public:
    Cimpl() {
        printf("Cimpl::Cimpl()\n");
    }
    ~Cimpl() {
        printf("Cimpl::~Cimpl()\n");
    }
};    

Cpimpl::Cpimpl() {
    this->impl.reset(new Cimpl);
}

main.cpp:

#include "classes.h"

int main() {
    Cpimpl c;
    return 0;
}

这是我能够进一步发现的:

g++ -Wall -c main.cpp
g++ -Wall -c classes.cpp
g++ -Wall main.o classes.o -o app_bug
g++ -Wall classes.o main.o -o app_ok

看起来在两种可能的情况之一中调用了析构函数,这取决于链接顺序。使用 app_ok 我能够获得正确的场景,而 app_bug 的行为与我的应用程序完全一样。

在这种情况下我是否缺少任何智慧? 感谢您提前提出任何建议!

【问题讨论】:

  • 有些情况是不调用析构函数的。 This answer 会有所帮助。
  • std::auto_ptr 尝试调用Cimpl 的析构函数,该析构函数未在“classes.h”中声明。我不确定标准对这种情况有什么要求,但是您可以通过从具有虚拟析构函数并使用基类指针的基类派生Cimpl 或手动删除实现实例来解决此问题。
  • 我强烈建议您阅读this question 及其讨论的博客文章。涵盖了所有相关问题。

标签: c++ gcc destructor


【解决方案1】:

代码违反了单一定义规则。在 classes.h 中有一个类 Cimpl 的定义,在文件 classes.cpp 中有一个不同的类 Cimpl 定义。结果是未定义的行为。一个类可以有多个定义,但它们必须相同。

【讨论】:

  • 头文件中没有Cimpl的定义,只有前向声明。正如我在回答中指出的那样,尽管这 确实 仍然会导致违反单一定义规则。
  • @MarkB - 阅读您发布的代码。有一个前向声明,后面跟着一个定义。
  • @MarkB - 哎呀,那不是发布的代码。对此感到抱歉。
  • 您可能错过了前向标头声明Cimpl,然后定义Cpimpl,因为名称看起来非常相似。
  • @MarkB - 是的,我错过了。感谢您指出。名字太像了。
【解决方案2】:

为清楚起见进行了编辑,原始保留在下面。

此代码具有未定义的行为,因为在main.cpp 的上下文中,隐式Cpimpl::~Cpimpl 析构函数只有Cimpl 的前向声明,但auto_ptr(或任何其他形式的delete)需要一个合法清理Cimpl的完整定义。鉴于这是未定义的行为,您的观察结果无需进一步解释。

原答案:

我怀疑这里发生的情况是Cpimpl 的隐式析构函数是在classes.h 的上下文中生成的,并且没有 可以访问Cimpl 的完整定义。然后当auto_ptr 尝试做它的事情并清理它包含的指针时,它会删除一个不完整的类,这是未定义的行为。鉴于它是未定义的,我们不必进一步解释它完全可以接受,它可以根据链接顺序以不同的方式工作。

我怀疑 Cpimpl 在源文件中定义的显式析构函数可以解决您的问题。

编辑:实际上现在我再看一遍,我相信你的程序违反了一个定义规则。在main.cpp 中,它看到一个隐式析构函数,它不知道如何调用Cimpl 的析构函数(因为它只有一个前向声明)。在classes.cpp 中,隐式析构函数确实可以访问Cimpl 的定义以及如何调用它的析构函数。

【讨论】:

  • Re:“看到一个隐式析构函数......”等等。这一切都很好,但未定义的行为是未定义的行为。而已。在这种情况下,试图分析编译器做了什么是没有意义的。
  • 正式地说,问题不是违反 ODR,而是违反了 delete 运算符的特定禁令。可以删除指向不完整类型的指针,但如果该类型实际上具有析构函数,则行为未定义。
【解决方案3】:

pimpl 习惯用法的目标是不必在头文件中公开实现类的定义。但是所有标准的智能指针都需要在声明时对其模板参数的定义可见才能正常工作。

这意味着这是您真正想要使用newdelete 和裸指针的少数情况之一。 (如果我对此有误,并且有一个可用于 pimpl 的 标准 智能指针,请告诉我。)

classes.h

struct Cimpl;

struct Cpimpl
{
    Cpimpl();
    ~Cpimpl();

    // other public methods here

private:
    Cimpl *ptr;

    // Cpimpl must be uncopyable or else make these copy the Cimpl
    Cpimpl(const Cpimpl&);
    Cpimpl& operator=(const Cpimpl&);
};

classes.cpp

#include <stdio.h>

struct Cimpl
{
    Cimpl()
    {
        puts("Cimpl::Cimpl()");
    }
    ~Cimpl()
    {
        puts("Cimpl::~Cimpl()");
    }

    // etc
};

Cpimpl::Cpimpl() : ptr(new Cimpl) {}
Cpimpl::~Cpimpl() { delete ptr; }

// etc

【讨论】:

  • auto_ptr 在实例化时确实需要一个完整的类型。新的智能指针shared_ptrunique_ptr 不会;但是如果你在一个不完整的类型上实例化它们中的一个,你必须提供一个可以正确处理完整类型的自定义删除器。
  • 我不知道这是新智能指针的要求。我是否正确理解您,如果可以要求 C++11,裸指针可能变为 std::unique_ptr&lt;Cimpl&gt; 并且外联析构函数可能有一个空的主体,但外联构造函数和析构函数仍然是有必要吗?
  • IIRC unique_ptr 在使用时需要完整的类型,但 shared_ptr 不需要。我认为,即使它们不同,但我没有文档来支持这一点。我同意 auto_ptr 需要一个完整的类型,因此不适合 pmpl。
  • @Zack - 我说得太强烈了:unique_ptr 的默认删除器可以处理不完整的类型,因此在这种情况下您不需要提供自定义删除器。但是,与处理托管对象的所有其他成员函数一样,在您使用它的地方(即,在 unique_ptr 对象被销毁的地方),类型必须是完整的。
  • @Kevin - 在我看来,auto_ptrunique_ptr 在这方面的主要区别在于 auto_ptr 必须在完整类型上实例化,但是unique_ptr 只需要在它被销毁的地方有一个完整的类型(这样它就可以正确地删除托管对象)。
【解决方案4】:

问题是在auto_ptr&lt;Cimpl&gt;对象的定义点,Cimpl是一个不完整的类型,也就是说,编译器只看到了Cimpl的前向声明。没关系,但是由于它最终会删除它所指向的对象,因此您必须遵守此要求,从 [expr.delete]/5 开始:

如果被删除的对象的类类型不完整 删除和完整的类有一个非平凡的析构函数或 释放函数,行为未定义。

所以这段代码遇到了未定义的行为,所有的赌注都被取消了。

【讨论】:

    猜你喜欢
    • 2018-05-08
    • 2020-04-08
    • 1970-01-01
    • 1970-01-01
    • 2017-04-28
    • 2012-04-10
    • 2013-06-24
    • 2018-01-02
    相关资源
    最近更新 更多