c++ vptr和vptr_table

1.基础理论

为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。

虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。

其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。

因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。

2.调用图

类调用图

3. 代码展示:

#include <iostream>
#include <stdio.h>
using namespace std;

/**
*@ 函数指针
*/
typedef void(*Fun)();
/**
@基类
*/
class Base {
public:
    Base() {};
    virtual void fun1()
    {
        cout << "Base::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::fun2()" << endl;
    }
    virtual void fun3() {}
    ~Base() {};

};
/**
*@brief 派生类
*/
class Derived:public Base
{
public:
    Derived() {};
    void fun1()
    {
        cout << "Derived::fun1()" << endl;  
    }
    void fun2()
    {
        cout << "DerivedClass::fun2()" << endl;
    }
    ~Derived(){}
};
/**
 * @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,
 这块内存就是我们所说的虚表
 *
 * @param obj
 * @param offset
 * @return
 */
Fun getAddr(void   *obj, unsigned int offset)
{
    cout << "================" << endl;
    void  *vptr_addr = (void*)*(unsigned long*)obj;  //64位操作系统占8字节,通过*(unsigned long *)obj取出前8字节,
                                                    //即vptr指针
    printf("vptr_addr: %p\n", vptr_addr);
    /**
    * @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,
    因此通过*(unsigned long *)vptr_addr取出前8字节,后面加上偏移量就是每个函数的地址!
    */
    void  *func_addr = (void*)*((unsigned long*)vptr_addr + offset);
    printf("func_addr: %p\n", func_addr);
    return (Fun)func_addr;
}
int main(void)
{
    Base ptr;
    Derived d;
    Base *pt = new Derived(); //基类指向派生类实例
    Base &pp = ptr; // 基类引用指向基类实例
    Base &p = d; //基类引用指向派生类实例
    cout << "基类对象直接调用" << endl;
    ptr.fun1();
    cout << "派生类对象调用派生类实例" << endl;
    d.fun1();
    cout << "基类引用调用基类实例" << endl;
    pp.fun1();
    cout << "基类指针指向派生类实例并调用派生类虚函数" << endl;
    pt->fun1();
    cout << "基类引用指向派生类实例并调用派生类虚函数" << endl;
    p.fun1();

    //手动查找vptr和vtable
    Fun f1 = getAddr(pt, 0);
    (*f1)();
    Fun f2 = getAddr(pt, 1);
    (*f2)();
    delete pt;
    return 0;
}

4. 结果展示与分析

图2 结果展示

其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 9,728评论 0 45
  • 有那么多讲C++多态的文章,但是却没有一个能真正看明白的,神秘的多态机制,究竟是如何实现的,看黄老师来如何教你? ...
    黄老师精品IT课阅读 400评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 126,160评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,120评论 0 4