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

时间:2009-06-22 02:50:26

标签: c++ class oop constructor destructor

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

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

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

Foo::~Foo() { }

做与编译器生成的一样的事情吗?那么空构造函数 - 即Foo::Foo() { }

如果存在差异,它们存在于何处?如果没有,一种方法优于另一种方法吗?

7 个答案:

答案 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 DefaultC4714警告特别有趣。当标有__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