C/C++ 虚函数
虚函数的定义
- 虚函数是指在基类内部声明的成员函数前面添加关键字 virtual 指明的函数
- 虚函数存在的意义是为了实现多态,让派生类能够重写(override)其基类的成员函数
- 派生类重写基类的虚函数时,可以添加 virtual 关键字,但不是必须这么做
- 虚函数是动态绑定的,在运行时才确定,而非虚函数的调用在编译时确定
- 虚函数必须是非静态成员函数,因为静态成员函数需要在编译时确定
- 构造函数不能是虚函数,因为虚函数是动态绑定的,构造函数创建时需要确定对象关系。
- 析构函数一般是虚函数
- 虚函数一旦声明,就一直是虚函数,派生类也无法改变这一事实
虚函数工作原理
虚函数表 + 虚表指针
- 编译器在含有虚函数的类中创建一个虚函数表,称为 vtable,这个vtable用来存放虚函数的地址。另外还隐式的设置了一个虚表指针,称为vptr,这个vptr指向了该类对象的虚函数表。
- 派生类在继承基类的同时,也会继承基类的虚函数表。
- 派生类重写(override)了基类的虚函数时,则会将重写后的虚函数的地址替换掉由基类继承而来的虚函数表中对应虚函数地址。
- 若派生类没有重写,则由基类继承而来的虚函数的地址将直接保存在派生类的虚函数表中。
示例
class A
{
public:int a;int function() {return this->a;}virtual int vfunction() {return a;}
};void vCallFunction(A* obj) {obj->vfunction();
}void callFunction(A* obj) {obj->function();
}调用:A a;vCallFunction(&a);callFunction(&a);汇编解析,以说明 只有在运行时才能确定调用的是基类的虚函数还是派生类的虚函数:1. 静态绑定,不是虚函数,目标地址在编译阶段就确定了。
00007FF679471030 <test1. | 48:894C24 08 | mov qword ptr ss:[rsp+8],rcx | test1.cpp:35
00007FF679471035 | 48:83EC 28 | sub rsp,28 |
00007FF679471039 | 48:8B4C24 30 | mov rcx,qword ptr ss:[rsp+30] | test1.cpp:36
00007FF67947103E | E8 BDFFFFFF | call <test1.public: virtual int __cdecl A::vfunction(void)> |
00007FF679471043 | 48:83C4 28 | add rsp,28 | test1.cpp:37
00007FF679471047 | C3 | ret |2. 动态绑定,只能根据 rdx 的值来确定函数位置
00007FF679471010 <test1. | 48:894C24 08 | mov qword ptr ss:[rsp+8],rcx | test1.cpp:31
00007FF679471015 | 48:83EC 28 | sub rsp,28 |
00007FF679471019 | 48:8B4424 30 | mov rax,qword ptr ss:[rsp+30] | test1.cpp:32
00007FF67947101E | 48:8B00 | mov rax,qword ptr ds:[rax] |
00007FF679471021 | 48:8B4C24 30 | mov rcx,qword ptr ss:[rsp+30] |
00007FF679471026 | FF10 | call qword ptr ds:[rax] |
00007FF679471028 | 48:83C4 28 | add rsp,28 | test1.cpp:33
00007FF67947102C | C3 | ret |当类A有虚函数的时候它就会偷偷生成一个隐藏成员变量,它存放着虚函数表的位置,根据偏移就可以找到实际上的 vfunction 的地址,将其存在寄存器 rax 里面,随后 call[rax] 就正常调用了。
注意:
每个类都只有一个虚函数表,该类所有的对象共享这个虚函数表,而不是每个实例化对象都分别由一个虚函数表。
c++ 类的多态性是通过虚函数来实现的,如果基类通过引用或指针调用的是虚函数时,我们并不知道执行该函数的对象是什么类型的,只有在运行时才能确定调用的是基类的虚函数还是派生类的虚函数,这就是运行时多态。
虚函数表和虚函数表指针创建的时机
当我们发现某一个具体的类当中,存在 virtual 这样的字段,就会为这个类去生成虚函数表。它的内容在编译期就已经生成确定了。虚函数表存储的位置是在全局数据区的只读数据段 ,虚函数表是存放虚函数的地址的数组。
当我们为类去构建对象的时候,在构造函数中。将虚函数表的地址赋值给对象的 vptr (存放对象首地址)
继承下,虚函数表指针的复制过程:
继承下,先会调用基类的构造函数,先将基类的虚函数表地址赋值给vptr,接着调用子类的构造函数的时候又将子类的虚函数表地址赋值给vptr(这是覆盖的行为)。

什么函数不能是虚函数 为什么(重点)
- 不能被继承的函数
- 不能被重写的函数
- 普通函数 普通函数不属于成员函数 是不能被继承的 普通函数只能被重载 不能被重写 因此声明为虚函数没有意
义 因为编译器会在编译时绑定函数 而多态体现在运行时绑定 通常通过基类指针指向子类对象实现多态 - 友元函数 友元函数不属于类的成员函数 不能被继承 对于没有继承特性的函数没有虚函数的说法
- 构造函数 构造函数是用来初始化对象的 假如子类可以继承基类构造函数 那么子类对象的构造将使用基类的构造
函数 而基类构造函数并不知道子类有什么成员 显然是不符合语义的 从另外一个角度讲 多态是通过基类指针指
向子类对象来实现多态的 在对象构造之前并没有对象产生 因此无法使用多态特性 这是矛盾的 因此构造函数不
允许继承 - 内联成员函数 内联函数就是为了在代码中直接展开 减少函数调用花费的代价 也就是说内联函数是在编译时展开
的 而虚函数是为了实现多态 是在运行时绑定的 因此内联函数和多态的特性相违背 - 静态成员函数 首先静态成员函数理论是可继承的 但是静态成员函数是编译时确定的 无法动态绑定 不支持多态因此不能被重写
汇编角度看类
类大小的计算
class Person {private:int age; uint64_t num;
};
大小:
printf("%d", sizeof(Person)); // 16
这里只有类的成员,按照结构体算法,直接就是16
进阶算法,当添加一个成员函数时,计算大小:
class Person {void function(){printf("hello world");};
private:int age; uint64_t num;
};
printf("%d", sizeof(Person)); // 16
由此可见:类中的成员函数是不占用类对象内存空间的
为了验证以上说法,我们删除一个8位的成员变量,此时只剩下 int age 也就是4字节成员变量。
class Person {void function(){printf("hello world");};
private:int age; // 4
};
练一练,计算如下大小:
class Person {
public:virtual int getAge() { //虚函数定义return age;}
private:int age;
};
大小:
printf("%d", sizeof(Person)); // sizeof(Person) = 16 (64-bit)
这里为什么是16 字节呢,在这里由于出现了虚函数,那么编译器会初始化虚表指针
在 64 位的情况下指针占8个字节,对齐成员变量的话,就是16字节,是不是很简单。
对整体类逆向分析:
class Person {
public:
virtual int getAge() { //虚函数定义
return age;
}
virtual void setAge(int age) { //虚函数定义
this->age = age;
}
private:
int age;
};
int main(int argc, char* argv[]) {
Person person;
return 0;
}
反汇编:
------------------------ main ------------------------
00007FF76A17117C | 48:8D4C24 20 | lea rcx,qword ptr ss:[rsp+20] ;获取对象首地址
00007FF76A171181 | E8 1A000000 | call 0x00007FF76A1711A0 ;调用构造函数:<test1.public: __cdecl Person::Person(void)>------------------------ 构造函数(Person::Person())------------------------
00007FF76A1711A0 < | 48:894C24 08 | mov qword ptr ss:[rsp+8],rcx
00007FF76A1711A5 | 48:8B4424 08 | mov rax,qword ptr ss:[rsp+8] ; this 指针 rax = 0000004AF30FF7B0
00007FF76A1711AA | 48:8D0D CF200000 | lea rcx, ds:[0x00007FF76A173280] ; 给 虚函数表指针 <const Person::`vftable'> 地址 rcx = 00007FF76A173280
00007FF76A1711B1 | 48:8908 | mov qword ptr ds:[rax],rcx ; 取虚表的首地址,保存至虚表指针中
00007FF76A1711B4 | 48:8B4424 08 | mov rax,qword ptr ss:[rsp+8]; 返回对象首地址,rax = 0000004AF30FF7B0
00007FF76A1711B9 | C3 | ret 内存<test1.const Person::`vftable'> :
00007FF76A173280 <public: virtual int __cdecl Person::getAge(void)> 00007FF76A171120 ..j÷... test1.public: virtual int __cdecl Person::getAge(void)
00007FF76A173288 <public: virtual void __cdecl Person::setAge(int)> 00007FF76A171130 0..j÷... test1.public: virtual void __cdecl Person::setAge(int)
地址所在区段:
地址=00007FF76A173000
大小=0000000000002000
页面信息=".rdata"
当前类由于存在虚函数,那么编译器为 Person 生成了默认构造函数。该默认构造函数首先取得虚表的首地址,然后赋值到虚表指针中。
查看 内存 可见,虚表指针中存放了两个函数地址,分别是虚函数 getAge 和虚函数 setAge 的地址。因此,得到虚表指针就相当于得到了类中所有虚函数的首地址。
因为虚表信息在编译后会被链接到对应的执行文件中,所以获得的虚表地址是一个相对固定的地址。虚表中虚函数的地址排列顺序因虚表函数在类中的声明顺序而定,先声明的虚函数的地址会被排列在虚表靠前的位置。第一个被声明的虚函数地址在虚表首地址处。
在虚表指针初始化的过程中,对象执行了构造函数后,就得到了虚表指针,当其他代码访问这个对象的虚函数时,会根据对象的首地址,取出对应的虚表元素,当函数被调用时,会间接访问虚表,得到对应的虚函数首地址并调用执行。这种调用方式是一个间接的调用过程。需要多次寻址才能完成。
Person person;person.setAge(0x11);printf("Age = 0x%X", person.getAge());
汇编:
00007FF6A339125C | 48:8D4C24 20 | lea rcx,qword ptr ss:[rsp+20] | test1.cpp:89
00007FF6A3391261 | E8 4A000000 | call <test1.public: __cdecl Person::Person(void)> |
00007FF6A3391266 | BA 11000000 | mov edx,11 | test1.cpp:90
00007FF6A339126B | 48:8D4C24 20 | lea rcx,qword ptr ss:[rsp+20] |
00007FF6A3391270 | E8 9BFFFFFF | call <test1.public: virtual void __cdecl Person::setAge(int)> |
00007FF6A3391275 | 48:8D4C24 20 | lea rcx,qword ptr ss:[rsp+20] | test1.cpp:91
00007FF6A339127A | E8 81FFFFFF | call <test1.public: virtual int __cdecl Person::getAge(void)> |
00007FF6A339127F | 8BD0 | mov edx,eax |
00007FF6A3391281 | 48:8D0D F81F0000 | lea rcx,qword ptr ds:[7FF6A3393280] | 00007FF6A3393280:"Age = 0x%X"
00007FF6A3391288 | E8 13FEFFFF | call <test1.printf> |
上述通过虚表间接寻址访问的情况,只有在使用对象的指针或引用,调用虚函数的时候才会出现。当直接使用对象调用自身虚函数时,没必要查表访问,因为已经明确调用的是自身成员函数,根本没有构成多态性。查询虚表只会画蛇添足,降低程序执行效率,所以将这种情况处理为直接调用。
析构函数操作:
添加析构代码:~Person() { printf("~Person() \n");}汇编:
00007FF6DCBB1230 < | 48:894C24 08 | mov qword ptr ss:[rsp+8],rcx | test1.cpp:85, [rsp+08]:"Age = 0x%X \n"
00007FF6DCBB1235 | 48:83EC 28 | sub rsp,28 |
00007FF6DCBB1239 | 48:8B4424 30 | mov rax,qword ptr ss:[rsp+30] | [rsp+30]:拿到 this 指针也是虚表的位置
00007FF6DCBB123E | 48:8D0D 63200000 | lea rcx,qword ptr ds:[<const Person::`vftable'>] |将当前类虚表首地址赋值到虚表指针中
00007FF6DCBB1245 | 48:8908 | mov qword ptr ds:[rax],rcx |
00007FF6DCBB1248 | 48:8D0D 31200000 | lea rcx,qword ptr ds:[<"~Person() \n"...>] | 00007FF6DCBB3280:"~Person() \n"
00007FF6DCBB124F | E8 4CFEFFFF | call <test1.printf> |
00007FF6DCBB1254 | 48:83C4 28 | add rsp,28 |
00007FF6DCBB1258 | C3 | ret |
在汇编中识别析构函数的条件是,写入虚表指针,对象的虚表指针可能是有效的,已经指向了正确的虚函数表,将对象的虚表指针重新赋值后,其指针可能指向了另一个虚表,虚表内容不一定和原来的意义。
继承
相关文章:
C/C++ 虚函数
虚函数的定义 虚函数是指在基类内部声明的成员函数前面添加关键字 virtual 指明的函数虚函数存在的意义是为了实现多态,让派生类能够重写(override)其基类的成员函数派生类重写基类的虚函数时,可以添加 virtual 关键字,但不是必须这么做虚函…...
【3GPP】【5G】注销流程(Deregistration procedures)
1. 欢迎大家订阅和关注,精讲3GPP通信协议(2G/3G/4G/5G/IMS)知识点,专栏会持续更新中.....敬请期待! 目录 3.1.2 Deregistration procedures 3.1.2.1 UE-initiated Deregistration 3.1.2.2 Network-initiated Deregistration 3.1.2 Deregistration procedures 注销流程…...
【小游戏篇】三子棋游戏
硬控我一上午,小编还是太菜了,大家可以自行升级电脑难度,也可以升级游戏到五子棋 1.game.h #pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #define ROW 3 #define COL 3//初始化棋盘 void InitBoa…...
7-Zip Mark-of-the-Web绕过漏洞复现(CVE-2025-0411)
免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 0x0…...
2025年国产化推进.NET跨平台应用框架推荐
2025年国产化推进.NET跨平台应用框架推荐 1. .NET MAUI NET MAUI是一个开源、免费(MIT License)的跨平台框架(支持Android、iOS、macOS 和 Windows多平台运行),是 Xamarin.Forms 的进化版,从移动场景扩展到…...
关于ARM和汇编语言
一图流 ARM 计算机组成 输入设备 输出设备 存储设备 运算器 控制器 处理器读取内存程序执行的过程 取指阶段:控制器器通过地址总线向存储器发送想要获取的指令的地址编号,存储器将指定的指令发送给处理器 译码阶段:控制器对指令进行分…...
2024人工智能AI+制造业应用落地研究报告汇总PDF洞察(附原数据表)
原文链接: https://tecdat.cn/?p39068 本报告合集洞察深入剖析当前技术应用的现状,关键技术 创新方向,以及行业应用的具体情况,通过制造业具体场景的典型 案例揭示人工智能如何助力制造业研发设计、生产制造、运营管理 和产品服…...
QTableView和QTableWidget的关系与区别
QTableView 和 QTableWidget 都是 Qt 框架中用于显示表格数据的控件,但它们在设计和使用上有一些重要的区别。 QTableView 模型-视图架构:QTableView 是 Qt 模型-视图架构的一部分,它与模型(如 QStandardItemModel 或自定义的 QA…...
Java导出通过Word模板导出docx文件并通过QQ邮箱发送
一、创建Word模板 {{company}}{{Date}}服务器运行情况报告一、服务器:总告警次数:{{ServerTotal}} 服务器IP:{{IPA}},总共告警次数:{{ServerATotal}} 服务器IP:{{IPB}},总共告警次数:{{ServerBTotal}} 服务器IP:{{IPC}}&#x…...
ESP8266 MQTT服务器+阿里云
MQTT私有平台搭建(EMQX 阿里云) 阿里云服务器 EMQX 搭建私有MQTT平台 1、搜索EMQX开源版本 2、查看各版本EMQX支持的UBUNTU版本 3、查看服务器Ubuntu版本 4、使用APT安装模式 5、按照官网指示安装并启动 6、下载安装MQTTX测试工具 7、设置云服务…...
css动画水球图
由于echarts水球图动画会导致ios卡顿,所以纯css模拟 展示效果 组件 <template><div class"water-box"><div class"water"><div class"progress" :style"{ --newProgress: newProgress % }"><…...
【设计模式-行为型】状态模式
一、什么是状态模式 什么是状态模式呢,这里我举一个例子来说明,在自动挡汽车中,挡位的切换是根据驾驶条件(如车速、油门踏板位置、刹车状态等)自动完成的。这种自动切换挡位的过程可以很好地用状态模式来描述。状态模式…...
2024.1.22 安全周报
政策/标准/指南最新动态 01 工信部印发《关于加强互联网数据中心客户数据安全保护的通知》 原文: https://www.secrss.com/articles/74673 互联网数据中心作为新一代信息基础设施,承载着千行百业的海量客户数据,是关系国民经济命脉的重要战略资源。…...
idea修改模块名导致程序编译出错
本文简单描述分别用Idea菜单、pom.xml文件管理项目模块module 踩过的坑: 通过idea菜单创建模块,并用idea菜单修改模块名,结构程序编译报错,出错的代码莫名奇妙。双击maven弹窗clean时,还是报错。因为模块是新建的&am…...
root用户Linux银河麒麟服务器安装vnc服务
安装必要桌面环境组件 yum install mate-session-manager -y mate-session #确定是否安装成功安装vnc服务器 yum install tigervnc-server -y切换到root为root得vnc设置密码 su root vncpasswd给root用户设置vnc服务器文件 vi /etc/systemd/system/vncserver:1.service [Un…...
CentOS 7使用RPM安装MySQL
MySQL是一个开源的关系型数据库管理系统(RDBMS),允许用户高效地存储、管理和检索数据。它被广泛用于各种应用,从小型的web应用到大型企业解决方案。 MySQL提供了丰富的功能,包括支持多个存储引擎、事务能力、数据完整…...
OpenCV imread函数读取图像__实例详解
OpenCV imread函数读取图像__实例详解 本文目录: 零、时光宝盒 一、imread函数定义 二、imread函数支持的文件格式 三、imread函数flags参数详解 (3.1)、Flags-1时,样返回加载的图像(使用alpha通道,否…...
激光线扫相机无2D图像的标定方案
方案一:基于运动控制平台的标定 适用场景:若激光线扫相机安装在可控运动平台(如机械臂、平移台、旋转台)上,且平台的运动精度已知(例如通过编码器或高精度步进电机控制)。 步骤: 标…...
【安当产品应用案例100集】034-安当KSP支持密评中存储数据的机密性和完整性
安当KSP是一套获得国密证书的专业的密钥管理系统。KSP的系统功能扩展图示如下: 我们知道商用密码应用安全性评估中,需要确保存储的数据不被篡改、删除或者破坏,必须采用合适的安全方案来确保存储数据的机密性和完整性。KSP能否满足这个需求呢…...
08.七种排序算法实现(C语言)
目录 一.排序的基本概念 1.1 排序的概念 1.2 常见的排序算法 二.常见排序算法的实现 2.1 插入排序(直接) 1.基本思想 2.直接插入排序的特性 3.代码实现 2.2 希尔排序 1.基本思想 2.希尔插入排序的特性 3.代码实现 2.3 选择排序 1.基本思想 2…...
利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
