如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱。这个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. Redis 命令学习

    每天不学习点新的东西,感觉就有点会被社会淘汰掉了.也许现在学习的知识会很快忘记,下次学习用到这个知识点的时候,再回来翻记录的笔记,我想这样会比从头再学,效率会高点吧. 闲话不多聊,回归正题.今天学习r ...

  2. P1725 琪露诺

    P1725 琪露诺 单调队列优化dp 对于不是常数转移的dp转移,我们都可以考虑单调队列转移 然而我们要把数组开大 #include<cstdio> #include<algorit ...

  3. elsevier期刊要求翻译

    百度文库 http://wenku.baidu.com/view/e20a27e84afe04a1b071de4e.html 官网文档 http://www.elsevier.com/journals ...

  4. Shell简介:1分钟理解什么是Shell 脚本语言 解释器 以及编译器和编译语言

    Shell简介:1分钟理解什么是Shell 脚本语言 解释器 以及编译器和编译语言 现在我们使用的操作系统(Windows.Mac OS.Android.iOS 等)都是带图形界面的,简单直观,容易上 ...

  5. java基础必备单词讲解 day two

    variable 变量 count 统计 sum 总数 salary 薪水 Scanner 接收 import 导入 eclipse 日食 control 控制 shift 改变 alt 替换键 ha ...

  6. python 错误问题解决

    获取天气信息 #encoding:UTF-8 import urllib.request import re def getHtml(url): page=urllib.request.urlopen ...

  7. 1、React-Native的基础入门

    React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,目前支持iOS和安 ...

  8. Hive的数据库和表

    本文介绍一下Hive中的数据库(Database/Schema)和表(Table)的基础知识,由于篇幅原因,这里只是一些常用的.基础的. Hive的数据库和表 先看一张草图: Hive结构 从图上可以 ...

  9. No module named 'PyQt5.sip'

    使用pyinstaller打包python文件为windows可执行程序可能遇到的问题 pyinstaller yourprogram.py打包的程序双击打开一闪而过,提示上面标题的错误 把pycom ...

  10. Android弹出输入提示框--PopupWindow实现

    前言  之前直接用Dialog实现了弹出对话框.现在尝试用更好地解决方案--PopupWindow类--来实现 1.首先搞一个弹出框布局,和之前类似. 这样的东西,它的布局是这样: 1 <?xm ...