在上篇博文我们知道虚函数存在虚表,虚表存了一个虚函数的指针,因此sizeof()这个类中包含虚函数则会包含指针的大小,本篇文章我们将通过原理了解多态。
[ C++ ] 抽象类 虚函数 虚函数表 -- C++多态(1)
目录
下面这段代码中,这个Buy函数传Person的调用的Person::BuyTicket(),传Student调用的是Student::BuyTicket.这样构成了多态,因此多态调用实现,是依靠运行时,去指向对象的虚表中查调用的函数地址。
- class Person
- {
- public:
- Person(const char* name = "张三")
- :_name(name)
- {}
-
- virtual void BuyTicket()
- {
- cout << _name << "购票,需要排队,每人 100 ¥" << endl;
- }
- protected:
- string _name;
- };
-
- class Student : public Person
- {
- public:
- Student(const char* name)
- :_name(name)
- {}
-
- virtual void BuyTicket()
- {
- cout << _name << "购票,需要排队,每人 50 ¥" << endl;
- }
- private:
- string _name;
- };
-
- void Buy(Person* p)
- {
- p->BuyTicket();
- }
-
- int main()
- {
- Person p("张三");
- Buy(&p);
-
- Student st("张同学");
- Buy(&st);
- return 0;
- }
我们也可以通过监视窗口进行查看

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

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为了静态多态,比如:函数重载。
2.动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的和拿书,也成为了动态多态。
3.上图买票的汇编代码很好的解释了什么是静态绑定(编译时)和动态(运行时)绑定。
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Base::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Base::Func2()" << endl;
- }
- private:
- int _b = 1;
- };
-
- 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 _d = 2;
- };
- int main()
- {
- Base b;
- Derive d;
- return 0;
- }

在监视窗口我们发现Derive中看不见Fun3和Fun4,这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是VS的一个Bug。那么我们如何查看d的虚表呢?我们将使用代码打印出虚表中的函数。
打印的思路:取出b,d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
1.先取b的地址,强转成一个int*的指针。2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针。3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。4.虚表指针传递给PrintVTable进行打印虚表。5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
- typedef void(*V_FUNC)();
- //
- 打印虚表
- void PrintVFTable(V_FUNC a[])
- void PrintVFTable(V_FUNC a[])
- {
- printf("vfptr:%p\n", a);
-
- for (size_t i = 0; a[i] != nullptr; ++i)
- {
- printf("[%d]:%p->", i, a[i]);
- V_FUNC f = a[i];
- f();
- }
- }
- int main()
- {
- Base b;
- Derive d;
-
- PrintVFTable((V_FUNC*)(*((int*)&b)));
- PrintVFTable((V_FUNC*)(*((int*)&d)));
-
- return 0;
- }


以下有一段多继承中的虚函数表代码,我们通过打印出虚表的函数观察其中的奥妙

- class Base1
- {
- public:
- virtual void Func1()
- {
- cout << "Base1::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Base1::Func2()" << endl;
- }
- private:
- int _b1 = 1;
- };
-
- class Base2
- {
- public:
- virtual void Func1()
- {
- cout << "Base2::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Base2::Func2()" << endl;
- }
- private:
- int _b2 = 1;
- };
-
- class Derive :public Base1,public Base2
- {
- public:
- virtual void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
- virtual void Func3()
- {
- cout << "Derive::Func3()" << endl;
- }
- private:
- int _d1 = 2;
- };
-
- typedef void(*V_FUNC)();
- //
- 打印虚表
- void PrintVFTable(V_FUNC a[])
- void PrintVFTable(V_FUNC a[])
- {
- printf("vfptr:%p\n", a);
-
- for (size_t i = 0; a[i] != nullptr; ++i)
- {
- printf("[%d]:%p->", i, a[i]);
- V_FUNC f = a[i];
- f();
- }
- cout << endl;
- }
- int main()
- {
- Derive d;
-
- PrintVFTable((V_FUNC*)(*((int*)&d)));
- //PrintVFTable((V_FUNC*)(*((int*)&d+sizeof(Base1))));
-
- return 0;
- }
通过打印虚表我们可以看到多继承派生类的为重写的虚函数放在第一个继承基类部分的虚函数表中


在之前继承文章中提到过菱形继承和菱形虚拟继承,当时我们提到过菱形继承太复杂且容易出现问题,同时,菱形继承这样的模型在访问基类成员时有一定的性能消耗。所以菱形继承、菱形虚拟集成在实际中很少用。因此在此不在查看虚表。
(本篇完)