设为首页 加入收藏

TOP

C++对象模型――成员初始化列表(第二章)
2015-11-21 00:56:25 来源: 作者: 【 】 浏览:2
Tags:对象 模型 成员 初始 第二章

2.4 成员初始化列表 (Member Initialization List)

当编写一个类的构造函数时,有可能设定类成员的初始值,或者通过成员初始化列表初始化,或者在构造函数内初始化,除了四种情况,其实任何选择都差不多。
本节中,首先澄清何时使用初始化列表才有意义,然后解释初始化列表内部的真正操作是什么,然后再看一些微妙的陷阱。
下列情况中,为了让程序能够被顺利编译, 必须使用成员初始化列表(不能在构造函数内初始化):
1. 当初始化一个引用的成员时
2. 当初始化一个常量成员( const member)时
3. 当调用一个基类的构造函数,而它拥有一组参数时
4. 当调用一个成员类对象的构造函数,而它拥有一组参数时

在这四种情况中,程序可以被正确编译并执行,但是效率不佳。例如:
class Word {
private:
    String _name;
    int _cnt;
public:
    // 没有错误,但是太naive
    Word() {
        _name = 0;
        _cnt = 0;
    }
};
在这里,Word构造函数会先产生一个临时性的String对象,然后将它初始化,再以一个赋值运算符将临时对象指定给_name,然后再销毁临时对象。以下是构造函数可能的内部扩张结果:
// C++伪代码
Word::Word( /* This pointer goes here */) {
    // 调用String的default constructor
    _name.String::String();
    // 产生临时对象
    String temp = String(0);
    // memberwise地拷贝_name
    _name.String::operator=(temp);
    // 销毁临时对象
    temp.String::~String();

    _cnt = 0;
}
对程序代码反复审查并修正,得到一个明显更有效率的实现方法:
// 较佳的方式
Word::Word : _name(0) {
    _cnt = 0;
}
    它会被扩张成这样子:
// C++伪代码
Word::Word( /* This pointer goes here */ ) {
    // 调用String(int) constructor
    _name.String::String(0);
    _cnt = 0;
}
顺便一提,陷阱最可能发生在这种形式的template code中:
template 
  
   
foo
   
    ::foo(type t) { // 可能是也可能不是个好主意 // 视type的真正类型而定 _t = 0; }
   
  
这会引导某些程序员十分积极进取地坚持所有的成员初始化操作必须在成员初始化列表中完成,甚至即使是一个行为良好的成员如_cnt
// 坚持此种代码风格
Word::Word()
    : _cnt(0), _name(0)
{}
成员初始化列表中到底发生了什么事情?许多人对list的语法感到迷惑,误以为它是一组函数调用,当然不是!
编译器会一一操作初始化列表,以适当地次序在构造函数内插入初始化操作,并且在任何显式用户代码之前。例如,先前的Word构造函数被扩张为:
// C++伪代码
Word::Word( /* this pointer goes here */ ) {
    _name.String::String(0);
    _cnt = 0;
}
它看起来很像是在构造函数中指定_cnt的值,事实上,有一些微妙的地方要注意: 列表中的项目次序是由类中成员的声明次序决定的,而不是初始化列表中的排列顺序决定的。在本例的Word类中,_name被声明在_cnt之前,所以它的初始化比_cnt早。
初始化顺序和初始化列表中的项目排列顺序之间的差异,会导致下面意想不到的危险:
class X {
private:
    int i;
    int j;
public:
    X (int val)
        : j(val), i(j)
    {}
};
上述程序代码看起来好像要把j设初值为val,然后把i设初值为j。但是由于声明顺序的原因,初始化列表中的i(j)比j(val)更早执行。而j开始并没有初始值,所以i(j)的执行结果导致i会被初始化为一个无法预测的值。
这个bug的困难度在于它很不容易被观察出来,编译器应该发出一个警告消息。但是目前只有一个编译器(g++)做到这一点。(测试VS2010确实没有给出警告消息,Lippman确实牛掰,2000年提的问题,VS10都没解决),X x(0);然后输出x的i,j的值,结果如下图所示:
\
i的值不正常,j值正常.VS10中并未给出任何警告信息.
还有一个有趣的问题,初始化列表中的项目插入到构造函数中,会继续保存声明次序吗?也就是说,已知:
// 一个有趣的问题
X::x(int val) : j(val) {
    i = j;
}
j的初始化顺序会插入在显式用户赋值操作(i=j)之前还是之后呢?
如果声明次序继续被保存,则这段代码会出现很大问题(因为先要将i初始化,再将j初始化).事实上,这段代码是正确的,因为初始化列表的项目被插入在显式用户代码之前.
另一个常见的问题是,是否能够如下所示,调用一个成员函数设置成员的初始值:
// X::xfoo()被调用
X::X(int val) : i(xfoo(val)), j(val)
{}
其中xfoo()是X的一个成员函数,答案是yes.但是最好使用存在于构造函数提内的一个成员,而不要使用存在于成员初始化列表中的成员,来为另一个成员设定初始值.并不确定xfoo()对X object的依赖性有多高,如果把xfoo()放在构造函数体内,那么对于到底哪一个member在xfoo()执行时被设置初始值这件事,就可以给出确定的答案.
成员函数的使用是合法的,因为和此object相关的this指针已经被建构妥当,而构造函数大约被扩张为:
// constructor扩张后的结果
X::X( /* this pointer, */ int val) {
    i = this->xfoo(val);
    j = val;
}
如果一个派生类成员函数被调用,其返回值被当作基类构造函数的一个参数,将会如何?
// 调用FooBar::fval()可以吗?
class FooBar : public X {
private:
    int _fval;
public:
    int fval() { return _fval; }             // derived class member function
    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;
}
它的确不是一个好主意.

总结

编译器对初始化列表一一处理并可能重新排序,以反映出成员的声明次序,它会插入一些代码到构造函数体内,并且插入在任何显式用户代码之前.

?

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇hihoCoder_#1067_最近公共祖先.. 下一篇hihoCoder_#1066_无间道之并查集

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: