一个‘ em pty’构造函数或析构函数将执行与生成的构造函数相同的操作吗?

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

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

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

Foo::~Foo() { }

和编译器生成的构造函数做同样的事情? 那么一个空的构造函数——也就是 Foo::Foo() { }怎么样?

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

36371 次浏览

是的,这个空析构函数和自动生成的析构函数是一样的。我总是让编译器自动生成它们; 我认为没有必要显式地指定析构函数,除非您需要做一些不寻常的事情: 比如,将其设置为虚拟或私有。

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

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

我认为最好使用空声明,它告诉未来的维护者这不是一个疏忽,而且您确实有意使用默认声明。

我同意 David 的观点,但是我认为定义一个虚析构函数是一个很好的实践。

virtual ~Foo() { }

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

它将做同样的事情(本质上什么也不做)。但这和你没写不一样。因为编写析构函数将需要一个可工作的基类析构函数。如果基类析构函数是私有的,或者由于其他原因不能调用它,那么您的程序就是错误的。考虑一下

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

这是可以的,只要你不需要销毁一个 B 类型的对象(因此,隐式的 A 类型)-如果你从来没有调用删除一个动态创建的对象,或者你从来没有创建它的对象摆在首位。如果您这样做,那么编译器将显示一个适当的诊断。现在如果你明确地提供一个

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

它将尝试隐式调用基类的析构函数,并在 ~B的定义时引发一个诊断。

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

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

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

这实际上是实现皮普尔成语的一个常见问题。这里的解决方案是添加一个析构函数,并在定义结构 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和好友时。这些类型不再是 POD (普通的旧数据) ,并且不允许进行位复制。请注意,这个限制并不是真正需要的——下一个 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将保持未初始化状态)。

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

virtual ~GameManager() { };
空声明在外观
虚拟 ~ GameManager () ;
中看似相似,但是却引发了可怕的 没有虚析构函数的定义错误 < pre > < code > 未定义的符号: “ vtable for GameManager”,引自: _ _ ZTV11GameManager $non _ 惰 _ ptr in GameManager.o _ _ ZTV11GameManager $non _ Lazy _ ptr in Main.o Ld: 未找到符号

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

在查看使用表达式模板生成的程序集时,我意识到在发布模式中,对我的 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() */ {}

希望能帮上忙。