C++ 构造函数详解

C++ 构造函数详解

注意,本文以C++11为背景。
参考来源:《C++ primer 第五版》

    以前一直对C++类的构造函数工作一知半解,最近重新温习《C++ primer》,才详细理解了类的构造函数的工作,特意写个博客记录一下。


没有显式定义构造函数的情况

    相信稍微了解过C++的同学,都知道C++类有一个特殊的成员函数,也是必须的成员函数,那就是构造函数。

    构造函数,负责初始化类对象的数据成员。但是一直有个误区,就是大家一直认为,构造函数体内的语句也负责着对象的初始化工作。其实不然,构造函数分为两个部分:

  • 初始化部分:其实就是构造函数的初始化列表部分,关于初始化列表的简单介绍将在下文进行。
  • 普通计算部分:其实构造函数体内的语句就处于这个部分,比方说a有一个初值2,现在将它的值改变为3,而这个“改变它的值为3”的步骤就是构造函数体所要做的工作。

    所以严格意义上来讲,构造函数体并不属于对象的初始化。但是,普遍程度上,我们认为这两部分都属于对象的初始化。

    还记得我刚才说到构造函数是必须的么?你们可能会反驳,为什么如以下的类,却能成功编译。

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

    为什么呢?我们一起来分析一下这个过程:

  1. 因为B是A的子类,并且B的构造函数并未显式调用父类A的构造函数,所以调用A的默认构造函数,所以打印construct A。

  2. 此时开始执行B自身的构造函数,先进行初始化部分,由于B的初始化部分没有显式初始化其数据成员c,所以默认初始化c,即调用C的默认构造函数。

  3. 由于C也是A的子类,并且C的构造函数显式调用了A的有参构造函数,所以打印construct A with param,然后执行C自身的构造函数,即打印construct C。

  4. 最后执行B自身的构造函数体部分,即打印construct B。

你可能感兴趣的:(c++)