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(这是覆盖的行为)。
在这里插入图片描述

什么函数不能是虚函数 为什么(重点)

  • 不能被继承的函数
  • 不能被重写的函数
  1. 普通函数 普通函数不属于成员函数 是不能被继承的 普通函数只能被重载 不能被重写 因此声明为虚函数没有意
    义 因为编译器会在编译时绑定函数 而多态体现在运行时绑定 通常通过基类指针指向子类对象实现多态
  2. 友元函数 友元函数不属于类的成员函数 不能被继承 对于没有继承特性的函数没有虚函数的说法
  3. 构造函数 构造函数是用来初始化对象的 假如子类可以继承基类构造函数 那么子类对象的构造将使用基类的构造
    函数 而基类构造函数并不知道子类有什么成员 显然是不符合语义的 从另外一个角度讲 多态是通过基类指针指
    向子类对象来实现多态的 在对象构造之前并没有对象产生 因此无法使用多态特性 这是矛盾的 因此构造函数不
    允许继承
  4. 内联成员函数 内联函数就是为了在代码中直接展开 减少函数调用花费的代价 也就是说内联函数是在编译时展开
    的 而虚函数是为了实现多态 是在运行时绑定的 因此内联函数和多态的特性相违背
  5. 静态成员函数 首先静态成员函数理论是可继承的 但是静态成员函数是编译时确定的 无法动态绑定 不支持多态因此不能被重写

汇编角度看类

类大小的计算

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   .....  test1.public: virtual int __cdecl Person::getAge(void)
00007FF76A173288 <public: virtual void __cdecl Person::setAge(int)>    00007FF76A171130  0.....  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                                                                               |

在汇编中识别析构函数的条件是,写入虚表指针,对象的虚表指针可能是有效的,已经指向了正确的虚函数表,将对象的虚表指针重新赋值后,其指针可能指向了另一个虚表,虚表内容不一定和原来的意义。

继承

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/893830.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

爬虫基础之爬取某基金网站+数据分析

声明: 本案例仅供学习参考使用&#xff0c;任何不法的活动均与本作者无关 网站:天天基金网(1234567.com.cn) --首批独立基金销售机构-- 东方财富网旗下基金平台! 本案例所需要的模块: 1.requests 2.re(内置) 3.pandas 4.pyecharts 其他均需要 pip install 模块名 爬取步骤: …

RKNN_C++版本-YOLOV5

1.背景 为了实现低延时&#xff0c;所以开始看看C版本的rknn的使用&#xff0c;确实有不足的地方&#xff0c;请指正&#xff08;代码借鉴了rk官方的仓库文件&#xff09;。 2.基本的操作流程 1.读取模型初始化 // 设置基本信息 // 在postprocess.h文件中定义&#xff0c;详见…

Learning Vue 读书笔记 Chapter 2

2. Vue 基本工作原理 2.1 Virtual DOM 概念&#xff1a; DOM: DOM以内存中树状数据结构的形式&#xff0c;代表了网页上的HTML&#xff08;或XML&#xff09;文档内容。它充当了一个编程接口&#xff0c;将网页与实际的编程代码&#xff08;如JavaScript&#xff09;连接起来…

Python标准库 - os (1) 环境变量、进程的用户和组

文章目录 1 访问和修改环境变量1.1 访问环境变量1.2 修改环境变量 2 进程的用户和组2.1 进程的ID2.2 进程的用户2.3 进程组 os模块提供了各种操作系统接口。包括环境变量、进程管理、进程调度、文件操作等方面。 这里整理了环境变量、进程的用户和用户组相关的控制方法。 参考…

Synology 群辉NAS安装(4)docker-compose

Synology 群辉NAS安装&#xff08;4&#xff09;docker-compose Synology 群辉NAS安装&#xff08;4&#xff09;docker-composeerror while loading shared libraries: libz.so.1 Synology 群辉NAS安装&#xff08;4&#xff09;docker-compose 1.下载最新版docker-compose |…

【C++高并发服务器WebServer】-7:共享内存

本文目录 一、共享内存1.1 shmget函数1.2 shmat1.3 shmdt1.4 shmctl1.5 ftok1.6 共享内存和内存映射的关联1.7 小demo 二、共享内存操作命令 一、共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域&#xff08;通常被称为段&#xff09;。由于一个共享内存段会称…

【C语言指针】数组指针和指针数组

一、数组指针 1.1 含义 数组指针本质是一个指针&#xff0c;它指向一个数组也就是说它指向数组在内存中的起始地址。数组指针可以用来处理多维数组&#xff0c;尤其是二维数组。 1.2 数组指针的一般形式 首先数组的一般形式是&#xff1a; int a[10] {1,2,3,4,5};这里a代…

关于av_get_channel_layout_nb_channels函数

问题&#xff1a;ffmpeg5.1 使用av_get_channel_layout_nb_channels函数时报错。 过程&#xff1a;经过检查&#xff0c;发现对应头文件内已经不包含该函数。遂查找资料&#xff0c;发现在ffmpeg5.1之后该函数被废弃&#xff0c;具体而言&#xff0c;新增了AVChannelLayout。 …

CrypTen——基于pytorch的隐私保护机器学习框架

目录 一、CrypTen概述 二、应用场景 三、CrypTen优势 四、CrypTen技术解析 1.基于pytorch的构建基础 2.核心密码学原语 3.加密模型训练流程 五、传统隐私保护技术与CrypTen的对比 1.传统隐私保护技术介绍 2.CrypTen与传统隐私保护技术的区别 六、CrypTen的环境配置…

ES6 简单练习笔记--变量申明

一、ES5 变量定义 1.在全局作用域中 this 其实就是window对象 <script>console.log(window this) </script>输出结果: true 2.在全局作用域中用var定义一个变量其实就相当于在window上定义了一个属性 例如: var name "孙悟空" 其实就相当于执行了 win…

Arduino大师练成手册 -- 控制 PN532 NFC 模块

要在 Arduino 上控制 PN532 NFC 模块&#xff0c;你可以按照以下步骤进行&#xff1a; 硬件连接 VCC&#xff1a;连接到 Arduino 的 3.3V 引脚。 GND&#xff1a;连接到 Arduino 的 GND 引脚。 SDA&#xff1a;连接到 Arduino 的 SDA 引脚&#xff08;通常是 A4&#xff09…

.NET Core跨域

CORS 跨域通讯的问题。解决方案&#xff1a;JSONP、前端代理后端请求、CORS等。CORS原理&#xff1a;在服务器的响应报文头中通过access-control-allow-origin告诉浏览器允许跨域访问的域名。在Program.cs的“var appbuilder.Build()”这句代码之前注册 string[] urls new[] …

python——Django 框架

Django 框架 1、简介 Django 是用python语言写的开源web开发框架&#xff0c;并遵循MVC设计。 Django的**主要目的是简便、快速的开发数据库驱动的网站。**它强调代码复用&#xff0c;多个组件可以很方便的以"插件"形式服务于整个框架&#xff0c;Django有许多功能…

大模型正确调用方式

1、ollama 安装 curl -fsSL https://ollama.com/install.sh | sh 如果是AutoDl服务器&#xff0c;可以开启学术加速。 source /etc/network_turbo 本次使用腾讯云Cloud Studio&#xff0c;所以已经安装好了 Ollama 2、启动 ollama run 模型的名字 ollama serve # 开启服务 olla…

CE-PBFT:大规模联盟区块链的高可用一致性算法

摘要 区块链已广泛应用于农产品溯源、供应链管理、物流运输等各个领域。作为联盟区块链不可缺少的组成部分&#xff0c;共识算法保证了网络中每个节点的一致性和可信度。然而&#xff0c;由于通信过程的复杂性&#xff0c;现有的大规模联盟区块链场景中的共识算法存在低系统吞…

2025年新开局!谁在引领汽车AI风潮?

汽车AI革命已来。 在2025年伊始开幕的CES展上&#xff0c;AI汽车、AI座舱无疑成为了今年汽车行业的最大热点。其中不少车企在2025年CES上展示了其新一代AI座舱&#xff0c;为下一代智能汽车的人机交互、场景创新率先打样。 其中&#xff0c;东软集团也携带AI驱动、大数据支撑…

通义灵码插件保姆级教学-IDEA(安装及使用)

一、JetBrains IDEA 中安装指南 官方下载指南&#xff1a;通义灵码安装教程-阿里云 步骤 1&#xff1a;准备工作 操作系统&#xff1a;Windows 7 及以上、macOS、Linux&#xff1b; 下载并安装兼容的 JetBrains IDEs 2020.3 及以上版本&#xff0c;通义灵码与以下 IDE 兼容&…

Kafka常见问题之 `javax.management.InstanceAlreadyExistsException`

文章目录 Kafka常见问题之 javax.management.InstanceAlreadyExistsException1. 概述2. 常见原因3. 具体异常示例4. 解决方案4.1 确保单一 Kafka Producer 实例4.2 配置 Kafka Broker 和 Producer 使用唯一的 JMX 名称&#xff08;对于Producer重点检查 client.id&#xff09;4…

跟我学C++中级篇——64位的处理

一、计算机的发展 计算机从二进制为基础开始描述整个世界&#xff0c;但正如现实世界一样&#xff0c;十进制为主的世界也会有万千百概念。所以在实际的应用中&#xff0c;会出现32位和64位的计算机系统。当然&#xff0c;前面还有过16位、8位和4位等&#xff0c;以后还可以会…

文献阅读 250125-Accurate predictions on small data with a tabular foundation model

Accurate predictions on small data with a tabular foundation model Accurate predictions on small data with a tabular foundation model | Nature 使用一种基于表格的模型来对小型数据实现准确预测 ## Abstract: 基于其他列来填充标签列中缺失值的基本预测任务对于各种应…