带虚函数的类的对象布局(2)

  接下来我们看看多重继承。定义两个类,各含一个虚函数,及一个数据成员。再从这两个类派生一个空子类。
struct C041
{
    C041() : c_(0x01) {}
    virtual void foo() { c_ = 0x02; }
    char c_;
};
struct C042
{
    C042() : c_(0x02) {}
    virtual void foo2() {}
    char c_;
};
struct C051 : public C041, public C042
{
};
  运行如下代码:
PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C042)
PRINT_SIZE_DETAIL(C051)
  结果为:
The size of C041 is 5
The detail of C041 is 64 b3 45 00 01
The size of C042 is 5
The detail of C042 is 68 b3 45 00 02
The size of C051 is 10
The detail of C051 is 6c b4 45 00 01 68 b4 45 00 02
  注意,首先我们观察C051的对象输出,发现它的大小为10字节,这说明它有两个虚表指针,从导出的内存数据我们可以推断,首先是一个虚表指针,然后是从C041继承的成员变量,值也是我们在C041的构造函数中赋的值0x01,然后又是一个虚表指针,再是从C042继承的成员变量,值为0x02。
  为了验证,我们再运行如下代码:
C041 c041;
C042 c042;
C051 c051;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c051, 0, 0)
PRINT_VTABLE_ITEM(c051, 5, 0)
  注意最后一行的第二个参数,5。它是从对象起始地址开始到虚表指针的偏移值(按字节计算),从上面的对象内存输出我们看到C041的大小为5字节,因此C051中第二个虚表指针的起始位置距对象地址的偏移为5字节。输出的结果为:
  (注:这个偏移值是通过观察而判断出来的,并不通用,而且它依赖于我们前面所说的编译器在生成代码时所用的结构成员对齐方式,我们将这个值设为1。如果设为其他值会影响对象的大小及这个偏移值。参见第一篇起始处的说明。下同。)
c041   : objadr:0012FB88 vpadr:0012FB88 vtadr:0045B364 vtival(0):0041DF1E
c042   : objadr:0012FB78 vpadr:0012FB78 vtadr:0045B368 vtival(0):0041D43D
c051   : objadr:0012FB64 vpadr:0012FB64 vtadr:0045B46C vtival(0):0041DF1E
c051   : objadr:0012FB64 vpadr:0012FB69 vtadr:0045B468 vtival(0):0041D43D
  这下我们可以看到C051的两个虚表指针指向两个不现的虚表(第3、4行的vtadr列),而虚表中的条目的值分别等于C041和C042(即它的两个父类)的虚表条目的值(第1、3行和2、4行的vtival列的值相同)。
  为什么子类要有两个虚表,而不是将它们合并为一个。主要是在处理类型的动态转换时这种对象布局更方便调整指针,后面我们看到这样的例子。

  如果子类重写父类的虚函数会怎么样?前面的类C071我们已经看到过一次了。我们再定义一个从C041和C042派生的类C082,并重写这两个父类中的虚函数,同时再增加一个虚函数。
struct C041
{
    C041() : c_(0x01) {}
    virtual void foo() { c_ = 0x02; }
    char c_;
};
struct C042
{
    C042() : c_(0x02) {}
    virtual void foo2() {}
    char c_;
};
struct C082 : public C041, public C042
{
    C082() : c_(0x03) {}
    virtual void foo() {}
    virtual void foo2() {}
    virtual void foo3() {}
    char c_;
};
  运行和上面类似的代码:
PRINT_SIZE_DETAIL(C082)
C041 c041;
C042 c042;
C082 c082;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c082, 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)
  结果为:
The size of C082 is 11
The detail of C082 is 70 b3 45 00 01 6c b3 45 00 02 03
c041   : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
c042   : objadr:0012FA64 vpadr:0012FA64 vtadr:0045B368 vtival(0):0041D43D
c082   : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(0):0041D87A
c082   : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
  果然C082的两个虚表中的条目值都和父类的不一样了(vtival列),指向了重写后的新函数地址。观察C082的大小和对象内存,我们可以知道它并没有为新定义的虚函数foo3生成新的虚表。那么foo3的函数地址到底是加到了类的第一个虚表,还是第二个虚表中?在调试状态下,我们在“局部变量”窗口中展开c082对象。我们可以看到两个虚表及其中的条目,但两个虚表都只能看到第一个条目。这应该是VC7.1IDE的一个小BUG。看来我们只有另想办法来验证。我们先把两个虚表中的第二个条目位置上的值打印出来。运行如下代码。
PRINT_VTABLE_ITEM(c082, 0, 1)
PRINT_VTABLE_ITEM(c082, 5, 1)
  结果如下:
c082   : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(1):0041D32F
c082   : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(1):0041D87A
  然后我们调用一下foo3函数:
c082.foo3();
  查看它的汇编代码:
004225F3  lea         ecx,[ebp+FFFFFB74h]
004225F9  call        0041D32F
  第2条call指令后的地址就是foo3的函数地址了(实际上是一个跳转指令),对照前面的输出我们就可以知道,子类新定义的虚函数对应的虚表条目加入到了子类的第一个虚表中,并位于继承自父类的虚表条目之后。

  (未完待续)

潘凯:C++对象布局及多态实现的探索(三)相关推荐

  1. 潘凯:C++对象布局及多态实现的探索(十)

    菱形结构的虚继承(2) 我们再看一个例子,这个例子的继承结构和上一篇中是一样的,也是菱形结构.不同的是,每一个类都重写了顶层类声明的虚函数.代码如下: struct C041 {     C041() ...

  2. 潘凯:C++对象布局及多态实现的探索(九)

    菱形结构的虚继承 这次我们看看菱形结构的虚继承.虚继承的引入本就是为了解决复杂结构的继承体系问题.上一篇我们在讨论虚继承时用的是一个简单的继承结构,只是为了打个铺垫. 我们先看看这几个类,这是一个典型 ...

  3. 潘凯:C++对象布局及多态实现的探索(六)

    虚函数调用 我们再看看虚成员函数的调用.类C041中含有虚成员函数,它的定义如下: struct C041 {     C041() : c_(0x01) {}     virtual void fo ...

  4. 潘凯:C++对象布局及多态实现的探索(一)

    前言 本文通过观察对象的内存布局,跟踪函数调用的汇编代码.分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等. 写这篇文章源于我在论坛上看到的一个贴子.有人问VC使用了哪种方式来实现虚 ...

  5. 潘凯:C++对象布局及多态实现的探索(十一)

    菱形结构的虚继承(3) 最后我们看看,如果在上篇例子的基础上,子类及左.右父类都各自定义了自己的虚函数,这时的情况又会怎样. struct C140 : public virtual C041 {   ...

  6. 潘凯:C++对象布局及多态实现的探索(八)

    普通的虚继承 下面我们来看虚继承.首先看看这C020类,它从C010虚继承: struct C010 {     C010() : c_(0x01) {}     void foo() { c_ = ...

  7. 潘凯:C++对象布局及多态实现的探索(二)

    带虚函数的类的对象布局(1) 如果类中存在虚函数时,情况会怎样呢?我们知道当一个类中有虚函数时,编译器会为该类产生一个虚函数表,并在它的每一个对象中插入一个指向该虚函数表的指针,通常这个指针是插在对象 ...

  8. 潘凯:C++对象布局及多态实现的探索(七)

    构造函数中的虚成员函数调用 在构造函数中调用虚成员函数,虽然这是个不很常用的技术,但研究一下可以加深对虚函数机制及对象构造过程的理解.这个问题也和一般直观上的认识有所差异.先看看下面的两个类定义. s ...

  9. 潘凯:C++对象布局及多态实现的探索(十二)

    后记 结合前面的讨论,我们可以看到,只要牵涉到了虚继承,在访问父类的成员变量时生成的代码相当的低效,需要通过很多间接的计算来定位成员变量的地址.在指针类型转换,动态转型,及虚函数调用时,也需要生成很多 ...

最新文章

  1. 小米5x_除了一亿像素,对于小米CC9pro可能你还要了解这些
  2. 缺少libtool依赖导致编译安装失败
  3. 不用比较运算符及循环控制语句,判断int型的a、b两数的大小
  4. linux查看重传次数,TCP-聊一聊重传次数
  5. UVA532 - Dungeon Master(裸BFS)
  6. linux防火墙常用缩写,Linux iptables常用防火墙规则
  7. linux下根据端口查进程,linux根据进程查端口,根据端口查进程
  8. 组件php53 php55区别,分享下php5类中三种数据类型的区别
  9. 编译的 Ruby 2.3.0 缺少 openssl 支持的解决方法 (已解决)
  10. 如何在win10搜索计算机,如何在win10电脑的任务栏搜索框中添加地址?
  11. Train Problem I(STL)基本运用stack
  12. 宇枫资本投资理财这些要注意
  13. 远程安全接入解决方案
  14. android落花效果 字体渐变,落花有情 亲花有趣
  15. 校招22届大疆 嵌入式面经/23届投递可私戳内推!
  16. POS、银联密钥体系
  17. 使用 font-spider 对 webfont 网页字体进行压缩
  18. CenterPoint 学习笔记
  19. 别再付费了!霸屏朋友圈的“蚂蚁呀嘿”视频教程大公开
  20. Maven 三种archetype说明

热门文章

  1. 环形进度条ProgressBar
  2. linux下xxx:warning: ignoring unsupported character ‘
  3. 手把手教你做测试分析
  4. LeetCode_189.轮转数组
  5. 高中信息第四节计算机的软件,高中信息技术基础《计算机的软件》教案设计.doc...
  6. cocos2d-x自制RPG游戏总结
  7. 树莓派高清图传教程2——基本参数设置
  8. 首尔半导体的LED “SunLike”产品临床试验结果显示:对眼部健康和睡眠质量具有改善效果
  9. Windows XP操作系统自带工具应用
  10. GPS全球定位系统构成及原理