C++多态

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

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,并在主函数中调用了他们,下面通过反汇编的方式来展示编译器是如何调用虚函数的

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,我们执行后面的代码发现,这两个地址给定的是虚函数所在的地址。
虚函数地址
在调用时编译器直接调用对应的虚函数,并没有通过虚表来寻址到对应的函数地址。下面看另外一个例子:

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,在调用时用分别利用基类的指针指向基类和派生类的对象来调用这个虚函数,得到的结果自然是不同的,这样构成了多态。下面是它的反汇编代码,这段代码基本展示了多态的实现原理

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

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

posted @ 2017-10-24 20:55  masimaro  阅读(103)  评论(0编辑  收藏  举报