初始化列表
什么是初始化列表
与其他函数不同,构造函数中除了有名字、参数列表和函数体之外,还可以有初始化列表。 初始化列表以:
开头,后跟一系列以,
分隔的初始化字段。比如:
struct foo
{
string name ;
int id ;
foo(string s, int i):name(s), id(i){
} ; // 初始化列表
};
构造函数的两个执行阶段
构造函数的执行可以分为两个阶段,初始化阶段和计算阶段,初始化阶段早于计算阶段
初始化阶段
所有类类型
的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中
计算阶段
一般用于执行函数体内的赋值操作,下面的代码定义两个结构体,其中Test1有构造函数,拷贝构造函数及赋值运算符,为的是方便查看结果。Test2是个测试类,它以Test1的对象为成员,我们看一下Test2的构造函数是怎么样执行的。
#include <iostream>
using namespace std;
struct Test1
{
Test1() // 无参构造函数
{
cout << "Construct Test1" << endl ;
}
Test1(const Test1& t1) // 拷贝构造函数
{
cout << "Copy constructor for Test1" << endl ;
this->a = t1.a ;
}
Test1& operator = (const Test1& t1) // 赋值运算符
{
cout << "assignment for Test1" << endl ;
this->a = t1.a ;
return *this;
}
int a ;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1) // 先调用无参构造函数,然后调用赋值运算符
{
test1 = t1 ;
}
};
struct Test3
{
Test1 test1 ;
Test3(Test1 &t1): test1(t1 ){
} // 初始化列表调用的是拷贝构造函数
};
int main(){
Test1 t1; // // 无参构造函数
printf("\n");
Test2 t2(t1);
printf("\n");
Test3 t3(t1);
}
解释一下,第一行输出结果对应调用代码中第一行,构造一个Test1对象。第二行输出对应Test2构造函数中的代码,用默认的构造函数初始化对象test1,这就是所谓的初始化阶段。第三行输出对应Test1的赋值运算符,对test1执行赋值操作,这就是所谓的计算阶段。
初始化顺序
列表中的成员初始化器的初始化顺序与出现顺序无关,实际上,初始化的实际顺序如下:
- 如果构造函数是最终派生类,则按基类声明的深度优先、从左到右的遍历中出现顺序(从左到右指的是基说明符列表中所呈现的),初始化各个虚基类
- 然后,以在此类的基类说明符列表中出现的从左到右顺序,初始化各个直接基类
- 然后,以类定义中的声明顺序,初始化各个非静态成员
- 最后,指向构造函数体
(注意:如果初始化的顺序是由不同构造函数中的成员初始化器列表中的出现所控制,那么析构函数就无法确保销毁顺序是构造顺序的逆序了)
#include <fstream>
#include <string>
#include <mutex>
struct Base {
int n;
};
struct Class : public Base
{
unsigned char x;
unsigned char y;
std::mutex m;
std::lock_guard<std::mutex> lg;
std::fstream f;
std::string s;
Class ( int x )
: Base {
123 }, // 初始化基类
x ( x ), // x(成员)以 x(形参)初始化
y {
0 }, // y 初始化为 0
f{
"test.cc", std::ios::app}, // 在 m 和 lg 初始化之后发生
s(__func__), //__func__ 可用,因为初始化器列表是构造函数的一部分
lg ( m ), // lg 使用已经初始化的 m
m{
} // m 在 lg 前初始化,即使此出它最后出现
{
} // 空复合语句
Class ( double a )
: y ( a+1 ),
x ( y ), // x 将在 y 前初始化,其值不确定
lg ( m )
{
} // 基类初始化器未出现于列表中,它被默认初始化(这不同于使用 Base(),那是值初始化)
Class()
try // 函数 try 块始于包含初始化器列表的函数体之前
: Class( 0.0 ) // 委托构造函数
{
// ...
}
catch (...)
{
// 初始化中发生的异常
}
};
int main() {
Class c;
Class c1(1);
Class c2(0.1);
}
为什么要使用初始化列表
初始化类的成员有两种方式:
- 使用初始化列表
- 在构造函数体内进行赋值操作
使用初始化列表主要是基于性能问题。对于内置类型,如int、float等,使用初始化列表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?由上面的测试可知,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的
所以一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。
哪些东西必须放在初始化列表中
除了性能问题之外,有些时场合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写到初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
对于没有默认构造函数的类,我们看一个例子。
using namespace std;
struct Test1
{
Test1(int a):i(a){
}
int i ;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1)
{
test1 = t1 ;
}
};
以上代码无法通过编译,因为Test2的构造函数中test1 = t1这一行实际上分成两步执行。
-
调用Test1的默认构造函数来初始化test1
-
调用Test1的赋值运算符给test1赋值
但是由于Test1没有默认的构造函数,所谓第一步无法执行,故而编译错误。正确的代码如下,使用初始化列表代替赋值操作。
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){
}
}