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++ 虚函数的内存分配的更多相关文章

  1. DLL函数中内存分配及释放的问题

    DLL函数中内存分配及释放的问题 最近一直在写DLL,遇到了一些比较难缠的问题,不过目前基本都解决了.主要是一些内存分配引起问题,既有大家经常遇到的现象也有特殊的 情况,这里总结一下,做为资料. 错误 ...

  2. C++虚函数在内存中的实现

    首先来一张图,一目了然: 然后把相应的代码贴上来: class A { int a; public: virtual void f(); virtual void g(int); virtual vo ...

  3. c++ 入门之深入探讨拷贝函数和内存分配

    在c++入门之深入探讨类的一些行为时,说明了拷贝函数即复制构造函数运用于如下场景: 对象作为函数的参数,以值传递的方式传给函数. 对象作为函数的返回值,以值的方式从函数返回 使用一个对象给另一个对象初 ...

  4. VS2013命令行界面查看虚函数的内存布局

    内存布局可能使用vs的界面调试看到的旺旺是一串数字,很不方便,但是vs的命令行界面可以很直观的显示出一个类中具体的内存布局. 打开命令行.界面如下所示: 测试代码如下所示: class Base1 { ...

  5. [GeekBand] C++继承关系下虚函数内存分布

    本文参考文献:GeekBand课堂内容,授课老师:侯捷 :深度探索C++对象模型(侯捷译) :网络资料,如:http://blog.csdn.net/sanfengshou/article/detai ...

  6. c++内存分布之虚函数(单一继承)

    系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  7. C++ 虚函数的内部实现

    C++ 虚函数的内部实现 虚函数看起来是个玄之又玄的东西,但其实特别简单!了解了虚函数的内部实现,关于虚函数的各种问题都不在话下啦! 1. 知识储备 阅读这篇文章,你需要事先了解以下几个概念: 什么是 ...

  8. 详解C++中的多态和虚函数

    一.将子类赋值给父类 在C++中经常会出现数据类型的转换,比如 int-float等,这种转换的前提是编译器知道如何对数据进行取舍.类其实也是一种数据类型,也可以发生数据转换,但是这种转换只有在 子类 ...

  9. C/C++内存分配

    一.      预备知识—程序的内存分配: 一个由C/C++编译的程序占用的内存分为以下几个部分:1.栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结 ...

随机推荐

  1. 第5章 什么是寄存器—零死角玩转STM32-F429系列

    第5章     什么是寄存器 集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/firege 本章参考资料 ...

  2. data-ng-init 指令

    1.data-ng-init指令为AngularJS应用程序定义了一个初始值. 2.通常情况下,data-ng-init指令并不常用,将会使用控制器或模块来代替它.

  3. Python爬虫,看看我最近博客都写了啥,带你制作高逼格的数据聚合云图

    转载请标明出处: http://blog.csdn.net/forezp/article/details/70198541 本文出自方志朋的博客 今天一时兴起,想用python爬爬自己的博客,通过数据 ...

  4. 基于mybatis设计简单OA系统问题2

    1.<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <fm ...

  5. 浅谈PHP中的数组和JS中的数组

    最近在做前后端对接的时候,遇到一个问题,前端要求返回的数据格式是左边的,但是我通过json_encode返回到的数据格式是右边的   注意:数据格式从"[]"(数组)变成了&quo ...

  6. windows和linux上mysql的安装

    mysql基于多平台,多版本的安装 mysql.tar.gz  链接:https://pan.baidu.com/s/1lG9BNL1mG4qbjM8xLHtrjQ 密码:s4tk MySQL 是一个 ...

  7. cx_freeze的安装使用

    python是一个非常非常优秀的编程语言,它最大的特性就是跨平台.python程序几乎可以在所有常见的平台中进行使用,而且大部分无需修改任何代码!不过,python也有一点点小缺憾(这个是由于自身本质 ...

  8. oauth2.0协议接口-第一篇-api逻辑

    开放平台是支持OAuth2.0和RESTful协议的资源分享平台,经过授权的合作伙伴可以读取和写入资讯.用户.文件.数据库等资源. 1.创建数据库表结构 CMSSyncClient(数据同步客户端) ...

  9. php 变量的8类类型

    整形,布尔,浮点形,字符串,数组,资源,对象和null php数据类型之查看和判断数据类型 php数据类型之自动转换和强制转换

  10. 数据库中where与having的区别

    从整体声明角度分析: “where”是一个约束声明,在查询数据库结果返回之前对数据库的查询条件做一个约束,即返回结果之前起作用,“where”后面不能跟聚合函数: “having”是一个过滤声明,在查 ...