虚函数表

  

 

  虚函数表

#include<stdlib.h>
#include<stdio.h>

struct Base1
{
private:
    int x;
    int y;
public:
    void Fun()
    {

    }
};

struct Base2
{
private:
    int x;
    int y;
public:
    virtual void Fun()
    {

    }
};

struct Base3
{
public:
    int x;
    int y;
private:
    virtual void Fun1()
    {

    }
    virtual void Fun2()
    {

    }
};

int main()
{
    printf("%d\n%d\n%d\n", sizeof(Base1),sizeof(Base2),sizeof(Base3));
    
    return 0;
}

代码中定义了三个类,分别为Base1,Base2,Base3。Base1中没有定义虚函数,Base2定义了1个虚函数,Base3定义了2个虚函数,并且用打印了三个类的大小

 

 三个类的大小分别为8,8,12,由此可见,当类中定义了虚函数,不管定义了多少个,编译器都为类中增加了4个字节,而这四个字节正是存贮的是虚函数表的地址。

还要注意的是,虚函数表的地址位于对象的首地址,即首地址所对应的前四个字节所存的就是虚函数的地址。

虚函数表的间接调用

一般的对象调用虚函数是都是直接调用,而对象指针调用虚函数使用的是间接调用,这时候就用到了虚函数表。

下面代码所对应的汇编代码

Base2 base2;
Base2* pbase2 = &base2;
base2.Fun();
pbase2->Fun();

-----------------------------------

base2.Fun();
00FD18D0 lea ecx,[base2]
00FD18D3 call Sub::Sub (0FD1424h)
pbase2->Fun();
00FD18D8 mov eax,dword ptr [pbase2]
00FD18DB mov edx,dword ptr [eax]
00FD18DD mov esi,esp
00FD18DF mov ecx,dword ptr [pbase2]
00FD18E2 mov eax,dword ptr [edx]
00FD18E4 call eax

-------------------------------------

当base2调用Fun时,直接跳转到了函数所在的地址。

而当pbase2调用Fun时,先通过对象的首地址得到虚函数表的地址,在通过该地址得到虚函数的地址,在进行跳转。

同类不同对象的虚函数表相同

 

struct Base
{
private:
    int x;
public:
    virtual void Fun()
    {

    }
};

int main()
{
     Base base1;
     Base base2;
     printf("虚函数表地址为:%X\n", *(int*)&base1);
     printf("虚函数表地址为:%X\n", *(int*)&base1);
     return 0;
}

 

 base1和base2的虚函数表的地址相同,也就是说,每个类只有一张虚函数表。

单继承无函数覆盖              

struct Base
{
public: virtual void Function_1() { } virtual void Function_2() { } virtual void Function_3() { } }; struct Sub:Base { public: virtual void Function_4() { } virtual void Function_5() {
}
virtual void Function_6() { } };

int main()
{
Base base;
Sub sub;
printf("虚函数表地址为:%X\n", *(int*)&base);
for (int i = 0; i < 3; i++)
{
printf("%X\n", *((int*)(*(int*)&base) + i));
}


printf("********\n");
printf("虚函数表首地址为:%X\n", *(int*)&sub);
for (int i = 0; i < 6; i++)
{
printf("%X\n", *((int*)(*(int*)&sub) + i));
}

return 0;

}


 

 

 可以看出,派生类继承基类的虚函数函数,并不是数据的复制,而是直接调用基类的虚函数,派生类拥有自己的虚函数表

  单继承有函数覆盖

#include<stdlib.h>
#include<stdio.h>
struct Base                    
{                    
public:                    
    virtual void Function_1()                    
    {                    
        printf("Base:Function_1...\n");                    
    }                    
    virtual void Function_2()                    
    {                    
        printf("Base:Function_2...\n");                    
    }                    
    virtual void Function_3()                    
    {                    
        printf("Base:Function_3...\n");                    
    }                    
};                    
struct Sub:Base                    
{                    
public:                    
    virtual void Function_1()                    
    {                    
        printf("Sub:Function_1...\n");                    
    }                    
    virtual void Function_2()                    
    {                    
        printf("Sub:Function_2...\n");                    
    }                    
    virtual void Function_6()                    
    {                    
        printf("Sub:Function_6...\n");                    
    }                    
};                    

int main()
{
    Base base;
    Sub sub;
    printf("虚函数表地址为:%X\n", *(int*)&base);
    for (int i = 0; i < 3; i++)
    {
        printf("%X\n", *((int*)(*(int*)&base) + i));
    }

    printf("********\n");
    printf("虚函数表首地址为:%X\n", *(int*)&sub);
    for (int i = 0; i < 3; i++)
    {
        printf("%X\n", *((int*)(*(int*)&sub) + i));
    }
    
    return 0;


}

 

 其中Fun1,Fun2都已经在派生类中被重写,与基类的不同。

多重继承有函数覆盖

#include<stdlib.h>
#include<stdio.h>
struct Base1
{
public:
virtual void Fn_1()
{
printf("Base1:Fn_1...\n");
}
virtual void Fn_2()
{
printf("Base1:Fn_2...\n");
}
};
struct Base2
{
public:
virtual void Fn_3()
{
printf("Base2:Fn_3...\n");
}
virtual void Fn_4()
{
printf("Base2:Fn_4...\n");
}
};
struct Sub :Base1, Base2
{
public:
virtual void Fn_1()
{
printf("Sub:Fn_1...\n");
}
virtual void Fn_3()
{
printf("Sub:Fn_3...\n");
}
virtual void Fn_5()
{
printf("Sub:Fn_5...\n");
}
};
int main(int argc, char* argv[])
{
//查看 Sub 的虚函数表
Sub sub;

//通过函数指针调用函数,验证正确性
typedef void(*pFunction)(void);


//对象的前四个字节是第一个Base1的虚表
printf("Sub 的虚函数表地址为:%x\n", *(int*)&sub);

pFunction pFn;

for (int i = 0; i < 6; i++)
{
int temp = *((int*)(*(int*)&sub) + i);
if (temp == 0)
{
break;
}
pFn = (pFunction)temp;
pFn();
}

//对象的第二个四字节是Base2的虚表
printf("Sub 的虚函数表地址为:%x\n", *(int*)((int)&sub + 4));

pFunction pFn1;

for (int k = 0; k < 2; k++)
{
int temp = *((int*)(*(int*)((int)&sub + 4)) + k);
pFn1 = (pFunction)temp;
pFn1();
}

return 0;
}

 

 如上图所示,当直接基类有两个是,派生类的虚函数表也有两个。以此类推,派生类的虚函数表的个数取决与直接基类的个数。

 函数的多态

首先我们要理解一个概念:绑定——将调用代码和地址联系起来,即代码知道要跳转的地址。

绑定分为两种,编译期绑定——在编译的时候确定地址,动态绑定——在运行的时候确定地址

#include<stdlib.h>
#include<stdio.h>

class Base
{
public:
    void Fun1()
    {
        printf("Bass 1");
    }
    virtual void Fun2()
    {
        printf("Bass 2");
    }
};

class Sub :Base
{
public:
    void Fun1()
    {
        printf("Sub 1");
    }
    void Fun2()
    {
        printf("Sub 2");
    }
};

void Test(Base* pb)
{
    pb->Fun1();
    pb->Fun2();
}

int main()
{
    Base base;
    Test(&base);

    return 0;
}

 

代码中定义了两个类,基类Base和派生类Sub,其中Fun2为虚函数,而Fun1不是。Test函数是以基类指针作为参数,众所周知,基类的指针可以指向基类,也可以指向自身的派生类。

让我们来看一下函数Test的反汇编代码:
其中Fun1的call指令已经确定了要跳转的地址——Base::Fun1,这也就是编译期绑定,或是前期绑定;而Fun2没有确定。在Fun2中,mov eax,dword ptr [pb]是将对象首地址放入eax中。

mov edx,dword ptr [eax],是将虚函数表的地址放入edx中,而最后的mov,dword ptr [edx]是将虚函数表中的第一个地址放入eax,然后call eax。由此可见,

根据传进来的参数不同,虚函数表也有可能会不同,因此这实现了在运行阶段将代码与跳转地址联系在了一起,这也就是动态绑定。

多态实现了一个属性表现了不同的行为,多态也可以成为动态绑定。

虚函数的动态绑定可以通过虚函数表来实现,因为根据之前所提到的虚函数可以间接寻址,或是间接调用,所以可以不用再编译期间确定函数要跳转的地址。

在PE文件结构中的IAT表也有相同的行为。

 

posted @ 2021-03-24 20:04  Yanmo  阅读(271)  评论(0)    收藏  举报