“空”构造函数或析构函数会与生成的构造函数做同样的事情吗?

Posted

技术标签:

【中文标题】“空”构造函数或析构函数会与生成的构造函数做同样的事情吗?【英文标题】:Will an 'empty' constructor or destructor do the same thing as the generated one? 【发布时间】:2010-11-04 17:58:24 【问题描述】:

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

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

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

Foo::~Foo()  

和编译器生成的一样吗?一个空的构造函数呢——也就是Foo::Foo()

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

【问题讨论】:

我已经稍微修改了这个问题,以便将事后编辑变成问题的实际部分。如果我编辑的部分有任何语法错误,请骂我,而不是最初的提问者。 @Andrew,如果您觉得我改变了您的问题太多,请随时回复它;如果您喜欢更改但认为这还不够,显然欢迎您编辑自己的问题。 【参考方案1】:

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

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();
;

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

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

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 之类的事情上,这些调用(即使实际上没有在运行时完成——纯粹的可能性在这里已经很重要了)将对析构函数造成同样的伤害,并且当构造函数中的某些东西抛出时发生——然后编译器需要调用析构函数的成员。 This answer 使用了默认构造函数的隐式定义。

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

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

struct A 
    int a;
;

struct B 
    int b;
    B()  
;

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

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

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

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

new 中使用此语法也是如此,例如new A()(请注意末尾的括号 - 如果省略它们,则不会进行值初始化,因为没有用户声明的构造函数可以初始化它, a 将保持未初始化)。

【讨论】:

+1 用于提及前向声明的自动指针和自动析构函数。开始向前声明内容时的常见问题。 你的第一个例子有点奇怪。您编写的 B 根本无法使用(新的 B 将是一个错误,任何强制转换为一个将是未定义的行为,因为它是非 POD)。 另外,A().a == 0 仅适用于静态。 A 类型的局部变量将未初始化。 @James,你仍然可以新建一个 B - 只要你不删除它:) 如果没有用户声明的构造函数,A() 将对其成员进行值初始化。这意味着 a 将为零。它不同于 A a; 这不会初始化它。 一个 (); 会,如果它不是一个函数 =) @litb:您在构造函数部分的第一行是否应该写成:“请注意,构造函数也包含对构造函数的隐式调用”而不是析构函数【参考方案2】:

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

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

为了进一步诊断问题,我启用了各种Compiler Warnings That Are Off by Default。 C4714 警告特别有趣。当标有 __forceinline 的函数没有被内联时,编译器会发出它。

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

在文档中描述的原因中,编译器未能内联标有__forceinline 的函数:

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

这是我的BinaryVectorExpression operator + (const Vector&amp; lhs, const Vector&amp; rhs) 的情况。 BinaryVectorExpression 是按值返回的,即使它的析构函数是空的,它也会使这个返回值被认为是一个不可缠绕的对象。将throw () 添加到析构函数并没有帮助编译器和I avoid using exception specifications anyway。注释掉空的析构函数让编译器完全内联代码。

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

//~Foo() /* throw() */ 

希望对您有所帮助。

【讨论】:

【参考方案3】:

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

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

【讨论】:

关于 3 的注释:异常规范在隐式定义的析构函数中并不总是空的,如 [except.spec] 中所述。 @dalle +1 评论-感谢您引起注意-您确实是正确的,如果 Foo 派生自基类,每个基类都具有带有异常规范的非隐式析构函数- Foo 的隐式 dtor 将具有“继承”这些异常规范的联合——在这种情况下,由于没有继承,隐式 dtor 的异常规范恰好是 throw()。【参考方案4】:

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

【讨论】:

【参考方案5】:

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

virtual ~Foo()  

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

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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

virtual ~GameManager()  ;
空声明在外观上看似相似
virtual ~GameManager();
但会引发可怕的 no definition for virtual destructor 错误
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

【讨论】:

以上是关于“空”构造函数或析构函数会与生成的构造函数做同样的事情吗?的主要内容,如果未能解决你的问题,请参考以下文章

C++中将构造函数或析构函数定义为private

C++虚函数

“纯虚函数调用”崩溃从何而来?

C++在构造函数中调用最终的虚函数

条款09:不要在构造过程和析构过程中调用 virtual 方法

条款09:不要在构造过程和析构过程中调用 virtual 方法