【C++:多态】C++多态实现深度剖析:从抽象类约束到虚函数表机制

🔥艾莉丝努力练剑:个人主页
❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬艾莉丝的简介:
🎬艾莉丝的C++专栏简介:
目录
本文内容索引
C++的两个参考文档
3 ~> 纯虚函数与抽象类:从语法规范到底层约束
3.1 纯虚函数的语法语义深度解析
3.2 抽象类的设计意义与使用场景
3.3 实践验证:抽象类实例化的编译器级限制
3.3.1 分析:抽象类实例化编译错误
3.3.2 对象实例化条件验证
3.3.3 调试运行行为观察
4 ~> 多态机制的底层原理与实现剖析
4.1 虚函数表指针(vfptr)机制详解
4.1.1 内存布局选择题实践
4.1.2 虚函数表(vtable)与vfptr概念深度解析
4.1.3 监视窗口与内存窗口的对比分析
4.1.4 vfptr在对象内存中的位置验证
4.1.5 反汇编窗口在原理分析中的价值
4.1.6 汇编语言在C++研究中的必要性探讨
4.2 多态的实现机制深度解密
4.2.1 多态调用的底层实现流程
4.2.2 动态绑定与静态绑定的机器级差异
4.2.3 虚函数表的结构与工作原理
4.2.4 基类指针与虚函数表指针的关系辨析
4.2.7 虚函数表实践:内存分区验证
4.2.8 x64环境和x86环境地址对比:内存分区验证
完整代码示例与实践演示
Test.cpp:
结尾
本文内容索引

C++的两个参考文档
老朋友(非官方文档):cplusplus
官方文档(同步更新):cppreference
3 ~> 纯虚函数与抽象类:从语法规范到底层约束
3.1 纯虚函数的语法语义深度解析
在虚函数的后面写上 = 0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
如下图所示,就是一个纯虚函数——

3.2 抽象类的设计意义与使用场景
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就实例化不出对象。
那么上图中的这个Car类就是一个抽象类——

3.3 实践验证:抽象类实例化的编译器级限制
代码演示如下——
// 纯虚函数、抽象类
class Car // 抽象类
{
public:virtual void Drive() = 0; // 纯虚函数
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz - 舒适" << endl;}
};class BMW : public Car
{virtual void Drive(){cout << "BMW - 操纵" << endl;}
};class Ferrari : public Car
{virtual void Drive(){cout << "Ferrari - 极致性能" << endl;}
};class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}virtual void Func3(){cout << "Func3()" << endl;}protected:int _b = 1;char _ch = 'x';
};int main()
{//Car car; // 编译报错:C2259“Car":无法实例化抽象类Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();Car* pFerrari = new Ferrari;pFerrari->Drive();Base b;cout << sizeof(b) << endl;// 32位环境:12// 64位环境:16return 0;
}
3.3.1 分析:抽象类实例化编译错误
如下图所示——

Car就是一个抽象类,这个实践验证了抽象类确实无法实例化出对象。
3.3.2 对象实例化条件验证
我们实现这样一个运行时多态,给出以下几种派生类对象——

多态允许使用基类(如car)的指针来引用派生类(如Benz、BMW、Ferrari)的对象,并调用在派生类中重写的虚函数。
以Benz为例——
Car* pBenz = new Benz;
pBenz->Drive(); // 调用 Benz::Drive(),如果 Drive() 是虚函数
其他两个派生类也是同样的道理。
这需要基类car中声明Drive() 为虚函数(使用virtual关键字),然后在派生类中重写该方法。这
样,通过基类指针调用Drive() 时,会根据实际对象的类型动态决定调用哪个派生类的实现。
3.3.3 调试运行行为观察
运行一下——

4 ~> 多态机制的底层原理与实现剖析
4.1 虚函数表指针(vfptr)机制详解
4.1.1 内存布局选择题实践
下面编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
// 选择题
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}virtual void Func3(){cout << "Func3()" << endl;}protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;// 32位环境:12// 64位环境:16return 0;
}
正确答案:选项D。
32位环境下运行一下——

64位环境下运行一下——


根据题意——

32位环境下,运行结果是12,所以选择D选项。
4.1.2 虚函数表(vtable)与vfptr概念深度解析
上面题目运行结果12bytes,除了_b和_ch成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.1.3 监视窗口与内存窗口的对比分析
监视窗口会特殊处理,监视窗口看不到的话,内存窗口可以勉强一看。
监视窗口会进行一些特殊处理,方便程序员看和调试,但是这导致窗口里面的东西不一定是真实的,不过地址是对的。

验证有两种方式——


4.1.4 vfptr在对象内存中的位置验证



4.1.5 反汇编窗口在原理分析中的价值

转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为,如下图,我们通过对比两种调用方式的call指令,可以明显地感受到两者的区别——满足多态时,运行时到指向对象的虚函数表中找到对应的虚函数进行调用;不满足多态的时候,编译时变成调用person:BuyTicket(),普通调用直接去调用Person作用域的函数了,并且普通调用是在编译的时候确定地址的;而运行时的多态调用则是到指向对象的虚函数表中找到对应的虚函数进行调用,一个类所有虚函数的地址要被放到这个类对象的虚函数表,运行时到指向的对象找地址。

这里我们打开反汇编,可以看一下普通调用的汇编,对比一下多态的汇编。
像上图中多态调用的这个call指令——

这个就是真实的调用。
4.1.6 汇编语言在C++研究中的必要性探讨
我们可以转到反汇编看一下,这里艾莉丝再说明一下,uu们可能会觉得会有这样的疑问:好像汇编平常也是会用到的,那我需不需要专门去深入了解一下?如果大家有这样的疑问,就可以看看艾莉丝的想法:确实,有时候转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为,但是,汇编是很偏底层的,我们如果理想的岗位是算法岗、研发岗、测试开发岗、测试岗位的话,对汇编的需求就只是了解即可。
详情可以参考下面的图示——

4.2 多态的实现机制深度解密
4.2.1 多态调用的底层实现流程

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用到Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
// ===============买票=============class Person
{
public:virtual void BuyTicket(){cout << "买票 - 全价" << endl;}private:string _name;
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票 - 打折" << endl;}private:string _id;
};void Func(Person ptr)
{//这里可以看到虽然都是Person指针ptr在调用BuyTicket//但是跟ptr没关系,而是由ptr指向的对象决定的。ptr.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);Person p1;Person p2;Person p3;return 0;
}
运行一下——

我们打开监视观察一下
上面的这张图,ptr指向的Person对象,调用的是Person的虚函数。
下面这张图,ptr指向Student对象,调用的是Student的虚函数——

4.2.2 动态绑定与静态绑定的机器级差异
1、静态绑定:对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
2、动态绑定:满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也叫动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。 // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax// BuyTicket不是虚函数,不满足多态条件。 // 这里就是静态绑定,编译器直接确定调用函数地址 ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
4.2.3 虚函数表的结构与工作原理
1、基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
2、派生类由下面这两部分构成——

一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
3、派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
4、派生类的虚函数表中由下面三个部分组成——

5、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记(这个C++并没有进行规定,各个编译器自行定义的,VS系列编译器会再后面放个0x00000000标记,g++系列编译不会放)。
6、虚函数存在哪的?虚函数和普通函数是一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中——

常量区通常包含以下几个主要段——

静态区的.bss段,我们也来简单了解一下——

7、虚函数表存在哪的?这个问题严格说并没有标准答案,而且C++标准并没有规定,我们写下面的代码可以对比验证一下。至少可以肯定,VS下是存在代码段(常量区)里面的。
写程序是一个很好的验证方法——

对比上面几个地址,我们就可以得出我们的结论了。
4.2.4 基类指针与虚函数表指针的关系辨析
基类的虚函数表指针和基类对象的指针不是同一个——

4.2.5 虚函数重写为什么也叫虚函数覆盖?

4.2.6 虚函数表实践
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;
};class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}
运行一下——

4.2.7 虚函数表实践:内存分区验证

// ----------虚函数表------------
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;
};class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};//int main()
//{
// Base b;
// Derive d;
//
// return 0;
//}// 分区
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;printf("Base虚函数地址:%p\n", *(int*)&b);printf("Derive虚函数表地址:%p\n", *((int*)&d));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
运行一下——

从运行结果可以看出,虚函数地址和常量区的地址十分接近,由此可知——
VS下,虚函数地址是存在代码段(常量区)里面的。
在本文的最后,艾莉丝会对比x64环境和x86环境的几个地址。
4.2.8 x64环境和x86环境地址对比:内存分区验证

完整代码示例与实践演示
Test.cpp:
// ======================多态语法层剩余 + 原理层======================//// 纯虚函数、抽象类
//class Car // 抽象类
//{
//public:
// virtual void Drive() = 0; // 纯虚函数
//};
//
//class Benz : public Car
//{
//public:
// virtual void Drive()
// {
// cout << "Benz - 舒适" << endl;
// }
//};
//
//class BMW : public Car
//{
// virtual void Drive()
// {
// cout << "BMW - 操纵" << endl;
// }
//};
//
//class Ferrari : public Car
//{
// virtual void Drive()
// {
// cout << "Ferrari - 极致性能" << endl;
// }
//};
//
////// 选择题
////class Base
////{
////public:
//// virtual void Func1()
//// {
//// cout << "Func1()" << endl;
//// }
////
//// virtual void Func2()
//// {
//// cout << "Func2()" << endl;
//// }
////
//// virtual void Func3()
//// {
//// cout << "Func3()" << endl;
//// }
////
////protected:
//// int _b = 1;
//// char _ch = 'x';
////};
////
////int main()
////{
//// //Car car; // 编译报错:C2259“Car":无法实例化抽象类
////
//// Car* pBenz = new Benz;
//// pBenz->Drive();
////
//// Car* pBMW = new BMW;
//// pBMW->Drive();
////
//// Car* pFerrari = new Ferrari;
//// pFerrari->Drive();
////
//// Base b;
//// cout << sizeof(b) << endl;
////
//// return 0;
////}
//
//// 选择题
//class Base
//{
//public:
// virtual void Func1()
// {
// cout << "Func1()" << endl;
// }
//
// virtual void Func2()
// {
// cout << "Func2()" << endl;
// }
//
// virtual void Func3()
// {
// cout << "Func3()" << endl;
// }
//
//protected:
// int _b = 1;
// char _ch = 'x';
//};
//
//int main()
//{
// Base b;
// cout << sizeof(b) << endl;
// // 32位环境:12
// // 64位环境:16
//
// return 0;
//}// ===============买票=============//class Person
//{
//public:
// virtual void BuyTicket()
// {
// cout << "买票 - 全价" << endl;
// }
//
//private:
// string _name;
//};
//
//class Student : public Person
//{
//public:
// virtual void BuyTicket()
// {
// cout << "买票 - 打折" << endl;
// }
//
//private:
// string _id;
//};
//
//void Func(Person ptr)
//{
// //这里可以看到虽然都是Person指针ptr在调用BuyTicket
// //但是跟ptr没关系,而是由ptr指向的对象决定的。
// ptr.BuyTicket();
//}
//
//int main()
//{
// Person ps;
// Student st;
//
// Func(ps);
// Func(st);
//
// Person p1;
// Person p2;
// Person p3;
//
// return 0;
//}class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;
};class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}// // 分区
//int main()
//{
// int i = 0;
// static int j = 1;
// int* p1 = new int;
// const char* p2 = "xxxxxxxxxxxx";
// printf("栈:%p\n", &i);
// printf("静态区:%p\n", &j);
// printf("堆:%p\n", p1);
// printf("常量区:%p\n", p2);
//
// Base b;
// Derive d;
//
// printf("Base虚函数地址:%p\n", *(int*)&b);
// printf("Derive虚函数表地址:%p\n", *((int*)&d));
//
// printf("虚函数地址:%p\n", &Base::func1);
// printf("普通函数地址:%p\n", &Base::func5);
//
// return 0;
//}
结尾
往期回顾:
【C++:继承和多态】多态加餐:面试常考——多态的常见问题11问
【C++:多态】深入剖析C++多态精髓:虚函数机制、重写规范与现代C++多态控制
结语:都看到这里啦!那请大佬不要忘记给博主来个“一键四连”哦!
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡
૮₍ ˶ ˊ ᴥ ˋ˶₎ა

