C++多态:深入虚函数,理解晚绑定

 C++的多态特性是通过晚绑定实现的。晚绑定(late binding),指的是编译器或解释器程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
 在C++中,晚绑定通常发生在使用virtual声明成员函数时。此时,C++创建一个虚函数表,当某个函数被调用时需要从这个表中查找该函数的实际位置。通常,晚绑定也叫做动态函数分派(dynamic dispatch)。
 考虑如下的代码:

#include<iostream>
using namespace std;

class D {
public:
    int num;
    D(int i = 0) { num = i; }
    virtual void print() { cout << "I'm a D. my num=" << num << endl; };

};
class E :public D {
public:
    E(int i = 0) { num = i; }
    void print() { cout << "I'm a E. my num=" << num << endl; }
    void ppp() { int ttt = 1; }
};

int main()
{
    void (D::*i)() = &D::print;
    E* e = new E(1);
    e->print();
    (((D*)e)->*i)();
    delete e;
    return 0;
}

输出结果为:

I'm a E. my num=1
I'm a E. my num=1

使用VS命令/d1 reportSingleClassLayoutD和/d1 reportSingleClassLayoutE,可以得到类D和类E的内存布局。可以看到,D的大小是8个字节,头四个字节存储指向虚函数表的指针vfptr,后四个字节存储成员变量num。E的大小也是8个字节,头四个字节存储指向虚函数表的指针,后四个字节存储从基类继承的成员变量num。

1>  class D size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | num
1>      +---
1>
1>  D::$vftable@:
1>      | &D_meta
1>      |  0
1>   0  | &D::print


1>  class E size(8):
1>      +---
1>   0  | +--- (base class D)
1>   0  | | {vfptr}
1>   4  | | num
1>      | +---
1>      +---
1>
1>  E::$vftable@:
1>      | &E_meta
1>      |  0
1>   0  | &E::print

内存布局图:

这里写图片描述

接下来从汇编角度解释一下晚绑定是怎么发生的。

int main()
{
000C27B0  push        ebp  
000C27B1  mov         ebp,esp  
000C27B3  push        0FFFFFFFFh  
000C27B5  push        0C7242h  
000C27BA  mov         eax,dword ptr fs:[00000000h]  
000C27C0  push        eax  
000C27C1  sub         esp,100h  
000C27C7  push        ebx  
000C27C8  push        esi  
000C27C9  push        edi  
000C27CA  lea         edi,[ebp-10Ch]  
000C27D0  mov         ecx,40h  
000C27D5  mov         eax,0CCCCCCCCh  
000C27DA  rep stos    dword ptr es:[edi]  
000C27DC  mov         eax,dword ptr [__security_cookie (0CC004h)]  
000C27E1  xor         eax,ebp  
000C27E3  push        eax  
000C27E4  lea         eax,[ebp-0Ch]  
000C27E7  mov         dword ptr fs:[00000000h],eax  
    void (D::*i)() = &D::print;//vcall是虚函数表,vcall{0}就是虚函数D::print(),这里把D::print()偏移地址赋给ptr[i]
000C27ED  mov         dword ptr [i],offset D::`vcall'{0}' (0C146Fh)  
    E* e = new E(1);
000C27F4  push        8  
000C27F6  call        operator new (0C1311h)  
000C27FB  add         esp,4  
000C27FE  mov         dword ptr [ebp-0F8h],eax  
000C2804  mov         dword ptr [ebp-4],0  
000C280B  cmp         dword ptr [ebp-0F8h],0  
000C2812  je          main+79h (0C2829h)  
000C2814  push        1  
000C2816  mov         ecx,dword ptr [ebp-0F8h]  
000C281C  call        E::E (0C137Fh)  
000C2821  mov         dword ptr [ebp-10Ch],eax  
000C2827  jmp         main+83h (0C2833h)  
000C2829  mov         dword ptr [ebp-10Ch],0  
000C2833  mov         eax,dword ptr [ebp-10Ch]  
000C2839  mov         dword ptr [ebp-0ECh],eax  
000C283F  mov         dword ptr [ebp-4],0FFFFFFFFh  
000C2846  mov         ecx,dword ptr [ebp-0ECh]  
000C284C  mov         dword ptr [e],ecx  
    e->print();
000C284F  mov         eax,dword ptr [e]//e的指针赋给eax  
000C2852  mov         edx,dword ptr [eax]//打开e的指针,e中vfptr存在头四个字节,所以edx获取vfptr
000C2854  mov         esi,esp  
000C2856  mov         ecx,dword ptr [e]//成员函数调用是this->func(),这里this指针(也就是e)存入ecx    
000C2859  mov         eax,dword ptr [edx]//因为vcall{0}就是函数print(),所以这里直接把edx存储的指针,也就是vfptr,解引用之后赋值给eax调用就可以了 
    e->print();
000C285B  call        eax//调用eax指向的函数。由于这个过程是运行时确定的而不是编译时确定的,所以也叫动态函数分派,即晚绑定。(((D*)e)->*i)()更能体现动态性。
000C285D  cmp         esi,esp  
000C285F  call        __RTC_CheckEsp (0C1195h)  
    (((D*)e)->*i)();
000C2864  mov         esi,esp  
000C2866  mov         ecx,dword ptr [e]//成员函数调用是this->func(),这里this指针(也就是e)存入ecx  
000C2869  call        dword ptr [i]//打开指针i,获取偏移地址。此时基址变成了e所在的内存段,所以配合ecx中的指针e获取的是E::print(),而不是D::print()。因为E重写了D的print()。也可以不重写,那样的话调用的就是D::print(),读者可以自己验证。
000C286C  cmp         esi,esp  
000C286E  call        __RTC_CheckEsp (0C1195h)  
    delete e;
000C2873  mov         eax,dword ptr [e]  
000C2876  mov         dword ptr [ebp-104h],eax  
000C287C  push        8  
000C287E  mov         ecx,dword ptr [ebp-104h]  
000C2884  push        ecx  
000C2885  call        operator delete (0C105Ah)  
000C288A  add         esp,8  
000C288D  cmp         dword ptr [ebp-104h],0  
000C2894  jne         main+0F2h (0C28A2h)  
000C2896  mov         dword ptr [ebp-10Ch],0  
000C28A0  jmp         main+102h (0C28B2h)  
000C28A2  mov         dword ptr [e],8123h  
000C28A9  mov         edx,dword ptr [e]  
000C28AC  mov         dword ptr [ebp-10Ch],edx  
    return 0;
000C28B2  xor         eax,eax  
}

总结

 运行时多态通过多次对地址指针解引用,获得虚函数实体的地址,进而执行对应的虚函数。
多态配合泛型算法简化编程,见我的另一篇博文:http://blog.csdn.net/popvip44/article/details/72674326

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中的继承、多态虚函数是面向对象编程的重要概念。 继承是指一个类可以从另一个类继承属性和方法。子类可以继承父类的公有成员和保护成员,但不能继承私有成员。通过继承,子类可以重用父类的代码,并且可以添加自己的特定功能。继承可以实现代码的重用和层次化的设计。 多态是指同一个函数可以根据不同的对象调用不同的实现。多态可以通过虚函数来实现。虚函数是在基类中声明为虚拟的函数,它可以在派生类中被重写。当通过基类指针或引用调用虚函数时,实际调用的是派生类中的实现。这样可以实现动态绑定,即在运行时确定调用的函数。 虚函数原理是通过虚函数来实现的。每个包含虚函数的类都有一个虚函数,其中存储了虚函数的地址。当调用虚函数时,编译器会根据对象的类型在虚函数中查找对应的函数地址并调用。 综上所述,C++中的继承、多态虚函数是实现面向对象编程的重要机制,它们可以提高代码的灵活性和可扩展性。 #### 引用[.reference_title] - *1* *3* [C++多态虚函数虚函数](https://blog.csdn.net/weixin_46053588/article/details/121231465)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [c++多态虚函数内部原理实战详解](https://blog.csdn.net/bitcarmanlee/article/details/124830241)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值