C++ 虚函数的内存分配
1.无继承的普通类:
在有虚函数的情况下类会为其增加一个隐藏的成员,虚函数表指针,指向一个虚函数表,虚函数表里面就是类的各个虚函数的地址了。那么,虚函数表指针是以什么模型加入到类里面的,虚函数表里面又是怎么安排的呢。简单来看下就可以知道了。
#include"stdafx.h"
#pragma pack(8) class A{
public:
int a; double a2;
A() :a(0xaaaaaaaa), a2(){}
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
A a;
return ;
}
定义一个A的变量,然后看其内存布局:
最开始的 4个字节就是虚函数表指针了,类A中有double类型的成员变量 a2,所以类 A的有效字节对齐数是 8,因此可以看到在虚函数表指针后又填充了 4个字节。放完虚函数表指针然后才到类 A 的成员变量。所以在普通类里面,如果有虚函数的话就会在最开始的地方添加一个隐藏的成员变量,虚函数表指针,然后才到正常的成员变量。然后我们再去看下虚函数表里面是什么样子的:
虚函数表也是以4字节为一项,每一项保存一个虚函数的地址。保存的虚函数的地址按照函数声明的顺序排放,第一项存放第一个声明的虚函数,第二项存放第二个,依此类推。我们看下这个表里面的每个项都是什么。
依次选择:调试 --> 窗口 --> 反汇编,打开汇编窗口,可以看到源程序的汇编代码。
我们先来看第一个虚函数:
virtual void funA2(){}
由上上图可知,该函数的地址是:0x00d41028(注意是小端序),在汇编窗口中找到该地址:
看到0x00d41028 处放置了一条 jmp 指令,virtual void funA2() 的真正地址是 0x00d41550
我们可以在汇编窗口中找到 0x00d41550地址,结果如下:
可以看到这虚函数表中的每一项地址实际上并不是虚函数的直接地址,而是一个跳转到相应虚函数的地址。
所以在有虚函数的情况下类的安排也是很简单的,和没有虚函数的情况相比就是在最前面加一个虚函数表指针而已。其他的东西就和没有虚函数的类的情况的时候一样了。然后好像也没有什么然后了,复杂的是在后面~
2.单继承的情况:
单继承大概又可以分为两种情况,一种是基类没有虚函数的情况,一种是基类已经有虚函数表指针的情况。我们分别来看下。
2.1 基类无虚函数的单继承
#include "stdafx.h"
#pragma pack(8)
class F2{ public: int f2; double f22; F2() :f2(0xf2f2f2f2), f22(){} };
class B : F2{
public:
int b;
B() :b(0xbbbbbbbb){}
virtual void funB(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B b;
return ;
}
B的布局抓数据如下:
可以看到虚函数表指针还是放在最开始的地方,也遵循它自己的地址对齐规则,主动填充了4个字节在后面。然后就是F2作为一个整体结构存放在其后,最后才是成员变量b,整个结构也要自身对齐,所以填充了4个字节在最后。虚函数表里面的就是B的虚函数funB的地址了。因为只有一个虚函数,所以虚函数表里面也就只有一项。
同样,我们打开反汇编窗口,找到 0x012e1221 地址处:
可以看到 0x012e1221处放置了一条 jmp 指令,virtual void funB(){} 的真正地址是 0x012e14e0
我们可以在汇编窗口中找到 0x012e14e0地址,结果如下:
果然是 virtual void funB(){} 的起始位置~
所以在基类没有虚函数的情况下,会产生一个虚函数表指针,而且也还是先存放类的虚函数表指针,然后才到基类等。其实在类有虚函数的情况下(暂不考虑虚继承),虚函数表指针都是会存放在最开始的。我们再来看下如果继承的基类已经有了虚函数表指针的情况会是什么样子。
2.2 基类有虚函数的单继承
#include "stdafx.h"
#pragma pack(8)
class A
{
public:
int a; double a2;
A() :a(0xaaaaaaaa), a2(){}
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){}
}; class B : A{
public:
int b;
B() :b(0xbbbbbbbb){}
virtual void funB(){}
virtual void funA2(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B b;
return ;
}
A的布局我们已经知道了,现在B继承A,而且还有覆盖了A的虚函数,来看下布局。
很明显,在基类已经有虚函数表指针的情况下派生类不会再主动产生一个虚函数表指针,基类的虚函数表指针是可以和派生类共用的,因为基类的虚函数肯定也是属于派生类的,如果派生类有虚函数覆盖掉基类的虚函数的话就会把虚函数表里面的相应的项改成正确的地址,而且虚函数表指针刚好也是放在类的最开始的位置。所以在这种情况下就是先放基类然后再排放成员变量。我们来看下现在派生类和基类共用的虚函数表是什么样子的。
虚指针表中共有 4 项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):
虚函数表有4个项:
1、 第一个项的虚函数已经被B里面的那个funA2所取代了,因为B里面的funA2已经覆盖了基类A里面的funA2,所以在虚函数表里面也要相应的改变,这也正是虚函数得以正确调用的前提。
2、 第二个项,也被替换成了B的虚析构函数,我们在代码里面没明写出B的虚析构函数,编译器会自动生成一个,而且B的虚析构函数也是会覆盖掉基类A的虚析构函数的。
3、 第三项还是A里面的函数funA,因为在派生类里面没有被覆盖,所以还应该是基类里面的函数。
4、 第四项是基类A没有的函数funB,所以在这个共用的虚函数表里面基类A只是用到了前3项而已,后面的项就是没有覆盖掉基类的其他虚函数了,而且是按照声明顺序依次排放的。
所以我们暂时可以得出的结论是,有虚函数的类在单继承的情况下,如果基类没有虚函数表指针的话会产生一个隐藏的成员变量,虚函数表指针,放在类的最前面,然后才是基类,最后是派生类的各个成员;如果基类已经有了虚函数表指针的话就不需要再产生一个虚函数表指针,派生类可以和基类共用一个虚函数表,此时派生类的布局是先放基类然后再放派生类的各个成员变量。如果派生类有函数覆盖了基类里面的虚函数的话,虚函数表里面的相应项就会改成这个函数的真正地址,其他没有覆盖的虚函数按照声明的顺序依次排放在虚函数表的后面各项中。
3.多继承的情况
鉴于有虚函数的类的第一项都要是虚函数表指针,所以在多继承的情况下会跟普通情况有所不同。但是有虚函数的类多继承情况下的对象模型也还是比较简单和明确的。
大概也有两种情况,一种是所有的基类都没有虚函数的情况,一种是基类中有些有虚函数有些又没有虚函数的混杂情况。
对于第一种情况,内存布局大概是这样,比如类A的基类都是没有虚函数的话
class A:F0,F1,F2{int a; (其他成员变量)…… virtual voidfun1(){} ……};
那么A肯定也还是要生成一个虚函数表指针的,放在最开始的位置,这种情况下的等价模型大概是这样 :
class A{void * vf_ptr;F0{};F1{};F2{};int a; (其他成员变量)……};
注意各个的字节对齐就可以了,特别是虚函数表指针。
对于第二种情况,基类是混杂的情况的时候,比如类A:
class A : F0, F1, V0, V1, F2, V2 { int a; (其他成员变量)…… virtual void fun1(){} ……};
V0、V1、V2是有虚函数的基类,F是没虚函数的基类,而且继承的声明顺序随意。像这种情况的话类A的对象模型大概是这样的:先排放基类中有虚函数的基类,按照声明顺序,然后再排放基类中没有虚函数的基类,也是按照声明顺序。比如A此时的对象模型就大概是这样:
class A{V0{};V1{};V2{};F0{};F1{};F2{};int a; (其他成员变量)……};
因为基类已经有了虚函数表指针了,所以派生类A也是可以和第一个有虚函数表指针的基类共用一个虚函数表的,这个和单继承的时候的道理是一样的,自然派生类就不会在生成一个虚函数表指针了。我们来实际来下这两种情况的实例。
3.1 基类没有虚函数
#include"stdafx.h"
#pragma pack(8)
class F0{ public:char f0; F0() :f0(0xf0){} };
class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} }; class C : F1, F0{
public:
int c;
virtual void funC(){}
virtual void funB(){}
virtual void funA2(){}
C() :c(0x33333333){}
}; int _tmain(int argc, _TCHAR* argv[])
{
C c;
return ;
}
在派生类有虚函数而基类都没有虚函数的情况下,派生类仍然会产生一个虚函数表指针放在最开始,然后才到各个基类,最后就是成员变量了。结合反汇编窗口,来看下虚函数表里面是些什么。
虚函数表指针共有 3 项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):
可以看到由于派生类的虚函数没有覆盖任何基类里面的虚函数所以虚函数表里面的各项就是各个虚函数按照声明的顺序的地址了。然后再来看下基类有虚函数而且派生类还有覆盖掉基类的虚函数的情况。
3.2 基类中有虚函数
#include"stdafx.h"
#pragma pack(8)
class F0{ public:char f0; F0() :f0(0xf0){} };
class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} }; class A
{
public:
int a; double a2;
A() :a(0xaaaaaaaa), a2(){}
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){}
}; class B : A{
public:
int b;
B() :b(0xbbbbbbbb){}
virtual void funB(){}
virtual void funA2(){}
}; class C : F1, A,F0, B{
public:
int c;
virtual void funC(){}
virtual void funB(){}
virtual void funA2(){}
C() :c(0x33333333){}
}; int _tmain(int argc, _TCHAR* argv[])
{
C c;
return ;
}
类C的模型大概是这样:
class C{
public:
A a;
B b;
F1 f1;
F0 f0;
int c;
};
很明显,虽然F1声明在基类的最前面但是存放顺序还是先存放有虚函数的基类A然后到也是有虚函数的基类B,再才是各个没有虚函数的基类F1、F0。最后才是派生类C的成员变量。C的虚函数funB 覆盖了基类B里面的虚函数,而另一个虚函数funA2既覆盖了基类A里面的虚函数也覆盖了基类B继承自基类A里面的虚函数funA2,理论上基类A和基类B里面被覆盖掉的虚函数其在各自虚函数表里面的对应项都要被改变成正确的函数地址,也就是C里面的虚函数的真实地址。然后我们看下A和B的虚函数表是什么样子的。
A和C共用的虚函数表:
虚函数表指针共有 4项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):
B的虚函数表:
虚函数表指针共有 4项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):
可以看到派生类和基类A共享的虚函数表里面的各个项已经修改成了函数的真正的地址,在最后还加了一个没有覆盖掉任何基类虚函数的虚函数地址项。而基类B里面的项就有点意外了,它并不是直接修改成跳转到正确的地址上去,而是使用了一个调整块的东西,把EAX寄存器减去相应的值,然后再跳转到正确的函数里面去,这个暂时不在这里赘述,反正最后还是跳转到了C里面的那个函数里面去就是了。其他的项有覆盖的也还是一样都要修改成正确的函数地址。
virtual void funA2(){}
virtual ~A(){}
virtual void funA(){} virtual void funB(){}
virtual void funA2(){} virtual void funC(){}
virtual void funB(){}
virtual void funA2(){}
c::funA2
C::~C
A:funA
c:func
至于上面为什么没有C:FUNB,因为B类中有的A类中没有,所有c类不需要替换,
相反对于funA2,A,B,C三个类中都有,那么我们优先选着A类的
c:funa2
c:~c
A:funA
C:funB
C++ 虚函数的内存分配的更多相关文章
- DLL函数中内存分配及释放的问题
DLL函数中内存分配及释放的问题 最近一直在写DLL,遇到了一些比较难缠的问题,不过目前基本都解决了.主要是一些内存分配引起问题,既有大家经常遇到的现象也有特殊的 情况,这里总结一下,做为资料. 错误 ...
- C++虚函数在内存中的实现
首先来一张图,一目了然: 然后把相应的代码贴上来: class A { int a; public: virtual void f(); virtual void g(int); virtual vo ...
- c++ 入门之深入探讨拷贝函数和内存分配
在c++入门之深入探讨类的一些行为时,说明了拷贝函数即复制构造函数运用于如下场景: 对象作为函数的参数,以值传递的方式传给函数. 对象作为函数的返回值,以值的方式从函数返回 使用一个对象给另一个对象初 ...
- VS2013命令行界面查看虚函数的内存布局
内存布局可能使用vs的界面调试看到的旺旺是一串数字,很不方便,但是vs的命令行界面可以很直观的显示出一个类中具体的内存布局. 打开命令行.界面如下所示: 测试代码如下所示: class Base1 { ...
- [GeekBand] C++继承关系下虚函数内存分布
本文参考文献:GeekBand课堂内容,授课老师:侯捷 :深度探索C++对象模型(侯捷译) :网络资料,如:http://blog.csdn.net/sanfengshou/article/detai ...
- c++内存分布之虚函数(单一继承)
系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...
- C++ 虚函数的内部实现
C++ 虚函数的内部实现 虚函数看起来是个玄之又玄的东西,但其实特别简单!了解了虚函数的内部实现,关于虚函数的各种问题都不在话下啦! 1. 知识储备 阅读这篇文章,你需要事先了解以下几个概念: 什么是 ...
- 详解C++中的多态和虚函数
一.将子类赋值给父类 在C++中经常会出现数据类型的转换,比如 int-float等,这种转换的前提是编译器知道如何对数据进行取舍.类其实也是一种数据类型,也可以发生数据转换,但是这种转换只有在 子类 ...
- C/C++内存分配
一. 预备知识—程序的内存分配: 一个由C/C++编译的程序占用的内存分为以下几个部分:1.栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结 ...
随机推荐
- 等待唤醒机制,UDP通信和TCP通信
等待唤醒机制 通过等待唤醒机制使各个线程能有效的利用资源. 等待唤醒机制所涉及到的方法: wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中. notify():唤醒, ...
- GAN的调研和学习
近期集中学习了GAN,下面记录一下调研的结果,和学习的心得,疏漏的地方,敬请指正. 本文将分为几个部分进行介绍,首先是GAN的由来,其次是GAN的发展,最后是GAN的应用. 先把最近收集的资料列举一下 ...
- caffe安装中opencv的各种库问题
提示有些库 high**** opencv的问题,好像是这几个库版本冲突,不要用anaconda里的lib库,用系统的库就行了,删掉或者从新链接过去.
- 爬虫学习(十五)——json解析
json与jsonpath 对象{}:jsonobject 对象:对象在js中表现为{}括起来的内容,数据结构为{key:value,key:value...}键值对的结构,在面向对象的结构中,key ...
- 基于centos7实现的ftp
前言 FTP(File transfer Protocl),文件传输协议,用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式,属于网络传输协议的应用层.FTP服务运行在TCP/21和20端口 ...
- Centos7上搭建activemq集群和zookeeper集群
Zookeeper集群的搭建 1.环境准备 Zookeeper版本:3.4.10. 三台服务器: IP 端口 通信端口 10.233.17.6 2181 2888,3888 10.233.17.7 2 ...
- mysql,oracle表数据相互导入
mysql导入oracle: 例如mysql中有ts_user_info表,现在要导入到oracle中的user_info表 1:导出mysql表数据到data.txt文件 mysql> sel ...
- 海龟绘图turtle模块的使用
在本章中,我们将编写简短的.简单的程序来创建漂亮的.复杂的视觉效果.为了做到这一点,我们可以使用海龟作图软件.在海龟作图中,我们可以编写指令让一个虚拟的(想象中的)海龟在屏幕上来回移动.这个海龟带着一 ...
- php 人人商城 生成 临时微信二维码,并保存成海报图片 有效期一个月
public function getPoster(){ global $_W; global $_GPC; $mm = pdo_fetch('select nickname,codetime fro ...
- 单片机入门学习笔记8:STM32单片机使用
经常会在某个QQ群里看见某人的QQ昵称的名字"不会32绝不改名",其实无论会不会,之后名称都改了. STM32单片机在我看来就三部分组成:各部分的初始化,中断的使用,Main函数内 ...