当你写下一个constructor时,你就有机会设定class members的初值。要不就是在member initialization list,要不就是在constructor函数本体之内进行初始化。除了以下四种情况,你的任何选择其实都差不多。下面来看看这四种情况下的微妙的陷阱。
一、原始的初始化方法
下面代码片段中的初始化方法是最常见的一种初始化方法,程序可以被 正确运行,但效率不高。
class Word
{
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
};
为什么效率不高呢?因为编译器这样对代码进行转换:在这里,Word constructor 会产生一个临时性的String object,然后将它初始化为0,之后以一个assignment运算符将临时性的object指定给_name,随后摧毁那个临时性的object。下面是constructor可能的内部扩张的结果:
// C++伪码
Word::word(/*this pointer goes here*/)
{
// 调用String的default constructor
_name.String::String();
// 产生临时对象
String tmp = String(0);
// "memberwise"地拷贝_name
_name.String::operator=(tmp);
// 摧毁临时性对象
tmp.String::~String();
// 行为良好的member
_cnt = 0;
}
二、改进的初始化方法
Word::Word():_name(0){
_cnt = 0;
}
这种半constructor函数体的初始化,半“成员初始化列表”的初始化方法比上面原始的初始化方法的效率要高。这是因为上面的代码会被扩张称这个样子:
// C++伪码
Word::word(/*this pointer goes here*/)
{
// 调用String(int) constructor
_name.String::String(0);
// 行为良好的member
_cnt = 0;
}
三、程序员坚持的初始化方法
在构造函数体内使用“explicit user code”对data member进行初始化,程序会表现得效率低下;越多的data member的初始化工作加入到Member Initialization List中,构造函数体的扩张就越小,效率随之越高,显然程序员坚持初始化方法是:让全部的data member都使用Member Initialization List的方式进行初始化,甚至是行为良好的member。就像这个样子:
Word::Word():_name(0), _cnt(0)
{
}
使用“成员初始化列表”方式进行初始化的构造函数,将被扩张称酱紫:
// C++伪码
Word::word(/*this pointer goes here*/)
{
// 调用String(int) constructor
_name.String::String(0);
// 行为良好的member
_cnt = 0;
}
但是,问题来了!这里有需要我们注意的地方:list中的项目顺序是由class中的members声明的顺序决定的,不是由initialization list 中的排列顺序决定的。这是什么意思呢?请看下面的代码:
class X
{
int i;
int j;
public:
// 哇哦!你看出来问题了吗?
X(int val):j(val), i(j)
{}
// ...
};
在上述代码的list中,先把val赋值给j,然后把j的值指定给i。但是这是很危险的。再次强调,“成员初始化列表”中的初始化顺序是由class中members的声明顺序决定
。
这个“臭虫”的困难度在于它很难被观察出来,甚至有些编译器也不给出警告。这里Lippman的建议是:使用下面的方式。
// 比较受到喜爱的方式
X::X(int val):j(val)
{
i = j;
}
这里还有一个有趣的问题,initialization list中的项目被安插到constructor中,会继续保存声明的顺序吗?也就是说,已知:
X::X(int val):j(val)
{
i = j;
}
j的初始化操作会被安插在explicit user assignment操作(i=j)之前还是之后?如果声明的顺序被继续保存,那么这份代码大大不妙。但其实这是正确的,因为initialization list中的项目得初始化化操作被放在explicit user code之前。
请记住:
编译器会对initialization list一一处理并可能重新排序,以反映出memebers的声明顺序。它会安插一些代码到constructor体内,并置于任何explicit user code之前。
另外一个常见的问题是,你是否可以像下面这样,调用一个member function以设定一个member的初值:
// X::xfoo(int)被调用,这样好吗?
X::X(int val):i(xfoo(val)), j(val)
{
}
其中xfoo(int)为X的一个成员函数。答案是:可以。但是,似乎有时不妥。Lippman给出的忠告是:请使用“存在于constructor体内的一个member“,而不要使用”存在于member initialization list中的member“,来为一个member设定初值。你并不知道xfoo函数对X object对象的依赖性有多高,如果你把xfoo放在构造函数体内,那么对于”到底哪一个member在xfoo执行期间被设定初值“这件事,就可以确保不会发生模棱两可的事情。
Member function的使用是合法的,这是因为和此object相关的this指针已经被构建妥当,而constructor大约被扩充为:
// C++伪码:constructor被扩充后的结果
X::X(/*this pointer*/, int val)
{
i = this->xfoo(val);
j = val;
}
最后,如果一个derived class member function被调用,其返回值被当作base class constructor的一个参数,将会如何:
// 调用FooBar::fval()可以吗?
class FooBar : public X
{
itn _fval;
public:
int fval() { return _fval }
FooBar(int val): _fval(val),
X(fval())// fval()的返回值作为base class constructor的参数
{}
};
当然不妥,派生类创建实例时,首先调用基类的构造函数,再调用派生类的构造函数,显然派生类的成员函数的返回值作为基类构造函数的参数是不妥的。下面是它可能扩张的结果:
// C++伪码
FooBar::FooBar(/*this pointer goes here*/)
{
// 哇哦,实在不是一个好主意
X::X(this, this->fval());
_fval = val;
}
四、总结
请注意:
1. 成员初始化列表中项目的初始化操作顺序和类中成员的声明顺序有关,而非列表中的排列顺序;
2. 成员初始化列表中初始化的项目被成员转化后,一定是安插在explicit user code之前;
3. 尽量使用成员初始化列表的方式对members进行初始化,不过要注意初始化的顺序。
参考文献:《深度探索C++对象模型》侯捷 译