C++中是否有隐式的默认构造函数?

65

在我目前正在阅读的书(C++ Without Fear)中提到,如果你没有为一个类声明默认的构造函数,编译器会替你提供一个,并且“将每个数据成员清零”。我进行了测试,但我没有看到任何清零行为。我也没能在Google上找到任何相关信息。这是一个错误还是特定编译器的怪癖呢?


38
听起来你需要一本更好的书。 ;) - jalf
13
好的,我会尽力进行翻译。如果“每个数据成员都为零”确实是字面引用,那么这个错误是不可原谅的。 - MSalters
3
我认为这是对 C 语言的一个普遍误解。在我访问了这个页面之前,我也曾持有同样的想法。我确信自己在某处读到过或者是在上课时学到的。 - Mark Lakata
我怀疑这种混淆是因为人们学习到全局对象保证被清零,尽管最好也不要依赖于它。 - jrodman
11个回答

70
如果你没有定义构造函数,编译器会为你定义一个默认构造函数。
构造:
默认构造函数的实现如下:
  • 构造基类(如果基类没有默认构造函数,则是编译错误)
  • 按照声明顺序构造每个成员变量。 (如果成员没有默认构造函数,则是编译错误)。
注意:POD数据(int、float、指针等)没有显式构造函数,但默认操作是什么都不做(遵循C++哲学; 我们不想为某些东西付费,除非我们明确要求)。
复制:
如果未定义析构函数/复制构造函数/复制分配运算符,则编译器会为您构建其中一个(因此类始终具有析构函数/复制构造函数/分配运算符(除非您作弊并显式声明但未定义它))。
默认实现为:
析构函数:
  • 如果定义了用户定义的析构函数,请执行提供的代码。
  • 以声明的相反顺序调用每个成员的析构函数
  • 调用基类的析构函数。
复制构造函数:
  • 调用基类复制构造函数。
  • 按声明顺序调用每个成员变量的复制构造函数。
复制分配运算符:
  • 调用基类赋值运算符
  • 按声明顺序调用每个成员变量的复制赋值运算符。
  • 返回对此的引用。
注意:POD数据的复制构造/分配运算符仅是复制数据(因此与RAW指针相关的浅复制问题)。
移动:
如果未定义析构函数/复制构造函数/复制分配/移动构造函数/移动赋值运算符,则编译器为您构建移动运算符之一。
默认实现为:
隐式声明的移动构造函数。如果一个类类型(结构体、类或联合体)没有提供任何用户定义的移动构造函数,且以下所有条件均为 true:

移动构造函数:

  • 调用基类的复制构造函数。
  • 按照声明顺序调用每个成员变量的移动构造函数。

移动赋值运算符:

  • 调用基类的赋值运算符。
  • 按照声明顺序调用每个成员变量的移动赋值运算符。
  • 返回对此对象的引用。

4
根据标准中第8.5节的第5条款,“对于类型为T的对象进行默认初始化意味着:...[如果T是POD类型]该对象将被零初始化。” 标准同一节的第4条款也提到,“对于类型为T的对象进行零初始化意味着:如果T是标量类型,该对象的值将被设置为0(零),作为整数常量表达式,并转换为T类型”。因此,对于POD类型来说,如果我没有弄错的话,这意味着int i = int();将使得i == 0 - wilhelmtell
6
当你__显式地__使用默认初始化时,就是这种情况。在上述情况中,POD类型没有被显式地默认初始化,因此它们的值是未定义的。这就是为什么我使用稍微更加含糊的术语“默认构造函数”的原因。 - Martin York
如果我没有定义移动构造函数,编译器是否也会定义一个? - Jorge Luque
1
@JorgeLuque 添加了一个移动操作的部分。是的。但是只有在您不定义复制操作或析构函数时才能这样做。您可以通过使用Class(Class&&) = default;来强制创建默认版本。如果您定义了任一移动操作,则必须同时定义两个。 - Martin York

42

我认为值得指出的是,如果您没有提供任何构造函数,编译器才会创建默认构造函数。这意味着,如果您只提供了一个带有参数的构造函数,编译器将不会为您创建默认的无参数构造函数。

你的书讨论的清零行为可能是特定于某个编译器。我一直认为它可能会有所不同,因此您应该显式初始化任何数据成员。


39
  • 编译器会自动生成默认构造函数吗?
  • 隐式生成的默认构造函数是否执行零初始化?

如果您严格解析2003标准的语言,那么答案是。然而,这并不是全部内容,因为与用户定义的默认构造函数不同,在从头创建对象时,并非总是使用隐式定义的默认构造函数 - 存在两种其他情况: 无构造成员值初始化

"无构造" 的情况实际上只是技术性问题,因为它在功能上与调用 平凡 默认构造函数没有什么区别。另一种情况更有趣:成员值初始化是通过使用 "()" [就像显式调用没有参数的构造函数] 来调用的,并且它绕过了技术上称为 默认构造函数 的内容。相反,它在每个数据成员上递归执行值初始化,并对于原始数据类型,这最终解析为零初始化

因此,实际上,编译器提供了两种不同的隐式定义的默认构造函数。其中一个执行原始成员数据的零初始化,另一个则不执行。以下是如何调用每种类型构造函数的一些示例:

    MyClass a; // default-construction or no construction
    MyClass b = MyClass(); // member-wise value-initialization

并且

    new MyClass; // default-construction or no construction
    new MyClass(); // member-wise value-initialization

注意:如果存在用户声明的默认构造函数,则成员逐个值初始化只会调用该构造函数并停止。
以下是标准对此的详细说明...
  • If you don't declare a constructor, the compiler implicitly creates a default constructor [12.1-5]

  • The default constructor does not initialize primitive types [12.1-7]

      MyClass() {} // implicitly defined constructor
    
  • If you initialize an object with "()", this does not directly invoke the default constructor. Instead, it instigates a long sequence of rules called value-initialization [8.5-7]

  • The net effect of value initialization is that the implicitly declared default constructor is never called. Instead, a recursive member-wise value initialization is invoked which will ultimately zero-initialize any primitive members and calls the default constructor on any members which have a user-declared constructor [8.5-5]

  • Value-initialization applies even to primitive types -- they will be zero-initialized. [8.5-5]

      int a = int(); // equivalent to int a = 0;
    

对于大多数情况来说,这一切都是无关紧要的。类的编写者通常不能假设数据成员会在隐式初始化序列期间被清零——因此,任何自管理的类如果有任何需要初始化的原始数据成员都应该定义自己的构造函数。

那么什么时候这很重要呢?

  • There may be circumstances where generic code wants to force initialization of unknown types. Value-initialization provides a way to do this. Just remember that implicit zero-initialization does not occur if the user has provided a constructor.

  • By default, data contained by std::vector is value-initialized. This can prevent memory debuggers from identifying logic errors associated with otherwise uninitialized memory buffers.

      vector::resize( size_type sz, T c=T() ); // default c is "value-initialized"
    
  • Entire arrays of primitives type or "plain-old-data" (POD)-type structures can be zero-initialized by using value-initialization syntax.

      new int[100]();
    
这篇文章详细介绍了C++标准版本之间的差异,并且还提到在主要编译器中应用标准的方式有所不同的情况。您可点击此处查看更多细节。

4
这绝对是最佳答案 - 不幸的是,我只能投一次赞。所以其他人也应该点赞! - Kai Petzke
1
相关 -- 结构体的值初始化:https://dev59.com/M3NA5IYBdhLWcg3wKacx#1069634 - Brent Bradburn

22

C++会生成一个默认构造函数,但仅当您没有提供自己的构造函数时才会生成。标准对数据成员清零不做要求,在首次构造任何对象时,默认它们是未定义的。

这可能会令人困惑,因为大多数C ++原始类型确实具有默认的“构造函数”,可以将它们初始化为零(例如int(),bool(),double(),long(),等)。但编译器不会调用它们来初始化POD成员,就像它为对象成员所做的那样。

值得注意的是,STL 确实 使用这些构造函数来默认构造容纳原始类型的容器的内容。您可以查看此问题以获取有关STL容器中如何初始化事物的更多详细信息。


11

类的默认构造函数不会初始化内置类型,但它会调用所有用户定义成员的默认构造函数:

class Foo
{
public:
     int x;
     Foo() : x(1) {}
};

class Bar
{
public:
     int y;
     Foo f;
     Foo *fp;
};

int main()
{

    Bar b1; 
    ASSERT(b1.f.x == 1); 
    // We know nothing about what b1.y is set to, or what b1.fp is set to.

    // The class members' initialization parallels normal stack initialization.
    int y;  
    Foo f; 
    Foo *fp; 
    ASSERT(f.x == 1);
    // We know nothing about what y is set to, or what fp is set to.

}

5

仅在全局范围内声明的变量会被清零。因此,如果您的对象在全局作用域中声明,它的成员将被清零:

class Blah
{
public:
    int x;
    int y;
};

Blah global;

int main(int argc, char **argv) {
    Blah local;
    cout<<global.x<<endl;  // will be 0
    cout<<local.x<<endl;   // will be random
}

1
这是标准的一部分。你知道一个不会将全局变量清零的投诉编译器吗? - codelogic

5
编译器会在用户未创建构造函数和析构函数时生成默认的构造函数和析构函数。这些函数不会修改任何数据成员的状态。
在 C++(以及 C)中,任何分配的数据的内容都不能保证。在调试配置中,一些平台会将其设置为已知值(例如0xFEFEFEFE),以帮助识别错误,但不应依赖此行为。

在调试模式下使用的值取决于编译器。不同的值通常代表不同的状态,例如已分配/未分配/未初始化等。 - Martin York
1
澄清一下,这些标记值通常是由内存管理库(operator new()、malloc 和衍生物)设置的,而不是由创建默认构造函数的编译器设置的。 - Euro Micelli

4

C++不保证清零内存。Java和C#会(可以这么说)。

有些编译器可能会清零,但不要依赖它。


3
在 C++11 中,如果一个类有以下情况之一,则由编译器生成的默认构造函数将被标记为已删除:
  • 该类有一个引用字段
  • 或者一个带有 const 修饰符的字段没有用户定义的默认构造函数
  • 或者一个没有默认初始化器但有一个已删除默认构造函数的字段

http://en.cppreference.com/w/cpp/language/default_constructor


1
编译器默认情况下不会生成默认构造函数,除非实现不需要该函数。因此,基本上构造函数必须是非平凡的构造函数。
要使构造函数成为非平凡的构造函数,以下条件中的任何一个都可以满足: 1)类具有虚成员函数。 2)类成员子对象或基类具有非平凡的构造函数。 3)类具有虚继承层次结构。

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