如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱。这个C++中最复杂的继承层次在VS上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不到什么理由来解释它,也只算是知其然不知其所以然吧。

虚、实基类都没有虚函数

这种情况也还算比较简单。因为虚函数表指针一定是会放在最开始的,所以根据猜测也可以知道其大概布局情况。看下面一个简单的例子

#pragma pack(8)
class F1{
public:
int f1;
F1():f1(0xf1f1f1f1){}
}; class A : virtual F1{
public:
inta;
A():a(0xaaaaaaaa){}
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
A aa;
return ;
}

这是只有一个虚继承的简单情况,可以看到类A的前4个字节就是虚函数表指针,虚函数表里面有3个项,当然就是分别指向类A的那3个虚函数的指针了(注意是小端序):

可以看到 funA2() 函数的入口地址是0x00f81028;~A() 函数的入口地址是 0x00f811bd;

funA() 函数的入口地址是 0x00f810aa。

为了验证 funA2() 函数的入口地址是否正确(~A()和 funA() 可以用相同的方法验证),打开反汇编窗口:

找到地址为 0x00f81028 的地方:

可以看到 0x00f81028 存放着一条 jmp 指令,跳转到A::funA2() 函数的真正地址:0x00f815d0,我们接着到 0x00f815d0 处看看是什么情况

可以看到 0x00f815d0 处的确是A::funA2() 函数开始执行的地方,懂汇编的童鞋应该知道前两句指令就是建立A::funA2() 函数的堆栈~

可以看到虚基类表里面的两个偏移,一个是 -4,说明类A的起始地址位于虚基类表指针偏移-4 个字节的地方,也就是虚函数表指针的地址了。虚基类表里面的第二项就是第一个虚基类( F1 )相对于虚基类表指针的偏移,是8 字节,可以看到刚好也是 f1f1f1f1 的位置。

看了一个简单的情况我们再来看一个稍微复杂的继承情况,虚实多继承,所有的基类仍然都没有虚函数。

#pragma pack(8)
class F0{ public: char f0; F0() :f0(0xf0){}};
class F1{ public: int f1; F1():f1(0xf1f1f1f1){} };
class F2{ public: int f2; double f22; F2():f22(), f2(0xf2f2f2f2){} };
class F3 :virtual F2{ public: int f3; F3(): f3(0xf3f3f3f3){} };
class F4 :virtual F2{ public: int f4; F4(): f4(0xf4f4f4f4){} }; class A : virtual F0, F1, virtual F3, F4{
public:
inta;
A():a(0xaaaaaaaa){}
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
A aa;
return ;
}

我们知道,只要出现虚继承,那么派生类的内存布局就分为实部和虚部,我们来仔细分析一下这 64 个字节:

1.      派生类的实部:

1.1、74  68  ad  00:派生类 A 的虚函数表指针;(距派生类 C 的虚基类表指针 -16 个字节)

1.2、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -12 个字节)

1.3、f1  f1  f1  f1:实基类 F1 的成员变量,int  f1 = 0xf1f1f1f1;(距派生类 C 的虚基类表指针 -8 个字节)

1.4、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -4 个字节)

1.5、84  68  ad  00:实基类 F4 和派生类 A 共用的虚基类表指针;(距派生类 C 的虚基类表指针 0 个字节)

1.6、f4  f4  f4  f4:实基类 F4 的成员变量,int  f4 = 0xf4f4f4f4;(距派生类 C 的虚基类表指针 +4 个字节)

1.7、aa  aa  aa  aa:派生类 A 的成员变量,int  a = 0xaaaaaaaa;(距派生类 C 的虚基类表指针 +8 个字节)

实部的内存布局图如下:

2.      派生类实部和虚部的结合处:

cc  cc  cc  cc:这四个字节是为了字节对齐而填充的,同时,这四个字节之前的内容是派生类的实部,之后的内容就是派生类的虚部;(距派生类 C 的虚基类表指针 +12 个字节)

3.      派生类的虚部:

3.1、f0  cc  cc  cc:虚基类 F0 的成员变量,char  f0 = 0xf0,三个 cc 是为了字节对齐而填充的;(距派生类 C 的虚基类表指针 +16 个字节)

3.2、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 +20 个字节)

3.3、f2  f2  f2  f2:虚基类 F2 的成员变量,int  f2 = 0xf2f2f2f2;(距派生类 C 的虚基类表指针 +24 个字节)

3.4、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 +28 个字节)

3.5、00  00  00 00  00  00  00  00:虚基类 F2 的成员变量,double  f22 = 0x00000000;(距派生类 C 的虚基类表指针 +36 个字节)

3.6、98  68  ad  00:虚基类 F3 的虚基类表指针;(距派生类 C 的虚基类表指针 +40 个字节)

3.7、f3  f3  f3  f3:虚基类 F3 的成员变量,int  f3 = 0xf3f3f3f3;(距派生类 C 的虚基类表指针 +44 个字节)

虚部的内存布局图如下:

以上就是派生类 A 的内存布局

然后看下派生类 A 和其基类 F4 共用的虚基类表:

按照规则这个共用的虚基类表里面的项是这样安排的

1.      第一项是0,表示F4的起始地址就是虚基类表指针的起始地址;

2.      第二项是F4的虚基类F2相对于虚函数表指针的偏移,是0x18(十进制 24);

3.      第三项开始就不是F4的虚基类的项了,从第三项开始的各个项是类A的其他虚基类,按照声明的顺序排放,所以第三项就是虚基类F0 相对于虚基类表指针的偏移,是0x10(十进制 16);

4.      然后F1没有虚部跳过,再到 F3,因为F3的虚部F2已经包含在了之前的项里面了所以也跳得,所以第四项就到F3 的偏移,是0x28(十进制 40)个字节;

接下来我们看看 F3 的虚基类表:

F3的虚基类表是它自己独享的,先是自己起始地址的偏移,然后是虚基类的偏移,可以看到它的虚基类F2 相对于它这个虚基类表指针的偏移值-16(24 - 40)个字节,也是正确的。

虚函数&&虚继承的更多相关文章

  1. 【整理】C++虚函数及其继承、虚继承类大小

    参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/deta ...

  2. C++ - 类的虚函数\虚继承所占的空间

    类的虚函数\虚继承所占的空间 本文地址: http://blog.csdn.net/caroline_wendy/article/details/24236469 char占用一个字节, 但不满足4的 ...

  3. C++学习 - 虚表,虚函数,虚函数表指针学习笔记

    http://blog.csdn.net/alps1992/article/details/45052403 虚函数 虚函数就是用virtual来修饰的函数.虚函数是实现C++多态的基础. 虚表 每个 ...

  4. C++解析(25):关于动态内存分配、虚函数和继承中强制类型转换的疑问

    0.目录 1.动态内存分配 1.1 new和malloc的区别 1.2 delete和free的区别 2.虚函数 2.1 构造函数与析构函数是否可以成为虚函数? 2.2 构造函数与析构函数是否可以发生 ...

  5. C++基础 (6) 第六天 继承 虚函数 虚继承 多态 虚函数

    继承是一种耦合度很强的关系 和父类代码很多都重复的 2 继承的概念 3 继承的概念和推演 语法: class 派生类:访问修饰符 基类 代码: … … 4 继承方式与访问控制权限 相对的说法: 爹派生 ...

  6. C#中的虚函数及继承关系

    转载:http://blog.csdn.net/suncherrydream/article/details/8423991 若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法. 虚 ...

  7. C++继承-重载-多态-虚函数

    C++ 继承 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派生类,我们使用一个类派生列表来指定基类.类派生列表以一个或多个基类命名,形式如下: ...

  8. virtual之虚函数,虚继承

    当类中包含虚函数时,则该类每个对象中在内存分配中除去数据外还包含了一个虚函数表指针(vfptr),指向虚函数表(vftable),虚函数表中存放了该类包含的虚函数的地址. 当子类通过虚继承的方式从父类 ...

  9. 虚函数&纯虚函数&抽象类&虚继承

    C++ 虚函数&纯虚函数&抽象类&接口&虚基类   1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过 ...

随机推荐

  1. AngularJS 重复HTML元素

    data-ng-repeat指令会重复一个HTML元素 <!DOCTYPE html><html><head><meta http-equiv="C ...

  2. centos6 下查看SELinux状态 关闭SELinux

    转载自:https://blog.csdn.net/boomjane_testingblog/article/details/52859977 SELinux(Security-Enhanced Li ...

  3. django+xadmin在线教育平台(十五)

    7-4 课程机构列表页数据展示2 前去html中进行数据填充   mark 可以看到所有城市是通过a标签,当前选中城市为active.   mark 之后把下面的写死的城市删除掉.   mark 这时 ...

  4. centos7 多网卡修改默认路由

    最近在virtualbox里搭了一个centos7的虚拟机,但是网络这一块总是有问题. 单网卡下的问题: 1.当我配置连接方式为NAT网络地址转换的时候,虚拟机可以访问外网.但是在网络地址转换的情况下 ...

  5. php 多维数组相同键值处理合并

    一.前言 在实际情况中,有时需要针对多维数组相同键值作相应的处理(四则运算.比较大小等)后才能够使用到实际情况中,现给出三维数组(多维数组可相应拓展)任意多个相同键值处理的函数,以备查阅. 二.代码 ...

  6. 裸机——ADC

    1.首先是ADC的基本知识 模拟信号,连续的 数字信号,离散的 模拟信号,现实世界的很多东西都是连续的,所以使用模拟信号才能准确描述,但是模拟信号不方便控制. 数字信号,计算机中的信号大都为数字的,数 ...

  7. 边缘检测 opencv

    本次实验使用了两种方法进行了边缘检测,分别使用到了opencv中的两个API函数为Canny()和Sobel()函数.实验后加了Scharr滤波器,它其实是基于Sobel()函数的. 这三个API中的 ...

  8. [BZOJ3714]Kuglarz(最小生成树)

    Description 魔术师的桌子上有n个杯子排成一行,编号为1,2,-,n,其中某些杯子底下藏有一个小球,如果你准确地猜出是哪些杯子,你就可以获得奖品.花费\(C_{i,j}\)元,魔术师就会告诉 ...

  9. salt 通信及其安全

    salt 通信及其安全 模型架构 server-agent通信模型: server就是salt master; agent就是salt-minion salt也可以作为一个单点服务器管理工具使用,或者 ...

  10. MyBatis---简单增删改查的带事物的例子

    本例子包含了对数据库表简单的增删改查的操作,并且包含事物.该例子只适用于MySQL数据库.该例子需要手动创建数据库以及数据库表 例子中所需要的jar包,详查MyBatis---简介 一个entity类 ...