当前位置: 首页 > news >正文

从汇编的角度了解C++原理——虚函数

文章目录

  • 1、虚函数
    • 1.1、虚函数储存结构
    • 1.2、子类重写虚函数
    • 1.3、在栈上调用虚函数
    • 1.4、在堆上调用虚函数(通过指针调用,多态)

本文用到的反汇编工具是objconv,使用方法可以看我另一篇文章https://blog.csdn.net/weixin_45001971/article/details/128660642。

其它文章:
从汇编的角度了解C++原理——类的储存结构和函数调用
从汇编的角度了解C++原理——new和malloc的区别
从汇编的角度了解C++原理——虚函数

1、虚函数

1.1、虚函数储存结构

在这里插入图片描述
反汇编。

main:
        sub     rsp, 56                             
        lea     rcx, [rsp+20H]                    
        call    ??0A@@QEAA@XZ      				//调用构造函数                  
        mov     eax, 4294967295                  
        add     rsp, 56                                
        ret                                             
        
??0A@@QEAA@XZ:									//调用A类的构造函数
        mov     qword [rsp+8H], rcx                  
        mov     rax, qword [rsp+8H]                    
        lea     rcx, [rel ??_7A@@6B@]           //获取虚表??_7A@@6B@的地址
        mov     qword [rax], rcx                //把虚表地址放在对象的头部      
        mov     rax, qword [rsp+8H]                    
        mov     dword [rax+8H], 10              //在对象首地址偏移8个字节的位置定义d1变量       
        mov     rax, qword [rsp+8H]                     
        ret                                                           

??_7A@@6B@:                     				//A类虚表                       
        dq ?func2@A@@UEAAXXZ                    //虚函数func2      

以上例的汇编代码可以得出带虚函数的类的储存结构如下图所示。
在这里插入图片描述
带有虚函数的对象的头部会放置8个字节大小的虚表地址,有了虚表之后的对象会以8个字节为单位去对齐,如上例中的A类,如果没有虚函数,它的大小为4个字节,而加了虚函数之后,大小变为了16个字节。

1.2、子类重写虚函数

在代码中添加A的子类B,重写func2方法。
在这里插入图片描述

反汇编

main:
        sub     rsp, 56                             
        lea     rcx, [rsp+20H]                       
        call    ??0B@@QEAA@XZ          		//调用B类构造                
        mov     eax, 4294967295                        
        add     rsp, 56                                
        ret                                             
        
??0A@@QEAA@XZ:								//A类构造函数
        mov     qword [rsp+8H], rcx                  
        mov     rax, qword [rsp+8H]                     
        lea     rcx, [rel ??_7A@@6B@]     	//把A类虚表的地址放在头部              
        mov     qword [rax], rcx                      
        mov     rax, qword [rsp+8H]                   
        mov     dword [rax+8H], 10                  
        mov     rax, qword [rsp+8H]           
        ret                                             

??0B@@QEAA@XZ:								//B类构造函数
        mov     qword [rsp+8H], rcx                
        sub     rsp, 40                             
        mov     rcx, qword [rsp+30H]               
        call    ??0A@@QEAA@XZ    			//调用A类构造                   
        mov     rax, qword [rsp+30H]             
        lea     rcx, [rel ??_7B@@6B@]       //把B类虚表的地址放在头部           
        mov     qword [rax], rcx                    
        mov     rax, qword [rsp+30H]                   
        add     rsp, 40                                 
        ret                                           
       
??_7A@@6B@:                         		//A类虚表                        
        dq ?func2@A@@UEAAXXZ                //A::func2                             
        dq ?func3@A@@UEAAXXZ                //A::func3      
             
??_7B@@6B@:                                 //B类虚表                       
        dq ?func2@B@@UEAAXXZ                //B::func2,被替换为了B实现的func2          
        dq ?func3@A@@UEAAXXZ                //A::func3                         

从该例中我们可以看到,父类有虚函数时,不光它自己有一张虚表,它的子子孙孙都会各带有一个自己的虚表,子类重写虚函数时,会把子类实现的函数指针替换上虚表,把原先父类的函数指针覆盖掉。

1.3、在栈上调用虚函数

在main里添加方法的调用。
在这里插入图片描述
反汇编。

main:
        sub     rsp, 56                               
        lea     rcx, [rsp+20H]                         
        call    ??0B@@QEAA@XZ                        
        lea     rcx, [rsp+20H]                        
        call    ?func1@A@@QEAAXXZ   		//调用A::func1                 
        lea     rcx, [rsp+20H]                        
        call    ?func2@B@@UEAAXXZ   		//调用B::func2                       
        lea     rcx, [rsp+20H]                         
        call    ?func3@A@@UEAAXXZ   		//调用A::func3                         
        mov     eax, 4294967295                         
        add     rsp, 56                                
        ret   

在栈上调用方法时,因为类型是确定的,所以编译器在编译阶段就会找到对应的函数去调用,调用过程与普通方法一样。

1.4、在堆上调用虚函数(通过指针调用,多态)

修改例程如下。
在这里插入图片描述
反汇编

main:
        sub     rsp, 72                                
        mov     ecx, 16                                
        call    ??2@YAPEAX_K@Z                         
        mov     qword [rsp+28H], rax                   
        cmp     qword [rsp+28H], 0                    
        jz      ?_001                                 
        mov     rcx, qword [rsp+28H]		//定义指针b                   
        call    ??0B@@QEAA@XZ                       
        mov     qword [rsp+30H], rax        //rsp+30H指向对象          
        jmp     ?_002                       //跳到?_002            

?_001:  mov     qword [rsp+30H], 0   
                  
?_002:  mov     rax, qword [rsp+30H]                   
        mov     qword [rsp+38H], rax        //rsp+38H指向对象         
        mov     rax, qword [rsp+38H]        //rax指向对象           
        mov     qword [rsp+20H], rax        //rsp+20H指向对象       
        mov     rcx, qword [rsp+20H]        //rcx指向对象          
        call    ?func1@A@@QEAAXXZ           //调用A::func1       
        mov     rax, qword [rsp+20H]                 
        mov     rax, qword [rax]            //取虚表        
        mov     rcx, qword [rsp+20H]                   
        call    near [rax]                  //执行虚表第一个函数,即B::func2           
        mov     rax, qword [rsp+20H]                   
        mov     rax, qword [rax]                       
        mov     rcx, qword [rsp+20H]                
        call    near [rax+8H]             	//执行虚表第二个函数,即A::func3             
        mov     eax, 4294967295                         
        add     rsp, 72                               
        ret                                          
        
??_7B@@6B@:                                           
        dq ?func2@B@@UEAAXXZ                        
        dq ?func3@A@@UEAAXXZ                          

从该例可以看到,通过指针来调用函数时。
如果是普通函数,编译器会直接根据指针类型,找到对应的的方法,而不是根据对象本身的类型,如本例中B类也实现了func1方法,但通过A类指针调用时,写到汇编里的时A::func1。
如果是虚函数,编译器不会根据名字来查找函数,而是让汇编代码通过虚表中的偏移量来调用,如本例中,b指针执行了func2和func3,这两个函数都没有被直接调用,而是以“call near [rax + 偏移量]”的形式调用了,这也是C++中父类指针指向子类对象的多态的实现原理。

相关文章:

  • 网站301检测/新app推广方案
  • 17做网站广州沙河地址/百度如何投放广告
  • 高端网站设计开发/即刻搜索引擎入口
  • 网站建设移交内容/北京做网站的公司排行
  • 怎样做网站测评/志鸿优化网下载
  • 1核2g 做网站/app拉新渠道商
  • 纵有疾风起,Petterp与他的2022
  • 《和声学教程》学习笔记(一):原位正三和弦及连接
  • (黑马C++)L09 C++类型转换 异常 输入输出流
  • sqlmap源码分析—— 初始化检测DBMS闭合符号
  • MyBatis-Plus分析打印SQL(开发环境)
  • SAP 物料账未分摊差异分析
  • AutoLISP 演练(一)
  • feign漫谈
  • 1.浮动float
  • hud 1846巴什博弈(简单的解法 或 Sprague-Grundy解法)
  • Pytorch深度学习【十四】
  • CodeForces 438D The Child and Sequence(线段树区间取模)