6、构造函数初始化器
到目前为止,在构造函数体中对数据成员初始化,如下所示:
SpreadsheetCell::SpreadsheetCell(double initialValue)
{setValue(initialValue);
}
c++提供了在构造函数中初始化数据成员的另一种方法,叫做构造函数初始化器,也叫做成员初始化列表。下面是同样的SpreadsheetCell构造函数,重写以使用构造函数初始化器语法:
SpreadsheetCell::SpreadsheetCell(double initialValue): m_value{ initialValue }
{
}
可以看出,构造函数初始化器在构造函数参数列表与构造函数体的大括号之间出现了。列表以冒号开始,用逗号分隔。列表中的元素是使用函数名称或者统一的初始化语法的数据成员的初始化,对基础类构造函数的调用,或者是对代理构造函数的调用,我们后面会讨论。
使用构造函数初始化器初始化数据成员提供了与在构造函数体内对数据成员初始化相比不同的行为。当c++生成一个对象时,在调用构造函数之前必须生成对象的所有数据成员。在构造函数体内给对象赋值时,并不是构造了对象。只是修改了它的值。构造函数初始化器允许为生成的数据成员提供初值,这要比之后赋值效率更高。
如果类拥有数据成员的类的对象有默认的构造函数,在构造函数初始化器中就不必显式地初始化对象。例如,如果有一个std::string的数据成员,其缺省构造函数将其初始化为空的字符串,所以在构造函数初始化器中将其初始化为””就显得多余了。
另一方面,如果拥有数据成员的类的对象没有缺省构造函数,就一定要用构造函数初始化器来正确地构造该对象。例如,看下面的SpreadsheetCell类:
export class SpreadsheetCell
{
public:SpreadsheetCell(double d);
};
这个类只有一个构造函数接受一个double并且不包含缺省构造函数。可以使用类作为另一个类的数据成员,如下:
class SomeClass
{
public:SomeClass();private:SpreadsheetCell m_cell;
};
可以像下面这样实现SomeClass构造函数:
SomeClass::SomeClass() { }
然而,这样实现的代码编译不会成功。编译器不知道如何初始化SomeClass的m_cell数据成员,因为它没有提供缺省构造函数。
一定要像下面这样在构造函数初始化器中初始化m_cell数据成员:
SomeClass::SomeClass() : m_cell { 1.0 } { }
注意:构造函数初始化器允许在数据成员生成时初始化。
有些程序员喜欢在构造函数体中赋初值,即使效率不佳。然而,有些数据类型一定要在构造函数初始化器中初始化,或者使用类内初始化器。列表总结如下:
数据类型 | 解释 |
常量数据成员 | 在常量生成后不能合法地给其赋值。只能在生成时赋值 |
引用数据成员 | 引用在没有指向时无法存在,一旦生成,引用就不能修改为指向其它。 |
没有缺省构造函数的对象数据成员 | c++尝试使用缺省构造函数成初始化成员对象。如果缺省构造函数不存在,就不能初始化对象,必须显式地指出要调用哪个构造函数。 |
没有缺省构造函数的基础类 | 以后再讨论 |
对于构造函数初始化器有一个警告:要在类定义中以出现的顺序初始化数据成员,而不是在构造函数初始化器中的顺序!看下面的Foo类的定义。其构造函数只是保存了一个double值并将其打印到控制台。
class Foo
{
public:Foo(double value);private:double m_value { 0 };
};Foo::Foo(double value) : m_value { value }
{println("Foo::m_value = {}", m_value);
}
假设有另外一个类,包含了一个Foo对象作为它的一个数据成员。
class MyClass
{
public:MyClass(double value);private:double m_value { 0 };Foo m_foo;
};
其构造函数实现如下:
MyClass::MyClass(double value) : m_value { value }, m_foo { m_value }
{println("MyClass::m_value = {}", m_value);
}
构造函数初始化器首先保存给定的值到m_value中,然后使用m_value作为参数调用Foo构造函数。可以生成MyClass的一个实例如下:
MyClass instance { 1.2 };
下面是程序的输出:
Foo::m_value = 1.2
MyClass::m_value = 1.2
这样,一切显得很完美。现在对MyClass定义做一个小小的修改;简单地将m_value与m_foo数据成员调换一下顺序。其他什么都不动。
class MyClass
{
public:MyClass(double value);private:Foo m_foo;double m_value { 0 };
};
程序的输出依赖于你的系统。可能会像下面这样:
Foo::m_value = -9.255963134931783e+61
MyClass::m_value = 1.2
这就不是你想要的了。你可能会想,基于构造函数初始化器,在使用m_value调用Foo构造函数之前m_value已经初始化了。但是c++不是这么干的。数据成员的初始化是依据其出现在类中定义的顺序,而不是构造函数初始化器中的顺序!所以,在这种情况下,Foo构造函数会先用一个没有初始化的m_value来进行调用。
注意有些编译器会报警告,当构造函数初始化器的顺序与类定义的顺序不一致时。
在这个例子中,有一个很容易的修复方法,不要将m_value传给Foo构造函数,只将value参数传入:
MyClass::MyClass(double value) : m_value { value }, m_foo { value }
{println("MyClass::m_value = {}", m_value);
}
警告:构造函数初始化器依其在类中的定义顺序初始化其数据成员,而不是在构造函数初始化器列表中的顺序。