• 深入浅出C++虚函数表


    为什么要了解虚函数表

    了解虚函数表对理解C++实现多态的机制会有更深的了解,对对象的内存布局会有更好的认识。

     如要转载请注明作者和本url

    验证虚函数表的存在

    C++中的structclass实际上是一样的)

    C++中当一个类中存在virtual函数(虚函数)或者它的父类中存在虚函数,那么编译器就会为这个类生成虚函数表(virtual table,下面我用代码来验证这个事情。

    首先写一个class

    class A

    {

    };

    cout<<sizeof(A)<<endl;

    输出结果1,就是说这个类占1个字节(具体为什么要占1个字节我也不太清楚,应该就这么设计的吧,有高手请告诉我为什么)。

    下面改写这个类,为其加上虚函数

    class A

    {

      public: virtual int add(int i);

    };

    cout<<sizeof(A)<<endl;

    输出4

    我们知道函数不会影响类的大小,函数的地址是另外被存在一片内存区域。

    4个字节就是指向虚函数表的指针,编译器为有virtual函数的类加上去的,可以看到这是一个32位的指针,因为32位的操作系统的寻址范围是4G64位处理器这个数值应该是64位的,没有经过验证)。

    这个指针指向的虚函数表也是一片内存,这里面存储这所有虚函数的地址。内存的情况可能如下:

    虚函数表

    A中的int add (int i)地址

    。。。其他虚函数地址

    如果类A的定义如下:

    class A

    {

    public:

           int i;

           virtual int add(int i);

    }

    那么sizeof(A) == 8。所以类的大小等于vtablesize加上数据成员的size,如果他有父类,则还需要加上父类中数据成员的size。例如:

    class B: public A

    {

    public:

           int k;

    }

    cout<<sizeof(B);

    输出12

    Bvtable (4bytes)+ B::k (4bytes) + A::i (4bytes) =12 bytes

    多态的原理

    多态是如何利用这个虚函数表实现的呢?看下面的代码:

    struct Super

    {

         int data;

         virtual int add(int i){return i;};

         virtual string toString()=0;

    };

    class Sub: public Super

    {

    public:

         string toString()

         {

             return string("Sub class");

         }

    };

     

         Super* s = new Sub();

         cout<<s->toString()<<endl;

         delete s;

    输出”Sub class”

    Super vtable

    Super:: add地址

    Super::toString地址

    Sub vtable

    Super::add地址

    Sub::toString地址

    int data;

    编译器会把虚表编译成上面的样式,注意两件事情:

    第一,   虚函数表在类所有成员的最前面。

    第二,   第二,Sub因为重写了toStringSub的虚函数表就记录者SubtoString的地址。

    上面的程序Super* s = new Sub();

    s实际指向的是Sub类对象的内存区域,所以调用方法的时候会根据这片内存记录的函数地址进行调用,多态就是这么实现的。

     

    访问虚函数表,手动调用虚函数

    既然虚函数表存在于内存,那么就会有方法找到这片内存区域然后调用,下面就用程序来说明这项工作。

     

    首先来看一项技术

    struct S

    {

         int k;

         int m;

    };

         S a;

         int i = 5;

         a.k = 5;

         a.m = 4;

    cout<<*(int*)&a<<endl;

    输出5。

    说明一下这段程序,我想就是最一行语句理解起来不太容易,把它拆开来看看会很清晰。

    首先用int*型指向S类型

    int* pi = (int*)&a;

    然后用解引用来查看这片内存存储的int型数据,合起来就*(int*)&a是这种形式。

     

    如此咱们就有了方法访问一个类的任意一片内存了,那么就先找到虚函数表的地址吧!

    了解一个数据类型DWORD_PTR,它被定义为32位的指针(64位系统下64),我们就用这个指针来找到虚函数表的地址。

         DWORD_PTR pvTable;

         pvTable = (DWORD_PTR)&Sub();

    Sub()创建了一个Sub的临时对象,&Sub()表示取这个对象的地址,然后把它强制类型转换为DWORD_PTR,上文说过虚表是在类的最上面,所以pvTable就指向了这个类的虚函数表。

    如下代码访问此类的虚函数:

    ((Sub*)pvTable)->toString()

    利用这种技术可以实现动态调用,具体实现不说明了,可以参考<<精通MFC>>P19,基本思想是实现一个与被调用类匹配的结构,然后进行类型转换就可以映射到相应位置的内存。

    struct CompitibleStruct

    {

           DWORD_PTR vtable;

           int data;

    };

     

    其他

    C++中没有接口的概念,一般要实现接口都用pure virtual class(纯虚类),下面代码举一个实例:

    struct ISwitch

    {

           virtual void On() = 0;

           virtual void Off() = 0;

    };

    定义一个ISwitch接口。参考java或者c#的语言规定,一个接口不能从类继承,所以这个接口的虚函数表其实没有任何意义,Microsoft为这种情况发明了一个修饰符__declspec(novtable)。为纯虚类加上这个关键字不错的选择,可以减少编译出来的冗余信息,这个技术被用到__interface关键字上了,ATL中能看到。

     

    总结:本文由浅入深的介绍了C++的虚函数表,以及它在C++中扮演的角色,本文的技术参考了一些书籍加以总结揣测,上述代码在VS2005下测试运行通过,由于本人能力有限,可能会有错误的地方,希望读者指正。

     

    参考资料:

    <<深入浅出MFC>> <<精通MFC>> MSDN

    原文:http://blog.csdn.net/smalllixin/article/details/1728464#

    作者:smalllixin

  • 相关阅读:
    Redis
    IDEA编码相关,解决yml编码错误导致的 java.nio.charset.MalformedInputException: Input length = 1
    文件上传和下载
    SpringBoot+Mybatis+Postman实现增删改查
    多态与反射
    正则表达式
    原码、反码、补码的用法和理解
    @Conditional & @Profile SpringBoot中自动化配置条件注解。
    Spring Boot 中的 Starter
    第一个项目~千寻在线水果商城
  • 原文地址:https://www.cnblogs.com/ymy124/p/2451480.html
Copyright © 2020-2023  润新知