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…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
