摘 要:本文主要探索了在虚拟继承和简单多重继承情形下,类内的成员数据,成员函数,基类等在派生类中的结构。
关键词:虚拟继承;多重继承;层次结构;内存布局
对于编写C++的程序员,一定多少使用过它语法中最基本也是最大的特点:继承与派生。那么一个类的产生会带来多大的空间开销,派生类和基类在数据、函数的内存结构上又有什么特点?
让我们从最简单的开始,一步一步展开我们的继承探索之旅。
一、空基类空成员虚拟继承
我们看如下代码:
class A{};
class B:public virtual A{};
class C:public virtual A{};
class D:public B,public C{};
图1-1 各个类层次结构
可以明显的看到,上述各个类都是空类,所有语句只表达了继承关系而不具备任何数据处理功能,没有成员数据,没有成员函数。那么对他们使用Sizeof 的结果应该都是0。
但是结果是这样的吗?
结果如下:
为什么会出现这样的结果呢?
其实空基类隐藏一个带有编号性质的1字节Char用与区分该类生成的不同实体:
A a1,a2;
if(&a1= =&a2) cout<<""the same address.""<
事实上,这个测试具有三方面的影响共同造成:
1. 为虚基类地址指针付出的代价 当语言支持虚拟继承就会增加负担。在派生类中,这个额外的负担是一个指针,他指向虚基类或是一个与地址相关的表格,表格中可能带有虚基类的地址或者一个虚基类与派生类的内存偏移量,作用就是找到他的虚基类。
2.编译器对于派生类的优化 虚基类的1字节区分也会出现在B与C类中的尾部
3.Alignment限制 以上B、C两类就应该各有了5字节,在大部分机器中,逻辑数据块会受到“对齐”限制,使他们按照整数大小对齐存取而填空,这里填充3字节(Alignment就是将数值调整到当前编译器整数字节的倍数以使存取通道获得最大效率)
结果是8字节的空派生类,为什么某些编译器产生出4字节的空派生类?比如这次试验的结果呢,这是编译器对于派生类的优化措施,他的内存分布如下:
编译器为了优化内存空间,将虚基类视作派生类的一个数据成员,是派生类开头的一部分,所以导致派生类并非空类型,而节约了1字节的空区分码和3字节的补齐。这就造成了空派生类只有4字节大小。
同样的,对于D类而言,不同的优化策略造成两种不同的分布:
编译器在众多不同的结构中产生多种分布结果,正说明了C++在对象模型中的进化过程,一个非优化系统被提出,为一般情况提供了解决方案,当特殊的模型和例子被提出,很多新的优化处理方式被引入编译器,最后导致一种新的标准诞生。比如C++中的虚函数列表也是一样的例子。
这个实验中,如果给基类增加任何一个成员数据,那么编译器优化与非优化会产生相同的结果与对象布局。
二.多重继承
多重继承的模型相对来说就复杂一些,涉及到成员数据与成员函数,以及多个基类在派生类中分布的问题。
下面这组类是描述物理中质点空间坐标与速度的类,当然,我们只抽象出我们需要的简单部分内容,其他的都忽略了。
class Position2D //平面位置矢量
{ public: //省略若干virtual接口,有虚函数列表指针(vptr)
Protected:
Float x,y; };
class Position3D:public Position2D //位置矢量
{ public: //省略若干virtual接口,有虚函数列表指针(vptr)
Protected:
Float z; };
class Direction2D //2D速度矢量
{ public://省略若干virtual接口,有虚函数列表指针(vptr)
Protected:
Direction2D * direct;};
class Direction3D:public Position3D,Public Direction2D //3D速度矢量
{ public: //省略若干接口,有虚函数列表指针(vptr)
protected:
float derect_z;};
对于这样一个类群,我们根据集成关系图来分析各个类的数据在内存中的分布(对象模型).
按照内部数据成员存储在类中,虚函数以及成员函数存储在函数列表中的原则,大致可以看出其内存中的存储分布:
这样的模型有什么作用呢?这主要是在解决数据向上向下转换时,告诉我们,非标准的操作什么样的能做,什么样的不能做,为什么不能这样做:
Extern void change( const Direction2D &);
Direction3D d;
..........
//将一个Direction3D 转换为一个Direction2D对象,不自然,但仍然可以操作
Change(d);
而且支持调用一切Direction2D的函数。
再比如:
Direction3D d3d;
Direction2D *p2d;
Position2D *p2p;
Position3D *p3p;
那么 p2d=&d3d 这个操作需要内部转换。
而 p2p=&d3d; p3p=&d3d; 都只需要直接拷贝地址就可以完成(重合的内部结构,看图2.2)。
这对于我们更好的编写程序,在指针中赋值带来了可调节的余地。
至此,对于虚拟继承与多重继承的各个类在内存中的结构,我们已经都有了大致的了解,这对于我们写程序时,能够利用最底层的特点,编写出富有效率的程序带来了实际的意义。限于篇幅的关系,这里就暂时不讨论多重继承与虚拟继承的混合模式及内联函数、友元等等内容了。
参考文献:
《C++标准程序库》(德)Nicolai M.Josuttis著2002华中科技大学出版社