c++构造函数的初始化列表

时间:2021-08-06 05:55:36

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
例如:

struct foo
{
string name ;
int id ;
foo(string s, int i):name(s), id(i){} ; // 初始化列表
};

初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。使用初始化列表主要是基于性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
综上,使用初始化列表有两个原因:
原因1.必须这样做:
《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:
情况一、类成员为没有默认构造函数的类类型
情况二、const修饰的类成员引用成员数据;
情况三、子类初始化父类的私有成员; 如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数

对于情况一的说明:数据成员是对象,并且这个对象只有含参数的构造函数,没有无参数的构造函数;
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。
看一个例子:

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 ;
}
};
int main()
{
Test1 t1 ;
Test2 t2(t1) ;
return 0;
}

输出:
c++构造函数的初始化列表

第一行输出对应调用代码中第一行,构造一个Test1对象。第二行输出对应Test2构造函数中的代码,用默认的构造函数初始化对象test1,这就是所谓的初始化阶段。第三行输出对应Test1的赋值运算符,对test1执行赋值操作,这就是所谓的计算阶段。
对于这个例子,如果Test2中使用初始化列表:

struct Test2
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}

输出如下:
c++构造函数的初始化列表
这种形式来初始化test1,那么调用的时候就会直接使用test1的拷贝构造函数,而不会是先调用无参构造函数再对其进行赋值操作了,这于数据密集型的类来说,是非常高效的。
原因2.效率要求这样做:
类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。
注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在类中声明时的顺序是一致的,与列表的先后顺序无关,所以要特别注意,保证两者顺序一致才能真正保证其效率和准确性。

注意:
1、构造函数列表的初始化方式不是按照列表的的顺序,而是按照变量声明的顺序。所以,一个好的习惯是,按照成员定义的顺序进行初始化。
2、在子类中,只有初始化列表可以构造父类的private成员(通过显示调用父类的构造函数)。
3、一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。