派生类重载的delete操作符调用时可以动态绑定吗

我们来看一个和派生类重载delete操作符相关的C++程序:

class animal {
public:virtual ~animal() {}
};class dog : public animal {
public:virtual ~dog() {puts("destory dog");}void operator delete(void* p) {printf("delete dog storage in %p\n", p);::operator delete(p);}
};int main(int argc, char** argv) {animal* ap = new dog;delete ap;return 0;
}

派生类dog继承基类animal,并且重载了operator delete()。ap 是一个基类animal的指针,但是它指向了一个由派生类dog在堆上创建的对象。那么,当程序执行delete ap时,会调用dog类重载的operator delete()吗?也就是说当delete一个基类指针时,会调用派生类重载的operator delete()函数吗?

不过需要注意的是,这里void dog::operator delete(void* p)并不是一个virtual函数,我们试着把它声明成virtual看看,编译时会发生失败:

error: 'operator delete' cannot be declared 'virtual', since it is always static

编译失败的原因是operator delete()总是类的static函数,也就是它不可能当作virtual函数的,也不是非static成员函数。况且也并没有在基类animal中定义一个operator delete()虚函数,然后在派生类dog中重写override这个函数,并不是我们日常编程实践中常见的OOP编程套路。因此,既然无法使用virtual函数来动态绑定,感觉应该是调用了全局的operator delete()函数。我们运行一下程序,它的输出log如下:

destory dog
delete dog storage in 0x10ad2b0

可见,当程序执行delete ap时,还是调用了dog类提供的operator delete(),这有点出乎意料,既然不是virtual函数,基类指针又是怎么知道派生类中的这个static成员函数的呢?

我们看一下汇编代码,下面是gcc在O1优化选项下生成的汇编代码:

dog::~dog() [base object destructor]:sub     rsp, 8mov     QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16mov     edi, OFFSET FLAT:.LC0call    putsadd     rsp, 8ret
.LC1:.string "delete dog storage in %p\n"
dog::operator delete(void*):push    rbxmov     rbx, rdimov     rsi, rdimov     edi, OFFSET FLAT:.LC1mov     eax, 0call    printfmov     rdi, rbxcall    operator delete(void*)pop     rbxret
dog::~dog() [deleting destructor]:push    rbxmov     rbx, rdicall    dog::~dog() [complete object destructor]mov     rdi, rbxcall    dog::operator delete(void*)pop     rbxret
dog::dog() [base object constructor]:mov     QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16ret
main:push    rbxmov     edi, 8call    operator new(unsigned long)mov     rbx, raxmov     rdi, raxcall    dog::dog() [complete object constructor]mov     rax, QWORD PTR [rbx]mov     rdi, rbxcall    [QWORD PTR [rax+8]]mov     eax, 0pop     rbxret

在main函数中语句delete ap对应的汇编代码是:

mov     rax, QWORD PTR [rbx]  //rbx是this指针,rax是虚函数表指针vptr
mov     rdi, rbx
call    [QWORD PTR [rax+8]]    // 调用虚函数中的第2个虚函数

核心指令call [QWORD PTR [rax+8]],它调用了虚函数表中的第2个虚函数,在这里rax是指向dog类虚函数表的vptr指针,[rax+0]指向第1个虚函数,[rax+8]指向第2个虚函数。下面是dog类虚函数表的信息:

vtable for dog:
.quad 0
.quad typeinfo for dog
.quad dog::~dog() [complete object destructor]
.quad dog::~dog() [deleting destructor]

第2个虚函数是:dog::~dog() [deleting destructor],它的汇编代码如下:

dog::~dog() [deleting destructor]:push    rbxmov     rbx, rdicall    dog::~dog() [complete object destructor]mov     rdi, rbxcall    dog::operator delete(void*)pop     rbxret

第6行指令:call dog::operator delete(void*),此处调用了dog类重载的operator delete()函数,因此程序最终还是调用了dog类重载的operator delete()函数,并没有调用全局的operator delete()。

我们知道在C++中,delete的语义是先调用对象的析构函数,然后再调用delete操作符函数,看一下dog::~dog() [deleting destructor]的实现流程:
第4行代码:call dog::~dog() [complete object destructor],在这里它和dog::~dog() [base object destructor]相同,它的汇编代码如下:

dog::~dog() [base object destructor]:sub     rsp, 8mov     QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16mov     edi, OFFSET FLAT:.LC0call    putsadd     rsp, 8ret

它就是dog类的析构函数,可见函数dog::~dog() [deleting destructor]先调用了dog::~dog() [base object destructor],然后调用了call dog::operator delete(void*)。该函数先调用了dog类的析构函数,然后再调用dog类的重载的operator delete(),正好符合delete操作符的语义,也就是说在这里,编译器使用了一个独立的函数来封装了这个delete操作符的功能。可见,编译器生成了一个特殊的virtual析构函数,在这个析构函数中调用了operator delete()。

因此,派生类中重载的delete操作符在使用基类指针析构堆上对象时,也是动态绑定来调用的,只不过它并不是使用传统的方式,定义成虚函数来动态绑定的,而是被封装在一个编译器自动生成的虚析构函数中,通过动态绑定虚析构函数来间接的动态绑定。

我们再看一下虚函数表中的第1个虚函数:dog::~dog() [complete object destructor],它是dog类正常的析构函数,也就是程序中所定义的虚析构函数,它主要用于栈上对象和static对象的析构和在子类的析构函数中调用父类的析构函数,而第2个虚函数:dog::~dog() [deleting destructor],它是编译器为dog类新增的析构函数,主要用于delete操作符来析构堆上对象,这个函数用户并不可见,毕竟按照C++的语义,一个类只能有一个析构函数,故这个析构函数对用户是不可见的,只是编译器用来辅助进行对象的delete操作的,仅供编译器使用。

如果我们在测试程序中,编写下面的测试代码:

void foo() {dog d; // 创建栈上对象
}dog global; // 创建全局对象

编译器生成的汇编代码如下:

foo():sub     rsp, 24mov     QWORD PTR [rsp+8], OFFSET FLAT:vtable for dog+16lea     rdi, [rsp+8]call    dog::~dog() [complete object destructor]add     rsp, 24ret
__static_initialization_and_destruction_0():sub     rsp, 8mov     edx, OFFSET FLAT:__dso_handlemov     esi, OFFSET FLAT:global // 程序退出时,回调global的析构函数mov     edi, OFFSET FLAT:dog::~dog() [complete object destructor]call    __cxa_atexitadd     rsp, 8ret

可见,这两种创建类型的对象在析构时,都调用了正常实现的析构函数:dog::~dog() [complete object destructor]。

需要注意的是,这种动态绑定delete操作符的机制,是GCC和CLANG编译器所使用的方案,MSVC编译器并没有使用,它没有定义了一个新的析构函数,而是通过为析构函数传递不同标志参数的方式来实现的。

下面是MSVC编译器生成的汇编代码,传递的标志参数存放在edx寄存器中,具体细节可自己分析一下:

virtual void * dog::`scalar deleting destructor'(unsigned int) PROC                         ; dog::`scalar deleting destructor', COMDAT
$LN18:mov     QWORD PTR [rsp+8], rbxpush    rdisub     rsp, 32                             ; 00000020Hlea     rax, OFFSET FLAT:const dog::`vftable'mov     rbx, rcxmov     QWORD PTR [rcx], raxmov     edi, edx // 把标志参数存入edi寄存器lea     rcx, OFFSET FLAT:`string'call    putslea     rax, OFFSET FLAT:const animal::`vftable'mov     QWORD PTR [rbx], raxtest    dil, 1je      SHORT $LN4@scalar  // 参数edi的第0位为0时,不需要delete堆上内存test    dil, 4jne     SHORT $LN3@scalar  // 参数edi的第2位为0时,在22行调用dog重载的operator delete,否则在在27行调用全局的operator deletemov     rdx, rbxlea     rcx, OFFSET FLAT:`string'call    printfmov     rcx, rbxcall    void operator delete(void *)                     ; operator deletejmp     SHORT $LN4@scalar
$LN3@scalar:mov     edx, 8mov     rcx, rbxcall    void __global_delete(void *,unsigned __int64)         ; __global_delete
$LN4@scalar:mov     rax, rbxmov     rbx, QWORD PTR [rsp+48]add     rsp, 32                             ; 00000020Hpop     rdiret     0
virtual void * dog::`scalar deleting destructor'(unsigned int) ENDP  

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

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

相关文章

气膜冰雪项目:推动冰雪运动发展的新纪元—轻空间

随着2024年北京冬奥会的余温仍在延续,气膜冰雪项目在我国的冰雪运动发展中扮演着愈发重要的角色。气膜结构以其独特的优势,正吸引着越来越多的参与者,推动着冰雪运动的普及与发展。 突出的优势 气膜冰雪馆的设计理念充分体现了现代建筑的灵活…

市场分化!汽车零部件「变天」

全球汽车市场的动荡不安,还在持续。 本周,全球TOP20汽车零部件公司—安波福(Aptiv)发布2024年第三季度财报显示,三季度公司经调整后确认收入同比下降6%;按照区域市场来看,也几乎是清一色的下滑景…

ES6中数组新增了哪些扩展?

ES6中数组新增了哪些扩展? 1、扩展运算符的应⽤ ES6通过扩展元素符 … ,好⽐ rest 参数的逆运算,将⼀个数组转为⽤逗号分隔的参数序列 console.log(...[1, 2, 3]) // 1 2 3 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...documen…

基于物联网的户外环境检测装置教学文章

引言 随着物联网(IoT)技术的发展,越来越多的应用被广泛研究和应用于我们的日常生活中。户外环境检测装置是一种利用传感器、网络连接和数据分析技术,监测和分析环境数据(如温度、湿度、空气质量等)的设备。…

ubuntu20安装opencv3.2记录

系统环境 ubuntu20安装了ros-noetic,所以系统默认装了opencv4.2.0,但是跑fastlivo推荐的是opencv3.2.0,而且海康相机别人写的ros驱动(海康相机ros驱动)也是需要opencv3.2.0,最终还是选择安装多版本的openc…

全!新!LLM推理加速调研

本文主要内容 介绍一篇大模型推理加速综述论文,简单说明了LLM推理加速的基本内容。 介绍了推理阶段的prefilling(主要方向:计算优化)和decoding(主要方向:内存优化)差异。 prefilling优化方面…

大数据-206 数据挖掘 机器学习理论 - 多元线性回归 回归算法实现 算法评估指标

点一下关注吧!!!非常感谢!!持续更新!!! 目前已经更新到了: Hadoop(已更完)HDFS(已更完)MapReduce(已更完&am…

Python+Appium+Pytest+Allure自动化测试框架-安装篇

文章目录 安装安装ADT安装NodeJs安装python安装appium安装Appium Server(可选)安装Appium-Inspector(可选)安装allure安装pytest PythonAppiumPytestAllure框架的安装 Appium是一个开源工具,是跨平台的,用于…

Nature Methods | 新型三维光场显微成像技术

欢迎关注GZH《光场视觉》 近日,中科院脑科学与智能技术卓越创新中心王凯研究组在《自然方法》(Nature Methods)上,在线发表了题为Volumetric Voltage Imaging of Neuronal Populations in Mouse Brain by Confocal Light Field M…

深度学习基础—循环神经网络的梯度消失与解决

引言 深度学习基础—循环神经网络(RNN)https://blog.csdn.net/sniper_fandc/article/details/143417972?fromshareblogdetail&sharetypeblogdetail&sharerId143417972&sharereferPC&sharesourcesniper_fandc&sharefromfrom_link深…

基于向量检索的RAG大模型

一、什么是向量 向量是一种有大小和方向的数学对象。它可以表示为从一个点到另一个点的有向线段。例如,二维空间中的向量可以表示为 (𝑥,𝑦) ,表示从原点 (0,0)到点 (𝑥,𝑦)的有向线段。 1.1、文本向量 1…

Bartender 5 for Mac 菜单栏管理软件 安装教程【保姆级教程,操作简单小白轻松上手使用】

Mac分享吧 文章目录 Bartender 5 for Mac 菜单栏管理软件 安装完成,软件打开效果一、Bartender 5 菜单栏管理软件 Mac电脑版——v5.2.3⚠️注意事项:1️⃣:下载软件2️⃣:安装软件3️⃣:打开软件,根据自己…

国产操作系统重新安装软件商店

国产操作系统类似于手机的“应用商店”,都会有一个“软件商店”,方便用户安装管理电脑的软件。这个软件商店不仅有各种软件,还有各类外设驱动和移动应用环境模拟功能。软件商店可以下载安装软件,还可以更新、卸载软件。 软件商店 …

FastAPI中如果async def和def 路由的区别

在python的整体生态中,虽然已经有很多库支持了异步调用,如可以使用httpx或者aiohttp代替requests库发起http请求,使用asyncio.sleep 代替time.sleep, 但是依然还有很多优秀的第三方库是不支持异步调用也没有可代替的库&#xff0c…

Pinctrl子系统中Pincontroller和client驱动程序的编写

往期内容 本专栏往期内容: Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析inctrl子系统中Pincontroller构造过程驱动分析:imx_pinctrl_soc_info结构体Pinctrl子系统中c…

【C++动态规划】2435. 矩阵中和能被 K 整除的路径|1951

本文涉及知识点 C动态规划 LeetCode2435. 矩阵中和能被 K 整除的路径 给你一个下标从 0 开始的 m x n 整数矩阵 grid 和一个整数 k 。你从起点 (0, 0) 出发,每一步只能往 下 或者往 右 ,你想要到达终点 (m - 1, n - 1) 。 请你返回路径和能被 k 整除的…

【QT】Qt对话框

个人主页~ Qt窗口属性~ Qt窗口 五、对话框2、Qt内置对话框(1)Message Box(2)QColorDialog(3)QFileDialog(4)QFontDialog(5)QInputDialog 五、对话框 2、Qt内…

视频推荐的算法(字节青训)

题目: 西瓜视频 正在开发一个新功能,旨在将访问量达到80百分位数以上的视频展示在首页的推荐列表中。实现一个程序,计算给定数据中的80百分位数。 例如:假设有一个包含从1到100的整数数组,80百分位数的值为80&#…

线程基础知识、jmm(Java内存模型)

目录 线程基础知识 并发与并行 进程和线程 线程优先级 创建线程的方式主要有三种 休眠 作出让步 join() 方法 线程协作注意什么 理解线程状态 选择合适的协作工具 共享资源的访问控制 避免竞争条件 创建线程几种方式 线程状态,状态之间切换 新建&…

2.数组越界访问如何调试HardFault错误

数组越界 在项目开发过程中,配置串口外设是一个常见的任务,但在实际操作中,我们可能会遇到一些预料之外的问题。例如,在调试过程中,我们发现单片机只接受了一次数据后便不再接收,这无疑是一个棘手的问题。…