本文正在参加「金石计划 . 瓜分6万现金大奖」
一个子类的构建过程
我们这里有两个class,
- Animal, 表示动物
- Fish , 继承自 Animal,表示鱼
他们的代码是这样的:
class Animal {
public:
Animal() {
std::cout << "Animal 构建!" << std::endl;
}
~Animal() {
std::cout << "Animal 析构!" << std::endl;
}
};
class Fish : public Animal {
public:
Fish() {
std::cout << "Fish 构建!" << std::endl;
}
~Fish() {
std::cout << "Fish 析构!" << std::endl;
}
};
好了我们来搞一些测试代码,来观察Fish这个子类的示例是如何初始化的:
void test() {
Fish xiaoming{};
}
当我们执行test函数,我们的输出应该如下:
Animal 构建!
Fish 构建!
Fish 析构!
Animal 析构!
我们可以得知,
-
一个子类在【构建】的时候,应该是先将其包含的基类构建完毕,然后再轮到自己的构建。
-
一个子类在【析构】的时候,先将自己析构完毕,然后再轮到其包含的基类析构。
这里作为经典八股,应该是C++程序员都熟知的,但是……
顺序为什么是这样?
用一个盖房子的过程来做比喻是最合适不过的。
首先来说构建部分(constructor)
盖房子的时候,是不是得先打地基, 然后再在这个地基上,才能盖一楼,二楼,等等。
用程序的话来说,就是一个子类构建的过程里,含两个部分,一个是内部包含的基类部分,第二个才是自己额外的部分。
关键就是,在构建这个子类的额外的部分的时候,往往都会把基类的部分看成是一个已经OK的状态,并且还会依赖于其中(基类)某些字段的值。所以从构造函数的调用顺序来说,肯定是基类的构造函数先调用的。
再来说析构部分(destructor)
还是盖房子,不过这次是拆房子。
如果用传统的方式来拆房子,那么应该是从最高层慢慢拆解,毕竟里面可能有一些东西是可以回收的,比如说什么家具电器什么的。一层一层的往下回收,一层结束,就把这一层消除掉,很容易理解。
从析构函数来说,相应的就是从子类一层一层的析构,直到最底下的基类。这里还是可以用依赖性来解释:毕竟子类的某些字段或者逻辑是依赖于基类的字段的,子类析构的时候可能也会有这种依赖关系,所以必须要从子类开始析构,这样才不会产生依赖矛盾。
所谓依赖矛盾就是指,用到一个字段了,但是这个字段本身的内存已经被析构过,处于一种不可用不合法状态了。
点题:构造函数和析构函数不要调用虚函数
设想一个情景: 一个基类构造函数里调用了一个虚函数,按照虚函数的概念来讲,应该是调用了某个子类的函数,但是子类的函数很可能会使用到子类的一些字段,但是此时,子类本身还没有经过构建过程,也就是说,子类的字段处于一种非法状态,很明显,我们不应该去调用这个子类的函数的。所以C++标准里规定了,在构造函数里调用虚函数,不会有什么多态的行为,而是直接绑定,静态绑定到本身的函数。
析构的情况差不多,如果在基类析构函数里调用了虚函数,我们知道,此时子类的字段已经经过子类析构函数的清理了,这些字段处于非法状态,如果虚函数调用到了子类的函数,那么也是不太好的。所以C++标准依然规定,此时也不会有多态的行为。
为什么不要在里面使用虚函数,就是因为可能会产生认知上的差异,所以避免使用即可。
核心点总结
构造函数和析构函数的调用顺序,决定了,构造函数和析构函数里不会产生虚函数的多态行为,所以我们尽量不要在里面使用虚函数,而造成认知上的错误。