C++中初始化器和默认初始化器列表的区别

11

你好,我有一个问题很久以前就找不到答案了。那就是以下两个语句关于参数初始化的区别是什么?

class A::A() 
    : a(0), b(0), c(0) 
{ 
}

class A::A() 
{ 
    a = 0 
    b = 0; 
    c = 0; 
} 

我知道有"直接初始化"和"复制初始化",但我不知道其他的区别,而且第一个陈述是否有任何描述?

先谢谢了。


@user692270 我已经编辑并添加了一个缺失的冒号到你的初始化列表示例中。 - razlebe
2
请注意,第二个示例中的 = 既不是“直接初始化”也不是“复制初始化”,而是赋值。 - MSalters
3
另一个问题,用户692270接受了@karthik的答案。停止使用两个账户进行点赞刷分。 - Sebastian Mach
4个回答

35
使用初始化列表,成员变量只会被创建和初始化一次,采用给定值进行初始化。
使用赋值操作,成员变量会先被初始化为默认值,然后在构造函数体内重新赋值。
在某些情况下(如常量、引用),你只能使用初始化列表,因为这些成员必须用有效值进行初始化,而且一旦构造完成后,重新赋值是不合法的。
在其他情况下,即使赋值操作是可行的,初始化列表仍然更加推荐,因为它避免了额外的工作(双重初始化对于某些类型来说是代价高昂的)并且遵循常见的惯例,使你的代码更易于理解和维护。
需要注意的一个问题是成员初始化的顺序是由它们在类中声明的顺序定义的,而不是由它们在初始化列表中列出的顺序。例如:
class Example {
  int a, b, c;

  Example() : a(1), c(2), b(c) {}
}

由于bc之前被初始化,因此产生了未定义行为,其值未定义。为避免混淆和潜在的这种微妙错误,请始终按照类中声明成员的顺序在初始化列表中列出它们。

起初可能看起来有些晦涩,但其中存在一个原因。在C++中,保证按照创建顺序的完全相反顺序销毁类的所有成员。现在,类可以具有多个构造函数,每个构造函数都有自己的初始化列表,并且(不幸的是,有人可能会说)初始化列表可以按程序员想要的任何方式排序。如果初始化列表的顺序决定了实际的初始化顺序,则运行时应该以某种方式维护有关每个对象的数据,以记住它创建时使用的构造函数以及成员应该销毁的顺序。这将增加运行时开销,而没有明显的好处,因此 - 符合一般C++哲学的“只为所用付费”-决定初始化顺序一劳永逸地由声明顺序定义。


4
当初始化过程耗费巨大,或处理的类型没有默认构造函数时,这一点变得至关重要。 - Björn Pollex
@MSalters,你是对的,也许我在这里过于细致了。虽然我不禁想到,可能有人正在尝试编写像C():aConst(0){aConst = theRealValue;}这样的代码 :-) - Péter Török

10

默认初始化列表的目的是用于在类内初始化常量变量。 因为常量变量在对象初始化之前就被初始化了。

我提供一个示例来解释这两种初始化之间的区别:

 class A
   {
       private:
          const int x;

  };

  A::A():x(5)        //this code works fine
  {

    }

    A::A()       //this code is wrong.const variable is not initialized once object         
   {
    x=5;
    }

1
这不仅适用于初始化常量,而且适用于所有成员。对于引用类型也是强制要求的。 - Péter Török
2
没有默认构造函数的结构体类型成员、类类型成员和基类。 - MSalters

3

主要的区别在于,第一种情况下类成员是被初始化的,而在第二种情况下它们是被赋值的。对于非整数类型,这意味着在第二种情况下你会使用operator=来为你的类成员赋值。

通常情况下,更倾向于使用第一种情况,因为在这种情况下,类成员是在构造函数体之前被初始化。

此外,在某些情况下不能使用赋值操作,例如当类成员被声明为const时。


1
取自Marshall Cline的C++ FAQ第10.6节
初始化列表。实际上,构造函数应该尽量在初始化列表中初始化所有成员对象。下面讨论一个例外情况。考虑以下使用初始化列表初始化成员对象x_的构造函数:Fred::Fred() : x_(whatever) { }。这样做最常见的好处是提高性能。例如,如果表达式whatever与成员变量x_的类型相同,则无需创建对象的单独副本,而是直接将whatever表达式的结果构造在x_内——编译器不会执行单独的复制操作。即使类型不同,编译器通常也能够通过初始化列表比赋值更好地完成工作。另一种(低效的)构造函数构建方式是通过赋值完成,例如:Fred::Fred() { x_ = whatever; }。在这种情况下,表达式whatever会导致创建一个临时对象,并将该临时对象传递到x_对象的赋值运算符中。然后,在分号处销毁该临时对象。这是低效的。如果使用赋值运算符构造函数存在另一个低效的方面:成员对象将通过其默认构造函数完全构造,例如分配一些默认数量的内存或打开某个默认文件。如果这一切都是为了白费力气,那么就很可能是因为whatever表达式和/或赋值运算符导致对象关闭该文件和/或释放该内存(例如,如果默认构造函数没有分配足够大的内存池或打开了错误的文件)。结论:在其他一切条件相等的情况下,如果使用初始化列表而不是赋值,则您的代码将运行得更快。注意:如果x_的类型是某些内置类型,例如int、char*或float,则没有性能差异。但即使在这些情况下,我个人的偏好也是为了一致性,在初始化列表中设置这些数据成员,而不是通过赋值方式。另一个支持对内置类型使用初始化列表的对称论据:非静态常量和非静态引用数据成员不能在构造函数中赋值,因此出于对称性,初始化列表中的所有内容都应该被初始化。现在来讲例外情况。每个规则都有例外(嗯;“每个规则都有例外”是否有例外?这让我想起了哥德尔不完备定理),“使用初始化列表”的规则也有一些例外。底线是要用常识:如果不使用它们更便宜、更好、更快等,则绝不使用它们。当您的类具有需要以不同顺序初始化此对象的数据成员的两个构造函数时,或者当两个数据成员是自引用的时,或者当数据成员需要引用此对象时,并且您想要避免编译器在开始构造函数体的{之前发出关于使用this关键字的警告(当您的特定编译器发出那个特定的警告时),或者当您需要对变量(参数、全局等)进行if/throw测试以先使用该变量来初始化其中一个this成员时,可能会发生这种情况。这个列表并不详尽,请不要写信给我让我添加另一个“或者当……”。重点是:要用常识。

1
一个很好的答案,确实如此,但可能有点...冗长。 - deceleratedcaviar

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