假设我们有一个(玩具)C ++类,如下所示:
class Foo {
public:
Foo();
private:
int t;
};
由于没有定义析构函数,C ++编译器应该自动为类Foo
创建一个析构函数。如果析构函数不需要清理任何动态分配的内存(也就是说,我们可以合理地依赖编译器给我们的析构函数),那么将定义一个空的析构函数,即。
Foo::~Foo() { }
做与编译器生成的一样的事情吗?那么空构造函数 - 即Foo::Foo() { }
?
如果存在差异,它们存在于何处?如果没有,一种方法优于另一种方法吗?
答案 0 :(得分:115)
它会做同样的事情(实质上没有)。但是,如果你没有写它,那就不一样了。因为编写析构函数需要一个可操作的基类析构函数。如果基类析构函数是私有的,或者如果有任何其他原因无法调用它,那么您的程序就会出错。考虑一下这个
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的构造函数的定义中创建的,该文件还包含struct C
的定义。现在,如果使用struct A
并且需要销毁A
对象,编译器将提供析构函数的隐式定义,就像上面的情况一样。该析构函数还将隐式调用auto_ptr对象的析构函数。这将删除它所持有的指针,指向C
对象 - 不知道C
的定义!它出现在.cpp
文件中,其中定义了struct 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之类的东西上,这些调用(即使实际上并没有在运行时完成 - 纯粹的可能性在这里已经很重要)会对析构函数造成同样的伤害,并且当构造函数中的某些东西抛出时会发生 - 然后编译器需要调用析构函数的成员。 This answer使用默认构造函数的隐式定义。
此外,我所说的关于析构函数的可见性和POD也是如此。
初始化有一个重要区别。如果你放置一个用户声明的构造函数,你的类型就不再接收成员的值初始化,并且由构造函数来完成所需的任何初始化。例如:
struct A {
int a;
};
struct B {
int b;
B() { }
};
在这种情况下,以下内容始终为真
assert(A().a == 0);
虽然以下是未定义的行为,但因为b
从未初始化(构造函数省略了)。该值可能为零,但也可能是任何其他奇怪的值。尝试从这样的未初始化对象中读取会导致未定义的行为。
assert(B().b == 0);
在new
中使用此语法也是如此,例如new A()
(请注意末尾的括号 - 如果省略它们,则表示未完成值初始化,并且因为没有用户声明的构造函数可以初始化它,a
将保持未初始化状态。
答案 1 :(得分:17)
我知道我在讨论中已经迟到了,但是我的经验表明,与编译器生成的析构函数相比,面对空的析构函数时,编译器的行为会有所不同。至少在MSVC ++ 8.0(2005)和MSVC ++ 9.0(2008)中就是这种情况。
当查看生成的程序集中的某些代码使用表达式模板时,我意识到在发布模式下,对BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)
的调用从未被内联。 (请不要注意确切的类型和操作员签名)。
为了进一步诊断问题,我启用了各种Compiler Warnings That Are Off by Default。 C4714警告特别有趣。当标有__forceinline
的函数没有内联时,编译器会发出它。
我启用了C4714警告,并使用__forceinline
标记了操作符,我可以验证编译器报告它无法内联对操作员的调用。
在文档中描述的原因中,编译器无法内联标有__forceinline
的函数:
当-GX / EHs / EHa打开时,按函数返回可展开对象的函数
我的BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs)
就是这种情况。 BinaryVectorExpression
由值返回,即使其析构函数为空,也会将此返回值视为不可解除的对象。将throw ()
添加到析构函数对编译器和I avoid using exception specifications anyway没有帮助。注释掉空的析构函数,让编译器完全内联代码。
从现在开始,在每个课程中,我都会写出注释掉空的析构函数,让人们知道析构函数没有故意做任何事情,就像人们注释掉空的异常规范`/ * throw()一样* /表示析构函数不能抛出。
//~Foo() /* throw() */ {}
希望有所帮助。
答案 2 :(得分:12)
您在类之外定义的空析构函数在大多数情况下具有类似的语义,但并非全部。
具体来说,隐含定义的析构函数
1)是内联公共成员(您的内联不是内联)
2)被表示为一个简单的析构函数(必须制作可以在工会中的琐碎类型,你的不能)
3)有一个异常规范(throw(),你的没有)
答案 3 :(得分:8)
是的,空的析构函数与自动生成的析构函数相同。我总是让编译器自动生成它们;我认为没有必要明确指定析构函数,除非你需要做一些不寻常的事情:比如说虚拟或私有。
答案 4 :(得分:3)
我同意大卫的意见,但我认为定义虚拟析构函数通常是一种好习惯,即
virtual ~Foo() { }
错过虚拟析构函数会导致内存泄漏,因为从你的Foo类继承的人可能没有注意到他们的析构函数永远不会被调用!!
答案 5 :(得分:1)
我会说最好把空的声明,它告诉任何未来的维护者它不是一个疏忽,你真的打算使用默认声明。
答案 6 :(得分:0)
空定义很好,因为可以引用定义
virtual ~GameManager() { };
空声明在外观virtual ~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