文章目录

  • 1、多态原理
  • 2、动态绑定和静态绑定
  • 3、单继承和多继承关系的虚函数表
    • 3.1 单继承中的虚函数表
    • 5.2 多继承中的虚函数表

上一篇文章我们了解了虚函数表,虚函数表指针,本篇文章我们来了解多态的底层原理,更好的理解多态的机制。
[C++] 多态(上) – 抽象类、虚函数、虚函数表

1、多态原理

下面这段代码中,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket,这就是多态调用,但是这里我们并不知道原理是什么,接下来我们就来了解一下原理。

class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }};void Func(Person* p){p->BuyTicket();}int main(){Person p;Func(&p);Student s;Func(&s);return 0;}

  1. 观察监视窗口我们看到,p是指向p对象时,p->BuyTicket在p的虚表中找到虚函数是Person::BuyTicket。
  2. 观察监视窗口我们看到,p是指向s对象时,p->BuyTicket在s的虚表中找到虚函数是Student::BuyTicket。从Student中切片出来的父类,call BuyTicket的地址已经发生了改变。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件:1、一个是虚函数覆盖;2、一个是对象的指针或引用调用虚函数。
  5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
    多态调用:运行时,去虚表中找到地址去调用函数
    普通调用:编译时,确定函数地址

2、动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在 程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载;
  2. 动态绑定又称后期绑定(晚绑定),是在 程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态;
  3. 上面的买票汇编就解释了什么是具体的动态绑定和静态绑定。

3、单继承和多继承关系的虚函数表

3.1 单继承中的虚函数表

class Base{public:virtual void func1(){cout << "Base::func1" << endl;}virtual void func2(){cout << "Base::func2" << endl;}private:int _a;};class Derive :public Base {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }private:int b;};int main(){Base b;Derive d;return 0;}

观察上图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?我们写一段代码来将需表中的虚函数打印出来。

代码思路: 取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
1.先取b的地址,强转成一个int的指针;
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针;
3.再强转成VFPTR
,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组;
4.虚表指针传递给PrintVTable进行打印虚表;
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

typedef void(*VFPTR) (); // 重命名函数指针void PrintVTable(VFPTR vTable[]){// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址->" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;}int main(){Base b;Derive d;VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;}

5.2 多继承中的虚函数表

class Base1 {public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }private:int b1;};class Base2 {public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() {cout << "Base2::func2" << endl; }private:int b2;};class Derive : public Base1, public Base2 {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int d1;};typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]){cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;}int main(){Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

最后菱形继承、菱形虚拟继承就不再,因为正常情况下很少用,这里就不再多讲了。