一、什么是初始化列表?
C++类的成员变量初始化有两种途径,第一种是在类的constructor函数本体内,另一种途径就是经由初始化列表(initialization list)。如:
在构造函数体内初始化:
class Person
{
public:
Person()
{
m_name = 0;
m_age = 0;
}
private:
String m_name;
int m_age;
};
使用初始化列表初始化:
class Person
{
public:
Person() : m_age(0), m_name(0)
{}
private:
String m_name;
int m_age;
};
在大多数情况下,这两种方式都是可以的,但有四种情况必须使用初始化列表。
- 当初始化一个reference member时;
- 当初始化一个const member时;
- 当调用一个base class的constructor,而它有一组参数时;
- 当调用一个member class的constructor,而它有一组参数时。
二、两种初始化方式编译器实现的差异
// **在构造函数体内初始化**
class Person
{
public:
Person()
{
m_name = 0;
m_age = 0;
}
private:
String m_name;
int m_age;
};
这段程序可以编译并且正常执行,但是效率有所损耗。在这里,Person constructor会产生一个领实习的*String* object,然后将它初始化,之后以一个assignment运算符将临时的object指定给m_name,随后再摧毁那个临时性的object。以下是constructor内部扩张的结果:
// C++ 伪代码
Person()
{
//调用String的缺省构造函数
m _name.String::String();
//产生临时性对象
String temp = String(0);
//逐一成员地拷贝 _name
m_name.String::operator=( temp );
//摧毁临时性对象
temp.String::~String();
m_age = 0;
}
而使用初始化列表的方式,如
//**使用初始化列表初始化**
class Person
{
public:
Person() : m_age(0) , m_name(0)
{}
private:
String m_name;
int m_age;
};
其构造函数扩张的结果是:
// C++ 伪代码
Person()
{
//调用String(int) 构造函数
m_name.String::String(0);
m_age = 0;
}
三、编译器对初始化列表做了哪些事
编译器会一一操作initialization list,以适当的顺序在constructor之内安插初始化操作,并且在任何用户写的代码之前。要注意的是:list中的项目顺序是由class中的member声明顺序决定的,不是由initialization list中的排列顺序决定的。在本例的Person class中,m_name被声明与m_age之前,所以它的初始化比m_age早。
四、初始化列表的陷阱
“初始化顺序”与“initialization list中的项目排列顺序”之间的外观错乱,会导致下面意向不到的危险:
class X
{
public:
X(int val): j(val), i(j)
{}
public:
int i;
int j;
};
上述程序代码看起来像是要把j设置初始值为val,再把i的初始值设置为j。问题在于,由于声明的顺序缘故,initialization list中的i(j) 其实比j(val)更早执行。但是因为j一开始未初始化,所以i(j)的执行结果将导致i无法预知其初值。并且编译器一般不会报错和给出警告。(只有g++这个编译会给出警告!)如何避免这个陷阱呢?
建议:把一个member的初始化操作和另一个放在一起,放在constructor之内,如下。
X(int val): j(val)
{ i = j;} // i并不是由构造参数直接初始化,将其放在构造函数体内,而不是初始化列表中
下面给出有陷阱的代码和修改好的代码:
lass X
{
public:
X(int val): j(val), i(j)
{}
public:
int i;
int j;
};
class Y
{
public:
Y(int val): j(val)
{
i = j;
}
public:
int i;
int j;
};
int main()
{
X x88888888888888(3);
Y y(5);
std::cout << "x.i = " << x.i << " x.j = " << x.j << std::endl;
std::cout << "y.i = " << y.i << " y.j = " << y.j << std::endl;
return 0;
}
运行结果(vc++):
以上运行结果说明两个问题:
- 初始化列表的执行顺序与列表中项目的顺序无关,而是由成员变量的声明顺序决定的;
- 初始化列表执行的初始化代码是安插在用户写的代码之前的。(因为,在类Y中,若j(val)被安插在i=j之后,那么i必将是无法预知的值,因为执行这一句时,j还未被初始化,然而,执行的结果是正确。说明j(val)被安插在i=j之前)
五、总结
简略地说,编译器会对initialization list 一一处理并可能重新排序,以反映出menbers的声明顺序,编译器会安插一些代码到constructor体内,并置于任何用户写的代码之前。
- 初始化列表和构造函数内初始化大多数情况下都是可以用的,除了四种情况必须使用初始化列表;
- 初始化列表比直接在构造函数内初始化效率要高;
- 初始化列表执行的顺序是由成员变量声明的顺序决定的,而与初始化列表的元素顺序无关;
- 在执行顺序上,初始化列表的执行比构造函数体内初始化操作要早**