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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

其它文章
从汇编的角度了解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++中父类指针指向子类对象的多态的实现原理。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++