一个“空”的构造函数或析构函数是否与生成的函数执行相同的操作?

80
假设我们有一个(玩具级别的)C ++类,如下所示:
class Foo {
    public:
        Foo();
    private:
        int t;
};

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

Foo::~Foo() { }

和编译器生成的构造函数相比,它们是否执行相同的操作?那么空构造函数呢——即Foo::Foo() { }

如果存在差异,它们在哪里?如果没有,是否有一种方法比另一种更好?


我稍微修改了这个问题,将事后编辑变成了问题的实际部分。如果我编辑的部分有任何语法错误,请指出我,而不是原始提问者。@Andrew,如果你觉得我改变了你的问题太多,可以随意恢复它;如果你喜欢这个改变但认为它还不够,当然可以编辑你自己的问题。 - anon
7个回答

122
它将执行相同的操作(实质上是什么都不做)。但这并不等同于您未编写它。因为编写析构函数需要一个正常工作的基类析构函数。如果基类析构函数是私有的或者由于其他原因无法调用,则您的程序是有缺陷的。请考虑以下内容。
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();
};

假设在A的构造函数定义中创建了类型为C的对象,并且该构造函数定义还包含struct C的定义,现在如果使用struct A并要求销毁A对象,则编译器将提供析构函数的隐式定义,就像上面的情况一样。该析构函数也会隐式调用auto_ptr对象的析构函数。这将删除指向C对象的指针,而不知道C的定义!这出现在定义struct A的构造函数的.cpp文件中。
实际上,这是实现pimpl习惯的常见问题。解决方法是添加一个析构函数并在定义struct C的.cpp文件中提供一个空定义。在调用其成员的析构函数时,它将知道struct C的定义,并可以正确调用其析构函数。
struct C;
struct A {
    auto_ptr<C> a;
    A();
    ~A(); // defined as ~A() { } in .cpp file, too
};

请注意,boost::shared_ptr没有这个问题:在某些情况下调用其构造函数时需要完整的类型。
当前C++中另一个区别的点是,当你想在具有用户声明析构函数的对象上使用memset和friends时。这些类型不再是POD(plain old data),因此不允许进行位复制。请注意,这种限制实际上并不是必需的 - 下一个C++版本已经改进了这种情况,因此它允许您仍然可以位复制这些类型,只要不进行其他更重要的更改。

由于您要求构造函数:对于这些内容,情况基本相同。请注意,构造函数还包含对析构函数的隐式调用。对于像auto_ptr这样的东西,即使在运行时实际上并没有执行这些调用(在这里,纯粹的可能性已经很重要),它们会像析构函数一样造成伤害,并且当构造函数中的某些内容抛出异常时发生 - 编译器随后需要调用成员的析构函数。这个答案利用了默认构造函数的隐式定义。

此外,与我上面提到的析构函数相同,可见性和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 完全不能使用(new 一个将会出现错误,任何对其进行的类型转换将会是未定义行为,因为它不是 POD)。 - James Hopkin
1
@James,只要你不删除它,你仍然可以新建一个B。如果没有用户声明的构造函数,A()将对其成员进行值初始化。这意味着a将为零。这与{ A a; }不同,后者不会初始化它。{ A a(); }会初始化它,如果它不是一个函数=) - Johannes Schaub - litb
@litb:你的构造函数部分的第一行应该写成:“请注意,构造函数还包含对_constructors_的隐式调用”,而不是_destructors_吗? - e.James
哎呀!在我的看法中,这是标准允许的一件奇怪的事情。感谢您澄清了这一点。 - Thomas
@litb:很抱歉向您请教这个问题,但我似乎找不到一个好的在线参考资料。我试图创建一个最小的示例,但失败了。也许g++ 4.4.3只在特定情况下假定这个空析构函数……也许您可以做得更好?非常感谢! - Thomas
显示剩余9条评论

18
我知道我来讨论有点晚了,但我的经验表明,编译器在面对一个空析构函数和一个编译器生成的析构函数时会有不同的行为。至少这是在MSVC++ 8.0(2005)和MSVC++ 9.0(2008)中的情况。
当查看一些使用表达式模板的代码的生成汇编时,我发现在发布模式下,调用我的BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)从未被内联。(请不要注意确切的类型和运算符签名)。
为了进一步诊断问题,我启用了各种默认关闭的编译器警告C4714警告特别有趣。当标记为__forceinline的函数无法被内联时,编译器会发出此警告。
我启用了C4714警告并标记了该运算符为__forceinline,我可以验证编译器报告无法内联调用该运算符。
在文档中描述的原因中,当-GX/EHs/EHa打开时,编译器无法内联返回可取消展开对象的函数。
这是我BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)的情况。 BinaryVectorExpression按值返回,即使其析构函数为空,它也使得返回值被视为不可解开的对象。在析构函数中添加throw ()并没有帮助编译器,我无论如何都避免使用异常规范。注释掉空析构函数可以让编译器完全内联代码。
重点是从现在开始,在每个类中,我都会写注释的空析构函数,以便让人们知道析构函数故意什么也不做,就像人们注释空异常规范`/* throw() */`一样,表示析构函数不能抛出异常。
//~Foo() /* throw() */ {}

希望这有所帮助。

12
您在类外定义的空析构函数在大多数方面具有相似的语义,但并非全部相同。
特别地,隐式定义的析构函数: 1)是一个内联公共成员(您的不是内联) 2)被标记为平凡析构函数(必要以使可以在联合中使用平凡类型,而您的不行) 3)有异常规范(throw()),而您的没有。

1
关于3的说明:在隐式定义的析构函数中,异常规范并不总是为空,如[except.spec]所述。 - dalle
@dalle 在评论中加了+1 - 感谢你引起了注意 - 你的确是正确的,如果 Foo 是从具有非隐式析构函数和异常规格说明的基类派生而来的话,Foo 的隐式析构函数将继承这些异常规格说明的并集。在这种情况下,由于没有继承,隐式析构函数的异常规格说明恰好是 throw()。 - Faisal Vali

9

是的,这个空析构函数与自动生成的析构函数是一样的。我通常都是让编译器自动生成它们;除非需要做一些不寻常的事情,比如将其设为虚函数或私有函数,否则不必显式指定析构函数。


3

我同意David的看法,但我认为定义虚析构函数通常是一个好的实践。

virtual ~Foo() { }

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


1
我认为最好放置空声明,这会告诉未来的维护者这不是疏忽,而是你确实打算使用默认值。

0

An empty definition is fine since the definition can be referenced

virtual ~GameManager() { };
The empty declaration is deceptively similar in appearance
virtual ~GameManager();
yet invites the dreaded no definition for virtual destructor error
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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接