76

假设我们有一个(玩具)C++ 类,如下所示:

class Foo {
    public:
        Foo();
    private:
        int t;
};

由于没有定义析构函数,C++ 编译器应该为 class 自动创建一个析构函数Foo。如果析构函数不需要清理任何动态分配的内存(也就是说,我们可以合理地依赖编译器给我们的析构函数),将定义一个空的析构函数,即。

Foo::~Foo() { }

和编译器生成的一样吗?一个空的构造函数怎么样——也就是说,Foo::Foo() { }

如果存在差异,它们存在于哪里?如果不是,一种方法是否优于另一种方法?

4

7 回答 7

120

它会做同样的事情(本质上什么都没有)。但这和你不写它是不一样的。因为编写析构函数需要一个有效的基类析构函数。如果基类析构函数是私有的,或者有任何其他原因不能调用它,那么你的程序就有问题。考虑这个

struct A { private: ~A(); };
struct B : A { }; 

没关系,只要您不需要破坏 B 类型的对象(因此,隐式 A 类型) - 就像您从不对动态创建的对象调用 delete,或者您从不创建它的对象第一名。如果这样做,编译器将显示适当的诊断信息。现在,如果您明确提供一个

struct A { private: ~A(); };
struct B : A { ~B() { /* ... */ } }; 

那将尝试隐式调用基类的析构函数,并在定义时已经导致诊断~B

还有一个区别在于析构函数的定义和对成员析构函数的隐式调用。考虑这个智能指针成员

struct C;
struct A {
    auto_ptr<C> a;
    A();
};

假设 type 的对象C是在文件中 A 的构造函数的定义中创建的,该.cpp文件还包含 struct 的定义C。现在,如果您使用 structA并要求销毁A对象,编译器将提供析构函数的隐式定义,就像上面的情况一样。该析构函数还将隐式调用 auto_ptr 对象的析构函数。这将删除它持有的指针,该指针指向C对象 - 不知道C! 这出现在.cpp定义结构 A 的构造函数的文件中。

这实际上是实现 pimpl 习语的常见问题。.cpp这里的解决方案是添加一个析构函数并在定义结构的文件中提供它的空定义C。在它调用其成员的析构函数时,它将知道 struct 的定义C,并且可以正确调用其析构函数。

struct C;
struct A {
    auto_ptr<C> a;
    A();
    ~A(); // defined as ~A() { } in .cpp file, too
};

请注意,boost::shared_ptr没有那个问题:当它的构造函数以某些方式调用时,它需要一个完整的类型。

它在当前 C++ 中有所不同的另一点是,当您想memset在具有用户声明的析构函数的此类对象上使用和朋友时。此类类型不再是 POD(普通旧数据),并且不允许进行位复制。请注意,这个限制并不是真正需要的——下一个 C++ 版本已经改进了这方面的情况,因此它允许您仍然对这些类型进行位复制,只要不进行其他更重要的更改。


由于您要求构造函数:嗯,对于这些几乎相同的事情是正确的。请注意,构造函数还包含对析构函数的隐式调用。在诸如 auto_ptr 之类的事情上,这些调用(即使实际上没有在运行时完成——纯粹的可能性在这里已经很重要了)将造成与析构函数相同的危害,并且在构造函数中抛出某些东西时发生——然后编译器需要调用析构函数的成员。这个答案使用了默认构造函数的隐式定义。

同样,对于我上面提到的析构函数的可见性和 PODness 也是如此。

关于初始化有一个重要的区别。如果您放置用户声明的构造函数,则您的类型不再接收成员的值初始化,并且由您的构造函数来执行所需的任何初始化。例子:

struct A {
    int a;
};

struct B {
    int b;
    B() { }
};

在这种情况下,以下总是正确的

assert(A().a == 0);

虽然以下是未定义的行为,因为b从未初始化(您的构造函数省略了)。该值可能为零,但也可能是任何其他奇怪的值。试图从这样一个未初始化的对象中读取会导致未定义的行为。

assert(B().b == 0);

在 中使用这种语法也是如此newnew A()注意末尾的括号 - 如果省略它们,则不进行值初始化,并且由于没有用户声明的构造函数可以对其进行初始化,因此a将保持未初始化状态)。

于 2009-06-22T03:07:20.073 回答
18

我知道我在讨论中迟到了,但是我的经验表明,与编译器生成的析构函数相比,编译器在面对空析构函数时的行为不同。至少 MSVC++ 8.0 (2005) 和 MSVC++ 9.0 (2008) 就是这种情况。

在查看使用表达式模板的某些代码生成的程序集时,我意识到在发布模式下,对 my 的调用BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)从未内联。(请不要关注确切的类型和操作员签名)。

为了进一步诊断问题,我启用了默认关闭的各种编译器警告。C4714警告特别有趣。__forceinline 当标记为 的函数仍然没有被内联时,它由编译器发出。

我启用了 C4714 警告,并标记了运算符,__forceinline我可以验证编译器报告它无法内联对运算符的调用。

在文档中描述的原因中,编译器无法内联标记__forceinline为的函数:

当 -GX/EHs/EHa 开启时,函数按值返回可展开的对象

这是我的情况BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)BinaryVectorExpression是按值返回的,即使它的析构函数是空的,它也会使这个返回值被认为是一个不可缠绕的对象。添加throw ()到析构函数对编译器没有帮助,我还是避免使用异常规范。注释掉空的析构函数让编译器完全内联代码。

要点是,从现在开始,在每个类中,我都将空析构函数注释掉,让人们知道析构函数故意不做任何事情,就像人们注释空异常规范 `/* throw() */ 一样表示析构函数不能抛出。

//~Foo() /* throw() */ {}

希望有帮助。

于 2010-02-23T22:41:44.293 回答
12

您在类外定义的空析构函数在大多数方面具有相似的语义,但并非全部。

具体来说,隐式定义的析构函数
1)是一个内联公共成员(你的不是内联的)
2)被表示为一个普通析构函数(必须使普通类型可以在联合中,你的不能)
3)有一个异常规范(抛出(),你的没有)

于 2009-06-22T03:04:07.337 回答
9

是的,那个空的析构函数和自动生成的一样。我总是让编译器自动生成它们;我认为没有必要明确指定析构函数,除非你需要做一些不寻常的事情:比如让它成为虚拟的或私有的。

于 2009-06-22T02:53:52.160 回答
3

我同意大卫的观点,但我会说定义一个虚拟析构函数通常是一个好习惯,即

virtual ~Foo() { }

错过虚拟析构函数会导致内存泄漏,因为从您的 Foo 类继承的人可能没有注意到他们的析构函数永远不会被调用!

于 2009-06-22T03:05:12.403 回答
1

我会说最好放一个空的声明,它告诉任何未来的维护者这不是一个疏忽,你确实是故意使用默认的。

于 2009-06-22T03:04:33.153 回答
0

空定义很好,因为可以引用该定义

virtual ~GameManager() { };
空声明在外观上看似相似
虚拟〜GameManager();
然而却引发了可怕的没有定义虚拟析构函数 错误
Undefined symbols:
  "vtable for GameManager", referenced from:
      __ZTV11GameManager$non_lazy_ptr in GameManager.o
      __ZTV11GameManager$non_lazy_ptr in Main.o
ld: symbol(s) not found

于 2009-08-14T06:18:20.710 回答