c++中虚基类表和虚函数表的布局

本文涉及到C++中对象的内存布局知识,若无该方面基础建议先阅读haoel(陈皓)专栏的C++对象内存布局的博客:
http://blog.csdn.net/haoel/article/details/3081328

在拜读上述博客之后,我深受启发,且对C++关于虚函数表的问题有了新的认识和疑惑。比如,在上述博客的最后作者抛出了一个问题:在VS环境下,对象虚函数表指针的下一个字中,存储的不是对象的成员变量,而是一个取值后为-4的地址。这个地址代表什么呢?虚函数表上是如何放置基类和派生类的虚函数呢?

注意:对于在对象中存取虚基类的问题,虚基类表仅是Microsoft编译器的解决办法。在其他编译器中,一般采用在虚函数表中放置虚基类的偏移量的方式。

通过实验,我得到了这样的结论:
1. 一个对象实例只有一个虚函数表,只有一个虚基类表。
2. 对象的每个基类都有一个属于自己的虚函数表指针(vfptr)指向虚函数表(vftbl)的某一项,都有一个属于自己的虚基类表指针(vbptr)指向虚基类表(vbtbl)的某一项。
3. 虚函数表中按照对象继承的顺序排列对象的虚函数地址,虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。
4. 当基类无虚函数,且派生类有独立虚函数时,派生类对象起始位置为自己的虚函数表指针。否则派生类的虚函数会归到第一个带虚函数表指针的基类的虚函数表指向范围,这样就节省了一个vfptr的空间。

沿用上述博客的代码:

class B
{
public:
    int ib;
    char cb;
public:
    B() :ib(0), cb('B') {}

    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
    int ib1;
    char cb1;
public:
    B1() :ib1(11), cb1('1') {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }


};
class B2 : virtual public B
{
public:
    int ib2;
    char cb2;
public:
    B2() :ib2(12), cb2('2') {}

    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }

};

class D : public B1, public B2
{
public:
    int id;
    char cd;
public:
    D() :id(100), cd('D') {}

    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }

};
实验方法:

int main(void)
{
    typedef void(*F)();
    D d;

    int *op = reinterpret_cast<int*>(&d);    //1    
    int *vfptr = reinterpret_cast<int*>(*op);//2
    F fun = reinterpret_cast<F>(*(vfptr));   //3

    fun();


    return 0;
}

运行结果为:
D::f1()

将上述语句中3号语句加1呢?
F fun = reinterpret_cast(*(vfptr+1));
运行结果为:
B1::Bf1()

因此说明基类B1的虚函数表指针指向虚函数表挂载着基类B1可调用的虚函数,此时共有3个,指针偏移到3的时候fun的值为0,即VS编译器用来标识的终结。但如果继续增加偏移量呢?

将上述语句中3号语句加4呢?
运行失败,编译器显示此时fun访问一个名为RTTICompleteObjectLocator的类型对象指针。此类型用来进行运行时类型识别,该类型对象存储由编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述。一般放在基类虚函数表之前的一个字中。因此此时fun内存储的是与B2对应的RTTICompleteObjectLocator类型指针。

将上述语句中3号语句加5呢?
运行结果为:
D2::f2()

综上所述,我们可以得到虚函数表的构造:

这里写图片描述

下一个实验,我们探索虚基类表:

int main(void)
{
    D d;

    int *op = reinterpret_cast<int*>(&d);        //1
    int *vbptr = reinterpret_cast<int*>(*(op+1));//2
    int offset = *vbptr;                         //3

    std::cout << offset << endl;

    return 0;
}

运行结果为:
-4

这个-4是什么呢?其实此时vbptr为基类B1的虚基类表指针,位于vfptr的下方(从内存布局角度看是下方,但是地址更高)。对虚基类表的第一项取值得到的就是-4,表示虚基类表指针到基类B1的偏移量。

如果将3号语句的vbptr加1呢?
运行结果为:
40

此时40表示从基类B1的vbptr到B1虚继承的基类B的偏移量。我们按照我引用的文章给的内存布局推算,正好是40个字节。

如果将3号语句的vbptr加2呢?
运行结果为:
0

此处的0也是用来标识B1虚基类表的终结。

如果将3号语句的vbptr加3呢?
运行结果为:
-4

即B2的虚基类表指针到B2对象的偏移量。

如果将3号语句的vbptr加4呢?
运行结果为:
24

即B2的虚基类表指针到其虚继承的基类B对象的偏移量。

综上所述,我们可以得到虚基类表的构造:

这里写图片描述

注意:测试不同类型数据的时候发现虚函数表上不同类型到基类的offset分布未必相邻!。

  • 13
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
目 录 译者序 前言 第1章 对象的演化 1.1基本概念 1.1.1对象:特性十行为 1.1.2继承:类型关系 1.1.3多态性 1.1.4操作概念:OOP程序像什么 1.2为什么C++会成功 1.2.1较好的C 1.2.2采用渐进的学习方式 1.2.3运行效率 1.2.4系统更容易达和理解 1.2.5“库”使你事半功倍 1.2.6错误处理 1.2.7大程序设计 1.3方法学介绍 1.3.1复杂性 1.3.2内部原则 1.3.3外部原则 1.3.4对象设计的五个阶段 1.3.5方法承诺什么 1.3.6方法应当提供什么 1.4起草:最小的方法 1.4.1前提 1.4.2高概念 1.4.3论述(treatment) 1.4.4结构化 1.4.5开发 1.4.6重写 1.4.7逻辑 1.5其他方法 1.5.1Booch 1.5.2责任驱动的设计(RDD) 1.5.3对象建模技术(OMT) 1.6为向OOP转变而采取的策略 1.6.1逐步进入OOP 1.6.2管理障碍 1.7小结 第2章 数据抽象 2.1声明与定义 2.2一个袖珍C库 2.3放在一起:项目创建工具 2.4什么是非正常 2.5基本对象 2.6什么是对象 2.7抽象数据类型 2.8对象细节 2.9头文件形式 2.10嵌套结构 2.11小结 2.12练习 第3章 隐藏实现 3.1设置限制 3.2C++的存取控制 3.3友元 3.3.1嵌套友元 3.3.2它是纯的吗 3.4对象布局 3.5类 3.5.1用存取控制来修改stash 3.5.2用存取控制来修改stack 3.6句柄类(handleclasses) 3.6.1可见的实现部分 3.6.2减少重复编译 3.7小结 3.8练习 第4章 初始化与清除 4.1用构造函数确保初始化 4.2用析构函数确保清除 4.3清除定义块 4.3.1for循环 4.3.2空间分配 4.4含有构造函数和析构函数的stash 4.5含有构造函数和析构函数的stack 4.6集合初始化 4.7缺省构造函数 4.8小结 4.9练习 第5章 函数重载与缺省参数 5.1范围分解 5.1.1用返回值重载 5.1.2安全类型连接 5.2重载的例子 5.3缺省参数 5.4小结 5.5练习 第6章 输入输出流介绍 6.1为什么要用输入输出流 6.2解决输入输出流问题 6.2.1预先了解操作符重载 6.2.2插入符与提取符 6.2.3通常用法 6.2.4面向行的输入 6.3文件输入输出流 6.4输入输出流缓冲 6.5在输入输出流查找 6.6strstreams 6.6.1为用户分配的存储 6.6.2自动存储分配 6.7输出流格式化 6.7.1内部格式化数据 6.7.2例子 6.8格式化操纵算子 6.9建立操纵算子 6.10输入输出流实例 6.10.1代码生成 6.10.2一个简单的数据记录 6.11小结 6.12练习 第7章 常量 7.1值替代 7.1.1头文件里的const 7.1.2const的安全性 7.1.3集合 7.1.4与C语言的区别 7.2指针 7.2.1指向const的指针 7.2.2const指针 7.2.3赋值和类型检查 7.3函数参数和返回值 7.3.1传递const值 7.3.2返回const值 7.3.3传递和返回地址 7.4类 7.4.1类里的const和enum 7.4.2编译期间类里的常量 7.4.3const对象和成员函数 7.4.4只读存储能力 7.5可变的(volatile) 7.6小结 7.7练习 第8章 内联函数 8.1预处理器的缺陷 8.2内联函数 8.2.1类内部的内联函数 8.2.2存取函数 8.3内联函数和编译器 8.3.1局限性 8.3.2赋值顺序 8.3.3在构造函数和析构函数里隐藏行为 8.4减少混乱 8.5预处理器的特点 8.6改进的错误检查 8.7小结 8.8练习 第9章 命名控制 9.1来自C语言的静态成员 9.1.1函数内部的静态变量 9.1.2控制连接 9.1.3其他的存储类型指定符 9.2名字空间 9.2.1产生一个名字空间 9.2.2使用名字空间 9.3C++的静态成员 9.3.1定义静态数据成员的存储 9.3.2嵌套类和局部类 9.3.3静态成员函数 9.4静态初始化的依赖因素 9.5转换连接指定 9.6小结 9.7练习 第10章 引用和拷贝构造函数 10.1C++的指针 10.2C+十的引用 10.2.1函数的引用 10.2.2参数传递准则 10.3拷贝构造函数 10.3.1传值方式传递和返回 10.3.2拷贝构造函数 10.3.3缺省拷贝构造函数 10.3.4拷贝构造函数方法的选择 10.4指向成员的指针(简称成员指针) 10.5小结 10.6练习 第11章 运算符重载 1

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值