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…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...