是非功过无需言,自有后人常评谈,江湖儿女江湖去,哪管天上与人间。

1. 多态是什么?

自古有一句老话,OOP 的一个重要特征就是多态。但是到底什么是多态呢?这个问题我一直难以回答。在我看来,多态就是一个指针,调用同一个函数,最后呈现的效果可能是不一样的,这就是多态。因为这个指针可能指向的是不同类的对象。
更具体的来说,假如我们遇到了这样一个场景:我们有一个非常抽象的基类,叫动物;动物类派生出了猫猫和狗狗类。现在我们希望对动物们实现一个函数,能够在控制台输出这个动物的叫声。这就是一个非常朴素的多态的需求,对于一个猫猫动物来说,函数希望能输出“喵”,对于一个狗狗动物来说,函数希望能输出“汪”。

2. C++ 中的多态

其他编程语言的多态我也不知道是什么情况,我们在这里只介绍 C++ 的情况。在 C++ 中多态是依赖虚函数实现的。而虚函数的底层机制又是什么样的呢?我们在这里简单介绍一下。
虚函数的调用和正常函数有很大的不同。因为正常的函数在编译的时候就可以知道自己应该跳转到哪里去执行,虚函数肯定需要别的机制。具体来说,声明了虚函数或者是祖辈们声明了虚函数的类,它的地址的开头就会有一个虚函数表的地址。这个虚函数表是唯一对应一个类的,并不是对应一个对象的。每个对象的地址的开头都会存一个这个表的地址。这个表里存放着一个函数指针,或者说这个表的项都指向着一个个函数。我们通过这些函数指针就可以找到要执行的虚函数是哪个了。
为了更深刻的理解,我们其实可以考虑自己设计一个虚函数的机制,这样就能明白一些设置的道理所在。比如为什么要放在对象的地址的开头呢?因为我们如果放在别的地方,对于一个基类指针来说,它并不知道你这个类到底是怎么设计的,因为它并不知道你是哪个类的。所以放在开头大家人人平等皆大欢喜。
而后是在这个虚表里的顺序问题。比如你这个派生类自己的虚函数,应该在虚表的什么位置?这个派生类的爹的函数在前还是这个派生类的爷爷的函数在前呢?这些都是我们可以想到的。派生类自己的虚函数肯定是放在最后的,爹在中间,爷爷在最前面。这是因为信息的不对称,爷爷不知道爹都有什么函数,爹也不知道派生类自己有什么函数,所以必须让爷爷在前面,不然它无法计算出偏移量。
然而这种设计并不是完美无缺,直接可以避免一切类型判断的(很多人可能以为虚函数调用不会判断类型,只是算偏移量找指针)。因为假如现在一个派生类继承自两个基类,那么派生类是会有两个虚表的,会在地址的前两个 4bytes 放两个虚表的地址。这个时候算偏移量就不行了。因为一个关键点是:基类并不知道你都继承自几个爹,所以它不能无脑找地址的首 4byte,因为它的虚表在第二个 byte。所以这个时候我们还是需要类型推断,具体可见我的这篇文章:动态类型推断
以上全部都是扯淡,xyx 男士通过实践指出:真正的情况是这样的:
![TD[%W93P@]PAHQBFCQC3VW.png
通过这张图我们可以看出:在类型转换的时候,我们的指针指向的地方是会发生变化的。通过这种机制,我们就不需要所谓的动态类型推断来找到虚函数表了,因为我们的指针直接就乾坤挪移了,非常的巧妙,直接定位到了 C 对象种 B 所在的部分的开头。

3. 动态绑定与静态绑定

只有指针才能做到动态绑定,也就是说如果我们用一个对象去尝试调用虚函数,实现多态,这是不可能的。

4. 构造和析构

构造函数不可以是虚函数。这是因为虚函数表是在构造的时候才被加到地址的头的,所以构造函数本身不可是虚函数,而且它也没有任何多态的必要。
析构函数一般都是虚函数。因为如果不是,那么如果你用一个基类指针去接一个派生类对象,你把基类指针析构了反而会遇到内存泄漏。