写在前面

相信做过Java、C++或者其他面向对象语言开发的朋友们一定对构造函数这个概念不陌生。以前初学C++的时候笔者看过几次《C++ Primer》这本书,但是每次都是走马观花式的快速阅读,
每次浏览完之后内心就会冒出两个字:就这?现如今回想起来真是图样图森破

学习最忌讳的就是心急如焚,砍柴不磨刀,所谓欲速则不达,一步一个脚印才能走得更稳。

由问题开始

下面我们就从几个问题出发,加深一下对C++中构造函数的了解:

1、构造函数初始化与赋值的问题

以下的这两个写法有什么区别?

class Person {
public:
    Person(const string name, int age);

private:
    string name;
    int age;
};

// 第一种写法
Person::Person(const string name, int age) {
    this->name = name;
    this->age = age;
}

// 第二种写法
Person::Person(const string name, int age):name(name),age(age) {
    
}

在这个例子中第二种写法是使用构造函数初始值的写法,第一种写法虽然合法,也没有错误,但是并不是合理的写法,并不推荐。

那么这两种写法有什么区别呢?
第一种写法会经历先初始化,再赋值这么两个过程;而第二种写法则是直接初始化数据成员一步到位。所以这里面会存在一个效率的问题,第二种写法的效率更高。

我们再看一个例子,如果我们把类的成员使用const修饰呢,结果会怎样?

class Person {
public:
    Person(const string name, int age);

private:
    string name;
    const int age;
};

// 第一种写法,编译报错
Person::Person(const string name, int age) {
    this->name = name;
    this->age = age;
}

// 第二种写法
Person::Person(const string name, int age):name(name),age(age) {

}

我们发现第一种写法行不通了,不能编译通过了,这是因为age被const修饰了,必须在初始化时赋值,所以第一种写法就不行了,由此看出使用构造函数初始值的写法更加规范,更加安全。

建议:在《Effective C++》一书中的第4条"确定对象被使用前已先被初始化"中也强调了绝对必要使用构造函数初始值

2、成员变量的初始化顺序

如下例子,如果外部调用Point对象的getX方法,能拿到正确的值吗?答案是不能的,因为成员x比成员y先初始化。

class Point {

public:
    Point(int x, int y);

    int getX() const{
        return x;
    }

    int getY() const{
        return y;
    }

private:
    int x;
    int y;
};

// 本意是把 yVal的值赋值给成员变量y,然后把成员变量y的值赋值给成员变量x
Point::Point(int xVal, int yVal):y(yVal),x(y) {

}

一般按照我们常规的思维,我们在构造函数中先写了y,再x,那应该是写初始化y,再初始化x吧?然而事实并不是这样子的。

起始构造函数初始值是有一定的规则的:

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,
然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

所以上面构造函数的写法中虽然y出现在了x的前面,但是在成员变量声明的时候是先声明了x的,所以初始化的时候是先初始化了x,但是把一个未经初始化的y赋值给了x,那肯定是不能成功赋值的,
所以通过getX方法获取到的值也就不是你想要的那个值了。

3、对于继承而来的派生类的成员初始化顺序是怎么样的呢?

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

3、委托构造函数的执行顺序

所谓委托构造函数就是构造函数相互调用。

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
如果受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。

4、构造函数异常如何捕获

处理构造函数初始值异常的唯一方法是将构造函数写成函数try语句块。

5、如何让类不能在栈内构造

笔者查了下网上的资料说大概就是说将构造方法私有化,并且将拷贝构造函数私有化就能禁止类的对象在栈内构造了。笔者测试了一下其实这并不严谨,这样的做法只能做到在类的外部禁用了栈内初始化,
在类的内部依然可以使用栈的方式构造对象,比如一下例子:

class Data {
public:
// 在类的内部依然可以使用栈的方式构造
    Data create() {
        Data data = Data();
    }

private:
    Data();
    Data(const Data &data) {

    }

};

经过笔者的测试,私有化构造函数,再加上使用delete关键字移除拷贝构造函数即可实现禁用类在栈内构造的功能:

class Data {
public:
    // 不能在栈内构造,编译会报错
    Data create() {
        return Data();
    }

private:
    Data();
    Data(const Data &data) = delete;
};

但是这种做法实在是太过了,而且笔者笔者才疏学浅,也不知道这种做法会不会造成什么隐藏的坑,如有高手,请赐教。

《More EffectiveC++》一书中第27条:要求(或禁止)对象产生与heap之中,提到将构造函数和析构函数私有化即可达到禁止对象在栈内定义的目的。
但是这个做法太过了,比较好的办法是让析构函数r成为 private,而构造函数仍为 public。

6、如何让类不能在堆内构造对象

使用new在堆内构造对象主要会调用构造函数以及new运算符这两个步骤,所以我们只要把运算符new移除即可:

class Data {
public:
    Data();
    // 重载new运算符,禁止使用new在堆内构造对象
    void* operator new (size_t size) = delete;
};

然而笔者发现,虽然这样能够禁用new在堆内构造对象,但是我们知道使用 malloc 也能在堆内分配对象,只是使用 malloc 不会调用类的构造函数而已,所以类内的所有成员都需要自己手动初始化,
那么有没有办法把malloc也禁用掉呢?笔者并不知晓,恳请高手赐教。。。

在《Effective C++》一书中第06条有提到为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。

所以为了达到某个类只能在堆内或者只能在栈内构造的目的可以参考这一条。

总结一下

1、谁先声明谁先初始化,与构造函数中出现的顺序无关;

2、初始化值中的相关调用比构造函数中的函数体优先执行;

3、在派生类中首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

结语

不得不感叹一下,C++真是一门博大精深的语言,你学得越多,你不知道的就越多。

学C++三年,口出狂言
再学三年,不敢妄言
又学三年,沉默寡言

就笔者而言,我觉得学习是一个实践的过程,尤其是对于编程类的学习。看博客、看书、看文章只能解决你当时的疑惑,并不能在我脑海中留下根深蒂固的印象,如果不去实践证明、不去总结归纳的话,很快就会忘记了,过不了多久其实和没有学过差不多。

共勉!!!

关注我,一起进步,人生不止coding!!!

重温C与C++之构造函数_其他