前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++多态

C++多态

作者头像
Masimaro
发布2019-02-25 17:22:57
3570
发布2019-02-25 17:22:57
举报
文章被收录于专栏:MasiMaro 的技术博文

面向对象的程序设计的三大要素之一就是多态,多态是指基类的指针指向不同的派生类,其行为不同。多态的实现主要是通过虚函数和虚表来完成,虚表保存在对象的头四个字节,要调用虚函数必须存在对象,也就是说虚函数必须作为类的成员函数来使用。 编译器为每个拥有虚函数的对象准备了一个虚函数表,表中存储了虚函数的地址,类对象在头四个字节中存储了虚函数表的指针。 下面是一个具体的例子

代码语言:javascript
复制
class CVirtual
{
public:
    virtual void showNumber(){
        printf("%d\n", nNum);
    }

    virtual void setNumber(int n){
        nNum = n;
    }
private:
    int nNum;
};

int main()
{
    CVirtual cv;
    cv.setNumber(2);
    cv.showNumber();
    return 0;
}

上述这段代码定义了两个虚函数setNumber和showNumber,并在主函数中调用了他们,下面通过反汇编的方式来展示编译器是如何调用虚函数的

代码语言:javascript
复制
26:       CVirtual cv;
00401288   lea         ecx,[ebp-8];对象中有一个整形数字占4个字节,同时又有虚函数,头四个字节用来存储虚函数表指针,总共占8个字节
0040128B   call        @ILT+25(CVirtual::CVirtual) (0040101e);调用构造函数
27:       cv.setNumber(2);
00401290   push        2
00401292   lea         ecx,[ebp-8]
00401295   call        @ILT+0(CVirtual::setNumber) (00401005);调用虚函数
28:       cv.showNumber();
0040129A   lea         ecx,[ebp-8]
0040129D   call        @ILT+5(CVirtual::showNumber) (0040100a);调用虚函数
29:       return 0;
004012A2   xor         eax,eax
;构造函数
0401389   pop         ecx;还原ecx使得ecx保存对象的首地址
0040138A   mov         dword ptr [ebp-4],ecx
0040138D   mov         eax,dword ptr [ebp-4]
00401390   mov         dword ptr [eax],offset CVirtual::`vftable' (0042f020);将虚函数表的首地址赋值到对象的头4个字节
00401396   mov         eax,dword ptr [ebp-4]
00401399   pop         edi
0040139A   pop         esi
0040139B   pop         ebx
0040139C   mov         esp,ebp
0040139E   pop         ebp
0040139F   ret
;setNumber(int n)
0040134A   mov         dword ptr [ebp-4],ecx
18:           nNum = n;
0040134D   mov         eax,dword ptr [ebp-4];eax = ecx
00401350   mov         ecx,dword ptr [ebp+8]
00401353   mov         dword ptr [eax+4],ecx

从上面的汇编代码可以看到,当类中有虚函数的时候,编译器会提供一个默认的构造函数,用于初始化对象的头4个字节,这四个字节存储的是一个指针值,我们定位到这个指针所在的内存如下图:

虚函数表的内荣
虚函数表的内荣

这段内存中存储了两个值,分别为0x0040100AH和0x00401005H,我们执行后面的代码发现,这两个地址给定的是虚函数所在的地址。

虚函数地址
虚函数地址

在调用时编译器直接调用对应的虚函数,并没有通过虚表来寻址到对应的函数地址。下面看另外一个例子:

代码语言:javascript
复制
class CParent
{
public:
    virtual void showClass()
    {
        printf("CParent\n");
    }
};

class CChild:public CParent
{
public:
    virtual void showClass()
    {
        printf("CChild\n");
    }
};
int main()
{
    CParent *pClass = NULL;
    CParent cp;
    CChild cc;
    pClass = &cp;
    pClass->showClass();
    pClass = &cc;
    pClass->showClass();
    pClass = NULL;
    return 0;
}

上述代码定义了一个基类和派生类,并且在派生类中重写了函数showClass,在调用时用分别利用基类的指针指向基类和派生类的对象来调用这个虚函数,得到的结果自然是不同的,这样构成了多态。下面是它的反汇编代码,这段代码基本展示了多态的实现原理

代码语言:javascript
复制
29:       CParent *pClass = NULL;
00401288   mov         dword ptr [ebp-4],0
30:       CParent cp;
0040128F   lea         ecx,[ebp-8]
00401292   call        @ILT+30(CParent::CParent) (00401023);调用构造函数
31:       CChild cc;
00401297   lea         ecx,[ebp-0Ch]
0040129A   call        @ILT+0(CChild::CChild) (00401005)
32:       pClass = &cp;
0040129F   lea         eax,[ebp-8];取对象的首地址
004012A2   mov         dword ptr [ebp-4],eax;
33:       pClass->showClass();
004012A5   mov         ecx,dword ptr [ebp-4]
004012A8   mov         edx,dword ptr [ecx];将指针值放入到edx中
004012AA   mov         esi,esp
004012AC   mov         ecx,dword ptr [ebp-4];获取虚函数表指针
004012AF   call        dword ptr [edx];调转到虚函数指针所对应的位置执行代码
004012B1   cmp         esi,esp
004012B3   call        __chkesp (00401560)
34:       pClass = &cc;
004012B8   lea         eax,[ebp-0Ch]
004012BB   mov         dword ptr [ebp-4],eax
35:       pClass->showClass();
004012BE   mov         ecx,dword ptr [ebp-4]
004012C1   mov         edx,dword ptr [ecx]
004012C3   mov         esi,esp
004012C5   mov         ecx,dword ptr [ebp-4]
004012C8   call        dword ptr [edx]
004012CA   cmp         esi,esp
004012CC   call        __chkesp (00401560)
36:       pClass = NULL;
004012D1   mov         dword ptr [ebp-4],0
37:       return 0;
004012D8   xor         eax,eax

从上述代码来看,在调用虚函数时首先根据头四个字节的值找到对应的虚函数表,然后根据虚函数表中存储的内容来找到对应函数的地址,最后根据函数地址跳转到对应的位置,执行函数代码。由于虚函数表中的虚函数是在编译时就根据对象的不同将对应的函数装入到各自对象的虚函数表中,因此,不同的对象所拥有的虚函数表不同,最终根据虚函数表寻址到的虚函数也就不同,这样就构成了多态。 对于虚函数的调用,先后经历了几次间接寻址,比直接调用函数效率低了一些,通过虚函数间接寻址访问的情况只有利用类对象的指针或者引用来访问虚函数时才会出现,利用对象本身调用虚函数时,没有必要进行查表,因为已经明确调用的是自身的成员函数,没有构成多态,查表只会降低程序的运行效率。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-10-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档