【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入)

🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:世界上的另一个我
1:02━━━━━━️💟──────── 3:58
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
多态的原理
首先理解虚函数表
正式理解多态的原理
一些拓展
多态对于引用、指针和对象
虚表的拓展
多继承中的虚函数表
先了解如何打印虚函数表
然后理解多继承中的虚函数表
方法一:加上base1的大小
方法二:切片
结论
多态的原理
首先理解虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
请问sizeof(Base)的大小为多少?
答案为:x64下16字节,x86下8字节
解析如下:
Base类中包涵着int类型的成员变量占4字节,而由于有虚函数,因此会有一个虚函表的指针vfptr,因此根据内存对齐,得到上述答案。
这时就会有疑惑了?虚函数表指针和虚函数表是什么呢?
如下通过监视窗口可以看到vfptr指向了一个数组(也就是虚函数表),而数组中存储着虚函数指针:
继续分析,我们在上述代码的基础上增加代码:
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家自己去验证?
正式理解多态的原理
见以下代码:
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void vv() { cout << "打折" << endl; }int a = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }int b = 1;
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person m;Func(m);Student s;Func(s);return 0;
}
从监视中很明显的看到,子类继承了父类的虚函数表,但是很明显的看到虚函数表中我们的BuyTicket()的虚函数指针地址改变了,而vv()确没有改变,这就很明显了,因为BuyTicket()被重写了,而vv()没有,而重写也有另外一个名字:覆盖,当我们重写了虚函数,那么就会覆盖对应虚函数在虚函数表中的指针:
内存方面观察:
可以看到其中vfptr中存储的地址是发生了改变的,也就是说我们可以根据这个地址找到新的一张虚函数表,在前面我们学习过“切片”的概念,我们知道当以父类的类型去访问子类的类型会发生“切片”使得只访问父类的类型的空间,也就是说我们只访问上图中蓝色框内的内容,再结合上上张图监视中如果子类重写了虚函数则虚函数表中虚函数指针改变。当我们调用对应的虚函数时,就会调用子函数的虚函数而不是父类的虚函数!这就是多态实现的原理!因此,多态中指向父类调用父类,指向子类调用子类!
一些拓展
多态对于引用、指针和对象
为什么多态只允许引用和指针呢?我们都知道引用的底层实现实际上还是指针,多态的实现就是指向子类对象中切割出来的那一部分!而对象只会拷贝子类对象中父类的那一部分,但是不会拷贝虚函数表指针。为什么呢?因为如果允许虚函数表指针的拷贝会造成二义性,如下:
int main()
{Person m;Student s;m=s;Func(m);Func(s);return 0;
}
如果对象可以像引用和指针一样,那么当拷贝了虚函数表指针后,你会发现我们实现不了多态中指向父类调用父类,指向子类调用子类的场景。也会造成析构函数调用调错等等的错误。
虚表的拓展
如果子类与父类中不重写虚函数,子类与父类的续虚函数表一样吗?不一样!他们存在不同的位置!虽然他们的内容是一样的!同类对象的虚函数表一样吗?一样!
总结,不同的类不会共用虚函数表,只有相同的类才会共用虚函数表!
多继承中的虚函数表
先了解如何打印虚函数表
我们都知道虚函数表是一个函数指针数组,并且数组最后一位是以nullptr结尾的。因此,我们可以根据该特性打印虚函数表:
typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
例子,打印单继承的虚函数表:
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;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t 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;
}
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。需要注意的是:这是在x86的运行环境下的,如果是x64则需强转为long long:
1.先取b的地址,强转成一个int*的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

然后理解多继承中的虚函数表
概念:
C++中的多继承是指一个派生类可以同时从多个基类派生,从而继承它们的属性和行为。
多继承是面向对象编程中一个重要的概念,它允许一个类继承多个其他类的成员。这样做有几个目的:
- 代码重用:多继承可以提高代码的重用性,因为派生类可以访 问所有基类的公有成员和保护成员。
- 功能组合:通过继承多个类,派生类可以将不同基类的功能组合在一起,形成更复杂的功能。
然而,多继承也可能带来一些问题,如菱形继承问题,这可能导致二义性。为了解决这个问题,C++引入了虚基类的概念。
如下为一段多继承的代码,可以看到drive继承了base1和base2:
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;
};
那么他的虚函数表又是什么样的呢?如下:
可以看到正如我们猜测的那样,它包含着两张虚函数表!然而,我们在derive中重写了func1()函数,以及额外添加了一个func3()函数,但是并没有在监视中显示,这是因为编译器并没有让你实际的看到,也就是说编译器在骗人,实际上就是在其中的一张表当中,可以理解为监视的一个bug。我们通过上述打印虚函数表可以看到具体的效果(注意此为x64环境下):
typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}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;
};int main()
{cout << "base1:" << endl;base1 b;PrintVTable((VFPTR*)(*(long long*)&b));cout << "base2:" << endl;base2 c;PrintVTable((VFPTR*)(*(long long*)&c));cout << "derive 表1:" << endl;derive d;PrintVTable((VFPTR*)(*(long long*)&d));//printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1))));cout << "derive 表2:" << endl;base2* ptr = &d;PrintVTable((VFPTR*)(*(long long*)ptr));
}
得到第一个虚基表的方法很简单,因为第一个虚基表的指针正好处在前8个字节处,只需要向上面一样进行强转即可,如果要找到第二个虚基表则有如下两种方法:
方法一:加上base1的大小
printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1))));
也就是加上sizeof(base1)即可,但是!需要注意的是:d的类型是Derive,在&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小!而我们想要的是每次+1跳转1个字节,所以需要强制转换char* !
方法二:切片
base2* ptr = &d;PrintVTable((VFPTR*)(*(long long*)ptr));
把d利用切片的原理给到ptr,然后再按照上面强转的原理找到虚基表即可!
结论
如下为上面代码的运行结果:
可以看到上面的图示,我们可以得出相应的结论:多继承中重写的虚函数以及新增的虚函数都是在第一个虚基表当中进行修改以及增加的!如果重写的虚函数在其他基类中也有对应的虚函数,那么继承下来的虚基表也需要重写。
更加详细的图解如下:

这里又引申出来一个问题,为什么其derive继承的两个虚基表中func1()的地址不同呢?这里就需要从汇编的角度进行理解了:
在以上的代码的基础上调试下面这段代码(x86环境下),通过反汇编可得结果如下:
derive d;base1* p1 = &d;p1->func1();base2* p2 = &d;p2->func1();

从上图的图示可以看到p1只经过了一次jmp就找到了derive中的func1()的地址,而p2则是经过了多次的jmp才找到func1()地址。这是因为:p1的调用的地址恰好与derive* 类型的this指针的地址是重叠的,因此不需要去找这个地址,而p2要经过蓝框中的“8字节的偏移”才能找到this指针(可以看到有ecx标识(ecx是存储this指针的)),才能指向derive对象的开始,才可以调用derive的func1()(毕竟fun1也可能调用成员函数、成员变量等等)。
总结:这里是为了修正this指针指向derive对象,这里调用的是derive重写的func1()。
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!

给个三连再走嘛~
相关文章:
【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入)
🎬慕斯主页:修仙—别有洞天 ♈️今日夜电波:世界上的另一个我 1:02━━━━━━️💟──────── 3:58 🔄 ◀️ ⏸ ▶️ ☰ &am…...
【机构vip教程】Android SDK手机测试环境搭建
Android SDK 的安装和环境变量的配置 前置条件:需已安装 jdk1.8及 以上版本 1、下载Android SDK,解压后即可(全英文路径);下载地址:http://tools.android-studio.org/index.php/sdk 2、新建一个环境变量&…...
2024.2.18
使用fgets统计给定文件的行数 #include<stdio.h> #include<string.h> int main(int argc, const char *argv[]) {FILE *fpNULL;if((fpfopen("./test.txt","w"))NULL){perror("open err");return -1;}fputc(h,fp);fputc(\n,fp);fput…...
Haproxy实验
环境: servera(Haproxy):192.168.233.132 serverb(web1):192.168.233.144 serverc(web2):192.168.233.140 serverd(客户端):192.168.233.141 servera(Haproxy): yum install haproxy -y vim /etc/haproxy/haproxy.cfg(配置文件) # 设置日志&#…...
CSRNET图像修复,DNN
CSRNET图像修复 CSRNET图像修复,只需要OPENCV的DNN...
004 - Hugo, 分类
004 - Hugo, 分类content文件夹 004 - Hugo, 分类 content文件夹 ├─.obsidian ├─categories │ ├─Python │ └─Test ├─page │ ├─about │ ├─archives │ ├─links │ └─search └─post├─chinese-test├─emoji-support├─Git教程├─Hugo分类├─…...
Vue3之ElementPlus中Table选中数据的获取与清空方法
Vue3之ElementPlus中Table选中数据的获取与清空方法 文章目录 Vue3之ElementPlus中Table选中数据的获取与清空方法1. 点击按钮获取与清空选中表格的数据1. 用到ElementPlus中Table的两个方法2. 业务场景3. 操作案例 1. 点击按钮获取与清空选中表格的数据 1. 用到ElementPlus中…...
Leetcode 516.最长回文子序列
题意理解: 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 回文理解为元素对称的字串,这里…...
cool Node后端 中实现中间件的书写
1.需求 在node后端中,想实现一个专门鉴权的文件配置,可以这样来解释 就是 有些接口需要token调用接口,有些接口不需要使用token 调用 这期来详细说明一下 什么是中间件中间件顾名思义是指在请求和响应中间,进行请求数据的拦截处理…...
Leecode之面试题消失的数字
一.题目及剖析 https://leetcode.cn/problems/missing-number-lcci/description/ 数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗? 注意:本题相对书上原题稍作改动 示例 1&…...
STM32的三种下载方式
结果jlink,串口,stlink方式都没有问题,是当时缩减代码,看真正起作用的代码段有哪些,就把GPIO初始化中 /*开启GPIO外部时钟*/RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE); 把开启外部时钟的代码注释掉了。…...
华为 huawei 交换机 接口 MAC 地址学习限制接入用户数量 配置示例
目录 组网需求: 配置思路: 操作步骤: 配置文件: 组网需求: 如 图 2-14 所示,用户网络 1 和用户网络 2 通过 LSW 与 Switch 相连, Switch 连接 LSW 的接口为GE0/0/1 。用户网络 1 和用户网络 2 分别属于 VLAN10 和 V…...
使用Python生成二维码的完整指南
无边落木萧萧下,不如跟着可莉一起游~ 可莉将这篇博客收录在了:《Python》 可莉推荐的优质博主首页:Kevin ’ s blog 本文将介绍如何使用Python中的qrcode库来生成二维码。通过简单的代码示例和详细解释,读者将学习如何在Python中轻…...
排序前言冒泡排序
目录 排序应用 常见的排序算法 BubbleSort冒泡排序 整体思路 图解分析 代码实现 每趟 写法1 写法2 代码NO1 代码NO2优化 时间复杂度 排序概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递…...
红队笔记Day3-->隧道上线不出网机器
昨天讲了通过代理的形式(端口转发)实现了上线不出网的机器,那么今天就来讲一下如何通过隧道上线不出网机器 目录 1.网络拓扑 2.开始做隧道?No!!! 3.icmp隧道 4.HTTP隧道 5.SSH隧道 1.什么…...
C 练习实例70-求字符串长度
题目:写一个函数,求一个字符串的长度,在 main 函数中输入字符串,并输出其长度。 解答: #include <stdio.h> int length(char *s); int main() {int len;char str[20];printf("请输入字符串:\n");scan…...
HarmonyOS—@State装饰器:组件内状态
State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。 在状态变量相关装饰器中,State是最基础的,使变量拥有状态属性的装饰器&a…...
Linux系统——防火墙拓展及重点理解
目录 一、iptables 1.基本语法 2.四表五链——重点记忆 2.1四表 2.2五链 2.3总结 3.iptables选项示例 3.1 -Z 清空流量计数 3.2 -P 修改默认规则 3.3 -D 删除规则 3.4 -R 指定编号替换规则 5.白名单 6.通用匹配 7.示例 7.1添加回环网卡 7.2可以访问端口 7.3 主…...
阿里云短信验证码的两个坑
其它都参照官网即可,其中有两个坑需要注意: 1、除去官网pom引用的包之外,还需要引用以下包: <dependency><groupId>org.apache.httpcomponents.client5</groupId><artifactId>httpclient5</artifact…...
c入门第十五篇——学而时习之(阶段性总结)
古人说:“学而时习之。”古人又说:“温故而知新。”古人还说:“读书百遍,其义自见。” 总结一个道理那就是好书要反反复复的读,学习过的知识要时常去复习它,才有可能常读常新。 我:“师弟&…...
如何在Python中正确调用DeepSeek-Reasoner获取思考过程(附完整代码示例)
深度解析:Python调用DeepSeek-Reasoner获取思维链的工程实践 当开发者需要构建具备复杂推理能力的AI应用时,获取模型完整的思考过程(Reasoning Content)往往比最终答案更有价值。DeepSeek-Reasoner作为专为逻辑推理优化的模型&…...
Coze-Loop与Vue3前端性能优化:渲染速度提升方案
Coze-Loop与Vue3前端性能优化:渲染速度提升方案 1. 为什么Vue3项目需要Coze-Loop来诊断性能问题 在实际开发中,很多团队都遇到过这样的困惑:明明代码写得挺规范,但页面滚动卡顿、列表加载缓慢、交互响应迟滞。我们曾接手一个电商…...
VS Code玩转Arduino开发——插件配置与工程搭建全攻略
1. 为什么选择VS Code开发Arduino? 很多Arduino爱好者刚开始接触开发时,都会使用官方提供的Arduino IDE。这个编辑器确实简单易用,但随着项目复杂度提升,你会发现它缺少很多现代编辑器该有的功能——代码补全、语法高亮、项目管理…...
3步解决华硕笔记本显示异常:G-Helper色彩配置修复指南
3步解决华硕笔记本显示异常:G-Helper色彩配置修复指南 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目地址…...
VSCode安装与Qwen3开发环境配置一站式解决方案
VSCode安装与Qwen3开发环境配置一站式解决方案 为智能字幕开发量身打造的高效开发环境配置指南 1. 开篇:为什么需要专门的环境配置? 你是不是也遇到过这样的情况:好不容易下载了代码,却发现各种依赖报错,环境配置折腾…...
OpenClaw学习路径:从nanobot镜像入门到开发自定义技能
OpenClaw学习路径:从nanobot镜像入门到开发自定义技能 1. 为什么选择OpenClaw作为自动化助手 第一次听说OpenClaw时,我正在为重复性的文件整理工作头疼。作为一个经常需要处理大量技术文档的开发者,每天要花费数小时在机械的文件分类、重命…...
OpenClaw本地搜索引擎:GLM-4.7-Flash优化个人文件检索
OpenClaw本地搜索引擎:GLM-4.7-Flash优化个人文件检索 1. 为什么需要智能化的本地文件搜索 作为一个长期被文件管理困扰的技术写作者,我的MacBook里堆积着超过2万份文档——技术笔记、项目草稿、参考资料、会议记录杂乱地分布在各个角落。传统的文件名…...
Tiled2Unity:Tiled地图与Unity引擎的无缝数据转换解决方案
Tiled2Unity:Tiled地图与Unity引擎的无缝数据转换解决方案 【免费下载链接】Tiled2Unity Export Tiled Map Editor (TMX) files into Unity 项目地址: https://gitcode.com/gh_mirrors/ti/Tiled2Unity 副标题:基于自动化工作流的2D游戏地图资产转…...
OpenPLC Editor:重塑工业自动化编程的开源方案
OpenPLC Editor:重塑工业自动化编程的开源方案 【免费下载链接】OpenPLC_Editor 项目地址: https://gitcode.com/gh_mirrors/ope/OpenPLC_Editor 在工业自动化领域,PLC(可编程逻辑控制器)编程长期被商业软件垄断ÿ…...
低成本自动化方案:OpenClaw+GLM-4.7-Flash替代Zapier实现跨平台触发
低成本自动化方案:OpenClawGLM-4.7-Flash替代Zapier实现跨平台触发 1. 为什么选择本地AI替代SaaS自动化工具 三年前我开始使用Zapier自动化处理工作流时,每月29美元的订阅费看起来物有所值。但随着任务复杂度增加,去年我的账单悄然涨到了89…...
