当需要初始化类的成员时,可以在构造函数体中完成,也可以在成员初始化列表中完成
那么:
- 什么时候用成员初始化列表呢?
- 列表内部的真正操作是什么?
- 有什么缺点?
下列四种情况下,必须使用成员初始化列表:
- 当初始化一个引用成员时
- 当初始化一个const成员时
- 当调用一个基类的构造函数,而它拥有一组参数时
- 当调用一个成员类的构造函数,而它拥有一组参数时
例子:
class Word{
String _name;
String _cnt;
public:
// 没有错误,不过太天真
Word(){
_name = 0;
_cnt = 0;
}
};
在这里,Word构造函数会先产生一个临时的String对象,然后将它初始化,再以一个=运算符将林对对象指定给_name,然后再摧毁临时对象:
Word::Word(){
//调用String的默认构造函数
_name.String::String();
//生成一个临时对象
String temp = String(0);
//memberwise的拷贝_name;
_name.String::operator=(temp);
//销毁临时对象
temp.String::~String();
_cnt = 0;
}
实际上,一个更有效率的实现是:
Word::Word():_name(0){
_cnt = 0;
}
它会被扩张成这个样子:
Word::Word(){
// 调用String(int)构造函数
_name.String::String(0);
_cnt = 0;
}
顺便,陷进最有可能发生再这种形式的模板代码中:
template<class type>
foo<type>::foo(type t){
// 可能是也可能不是个好主意,视type的真正类型决定
_t = t;
}
这会引导某些程序员十分积极的坚持所有的成员初始化操作必须在成员初始化列表中完成,甚至即使是一个行为良好的成员比如_cnt:
Word::Word() : _cnt(0), _name(0) {}
那么:成员初始化列表中到底发生了什么事?
- 编译器会意义操作初始化列表,以适当的次序在构造函数中安插初始化操作,并在任何显式用户代码之前。比如:
Word::Word(){
_name.String::String(0);
_cnt = 0;
}
- 实际上,初始化列表中项目次序是由类中的成员初始化决定,而不是由初始化列表中排列次序决定。比如,上面的Word类中,_name先声明的,所以它的初始化也比较早
- 初始化次序和初始化列表中的项目排列次序之间的外观错乱,可能会导致意想不到的危险:
Class X{
int i;
int j;
public:
X(int val) : j(val), i(j) {};
}
- 上面由于声明此次序,i(j)先调用而j(val)后调用,但是i(j)调用时j还没有被初始化,因此i初始化的结果是不可预知的
问题:初始化列表的项目被安插到构造函数中,会保存声明次序吗?
X::X(int val)
: j(val){
i = j;
}
j的初始化操作会安插在i = j
的之前还是之后?
- 答案是之前。因为初始化列表的项目会被放在显式用户代码之前
问题:是否能够像下面这样,调用一个成员函数以设定一个成员的初值?
// X::xfoo()是否可以被调用?(xfoo是X的成员函数,不需要考虑xfoo中成员是否被初始化)
X::X(int val)
: i(xfoo(val)), j(val){
}
- 答案是yes。这是因为和此object相关的this指针已经被构建妥当,而构造函数被扩充为:
X::X(int val){
i = this->xfoo(val);
j = val;
}
问题:如果一个派生类成员函数被调用,其返回值被当作基类构造函数的一个参数,将会如何?
// 调用FooBar::fval()可以吗?
class FooBar : public X{
int _fval;
public:
int fval() {return _fval;};
FooBar(int val)
: _fval(val), X(fval()) // 派生类成员函数fval()作为基类构造函数的参数
{}
}
- 答案是no。下面是它可能的扩张结果:
FooBar::FooBar(int val){
X::X(this, this->fval());
_fval = val;
}
总结
简单的说:
- 编译器会对成员初始化列表一一处理并可能重新排序,以反映出成员的声明次序
- 它会安插一些代码到构造函数体内,并置于任何显式用户代码之前。