构造函数是完成非静态成员初始化的函数。许多程序员更喜欢在构造函数体中进行赋值。除了几个实际的特殊情况外,非静态成员的初始化应该在构造函数的初始化列表中完成,或者从C++11开始,当在类中声明它们时使用默认成员初始化方法进行初始化。在C++11之前,类的常量和非常量非静态数据成员必须在构造函数中初始化,类中声明的初始化仅适用于静态常量。正如我们即将看到的,C++11中取消了这个限制,它允许在类声明中初始化非静态成员。此初始化称为 默认成员初始化 ,将在后续章节进行说明。
本节将探索如何完成非静态成员初始化。对每个成员使用适当的初始化方法不仅会使代码更高效,而且会使代码结构更加良好,从而增强代码可读性。
要初始化类的非静态成员,应该:
❍ 对静态常量和非静态常量使用默认成员初始化(见以下代码中的[1]和[2])。
❍ 使用默认成员初始化可以为具有多个构造函数的类的成员提供默认值,构造函数会使用统一初始化方法初始化这些成员(见以下代码中的[3]和[4])。
❍ 使用构造函数初始化列表来初始化没有默认值但依赖于构造函数参数的成员(见以下代码中的[5]和[6])。
❍ 当其他选项不可用时,在构造函数体内使用赋值操作实现初始化(例如使用this指针初始化数据成员,检查构造函数参数值,在用那些值初始化成员或者两个非静态成员自引用之前抛出异常)。
以下代码展示了这几种形式的初始化:
非静态成员应该用构造函数初始化列表的方式进行初始化,像下面这个示例这样:
然而,许多开发人员并不喜欢使用初始化列表这种方式,他们更倾向于在构造函数体内进行赋值来完成初始化,甚至两者混用。这有几个原因:对于有很多成员的类,在构造函数体内进行赋值初始化比一长串的初始化列表(也许会显示多行)更简洁;他们所熟悉的别的语言根本就没有初始化列表。不幸的是,也有可能他们根本不知道初始化列表。
重要的是要注意,非静态数据成员初始化的顺序是它们在类定义中声明的顺序,而不是它们在构造函数初始化列表中初始化的顺序。另外,非静态数据成员的销毁顺序与构造顺序相反。
通过在构造函数体内进行赋值来实现初始化并不高效,因为这样会导致先创建一个临时对象,然后再销毁该临时对象。如果不用初始化列表进行初始化,非静态成员会通过默认构造函数进行初始化,然后在构造函数体赋值时,调用赋值操作符。这样做效率很低,因为如果默认构造函数分配一个资源(内存或者文件),这个资源将会被析构,然后在调用赋值操作符时被重新分配。请看下面的代码片段:
上面代码的输出结果如下,展示了成员f是如何在构造函数里面先被初始化,然后被重新赋一个值的:
初始化方法从构造函数体内赋值改为初始化列表,会用复制构造函数调用替换默认构造函数调用和赋值操作符:
加了上述代码之后的输出如下:
基于这些原因,至少对于内置类型(例如bool、char、int、float、double或者指针)以外的类型,都应该采用构造函数初始化列表这种形式进行初始化。然而,为了保持初始化风格的一致性,在可能的情况下也应该总是使用构造函数初始化列表进行初始化。在以下几种情况但不仅仅局限于这几种(可以适当扩展),不能用初始化列表进行初始化:
❍ 如果一个成员必须使用包含该成员的对象的引用或者指针来进行初始化,那么有的编译器在初始化列表中使用this指针会发出警告,因为编译器认为在该对象被构造之前使用了this指针。
❍ 如果两个成员必须互相引用。
❍ 如果想测试输入参数,并且想在用参数值初始化非静态数据成员之前抛出异常。
从C++11标准开始,非静态数据成员可以在类声明时初始化。这被称作 默认成员初始化 ,因为它表示应该用默认值初始化。默认成员初始化适用于常量以及那些不基于构造函数参数初始化的成员(换言之,成员的初始化不依赖对象的构造方式):
在前面的例子中,DefaultHeight和DefaultWidth都是常量,因此它们不依赖对象的构造方式,所以它们在声明时初始化。textFlow对象是非常量非静态数据成员,它的值不依赖对象的初始化(它的值可以通过另外的成员函数进行修改),因此也可以在声明的时候用默认成员初始化方法进行初始化。另外,text也是一个非常量非静态数据成员,但是它的初始值依赖对象的构造方式,因此它是通过传给构造函数的参数值以构造函数初始化列表的形式进行初始化的。
如果一个数据成员既可用默认成员初始化方法初始化,也可用构造函数初始化列表形式初始化,则后者优先,默认值会被丢弃。为了说明这一点,我们再看看之前代码中的foo类以及下面的bar类:
输出有所不同,在这个例子中,它的输出如下:
输出不同的原因是默认初始化列表的值被丢弃了,对象并没有被初始化两次。
❍ 阅读1.3节,以了解花括号初始化的工作原理。