本文原文出处为MSDN。如果你安装了MSDN,可以搜索到C++ Under the Hood。否则也可在网站上找到http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp 。

1 前言

本文着重回答这样一些问题:
1* 类如何布局?
2* 成员变量如何访问?
3* 成员函数如何访问?
4* 所谓的“调整块”(adjuster thunk)是怎么回事?
5* 使用如下机制时,开销如何:
  * 单继承、多重继承、虚继承
  * 虚函数调用
  * 强制转换到基类,或者强制转换到虚基类
  * 异常处理

首先,我们顺次考察C兼容的结构(struct)的布局,单继承,多重继承,以及虚继承;
接着,我们讲成员变量和成员函数的访问,当然,这里面包含虚函数的情况;
再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是如何动态构造和销毁的;
最后,简单地介绍对异常处理的支持。

2 类布局

本节讨论不同的继承方式造成的不同内存布局。

2.1 C结构(struct)

由于C++基于C,所以C++也“基本上”兼容C。特别地,C++规范在“结构”上使用了和C相同的,简单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。

2.2 有C++特征的C结构

C++不是复杂的C,C++本质上是面向对象的语言:包 含 继承、封装,以及多态 。原始的C结构经过改造,成了面向对象世界的基石——类。除了成员变量外,C++类还可以封装成员函数和其他东西。然而,有趣的是,除非 为了实现虚函数和虚继承引入的隐藏成员变量外,C++类实例的大小完全取决于一个类及其基类的成员变量!成员函数基本上不影响类实例的大小。

这里提供的B是一个C结构,然而,该结构有一些C++特征:控制成员可见性的“public/protected/private”关键字、成员函数、静态成员,以及嵌套的类型声明。
实际上,只有成员变量才占用类实例的空间 。
( 在VC++中,成员变量总是按照声明时的顺序排列)。

2.3 单继承

C++ 提供继承的目的是在不同的类型之间提取共性。

既然派生类要保留基类的所有属性和行为,自然地,每个派生类的实例都包含了一份完整的基类实例数据。在D中,并不是说基类C的数据一定要放在D的数据之前,只不过这样放的话,能够保证D中的C对象地址,恰好是D对象地址的第一个字节。这种安排之下,有了派生类D的指针,要获得基类C的指针,就不必要计算偏移量 了。
在单继承类层次下,每一个新的派生类都简单地把自己的成员变量添加到基类的成员变量之后 。

2.4 多重继承

结构F从C和E多重继承得来。与单继承相同的是,F实例拷贝了每个基类的所有数据。 与单继承不同的是,在多重继承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同;

观察类布局,可以看到F中内嵌的E对象,其指针与F指针并不相同。

VC++ 按照基类的声明顺序 先排列基类实例数据,最后才排列派生类数据。 当然,派生类数据本身也是按照声明顺序布局的(本规则并非一成不变 ,我们会看到,当一些基类有虚函数而另一些基类没有时,内存布局并非如此)。

2.5 虚继承

虚继承的语法很简单,在指定基类时加上virtual关键字即可。

使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。
 
在G对象中,内嵌的C基类对象的数据紧跟在G的数据之后,在H对象中,内嵌的C基类对象的数据也紧跟在H的数据之后。但是, 在I对象中,内存布局就并非如此了。VC++实现的内存布局中,G对象实例中G对象和C对象之间的偏移,不同于I对象实例中G对象和C对象之间的偏移。当 使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类型计算偏移,而必须找到另一种间接的方法,从 派生类指针计算虚基类的位置。 
在VC++ 中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr) 成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类 而言,“虚基类表指针”与虚基类之间的偏移量。

可以得到如下关于VC++虚继承下内存布局的结论:
1 首先排列非虚继承的基类实例;
2 有虚基类时,为每个基类增加一个隐藏的vbptr,除非已经从非虚继承的类那里继承了一个vbptr;
3 排列派生类的新数据成员;
4 在实例最后,排列每个虚基类的一个实例。

3 成员变量

介绍了类布局之后,我们接着考虑对不同的继承方式,访问成员变量的开销究竟如何。

没有继承: 没有任何继承关系时,访问成员变量和C语言的情况完全一样:从指向对象的指针,考虑一定的偏移量即可。

a. 当访问基类成员c1时,计算步骤本来应该为“pd+dDC+dCc1”,即为先计算D对象和C对象之间的偏移,再在此基础上加上C对象指针与成员变量c1 之间的偏移量。
b. 当访问派生类成员d1时,直接计算偏移量。

多重继承 :虽然派生类与某个基类之间的偏移量可能不为0,然而,该偏移量总是一个常数。只要是个常数,访问成员变量,计算成员变量偏移时的计算就可以被简化。可见即使对于多重继承来说,访问成员变量开销仍然不大。

F继承自C和E,pf是指向F对象的指针。
a. 访问C类成员c1时,F对象与内嵌C对象的相对偏移为0,可以直接计算F和c1的偏移;
b. 访问E类成员e1时,F对象与内嵌E对象的相对偏移是一个常数,F和e1之间的偏移计算也可以被简化;
c. 访问F自己的成员f1时,直接计算偏移量。

虚继承: 当类有虚基类时,访问非虚基类的成员仍然是计算固定偏移量的问题。然而,访问虚基类的成员变量,开销就增大了 , 因为必须经过如下步骤才能获得成员变量的地址:
1. 获取“虚基类表指针”;
2. 获取虚基类表中某一表项的内容;
3. 把内容中指出的偏移量加到“虚基类表指针”的地址上。

然而,事情并非永远如此。正如下面访问I对象的c1成员那样,如果不是通过指针访问,而是直接通过对象实例,则派生类的布局可以在编译期间静态获得,偏移量也可以在编译时计算,因此也就不必要根据虚基类表的表项来间接计算了。

4 强制转化

如果没有虚基类的问题,将一个指针强制转化为另一个类型的指针代价并不高昂。如果在要求转化的两个指针之间有“基类-派生类”关系,编译器只需要简单地在两者之间加上或者减去一个偏移量即可(并且该量还往往为0)。

5 成员函数

一个C++成员函数只是类范围内的又一个成员。X类每一个非静态的成员函数都会接受一个特殊的隐藏参数——this指针,类型为X* const。 该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访问是通过在后台计算与this指针的偏移来进行。
 
P有一个非虚成员函数pf(),以及一个虚成员函数pvf()。很明显,虚成员 函数造成对象实例占用更多内存空间,因为虚成员函数需要虚函数表指针。

5.1 覆盖成员函数

和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以覆盖,或者说替换掉基类的函数定义。覆盖是静态 (根据成员函数的静态类型在编译时决定)还是动态 (通过对象指针在运行时动态决定),依赖于成员函数是否被声明为“虚函数”。

对于非虚 的成员函数来说,调用哪个成员函数是在编译 时,根据“->”操作符左边指针表达式的类型静态决定 的。

VC++编译器把隐藏的vfptr成员变量放在P和Q实例的开始处。这就使虚函数的调用能够尽量快一些。实际上,VC++的实现方式是,保证任何有虚函数的类的第一项永远是vfptr。

5.2 多重继承下的虚函数

如果从多个有虚函数的基类继承,一个实例就有可能包含多个vfptr。

因为S从P和R多重继承,S的实例内嵌P和R的实例,以及S自身的数据成员S::s1。注意,在多重继承下,靠右的基类R,其实例的地址和P与S不同。 S::pvf覆盖了P::pvf()和R::pvf(),S::rvf()覆盖了R::rvf()。

在微软VC++实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。

5.3 地址点与“逻辑this调整”

考虑下一个虚函数S::rvf(),该函数覆盖了R::rvf()。我们都知道S::rvf()必须有一个隐藏的S*类型的this参数。但是,因为也可以用R*来调用rvf(),也就是说,R的rvf虚函数槽可能以如下方式被用到:

当覆盖非最左边的基类的虚函数时,MSC++一般不创建调整块,也不增加额外的虚函数项。

5.4 调整块

正如已经描述的,有时需要调整块来调整this指针的值(this指针通常位于 栈上返回地址之下,或者在寄存器中),在this指针上加或减去一个常量偏移,再调用虚函数。某些实现(尤其是基于cfront的)并不使用调整块机制。 它们在每个虚函数表项中增加额外的偏移数据。每当虚函数被调用时,该偏移数据(通常为0),被加到对象的地址上,然后对象的地址再作为this指针传入。

5.5 虚继承下的虚函数

T虚继承P,覆盖P的虚成员函数,声明了新的虚函数。如果采用在基类虚函数表末尾添加新项的方式,则访问虚函数总要求访问虚基类。在VC++中,为了避免获取虚函数表时,转换到虚基类P的高昂代价,T中的新虚函数通过一个新的虚函数表获取 ,从而带来了一个新的虚函数表指针。该指针放在T实例的顶端。

5.6 特殊成员函数

本节讨论编译器合成到特殊成员函数中的隐藏代码。

5.6.1 构造函数和析构函数

在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下操作:
1 * 如果是“最终派生类”,初始化vbptr成员变量,调用虚基类的构造函数;
2 * 调用非虚基类的构造函数
3 * 调用成员变量的构造函数
4 * 初始化虚函数表成员变量
5 * 执行构造函数体中,程序所定义的其他初始化代码

(注意:一个“最终派生类”的实例,一定不是嵌套在其他派生类实例中的基类实例)
所以,如果你有一个包含虚函数的很深的继承层次,即使该继承层次由单继承构成,对象的构造可能也需要很多针对虚函数表的初始化。
反之,析构函数必须按照与构造时严格相反的顺序来“肢解”一个对象。
1 * 合成并初始化虚函数表成员变量
2 * 执行析构函数体中,程序定义的其他析构代码
3 * 调用成员变量的析构函数(按照相反的顺序)
4 * 调用直接非虚基类的析构函数(按照相反的顺序)
5 * 如果是“最终派生类”,调用虚基类的析构函数(按照相反顺序)

在VC++中,有虚基类的类的构造函数接受一个隐藏的“最终派生类 标志”,标示虚基类是否需要初始化。对于析构函数,VC++采用“分层析构模型”,代码中加入一个隐藏的析构函数,该函数被用于析构包含虚基类的类(对于 “最终派生类”实例而言);代码中再加入另一个析构函数,析构不包含虚基类的类。前一个析构函数调用后一个。

5.6.2 虚析构函数与delete操作符

假如A是B的父类,  
A* p = new B();  
如果析构函数不是虚拟的,那么,你后面就必须这样才能安全的删除这个指针:  
delete (B*)p;  
但如果构造函数是虚拟的,就可以在运行时动态绑定到B类的析构函数,直接:  
delete p;  
就可以了。这就是虚析构函数的作用。
实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。

VC++扩展了其“分层析构模型”,从而自动创建另一个隐藏的析构帮助函数——“deleting析构函数”,然后,用该函数的地址来替 换虚函数表中“实际”虚析构函数的地址。析构帮助函数调用对该类合适的析构函数,然后为该类有选择性地调用合适的delete操作符。

6 数组

堆上分配空间的数组使虚析构函数进一步复杂化。问题变复杂的原因有两个:
1、 堆上分配空间的数组,由于数组可大可小,所以,数组大小值应该和数组一起保存。因此,堆上分配空间的数组会分配额外的空间来存储数组元素的个数;
2、 当数组被删除时,数组中每个元素都要被正确地释放,即使当数组大小不确定时也必须成功完成该操作。然而,派生类可能比基类占用更多的内存空间,从而使正确释放比较困难。

虽 然从严格意义上来说,数组delete的多态行为C++标准并未定义,然而,微软有一些客户要求实现该行为。因此,在MSC++中,该行为是用另一个编译 器生成的虚析构帮助函数来完成。该函数被称为“向量delete析构函数”(因其针对特定的类定制,比如WW,所以,它能够遍历数组的每个元素,调用对每 个元素适用的析构函数)。

7 异常处理

因 为C++是面向对象的语言,很自然地,C++中用对象来表达异常状态。并且,使用何种异常处理也是基于“抛出的”异常对象的静态或动态类型来决定的。不光 如此,既然C++总是保证超出范围的对象能够被正确地销毁,异常实现也必须保证当控制从异常抛出点转换到异常“捕获”点时(栈展开),超出范围的对象能够 被自动、正确地销毁。

谈到异常处理的具体实现方式,一般情况下,在抛出点和捕 获点都使用“表”来表述能够捕获异常对象的类型;并且,实现要保证能够在特定的捕获点真正捕获特定的异常对象;一般地,还要运用抛出的对象来初始化捕获语 句的“实参”。通过合理地选择编码方案,可以保证这些表格不会占用过多的内存空间。

所有这些表,函数调用的准备和善后工作,状态变量的更新,都会使异常处理功能造成可观的内存空间和运行速度开销。正如我们所见,即使在没有使用异常处理的函数中,该开销也会发生。
幸运的是,一些编译器可以提供编译选项,关闭异常处理机制。那些不需要异常处理机制的代码,就可以避免这些额外的开销了。

C++对象内存模型学习相关推荐

  1. C++对象模型4——多重继承的对象内存模型、vptr与vtbl的创建与重置的时机、不要在含有虚函数的类的构造函数中调用memset

    一.多重继承的对象内存模型 class Base1 { public:virtual void f() {cout << "base1::f()" << e ...

  2. Swift 对象内存模型探究(一)

    MemoryLayout 基本使用方法 HandyJSON 是 Swift 处理 JSON 数据的开源库之一,类似 JOSNModel,它可以直接将 JSON 数据转化为类实例在代码中使用. 由于 S ...

  3. python对象内存模型

    原文:Python的垃圾回收机制(二)之内存模型 1. Python对象内存模型 首先介绍一下Python对象的内存模型,如下图1所示: 图1. PyObject对象内存模型 上图可以看到,一个PyO ...

  4. JVM内存模型学习(入门)

    新手Java开发,探究比较浅,只是记录学习, 如果对您有帮助,荣幸之至.如有大佬,请指教 JVM内存模型 JVM运行机制 什么是内存模型 程序计数器(线程私有) Java虚拟机栈(线程私有) 本地方法 ...

  5. 解释 i = i++为什么等于本身的问题——JVM内存模型学习笔记

    前段时间在扫题的时遇到类似以下的例子: public class Test {public static void main(String [] args) {int i = 1 ;//int j = ...

  6. JMM内存模型学习一

    概念 java内存模型(Java Memory Model)简称JMM,是一种抽象概念,通过它定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.JMM定义了线程和主内存之间 ...

  7. java 内存模型 ——学习笔记

    一.Java 内存模型 java内存模型把 Java 虚拟机内部划分为线程栈和堆 下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上.      ==>>  一个局部变量可能是 ...

  8. java对象内存模型_Java对象的内存模型

    众所周知,函数调用在内存中是通过压栈,退栈实现的,而Java的方法调用则是在JVM栈中通过栈帧实现的,且所有的Java对象都只在堆上分配内存.那么一个Java对象在堆内存里到底长啥样呢?实际上,当一个 ...

  9. java内存模型---学习笔记

    主内存和工作内存 Java内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存. 线程.主内存.工作内存三者的交互关系: 内存间交互操作: lock(锁定):作用于主内存的变量,把一个变 ...

最新文章

  1. css rem 大屏开发_px和em、rem单元如何选择?
  2. Eclipse Neon 配置C/C++开发环境
  3. How does framework require TechnicalInfo.js
  4. Asp.Net Core Mvc Razor之RazorPage
  5. android 获取当前网络,Android 获取当前网络连接的类型信息
  6. 后端开发必备的 MySQL 日志文件知识点
  7. java数字高低呼唤_记录某电话交换台 5 分钟内接到的呼唤次数,则样本空间 S={0,1,2, … ,n, … }_Java答案_学小易找答案...
  8. 《MFC游戏开发》笔记八 游戏特效的实现(二):粒子系统
  9. springboot+mybatis+druid 多数据源整合
  10. 虚拟偶像发展史:TA们到底如何成功?万字复盘
  11. php gps坐标转换经纬度,GPS坐标(经纬度转换xy坐标算法)
  12. html中写模态框,Html 模态框操作
  13. 计算机二级vb考试教材,计算机二级考试VB基础题教材.doc
  14. No7.线程常用操作方法
  15. 【gitLab】gitLab新建项目步骤
  16. 微软今天发布的紧急安全公告 MS08-067
  17. Java、JSP网上教务评教管理系统(教学评价系统)
  18. springBoot的过滤器,监听器,拦截器
  19. 【模电实验】【超值1 + 1】【验证性实验——比例、求和运算电路实验】【验证性实验——各种非正弦信号发生器实验】
  20. Vue使用echarts图表实现k线图(分K,日K,周K,月K)

热门文章

  1. python类成员_Python的类成员和对象成员
  2. 手机型号大全资料_电子元器件知识资料大全
  3. Ribbon 负载均衡调用01——概述
  4. Lookup Relationship与Master-Detail Relationship的关系 || formula的特性|| picklist在代码编写时的注意事项
  5. Java 技术篇-IntelliJ IDEA 导入数据库驱动jar包实例演示
  6. nefu 628 扩展卢卡斯
  7. 字符流中第一个不重复的字符
  8. t1plus 用什么服务器系统,T1 Plus商贸宝普及版与用友T1系统哪个更好呢?
  9. VK1629A 驱动芯片
  10. domain logic approaches