当前位置: 首页 > news >正文

【C++】透视C++多态:从虚函数表到底层内存布局的完全拆解

🔥小陈又菜个人主页

📖个人专栏《MySQL:菜鸟教程》 《小陈的C++之旅》《Java基础》

✨️想多了都是问题,做多了都是答案!



目录

问题引入:

1. 多态原理

2. 动态绑定与静态绑定

3. 单继承和多继承中的虚函数表

3.1. 单继承中的虚函数表

3.2. 多继承中的虚函数表


问题引入:

上篇文章我们知道了虚表的存在,虚表中存储了虚函数的指针,所以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;
}

通过监视窗口我们可以发现:

  • Person指向对象p时,p->BuyTicket()在p的虚表中找到虚函数是Person::Ticket
  • Student指向对象st时,st->BugTicket在st的虚表中找到虚函数是Student::Ticket
  • 通过查找不同的虚函数就实现了,不同的对象调用会有不同的行为,也就是多态
  • 我们再明确一下实现多态的两个条件:存在虚函数、需要对象指针或引用调用虚函数
  • 通过反汇编窗口可以发现,构成了多态之后,函数的调用是在运行了程序过程中去对象中取的,而不是编译时就决定的(如果不是多态,函数调用会在编译时就决定好)

多态调用:运行时决议,运行时才确定函数的地址

普通函数:编译时决议,编译时确认调用函数的地址

2. 动态绑定与静态绑定

  • 静态绑定(前期绑定):编译时就决定了调用哪个函数,根据变量或表达式的静态类型决定,也就形成了静态多态,如函数重载
  • 动态绑定(后期绑定):在程序运行时,根据拿到的具体类型来确定函数的具体行为,也就形成动态动态

如下面这个和上面那个p->BuyTicket()对比就知道,动态绑定和静态绑定的区别:

3. 单继承和多继承中的虚函数表

3.1. 单继承中的虚函数表

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;
}

通过调试时我们发现似乎有些问题,理论上派生类d,应该会有三个虚函数(继承基类的两个,新增加的两个),但是我们发现虚表中只有两个指针,我们看不到Func3()和Func4(),这里是编译器隐藏了这两个函数,可以认为是VS的一个Bug。

这里我们采用比较底层的方式打印出虚表的指针:

  • 首先我们明确一点,虚函数指针会隐藏的存储在对象内存的开头
  • 我们先 &b 取地址
  • 然后强制转换成三重指针 (void***)(&d),这相当于告诉编译器,将这块内存看做指向void**的指针
  • 然后进行解引用 *(void***)(&d),这也就是对象开头的虚函数表指针
// 打印虚表并执行函数
void PrintVFTable_Safe(void** vtable, int max_entries = 10)
{cout << "Virtual Table Address: " << vtable << endl;if (vtable == nullptr) {cout << "Invalid vtable pointer!" << endl;return;}for (int i = 0; i < max_entries; ++i){// 检查地址是否有效if (vtable[i] == nullptr || (uintptr_t)vtable[i] < 0x1000) {cout << "  [" << i << "]: END OF TABLE" << endl;break;}cout << "  [" << i << "]: " << vtable[i];// 直接执行函数typedef void(*FuncPtr)();FuncPtr func = (FuncPtr)vtable[i];cout << " -> ";func();  // 执行函数// 安全限制,避免无限循环if (i >= max_entries - 1) {cout << "  ... (reached max entries)" << endl;break;}}cout << endl;
}int main()
{Base b;Derive d;void** vtable_b = *(void***)(&b);void** vtable_d = *(void***)(&d);cout << "=== Base Virtual Table ===" << endl;PrintVFTable_Safe(vtable_b);cout << "=== Derive Virtual Table ===" << endl;PrintVFTable_Safe(vtable_d);return 0;
}

3.2. 多继承中的虚函数表

下面我们给出一段多继承的代码,来分析一下:

#include <iostream>
using namespace std;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;
};// 打印虚表并执行函数
void PrintVFTable_Safe(void** vtable, int max_entries = 10)
{cout << "Virtual Table Address: " << vtable << endl;if (vtable == nullptr) {cout << "Invalid vtable pointer!" << endl;return;}for (int i = 0; i < max_entries; ++i){// 检查地址是否有效if (vtable[i] == nullptr || (uintptr_t)vtable[i] < 0x1000) {cout << "  [" << i << "]: END OF TABLE" << endl;break;}cout << "  [" << i << "]: " << vtable[i];// 直接执行函数typedef void(*FuncPtr)();FuncPtr func = (FuncPtr)vtable[i];cout << " -> ";func();  // 执行函数// 安全限制,避免无限循环if (i >= max_entries - 1) {cout << "  ... (reached max entries)" << endl;break;}}cout << endl;
}int main()
{Derive d;void** vtable_d = *(void***)(&d);PrintVFTable_Safe(vtable_d);return 0;
}

构成这样一个继承关系:

我们通过调试先来看一下对象d的虚函数指针:

然后通过打印的结果我们可看出派生类新增的虚函数一般会储存在第一个继承基类的虚函数表中:

通过下面这张图大家应该可以更好地理解:


(本篇完)

http://www.dtcms.com/a/431604.html

相关文章:

  • PSG技术分析:战术体系与关键角色
  • 在线做视频的网站重庆装修公司电话
  • 自己做的网站打不开了网站搜索排名优化怎么做
  • 开源 C# 快速开发(十四)进程--内存映射
  • ps个人网站设计江苏网页设计报价
  • 机器视觉检测中,二值化的含义以及阈值
  • 设计发明的网站域名怎么进入网址
  • 东城网站建设公司黄骅贴吧新鲜事
  • 28.CSS 3D 玻璃形态动画效果
  • 51单片机串口中断
  • 调用链监控系统 - CAT
  • 白酒公司网站的建设阜宁网站制作费用
  • 太白 网站建设高州网站建设公司
  • 怎么搭建一个博客网站wordpress信息量几百万
  • 用 【C# + Winform + MediaPipe】 实现人脸468点识别
  • C++查缺补漏《4》_时间复杂度、空间配置器和内存池、排序总结、右值引用和移动语义、函数出参和入参、类中的deafult和delete
  • wordpress 仿百度谷歌排名优化
  • 跟我学C++中级篇—non-transient异常
  • NSIS下载安装使用教程(附安装包,非常详细)
  • 怎样下载网站模板济南seo优化外包服务公司
  • 申请手机网站网站怎么做图片动态图片不显示
  • 【导航】沁恒微 RISC-V 蓝牙 入门教程目录 【快速跳转】
  • DoFoto AI 1.270.80 | 支持AI抠图、AI消除、AI照片转漫画等功能,比美图秀秀更好用
  • dt9205a数字万用表使用说明
  • 信息系统项目的质量管理(AI地铁车辆管理)
  • 爱站seo查询做外贸网站需要什么卡
  • C语言-深度剖析数据在内存中的存储
  • AI时代,我们仍然需要真实的人吗?
  • jsp网站开发实例标题栏ck播放器整合WordPress
  • 网站设计色彩搭配做网站要求高吗