C++ 默认构造函数

21

给定以下代码:

class temp
{
public:
    string str;
    int num;
};

int main()
{
    temp temp1;
    temp temp2 = temp();

    cout << temp1.str << endl; //Print ""
    cout << temp2.str << endl; //Print ""

    cout << temp1.num << endl; //Print a rand num
    cout << temp2.num << endl; //Print 0
}
这两者之间有什么不同吗?—
temp temp1;
and
temp temp2 = temp();

请问您使用的是哪个编译器? - Seth Carnegie
1
我也这么认为。我认为GCC/G++比MSVC++更容易将变量初始化为零,所以这肯定是与编译器有关的。 - Seth Carnegie
3个回答

23
temp temp1;

这将在名为temp1的实例上调用temp的默认构造函数。

temp temp2 = temp();

这将在临时对象上调用temp的默认构造函数,然后使用临时对象作为参数调用temp2上的编译器生成的拷贝构造函数(当然,这假设编译器不会省略拷贝; 其取决于您的编译器优化设置)。
至于为什么会得到不同的初始化值,标准的第8.5节是相关的:
8.5 初始化[dcl.init]
段落5: 对类型为T的对象进行零初始化 的意思是:
  • 如果T是标量类型(3.9),则将对象设置为转换为T的值为0(零);
  • 如果T是非联合类类型,则对每个非静态数据成员和每个基类子对象进行零初始化;
  • 如果T是联合类型,则对对象的第一个命名数据成员进行零初始化;
  • 如果T是数组类型,则对每个元素进行零初始化;
  • 如果T是引用类型,则不执行初始化。
对类型为T的对象进行默认初始化的意思是:
  • 如果T是非POD类类型(第9条),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化是非法的);
  • 如果T是数组类型,则对每个元素进行默认初始化;
  • 否则,对象将被零初始化。
对类型为T的对象进行值初始化的意思是:
  • 如果T是带有用户声明的构造函数(12.1)的类类型(clause 9),那么将调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化是非法的);
  • 如果T是一个非联合类类型且没有用户声明的构造函数,则初始化T的每个非静态数据成员和基类组件的值;
  • 如果T是数组类型,则对每个元素进行值初始化;
  • 否则,对象将被零初始化。
段落7: 初始化程序为空括号集的对象,即(),应进行值初始化。

第9段:

如果一个对象没有指定初始化器,并且该对象是(可能带有cv限定符的)非POD类类型(或其数组),则该对象应进行默认初始化;如果该对象是const限定类型,则底层类类型必须具有用户声明的默认构造函数。否则,如果未为非静态对象指定初始化器,则该对象及其子对象(如果有)具有不确定的初始值;如果该对象或其任何子对象是const限定类型,则程序是非法的。

12特殊成员函数[special]

第7段:

对于一个类,当它被用于创建其类类型的对象时(1.8),隐式声明的默认构造函数会被隐式定义。隐式定义的默认构造函数执行与该类的空mem-initializer-list(12.6.2)和空函数体的用户编写的默认构造函数将执行的类的初始化集相同。

12.6.2初始化基类和成员[class.base.init]

第4段:

如果给定的非静态数据成员或基类没有被mem-initializer-id命名(包括构造函数没有ctor-initializer的情况),则

  • 如果实体是(可能带有cv限定符的)类类型(或其数组)或基类的非静态数据成员,并且实体类是非POD类,则该实体将被默认初始化(8.5)。如果实体是const限定类型的非静态数据成员,则该实体类必须具有用户声明的默认构造函数。
  • 否则,该实体未被初始化。如果实体是const限定类型或引用类型,或者是包含(直接或间接地)const限定类型成员的(可能带有cv限定符的)POD类类型(或其数组),则程序是非法的。

现在规则已经说明,让我们看看它们如何应用:

temp temp1;
temp是一个非POD类型(因为它有一个std::string成员),由于temp1没有指定初始化值,因此将进行默认初始化(8.5/9)。这将调用默认构造函数(8.5/5)。temp具有隐式的默认构造函数(12/7),该函数会对std::string成员进行默认初始化,而int成员则不会被初始化(12.6.2/4)。请注意保留HTML标签。
temp temp2 = temp();

另一方面,临时的 temp 对象会进行值初始化 (8.5/7),这会对所有数据成员进行值初始化 (8.5/5),也就是会调用 std::string 成员的默认构造函数,并将 int 成员进行零初始化 (8.5/5)。
当然,如果你不想在五个或更多地方引用标准文档的话,可以确保显式初始化每一个变量 (例如,使用 int i = 0; 或者使用初始化列表)。

@In silico:关于我的回答,似乎MSVC在这种情况下不符合规范?由于没有用户声明的构造函数,因此int temp :: num应该进行零初始化,而不是在第二个示例temp temp2 = temp();中产生随机数字,对吗?或者我哪里错了吗? - Xeo
@Xeo:一些编译器在§8.5方面并不完全符合标准,我相信我看到了一个与此有关的Microsoft Connect条目。值初始化是C++03中的新内容;我认为MSVC++使用C++98规则来处理§8.5。 - In silico
1
@Xeo:上面的引用来自C++03标准,而MSVC编译器仍然大多遵循C++98。 - AnT stands with Russia
@In silico: 严格来说,第二次初始化并不会产生一个名为 temp 的临时对象。这是一种 拷贝初始化 的语法,但由于左侧和右侧的类类型相同,因此这种初始化被处理为 直接初始化(参见8.5/14)。没有创建临时对象。请注意,这不是由于省略了临时对象的 拷贝省略 的结果。该临时对象从一开始就不存在。 - AnT stands with Russia
@AndreyT:这正是我感到困惑的一件事情(我已经回滚了我的最后一次编辑)。除了编译器允许省略临时变量之外,我没有看到任何地方说它是一种特殊情况。而且,不需要道歉;无论如何,我都需要更仔细地阅读标准。 :-) - In silico
显示剩余2条评论

5
您的代码行为在很大程度上取决于您使用的编译器。更确切地说,它取决于您的编译器实现哪个版本的语言规范。
对于C++98编译器,这两个声明对被声明对象的最终值具有相同的影响:str成员应变为空,而num成员应包含不可预测的值。在这两种情况下,实际的初始化是由编译器提供的类temp的默认构造函数执行的默认初始化。该默认构造函数初始化str,但保留num未初始化。
对于C++03编译器,行为是不同的。对于temp1对象没有区别(其num仍然不可预测)。但是,temp2初始化处理方式不同。在C++03中,“()”初始化程序触发了新的初始化方式——所谓的“值初始化”。值初始化忽略顶层对象的编译器提供的默认构造函数,并直接作用于其子对象(在本例中为数据成员)。因此,temp2对象通过值初始化有效地初始化,这也将num成员设置为零(除了使用空字符串初始化str)。因此,在C++03编译器中,temp2.num最终为零。
如果在您的实验中,您观察到temp2.num始终为零,则意味着您的编译器在这方面遵循C++03规范。

4
temp temp1;

将创建一个默认初始化的temp对象。由于您没有为temp提供默认构造函数,因此temp的每个成员也将被默认初始化。由于std::string提供了默认构造函数,它会得到正确的初始化并具有明确定义的值。然而,整数会被默认初始化,这是实现定义的,通常是一个随机值。

temp temp2 = temp();

这将首先创建一个初始化为temp的对象。这很重要,因为对象本身是值初始化的,所以它的成员也是值初始化的。对于字符串来说没有关系,因为默认初始化和值初始化是相同的,但对于整数来说就很重要了。值初始化的整数具有值0
之后,你只需要将这些成员复制到temp2中即可。
此外,这个相关问题可能会对你有所帮助。
编辑:请参见我在@In silico的答案评论中的解释,说明为什么这不适用于MSVC。 :/

1
请注意,如果temp有一个用户定义的默认构造函数,那么值初始化将不会将整数数据成员清零。 - Jason
1
@Jason:如果提供正确的话,它就会起作用 :) temp() : str(), num() - Xeo
1
@Seth:看看我在@In silico的回答中的评论,似乎MSVC在这种情况下只是不符合标准。 - Xeo
@Jason:你从来没有说过不允许显式初始化每个成员。:P 你只是说如果用户提供了构造函数,它仍然不会将整数成员清零。 - Xeo
@Jason:请看@AndreyT对@In silico答案的评论。 - Xeo
显示剩余4条评论

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