本文探讨C++构造函数体内初始化与列表初始化的区别:
结论是:
若某个类(下文的class B)有一个类成员是类类型(下文的class A),那么
1、若类B通过构造函数体内初始化,会先调用类A的默认构造函数(无参构造函数),再调用类A的赋值运算符;
2、若类B通过初始化列表去初始化,则只调用类A的拷贝构造函数。
另外,虽然对于成员类型是内置类型的情况,通过上述两种情况去初始化是相同的,但是为了标准化,推荐使用初始化列表。
接下来,进入验证正题:
下面分别陈列类A和类B的声明:
class A {
public:
A() {
cout << "A Default constructor" << endl;
}
A(int a) {
mA_a = a;
cout << "A constructor" << endl;
}
A(const A&) {
cout << "A copy constructor" << endl;
}
A& operator=(const A& a) {
cout << "A assign operator" << endl;
this->mA_a = a.mA_a;
return *this;
}
~A() {
cout << "A deconstructor" << endl;
}
private:
int mA_a;
};
1.此时类B采用的是在构造函数内初始化:
class B {
public:
B() {
cout << "B Default Constructor" << endl;
}
B(A &a) {
cout << "come into B" << endl;
_a = a;
cout << "B Constructor" << endl;
}
private:
A _a;
};
int main(){
A a;
B b(a);
return 0;
}
对应的输出为:
有图可得:
第一行的“A Default constructor”是构造a调用的无参数构造函数。
【此处经过大佬点拨,再详细说明下】
第二行的“A Default constructor”是因为进入到类B对应的构造函数体内后,编译器首先会插入一些对成员变量的初始化,也就是编译器会隐形的对未初始化的成员变量进行默认初始化,这里类B有一个类型为类A的成员变量_a,那么初始化它的时候会调用类A的默认构造函数,所以会有第二个“A Default constructor”出现。再才是执行类B对应的构造函数的函数体,从而出现结果中的第3-5行的内容。所以,通过在构造函数体内初始化,首先会调用该成员的默认构造函数(无参构造函数,即结果中的第二行),然后再调用其赋值运算符。
2.此时类B采用的是在初始化列表中初始化:
class B {
public:
B() {
cout << "B Default Constructor" << endl;
}
B(A &a) : _a(a){
cout << "come into B" << endl;
cout << "B Constructor" << endl;
}
private:
A _a;
};
对应的输出为:
与第一种的输出相比,没有了赋值运算符“A assign operator”,也没有了第二个“A Default Constructor”,改变的是新添了“A copy constructor”。这是因为列表中的初始化操作先于函数体执行,而这里应该调用的便是拷贝构造函数。所以,通过初始化列表去初始化成员,只会调用该成员的拷贝构造函数(成员类型是某类类型)或者对应的构造函数(成员类型是内置类型)。
总结:
以下几种情况必须使用初始化列表去初始化类成员:
- 当初始化一个reference member时,即成员类型是引用。
- 当初始化一个const member时,即成员类型是常量。
- 当调用一个基类的constructor,而它拥有一组参数时。如果此时不使用列表初始化,那么就需要自己重载赋值运算符。
- 当调用一个类成员的constructor,而它拥有一组参数时。
- 若某个类成员没有定义无参构造函数,而定义了其它的构造函数,也必须使用初始化列表。
总之,为了标准化,建议使用列表初始化。不过小心一些陷阱:
因为类成员的初始化顺序不是按照初始化列表的顺序来的,而是按照类成员的声明顺序,假如出现下列情况:
class X {
int i;
int j;
public:
X(int val) :j(val), i(j) {
}
};
由于类成员的初始化顺序是按照其声明顺序,故先初始化i,然而初始化列表中用j去初始化它,但是此时j并没有被初始化,所以会出错。