为什么对象使用前需要初始化
1.读取未初始化的值会导致不明确的行为
在某些平台上,仅仅是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机”bits,污染了正在进行读取动作的那个对象,最终导致不可预知的程序行为,以及许多令人不愉快的调试过程。
永远在使用对象之前将它初始化:对于无任何成员的内置类型,你必须手工完成此事;对于内置类型以外的任何其他东西,初始化责任落在构造函数身上,规则是:确保每一个构造函数都将对象的每一个成员初始化。
2.构造函数的初始值有时候必不可少
(1) 当初始化一个reference menber时;
(2) 当初始化一个const menber时;
(3) 当调用一个base class的constructor,而它拥有一组参数时;
(4) 当调用一个member class的constructor,而它拥有一组参数时。
初始化和赋值的区别
1.底层效率
初始化操作直接初始化成员,赋值操作则先初始化再赋值。例如:
(1) 没有使用成员初始化列表
class Word {
string _name;
int _cnt;
public:
//没有使用member initialization list
Word() {
_name = 0;
_cnt = 0;
}
};
在这里,Word constructor会先产生一个临时性额String object,然后将它初始化,之后以一个saaignment运算符将临时性object指定给_name,随后再摧毁那个临时性object。以下是constructor可能的内部扩张结果:
//C++伪代码
Word::Word() {
//调用String的default constructor
_name.String::String();
//产生临时对象
String temp = String(0);
//将temp每个成员都拷贝给_name
_name.String::operator=(temp);
//析构临时对象
temp.String::~String();
_cnt = 0;
}
(2) 使用初始化列表
//使用初始化列表
Word::Word() : _name(0) {
_cnt = 0;
}
它会被扩张成这样:
//C++伪代码
Word::Word() {
//调用String(int) constructor
_name.String::String(0);
_cnt = 0;
}
这个构造函数和上一个的最终结果相同,但通常效率高很多。对于大多数类型而言,比起先调用default constructor然后再调用copy assignment操作符,值调用一次copy constructor是比较高效的。
2.一些成员必须要求初始化
如果成员是const或者是引用,必须将其初始化;当成员属于某种类类型而且该类设有定义默认构造函数时,也必须将这个成员初始化。
成员初始化顺序
1.C++成员初始化次序
C++有着十分固定的成员初始化次序:base classes更早于其derived classes被初始化,而class的成员变量总是以其声明次序被初始化。构造函数初始值列表中初始值的前后位置关系不会影响实际的成员初始化顺序。例如:
Word::Word() : _cnt(0), _name(0) {
}
编译器会一次操作initialization list,以适当顺序在constructor之内安插初始化操作,并且在任何explicit user code之前。例如,上面的Word constructor被扩充为:
//C++伪代码
Word::Word() {
_name.String::String(0);
_cnt(0);
}
列表中的初始化顺序是由class中的members声明顺序决定的,不是成员初始值列表的排列顺序决定的。
2.一个成员用另一个成员来初始化
举个例子:
class X {
int i;
int j;
public:
X(int val) : j(val), i(j) {}
};
i先被初始化,因为这个初始值的效果是试图使用未定义的值j初始化i。
测试代码如下:
#include<iostream>
using namespace std;
class X{
public:
int i;
int j;
public:
//有陷阱的写法
X(int val) : j(val), i(j) {}
};
class Y {
public:
int i;
int j;
public:
//修改后的写法
Y(int val) : j(val) {
i=j;
}
};
int main() {
X x(3);
Y y(5);
cout<<"x.i="<<x.i<<" x.j="<<x.j<<endl;
cout<<"y.i="<<y.i<<" y.j="<<y.j<<endl;
return 0;
}
x.i的值果然不是我们所期望的3。
总结
- 为内置类型对象进行手动初始化,因为C++不能保证初始化它们;
- 构造函数最好使用成员初始值列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment);
- 最好令构造函数初始值的顺序与成员变量的顺序保持一致,如果可能的话,尽量避免使用某些成员初始化其他成员。
参考资料
- 《C++ Primer》7.5 构造函数再探
- 《Effective C++》 条款4 确定对象被使用前已先被初始化
- 《深度探索C++对象模型》 2.4 成员们的初始化队伍