C++继承中的内存布局

今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的。看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用。

——谈VC++对象模型
(美)简.格雷
程化    译

译者前言

一个C++程序员,想要进一步提升技术水平的话,应该多了解一些语言的语意细节。对于使用VC++的程序员来说,还应该了解一些VC++对于C++的诠释。 Inside the C++ Object Model虽然是一本好书,然而,书的篇幅多一些,又和具体的VC++关系小一些。因此,从篇幅和内容来看,译者认为本文是深入理解C++对象模型比较好的一个出发点。
这篇文章以前看到时就觉得很好,旧文重读,感觉理解得更多一些了,于是产生了翻译出来,与大家共享的想法。虽然文章不长,但时间有限,又若干次在翻译时打盹睡着,拖拖拉拉用了小一个月。
一方面因本人水平所限,另一方面因翻译时经常打盹,错误之处恐怕不少,欢迎大家批评指正。

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

1 前言

了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义。首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。

本文着重回答这样一些问题:
1* 类如何布局?
2* 成员变量如何访问?
3* 成员函数如何访问?
4* 所谓的“调整块”(adjuster thunk)是怎么回事?
5* 使用如下机制时,开销如何:
  * 单继承、多重继承、虚继承
  * 虚函数调用
  * 强制转换到基类,或者强制转换到虚基类
  * 异常处理
首先,我们顺次考察C兼容的结构(struct)的布局,单继承,多重继承,以及虚继承;
接着,我们讲成员变量和成员函数的访问,当然,这里面包含虚函数的情况;
再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是如何动态构造和销毁的;
最后,简单地介绍对异常处理的支持。

对每个语言特性,我们将简要介绍该特性背后的动机,该特性自身的语意(当然,本文决不是“C++入门”,大家对此要有充分认识),以及该特性在微软的 VC++中是如何实现的。这里要注意区分抽象的C++语言语意其特定实现微软之外的其他C++厂商可能提供一个完全不同的实现,我们偶尔也会将 VC++的实现与其他实现进行比较

2 类布局

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

2.1 C结构(struct)

由于C++基于C,所以C++也“基本上”兼容C。特别地,C++规范在“结构”上使用了和C相同的,简单的内存布局原则成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐所有的C/C++厂商都保证他们的C/C++编译器对于有效的C结构采用完全相同的布局。这里,A是一个简单的C结构,其成员布局和对齐方式都一目了然

1 struct A
2     {
3         char c;
4         int a;
5     };

译者注:从上图可见,A在内存中占有8个字节,按照声明成员的顺序,前4个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐),后4个字节包含一个整数。A的指针就指向字符开始字节处

2.2 有C++特征的C结构

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

这里提供的B是一个C结构,然而,该结构有一些C++特征:控制成员可见性的“public/protected/private”关键字、成员函数、静态成员以及嵌套的类型声明。虽然看着琳琅满目,实际上,只有成员变量才占用类实例的空间。要注意的是,C++标准委员会不限制由“public/protected/private”关键字分开的各段在实现时的先后顺序,因此,不同的编译器实现的内存布局可能并不相同( 在VC++中,成员变量总是按照声明时的顺序排列)。

 1 struct B
 2     {
 3 
 4     public:
 5         int bm1;
 6     protected:
 7         int bm2;
 8     private:
 9         int bm3;
10         static int bsm;
11 
12     void bf();
13     void static bsf();
14     typedef void* bpf;
15         
16     };

译者注:B中,为何static int bsm不占用内存空间?因为它是静态成员,该数据存放在程序的数据段,不在类实例中

-------------------------------------------------------------------------------------------------------------------------------------
下面说一下自己上课学到的东西:
从设计者的角度分析,为什么要引入一个静态数据成员和静态的成员函数?当时Stroustrup为什么要弄这两个东西呢?里面有没有配套的体系架构呢?

首先了解某个函数中的static变量,它是一个局部静态的变量,能在同一个函数不同调用间进行共享信息,做到了函数的作用域,全局的生存期

同时类的静态数据成员,起到的作用就是类的作用域,全局的生存期。类的静态数据成员,把原来全局变量的一些东西弄到类的里面(仅仅是形式上,具体的实现还是类似全局的样子)。这样就不会受到全局变量的拖累,有利于复用。否则你把自己的类给别人用,还必须带着个全局变量,就好比你去找工作,还要带着你“二大爷”,这样将会是十分的麻烦。

正如上面所说的,类的静态数据成员,仅仅是形式上的(有利于复用,你的类直接给别人用,不用拖拉着个全局变量),具体的实现还是类似全局的样子。类的静态数据成员,不属于任何一个实例化的类的对象,是所有的对象共享,在内存中就一份值。那么就带来了另一个问题:怎么找到这个类的静态数据成员?首先引入下面的概念:

类的每个普通成员函数(当然包括类的构造函数)都是带有this指针的,虽然你写成员函数的时候从来不写这个this指针,但是编译器会自动的在你的成员函数的参数列表中增添一个this指针。

this指针:指向当前对象的指针(当前是一个动态的,运行时的概念,谁当前处于激活的状态,this指针就指向它)。

 

 

上面的图示,就是编译器在你的每个普通的成员函数的参数列表中自动的加入的this指针,这个this指针的作用很大。因为同一个类的所有的对象共享一份放在代码区的代码,那么这一份代码怎么区分每个对象,从而操作正确的对象的数据呢?靠的就是this指针。例如:当有下面的语句时:a.f1(oneInt,twoInt);
在调用的时候,a就把自己的地址给调用函数的this指针,这样就使代码中的函数指向了a,从而准确的处理a的数据。

上面的分析就是说,类的成员函数都带this指针,能找到调用它的对象及该对象的数据成员,但是却找不到静态数据成员,因为他不属于某个对象。也就说不能用类的构造函数初始化静态数据成员。解决这个问题的方案就是:

弄一套配套的静态成员函数,专门负责处理类内的静态数据成员,这个静态的成员函数没有this指针(编译器不会帮你添加this指针)。


也就是说静态成员函数只能访问静态的数据成员,不能访问普通的数据成员,因为它没有this指针。
普通的成员函数只能访问普通的数据成员,不能访问静态的数据成员,因为它有this指针,而静态数据成员不属于某一个对象,所以this指针无法指向静态数据成员。

当然我们见过这样的引用静态的数据成员:
对象.静态数据成员;
但是后台的处理机制是:
类:静态数据成员;(建议这样用)

静态数据成员和静态成员函数在windows编程中的一个重要的应用:
在windows编程中,CallBack(回调)机制:你写好函数实现,等着别人来调用。
所以windows是主调函数负责形参压栈,但是os是不管你用什么语言实现的,也就是说他不会管你在成员函数中到底有没有加this指针,默认的是不会给你预留this指针的栈空间的。所以不能用普通的成员函数做线程函数(它带有this,会使主调函数开的栈空间很乱),只能用static成员函数,因为它没有this指针。因此线程函数(静态成员函数)所操作的数据理所当然的应当是静态数据成员。

作用域:对应源代码,这个东西是给人看的。
生存期:这个才是机器关注的。

-------------------------------------------------------------------------------------------------------------------------------------

2.3 单继承

C++提供继承的目的是在不同的类型之间提取共性。比如,科学家对物种进行分类,从而有种、属、纲等说法。有了这种层次结构,我们才可能将某些具备特定性质的东西归入到最合适的分类层次上,如“怀孩子的是哺乳动物”。由于这些属性可以被子类继承,所以,我们只要知道“鲸鱼、人”是哺乳动物,就可以方便地指出“鲸鱼、人都可以怀孩子”。那些特例,如鸭嘴兽(生蛋的哺乳动物),则要求我们对缺省的属性或行为进行覆盖(重写)。
C++中的继承语法很简单,在子类后加上“:base”就可以了。下面的D继承自基类C。

 

posted @ 2013-10-29 00:12  stemon  阅读(707)  评论(0编辑  收藏  举报