注意,本文以C++11为背景。
参考来源:《C++ primer 第五版》
以前一直对C++类的构造函数工作一知半解,最近重新温习《C++ primer》,才详细理解了类的构造函数的工作,特意写个博客记录一下。
相信稍微了解过C++的同学,都知道C++类有一个特殊的成员函数,也是必须的成员函数,那就是构造函数。
构造函数,负责初始化类对象的数据成员。但是一直有个误区,就是大家一直认为,构造函数体内的语句也负责着对象的初始化工作。其实不然,构造函数分为两个部分:
所以严格意义上来讲,构造函数体并不属于对象的初始化。但是,普遍程度上,我们认为这两部分都属于对象的初始化。
还记得我刚才说到构造函数是必须的么?你们可能会反驳,为什么如以下的类,却能成功编译。
class Rect{
int width;
int height;
public:
int getArea(){
return width * height;
}
};
的确如以上的类可以被正确编译,并且它的确没有显式定义构造函数。但是,请注意我在这里用了显式,结合我刚才说构造函数是必须的,那么只有一种情况:这里隐式地产生了构造函数。
那么这个构造函数是谁产生的呢?既然我们没有主动去构造,那么只有编译器了。实际上,就是编译器产生的这个构造函数。并且由编译器产生的构造函数有一个很规范的名字:合成的默认构造函数 。
默认构造函数:就是没有参数的构造函数。默认构造函数在什么时候被调用呢?就是如Rect c;
这种情况就会调用默认构造函数,当然还有一种情况等会再提。
合成:合成特指这个默认构造函数是由编译器创建的。
那么这个默认构造函数到底是如何工作的呢?它将会按照以下规则初始化类的数据成员。
如果存在类内的初始值,用它来初始化成员。
否则,默认初始化该成员。
别急,让我们来慢慢分析下这个规则:
class Rect{
int width = 0;
int height;
public:
int getArea(){
return width * height;
}
};
如果这个Rect类现在变成了这个样子,那么当执行Rect c
时,c
的数据成员是怎么样的呢?
c.width将会等于0,为什么呢?因为合成的默认构造函数发现了width有类内初始值0,所以就将它赋值为0。
c.height的值呢,显然它没有类内初始值,所以合成的默认构造函数就默认初始化它。至于它的值到底是多少?我们要记住这样两条规则。
如果数据成员是内置类型或者是复合类型(比如数组和指针),那么它们默认初始化的值是未定义的。
如果数据成员是类类型,那么将会自动调用该数据成员所属的类的默认构造函数进行初始化,这也是上文提到的另一种调用默认构造函数的情况。
我们再把上面的代码改动一下
class Rect{
int width = 0;
int height = 0;
public:
int getArea(){
return width * height;
}
Rect(int _width, int _height){
width = _width;
height = _height;
}
};
可以看到在这里我们定义了一个带参的构造函数。很容易理解,当执行Rect c(10,9);时,c.width=10,c.height=9。但是当执行Rect c时,会怎么样呢?没错,编译器会报错!你可能会说,不是有合成的默认构造函数么?对,编译器的确会创建合成的默认构造函数,但是那有一个前提,就是我们没有定义任何形式的构造函数。在以上代码中,定义了一个构造函数,所以编译器不会再帮我们创建合成的默认构造函数了。所以,我们最好再给类添加一个默认构造函数,如下:
class Rect{
int width = 0;
int height = 0;
public:
int getArea(){
return width * height;
}
Rect() = default;
Rect(int _width, int _height){
width = _width;
height = _height;
}
};
这里的Rect() = default;
表示要求编译器生成默认构造函数,当然你也可以自己写一个默认构造函数。
那么我们再来变一下代码。
class Rect{
int width = 0;
int height = 0;
public:
int getArea(){
return width * height;
}
Rect() = default;
Rect(int _width): width( _width) {}
};
Rect(int _width): width( _width) {}
这句代码采用了初始列表的写法,其实就是相当于width = _width;并且初始列表的执行在函数体之前,另外初始列表也有其他一些规则,但是在这里我们不详谈。我们要关注的是当执行Rect c(10);时c.height的值,c.width的值是明确的,就是10,那么c.height呢?因为Rect的构造函数只有对c.width的初始化,所以就以与合成的默认构造函数相同的方式隐式初始化,也就是先检查有没有类内初始值,没有的话就默认初始化。因为c.height有类内初始值0,所以c.height被初始化为0。
其实在子类继承父类这种情况下,构造函数的执行顺序其实是很简单的,只要牢记大方向是先祖先后自己。并且这个自己分为两个部分:先初始化列表部分,再是构造函数体部分。
父类没有任何构造函数:
父类由编译器创建合成的默认构造函数,子类对象创建时,先调用这个合成的父类默认构造函数,再调用子类的构造函数。
父类有默认构造函数:
子类对象创建时,先调用这个默认构造函数,再调用子类的构造函数。
父类有有参构造函数,没有默认构造函数:
子类对象创建时,必须要显式调用父类有参构造函数(即写入到子类构造函数的初始化列表中),再调用自身的构造函数。
父类有有参构造函数,也有默认构造函数:
有两种情况:
我们已经了解了继承中的构造函数执行情况。那么让我们做一个小小的测试。
class A{
public:
A(){ cout << "construct A" << endl; }
A(int b){ a = b; cout << "construct A with param" << endl; }
int a;
};
class C :public A{
public:
C(): A(2){ cout << "construct C" << endl; }
};
class B:public A{
public:
B(){ cout << "construct B" << endl;}
C c;
};
int main()
{
B b;
return 0;
}
请问最后的显示结果是什么?答案是:
construct A
coustruct A with param
construct C
construct B
为什么呢?我们一起来分析一下这个过程:
因为B是A的子类,并且B的构造函数并未显式调用父类A的构造函数,所以调用A的默认构造函数,所以打印construct A。
此时开始执行B自身的构造函数,先进行初始化部分,由于B的初始化部分没有显式初始化其数据成员c,所以默认初始化c,即调用C的默认构造函数。
由于C也是A的子类,并且C的构造函数显式调用了A的有参构造函数,所以打印construct A with param,然后执行C自身的构造函数,即打印construct C。
最后执行B自身的构造函数体部分,即打印construct B。