在C++中使用花括号实例化一个对象是什么意思?

10

假设我定义了一个结构体:

typedef
struct number{
    int areaCode;
    int prefix;
    int suffix;
} PhoneNumber;

当我创建这个结构体的一个实例时,如果我使用以下语法:

PhoneNumber homePhone = {858, 555, 1234};

它调用的是哪个构造函数?默认构造函数、拷贝构造函数还是根本没有调用任何构造函数,因为它没有调用“new”?

这个问题的真正目的是找出如何添加第四个字段。所以我想重新定义我的结构体为:

typedef
struct number{
    int areaCode;
    int prefix;
    int suffix;
    int extension; // NEW FIELD INTRODUCED
} PhoneNumber;

现在,我可以使用四个字段创建新的PhoneNumber对象:

PhoneNumber officePhone = {858, 555, 6789, 777}
然而,我已经创建了数百个这些PhoneNumber实例,只有3个字段(xxx,xxx,xxxx)。因此,我不想遍历并修改已经定义的每个PhoneNumber对象的每个实例。我想能够保留它们,但仍然能够创建具有四个字段的新电话号码实例。因此,我正在尝试找出如何重写构造函数,以便我的现有三参数实例化不会出错,但它也将支持我的新四参数实例化。
当我尝试定义一个覆盖默认构造函数的函数,该函数需要3个字段,并将第四个字段设置为默认值“0”时,我会得到错误(在代码的实例化部分,而不是构造函数定义中)。异常指出我的对象必须通过构造函数来初始化,而不是通过{...}。因此,如果我确实覆盖默认构造函数,那么我将无法使用花括号创建新对象?
抱歉,如果这远离了原始问题,请见谅。

相关:https://dev59.com/ZXVD5IYBdhLWcg3wGXlI#88960 - Don Neufeld
7个回答

4
成员变量实际上是被复制初始化的。与其他一些答案所说的相反,默认构造函数没有被调用,也没有涉及任何operator=。可以通过一个名为geordi的软件来展示这一点,或者通过阅读标准来了解。我将展示使用该软件的“有趣”方式。它具有一个名为tracked::B的类,可以向我们展示何时调用构造函数/复制构造函数或析构函数/复制赋值运算符。它显示的输出是(TRACK限制跟踪到其后面的语句):
B1*(B0) B1~

我使用了这段代码。
struct T { tracked::B b; }; int main() { tracked::B b; TRACK T t = { b };  }

正如您所看到的,第二个B对象——即局部变量t中的成员,是从另一个对象b进行复制初始化的。当然,没有激活赋值运算符。如果您愿意,可以在标准中阅读有关此内容的信息(请参阅12.6.1/2)。
顺便说一下,对于数组(也是聚合类型),情况也是如此。许多人认为,作为数组成员的对象必须具有默认构造函数。但这并不是正确的。它们可以通过其类型的另一个对象进行复制初始化,并且可以正常工作。
聚合类型中未明确初始化的所有其他元素都将进行值初始化。值初始化是默认初始化和零初始化的混合。实际上,如果成员具有具有用户声明的构造函数的类型,则会调用该构造函数。如果它具有没有用户声明的构造函数的类型,则会对其每个成员进行值初始化。对于内置类型(int、bool、指针等),值初始化与零初始化相同(这意味着这样的变量将变为零)。以下内容将将每个成员初始化为零,除了第一个(a),它将为一:
struct T { int a, b, c; }; int main() { T t = { 1 }; }

那些初始化规则确实令人生畏,尤其是因为C++ 2003修订版引入了值初始化。截至1998年,它还不是标准的一部分。如果您对那些用大括号括起来的初始化更感兴趣,可以阅读如何在C++中初始化嵌套结构?

3

正如其他人所写的那样,它并没有调用默认构造函数。从概念上讲是相同的,但实际上,在汇编代码中你将找不到任何函数调用。

相反,成员变量保持未初始化状态;你使用花括号结构进行初始化。

有趣的是,这个:

PhoneNumber homePhone = {858, 555, 1234};

这个程序集的结果(GCC 4.0.1,-O0):

movl  $858, -20(%ebp)
movl  $555, -16(%ebp)
movl  $1234, -12(%ebp)

没有太多惊喜。汇编代码位于包含上述C++语句的函数内联中。值(以$开头)被移动(movl)到堆栈偏移量(ebp寄存器)中。它们是负数,因为结构成员的内存位置在初始化代码之前。

如果您没有完全初始化结构体,例如省略了某些成员:

PhoneNumber homePhone = {858, 555};

...然后我得到了以下汇编代码:

movl  $0, -20(%ebp)
movl  $0, -16(%ebp)
movl  $0, -12(%ebp)
movl  $858, -20(%ebp)
movl  $555, -16(%ebp)

看起来编译器实际上执行的是调用默认构造函数,然后进行赋值的非常相似的操作。但是再次强调,这是在调用函数中内联完成的,而不是函数调用。

另一方面,如果您定义了一个初始化成员变量的默认构造函数,如下所示:

struct PhoneNumber {
  PhoneNumber()
    : areaCode(858)
    , prefix(555)
    , suffix(1234)
  {
  }

  int areaCode;
  int prefix;
  int suffix;
};

PhoneNumber homePhone;

然后你会得到汇编代码,它实际上调用一个函数,并通过指向结构体的指针初始化数据成员:

movl  8(%ebp), %eax
movl  $858, (%eax)
movl  8(%ebp), %eax
movl  $555, 4(%eax)
movl  8(%ebp), %eax
movl  $1234, 8(%eax)

每一行 movl 8(%ebp), %eax 都将指针值(eax 寄存器)设置为结构体数据的开头。在其他行中,eax 直接使用,偏移量为4和8,类似于前两个示例中的直接堆栈寻址。
当然,所有这些都是特定于编译器实现的,但如果其他编译器采取非常不同的方法,我会感到惊讶。

如果将成员初始化列表与默认构造函数一起使用,是否会在优化时产生指针解引用?例如,在gcc中,使用-O3选项进行编译是否会消除指针解引用? - oz10
仅凭查看编译器输出无法替代理解规范。 - Don Neufeld
@ceretullis:我还没有看过。 @don.neufeld:同意,但这并不意味着它在理解实际发生的事情方面是无用的。 - unwesen

2

C++中的结构体类似于类。默认构造函数被调用。之后,每个字段都使用其赋值运算符进行复制。


POD类型不调用默认构造函数,且字段不会被复制。语法定义了聚合成员的初始化方式。请参考C++ 8.5.1。 - David Rodríguez - dribeas

2
初始化是C++标准中最难的部分之一,至少对我来说是这样。你使用的语法:Aggregate x = { 1, 2, 3, 4 };定义了一个聚合类型的初始化,其中前四个成员被赋予值1、2、3和4。
作为对你特定情况的注释(结构体从3个元素增长到4个元素),在花括号之间未出现在初始化列表中的字段将被值初始化,对于标量类型来说,这相当于零初始化,它本身相当于赋值0。因此,你的第四个元素将被初始化为0。 参考资料 这都在C++标准的第8.5章中定义,更确切地说是在8.5.1聚合中定义。聚合将不会用任何隐式声明的默认构造函数进行初始化。如果你使用上述语法,你就要求编译器使用提供的值来初始化给定的字段。聚合中的任何额外字段都应该被值初始化
现在,值初始化被定义为调用用户定义的默认构造函数,如果该类型是具有这种构造函数的类。如果它是一个数组或一个没有用户定义构造函数的非联合类,则每个成员属性都将被值初始化。否则,对象将被零初始化,这又被定义为... 零初始化被定义为对标量类型设置值为0。对于类,每个类数据成员和基类都将被零初始化。对于联合,只有第一个成员(唯一的)将被零初始化,对于数组,所有成员都将被零初始化

1

这实际上是调用默认构造函数;发生的是分配了一个结构体,并使用默认“=”分配了每个值。


默认构造函数不会被调用,operator= 也不会被调用。在 C++ 8.5.1 中,它定义了聚合体初始化的语法。 - David Rodríguez - dribeas

0
然而,我已经创建了数百个这些PhoneNumber实例,只有3个字段(xxx,xxx,xxxx)。所以我不想去修改已经定义的每个PhoneNumber对象的每个实例。
没问题,你只需要调用update-instance-for-redefined-class,然后......嗯,算了。继续将其标记为“无用”。

0

您的类型上没有构造函数参与,因此现有的三个参数语法需要在聚合初始化站点进行修改。

除非您新添加的字段的语义即该新字段类型是“零设计感知”的(.NET编程、Brad和Co等在设计指南废话中推荐的习惯用语),否则您

不能:

a)如果涉及某种方法,则无法提供比C++默认参数更有意义的内容(默认/可选参数很快将在市场上的C#商店中出现,靠近杂货店的Web 4.0和旧COM)

b)遵循C#的模式,使用0作为无效值标记来设计值类型(在这种情况下,如果您无法控制常量,则可以说是非常糟糕的,并且一般情况下都很糟糕 - 源级库做得很好,所有-JavaTM MS-类似框架都很差)。

简而言之,如果0是有效值,则会出现问题,这是大多数编译器编写者在任何托管样式或习惯用语存在之前就认为有用的东西;但这并不一定正确。

无论如何,你最好的选择并不是 C++ 或者 C#。而是代码生成,即比现代 C++ 的模板更自由的元编程。你会发现它类似于 JSON 数组注释,并且可以使用 Spirit 或者(我的建议)撰写自己的工具。这会在长期内有所帮助,最终你会想要变得更加熟练(也就是他们所谓的普通人建模,例如 .NET 中的 Oslo)。

许多语言都存在此类问题,这是所有 C 风格语言(C、C++、Java、C# 等等)的缺点;非常类似于数组的数组hackery,这对于任何类型的跨域或语义 I/O 工作都是必需的,甚至在像 SOAP 等 Web 技术中也很明显。新的 C++0x 可变位和一些模板hackery玩起来可能没有什么用处,但据说能够带来指数级别的更快编译时间。谁在乎呢:)


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