虚函数与虚表浅分析


虚函数以及虚函数表的特征:

1.虚函数表是全局共享的元素,即全局仅有一个.
2.虚函数表类似一个数组,类对象中存储 vptr 指针,指向虚函数表.即虚函数表不是函数,不是程序代码,不肯能存储在代码段.
3.虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中.

根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定.

c/c++程序所占用的内存一共分为五种:

栈区,堆区,程序代码区,全局数据区(静态区),文字常量区.

所以个人推测虚函数表和静态成员变量一样,存放在全局数据区.

虚函数与虚表几个值得注意的问题:

1.虚函数表是class specific的,也就是针对一个类来说的,这里有点像一个类里面的staic成员变量,即它是属于一个类所有对象的,不是属于某一个对象特有的,是一个类所有对象共有的。

2.虚函数表是编译器来选择实现的,编译器的种类不同,可能实现方式不一样,就像前面我们说的vptr在一个对象的最前面,但是也有其他实现方式,不过目前gcc 和微软的编译器都是将vptr放在对象内存布局的最前面。

3.虽然我们知道 vptr 指向虚函数表,那么虚函数表具体存放在内存哪个位置呢,虽然这里我们已经可以得到虚函数表的地址。实际上虚函数指针是在构造函数执行时初始化的,而虚函数表是存放在可执行文件中的。

4.有一篇博客测试了微软的编译器将虚函数表存放在了目标文件或者可执行文件的常量段中,不过我在gcc下的汇编文件中没有找到 vtbl 的具体存放位置,主要是对可执行文件的装载和运行原理还没有深刻的理解,相信不久有了这些知识之后会很轻松的找到虚函数表到底存放在目标文件的哪一个段中。经过测试,在gcc编译器的实现中虚函数表vtable存放在可执行文件ELF的只读数据段.rodata中。

分析下面的一段代码,可以得出虚拟继承的特点


    class MyClass
    {
        int var;
    public:
        virtual void fun()
        {}
    };

    class MyClassA:public MyClass
    {
        int varA;
    public:
        virtual void fun()
        {}
        virtual void funA()
        {}
    };

    class MyClassB:public MyClass
    {
        int varB;
    public:
        virtual void fun()
        {}
        virtual void funB()
        {}
    };
    class MyClassC:public MyClassA,public MyClassB
    {
        int varC;
    public:
        virtual void funB()
        {}
    virtual void funC()
        {}
    };

    class MyClassA:virtual public MyClass // 虚继承
    class MyClassB:virtual public MyClass // 虚继承
    class MyClassC:public MyClassA,public MyClassB

虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。因此, 虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。

比如 MyClassA 的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass:: vfptr 相对于MyClassB::vbptr的偏移量。

虚函数的底层实现机制

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr)这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。

看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

编译器处理虚函数的方法是:

给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。

使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 针对每个类,编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/281603.html

(0)
上一篇 2022年8月22日
下一篇 2022年8月22日

相关推荐

发表回复

登录后才能评论