一个class的data members,通常而言,能够表现这个class在程序执行时的某种状态。Nonstatic data members放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据。ios
C++对象模型尽可能以空间优化和存取速度优化的考虑来表现nonstatic data members,而且保持和C语言struct数据配置的兼容性。它们把数据直接存放在每个class object之中。对于继承而来的nonstatic data members(无论是virtual仍是nonvirtual base class)也是如此。不过没有强制定义其间的排列顺序。
至于static data members,则被放置在程序的一个global data segment中,不会影响个别class object的大小。在程序之中,无论该class被产生出多少个objects(经由直接产生或间接派生),static data members永远只存在一份实例(甚至即便该class没有任何object实例,其static data members也已存在)。可是一个template class的static data members的行为稍有不一样。程序员
C++ Standard以“member scope resolution rules”来精炼这个“rewriting rule”,其效果是,若是一个inline函数在class声明以后当即被定义的话,那么就仍是对齐评估求值(evaluae)。也就是说,当一我的写下这样的代码:算法
extern int x; class Point3d{ public: //对于函数自己的分析将延迟直至class声明的右大括号出现才开始 float X() const { return x; } //... private: float x; }; //事实上,分析在这里运行
时,对于member functions自己的分析,会直到整个class的声明都出现了才开始。
所以,在一个inline member function躯体以内的一个data member绑定操做,会在整个class声明以后才发生。ide
已知下面一组data members:函数
class Point3d{ public: //... private: float x; static List<Point3d*> *freeList; float y; static const int chunkSize = 250; float z; };
nonstatic data members在class object中的排列顺序和其被声明的顺序同样,任何中间介入的static data members,如freeList和chunkSize都不会被放进对象布局之中。在上述例子中,每个Point3d对象是由三个float组成,次序是x,y,z。static data members存放在程序的data segment中,和个别的class objects无关。工具
C++ Standard要求,在同一个access section(也就是private、public、protected等区段)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件便可。也就是说,各个members并不必定连续排列。什么东西可能会介于被声明的members之间呢?members的边界调整(alignment)可能就须要填补一些bytes。对于C和C++ 而言这的确是真的,对目前的C++编译器实现状况而言,这也是真的。布局
编译器还可能会合成一些内部使用的data members,以支持整个对象模型,vptr就是这样的东西,当前全部的编译器都把它安插在每个“内含virtual function之class”的object内。测试
static data members,按其字面意义,被编译器提出于class以外,并被视为一个global变量(但只在class生命范围内可见)。每个member的存取许可(译注:private、protected或public),以及与class的关联,并不会招致任何空间上或执行时间上的额外负担——不管是在个别的class objects仍是在static data member自己优化
每个static data member只有一个实例,存放在程序的data segment之中。每次程序参阅(取用)static member时,就会被内部转化为对该惟一extern实例的直接参考操做。例如:this
Point3d origin, *pt; //origin.chunkSize = 250 Point3d::chunkSize = 250; //pt->chunkSize = 250 Point3d::chunkSize = 250;
从指令执行的观点来看,这是C++语言中“经过一个指针和经过一个对象来存取member,结论完成相同”的惟一一种状况,这是由于“经由member selection operators(也就是'.'运算符)对一个static data member进行存取操做”只是语法上的一种便宜行事而已,member其实并不在class object之中,所以存取static membeers并不须要经过class object。
若取一个static data member的地址,会获得一个指向其数据类型的指针,而不是一个指向其class member的指针,由于static member并不内含在一个class object之中。例如:
&Point3d::chunkSize;
会得到类型以下的内存地址:
const int*
若是有两个classes,每个都声明了一个static member freeList,那么当它们都被放在程序的data segment时,就会致使名称冲突。编译器的解决方法是暗中对每个static data member编码(对于这种手法有个很美的名称:name-mangling),以得到一个独一无的程序识别代码。有多少个编译器,就有多少中name-manglint作法。任何name-mangling作法都有两个要点:
Nonstatic data members直接存放在每个class object之中。除非经由显式的(explicit)或隐式的(implicit)class object,不然没有办法直接存取它们。只要程序员在一个member function中直接处理一个nonstatic data member,所谓“implicit class object”就会发生。例如:
Point3d Point3d::translate(const Point3d &pt){ x += pt.x; y += pt.y; z += pt.z; }
表面上所看到的对于x,y,z的直接存取,事实上是经由一个"implicit class object"(由this指针表达)完成,实际上这个函数的参数是:
//member function的内部转化 Point3d Point3d::translate(Point3d *const this, const Point3d &pt){ this->x += pt.x; this->y += pt.y; this->z += pt.z; }
欲对一个nonstatic data member进行存取操做,编译器须要把class object的起始地址加上data member的偏移位置(offset)。若是:
origin._y = 0.0;
那么地址&origin._y
将等于:
&origin + (&Point3d::_y - 1);
请注意其中的 -1 操做。指向data member的指针,其offset值老是被加上 1,这样可使编译系统区分出“一个指向data member的指针,用以指出class的第一个member”和“一个指向data member的指针,没有指出任何member”两种状况。
每个nonstatic data member的偏移位置(offset)在编译时期便可获知,甚至若是member属于一个base class subobject(派生自单一或多重继承串链)也是同样的。所以,存取一个nonstatic data member,其效率和存取一个C struct member或一个nonderived class的member是同样的。
在C++ 继承模型中,一个derived class object所表现出来的东西,是其本身的members加上其base class members的总和。至于derived class members和base class members的排列顺序,则未在C++ standard中强制指定:理论上编译器能够自由安排之。在大部分编译器上头,base class members老是先出现,但属于virtual base class的除外。(通常而言,任何一条通则一旦碰上virtual base class就没有辙了,这里亦不例外)
通常而言,具体继承(concrete inheritance,译注:相对于虚拟继承virtual inheritance)并不会增长空间或存取时间上的额外负担。
class Point2d{ public: Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) {} float x() { return _x; } float y() { return _y; } void x(float newX) { _x = newX; } void y(float newY) { _y = newY; } void operator+=(const Point2d& rhs){ _x += rhs.x(); _y += rhs.y(); } //... more members protected: float _x, _y; }; //inheritance from concrete class class Point3d : public Point2d{ public: Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : Point2d(x, y), _z(z) { }; float z() { return _z; } void z(float newZ){ _z = newZ; } void operator+=(const Point3d& rhs){ Point2d::operator(rhs); _z += rhs.z(); } protected: float _z; };
这样设计的好处就是能够把管理x和y坐标的程序代码局部化。此外这个设计明显表现出两个抽象类之间的紧密关系。当这两个classes独立的时候,Point2d object和Point3d object的声明和使用都不会有所改变,因此这两个抽象类的使用者不须要知道objects是否为独立的classes类型,或是彼此之间有继承的关系。
把两个本来不想干的classes凑出一对“type/subtype”,并带有继承关系,会有什么易犯的错误呢?经验不足的人可能会重复设计一些相同操做的函数。第二个易犯的错误是,把一个class分解为两层或更多层,有可能会为了“表现class体系之抽象化”而膨胀所需空间。C++ 语言保证“出如今derived class中的base class subobject有其完整原样性”。举例以下:
class Concrete{ public: //... private: int val; char c1; char c2; char c3; };
在一部32位机器中,每个Concrete class object的大小都是8bytes,细分以下:
如今假设,通过某些分析以后,咱们决定了一个更逻辑的表达方式,把Concrete分裂成三层结构:
class Concrete1{ public: //.. private: int val; char bit1; }; class Concrete2 : public Concrete1{ public: //... private: char bit2; }; class Concrete3 : public Concrete2{ public: //... private: char bit3; };
从设计的观点来看,这个结构可能更合理。可是从效率的观点来看,咱们可能会受困于一个事实:如今Concrete3 object的大小是16bytes,比原先的设计多了一倍。
怎么回事?还记得“base class subobject在derived class中的原样性”吗?
Concrete1内含两个members: val和bit1, 加起来是5bytes。而一个Concrete1 object实际用掉8bytes,包括填补用的3bytes,以使object可以符合一个机器的word边界。Concrete2加了惟一一个nonstatic data member bit2,数据类型为char,轻率的程序员会认为它会和Concrete1捆绑在一块儿,占用本来用来填补的1bytes。然而Concrete2的bit2实际上倒是被放在填补的3bytes以后,因而大小变成12bytes,而不是8bytes。其中有6bytes浪费在填补空间上。相同的道理是Concrete3 object的大小是16bytes,其中9bytes用于填补空间。
声明以下:
Concrete2 *pc2; Concrete1 *pc1_1, *pc1_2;
其中pc1_1
和pc1_2
二者均可以指向前述三种class objects。下面这个指定操做:
*pc1_2 = *pc1_1;
应该执行一个默认“memberwise”复制操做(复制一个个的members),对象是被指的object的Concrete1那一部分。若是pc1_1实际指向一个Concrete2 object或Concrete3 object,则上述操做应该将复制内容指定给其Concrete1 subobject。
然而,若是C++ 语言把derived class members(也就是Concrete2::bit2 或Concrete3::bit3)和Concrete subobject捆绑在一块儿,去除填补空间,上面那些语意就没法保留了,以下:
pc1_1 = pc2; //令pc1_1指向Concrete2对象 //derived class subobject被覆盖掉,因而bit2 member现有一个并不是预期的数值 *pc1_2 = *pc1_1;
class Point2d{ public: Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) { }; //x和y的存取函数与前一版相同 //因为对不一样维度的点,这些函数的操做固定不变,因此没必要设计为virtual //加上z的保留空间(当前什么也不作) virtual float z() { return 0.0; } virtual void z(float){ }; //设定如下的运算符为virtual virtual void operator+=(const Point2d& rhs){ _x += rhs.x(); _y += rhs.y(); } protected: float _x, _y; };
这样的设计,给Point2d class带来空间和存储时间的额外负担:
单一继承提供了一种“天然多态(natural polymorphism)”形式,是关于classed体系中的base type和derived type之间的转换。
多重继承既不像单一继承,也不容易模塑出其模型。多重继承的复杂度在于derived class和其上一个base class乃至于上上一个base class....之间的“非天然”关系。
考虑下面这个多重继承所得到的class Vertex3d
class Point2d{ public: //...拥有virtual接口,因此,Point2d对象中会有vptr protected: float _x, _y; }; class Point3d : public Point2d{ public: //... protected: float _z; }; class Vertex{ public: //... 拥有virtual接口,因此Vertex对象之中会有vptr protected: Vertex *next; }; class Vertex3d : public Point3d, public Vertex{ public: //... protected: float mumble; };
多重继承的问题主要发生于derived class objects和其第二或后继的base class object之间的转换。不管是直接转换以下:
extern void mumble(const Vertex&); Vertex3d v; ... //将一个Vertex3d转换为一个Vertex,这是“不天然的” mumble(v);
或是经由其所支持的virtual function机制作转换。
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class的指针”,状况将和单一继承时相同,由于两者都指向相同的起始地址。需付出的成本只有地址的指定操做而已。至于第二个或后继的base class的地址指定操做,则须要将地址修改过:加上(或减去,若是downcast的话)介于中间的base class subobjects大小。例如:
Vertex3d v3d; Vertex *pv; Point2d *p2d; Point3d *p3d;
通过下面这个指定操做:
pv = &v3d;
须要这样的内部转化:
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
而下面的指定操做:
p2d = &v3d; p3d = &v3d;
都只须要简单地拷贝其地址就好了。若是有两个指针以下:
Vertex3d *pv3d; Vertex *pv;
那么下面的指定操做:
pv = pv3d;
不可以只是简单地被转换为:
pv = (Vertex*)((char*)pv3d) + sizeof(Point3d);
由于若是pv3d为0,pv将得到sizeof(Point3d)的值,这是错误的。因此,对于指针,内部转换操做须要一个条件测试:
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0 ;
至于reference,则不须要针对可能的0值作防卫,由于reference不可能参考到“无物”
若是要存取第二个(或后继)base class中的一个data member会是怎样的状况?须要付出额外的成本吗? 不,members的位置在编译期就固定了,所以,存取members只是一个简单的offset运算,就像单一继承同样简单——无论是经由一个指针,一个reference或是一个object来存取。
多重继承的一个语意上的反作用就是,它必须支持某种形式的“shared subobject继承”。一个典型的例子就是最先的iostream library:
不管是istream或ostream都内含一个ios subobject,然而在iostream的对象布局中,咱们只须要一份ios subobject就好。语言层面的解决办法是导入所谓的虚拟继承。
通常的实现方法以下所述:Class若是内含一个或多个virtual base class subobjects,像istream那样,将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,无论后继如何衍化,老是拥有固定的offset(从object的开头算起),因此这一部分数据能够被直接存取。至于共享局部,所表现的就是virtual base class subobject。这一部分的数据,其位置会由于每次的派生操做而有变化,因此它们只能够被间接存取。各家编译器实现技术之间的差别就在于间接存取的方法不一样。
如下说明三种主流策略,下面是Vertex3d虚拟继承的层次结构:
通常的布局策略是先安排好derived class的不变部分,而后再创建其共享部分。
如何可以存取class的共享部分呢?
cfont编译器会在每个derived class object中安插一些指针,每一个指针指向一个virtual base class。要存取继承得来的virtual base class members,可使用相关指针间接完成。举例以下:
void Point3d::operator+=(const Point3d &rhs){ _x += rhs._x; _y += rhs._y; _z += rhs._z; }
在cfront策略下,这个运算会被内部转换为:
_vbcPoint2d->_x += rhs._vbcPoint2d->_x; // vbc意指virtual base class _vbcPoint2d->_y += rhs._vbcPoint2d->_y; _z += rhs._z;
而一个derived class和一个base class的实例之间的转换,如
Point2d *p2d = pv3d;
在cfront实现模型下,会变成:
Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;
这样的实现模型有两个主要的缺点:
MetaWare和其余编译器使用cfront的原始模型来解决第二个问题,它们经由拷贝操做去的全部的nested virtual base class指针,放到derived class object中,这就解决了"固定存储时间"的问题。虽然付出了一些空间上的代价。下图说明了这种“以指针指向base class”的实现模型。
对于第一个问题,通常有两个解决办法。Microsoft编译器引入所谓的virtual base class table。每个class object若是有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table。至于真正的virtual base class指针,固然是被放在表格中。
第二个解决办法是在virtual function table中放置virtual base class的offset。下图显示了base class offset实现模型。
在新近的Sun编译器中,virtual functon table可经由正值或负值来索引,若是是正值,很显然就是索引到virtual function;若为负值,则是索引到virtual base class offsets。在这样的策略下,Point3d的operator+=运算符必须被转换为如下形式:
(this + _vptr_Point3d[-1])->_x += (&rhs + rhs._vptr_Point3d[-1])->_x; (this + _vptr_Point3d[-1])->_y += (&rhs + rhs._vptr_Point3d[-1])->_y; _z += rhs._z;
上述的每一种方法都是一种实现模型,而不是一种标准,每一种模型都是用来解决“存取shared subobject内的数据(其位置会由于每次派生操做而有变化)”所引起的问题。因为对virtual base class的支持带来额外的负担以及高度的复杂性,每一种模型多少有点不一样,并且还会随着时间而进化。
通常而言,virtual base class最有效的一种运用形式就是:一个抽象的virtual base class,没有任何data members
考虑下面的Point3d声明,其中有一个virtual function, 一个static data member,以及三个坐标值:
class Point3d{ public: virtual ~Point3d(); //... protected: static Point3d origin; float x, y, z; };
取某个坐标成员的地址,表明什么意思? 以下:
&Point3d::z;
上述操做将获得z坐标的class object中的偏移量(offset),最低限度其值将是x和y的大小总和,觉得C++ 语言要求同一个access level中的members的排列次序应该和其声明次序相同。
然而vptr的位置就没有限制,实际上vptr不是放在对象的头部,就是放在对象的尾部。每个float是4bytes,因此咱们应该指望刚才得到的值要不是8,就是12(在32位机器上一个vptr是4bytes)
然而,这样的指望还少1bytes。
若是vptr放在对象的尾端,则三个坐标值在对象布局中的offset分别是0,4,8。若是vptr放在对象的起头,则三个坐标值在对象布局中的offset分别是4,8,12。然而你若去取data members的地址,传回的值老是多1, 也就是1,5,9或9,5,13等等。
如何区分一个“没有指向任何data member”的指针,和一个指向“第一个data member”的指针?考虑这样的例子:
float Point3d::*p1 = 0; float Point3d::*p2 = &Point3d::x; //Point3d::*的意思是:“指向Point3d data member”的指针类型 //如何区分 if(p1 == p2){ cout << "p1 & p2 contain the same value --" ; cout << " they must address the same member!" << endl; }
为了区分p1和p2, 每个真正的member offset值都被加上1。所以,不论编译器或使用者都必须记住,在真正使用该值以指出一个member以前,请先减掉1
认识“指向data members的指针”以后,咱们发现,要解释:
&Point3d::z; &origin.z;
之间的差别,就很是明确了。鉴于“取一个nonstatic data member的地址,将会获得它在class中的offset”,取一个“绑定于真正class object身上的data member”的地址,将会获得该member在内存中的真正地址。把
&origin.z
所得结果减z的偏移量(相对于origin的起始地址),并加1,就会获得origin的起始地址。上一行的返回值类型应该是float*
,而不是float Point3d::*
。
因为上述操做所参考的是一个特定实例,因此取一个static data member的地址,意义也相同。
在多重继承中,若要将第二个(或后继)base class的指针,和一个“与derived class object绑定”的member结合起来,那么将会由于“须要加入offset值”而变得至关复杂。例如:
struct Base1{ int val1; } struct Base2{ int val2; } struct Derived : Base1, Base2{ ... } void fun1(int Derived::*dmp, Derived *pd){ //指望第一个参数获得一个“指向derived class之member”的指针 //若是传进来的倒是一个"指向base class之member"的指针,会怎样 pd->*dmp; } void fun2(Derived *pd){ //bmp将成为1 int Base2::*bmp = &Base2::val2; //bmp = 1 //可是在Derived中,val2 = 5 fun1(bmp, pd); }
当bmp被做为fun1()的第一个参数时,它的值就必须因介入的Base1 class的大小而调整,不然fun1()中这样的操做:
pd->*dmp;
将存取Base1::val1,而非程序员因此为的Base2::val2。要解决这个问题,必须
//经由编译器内部转换 fun1(bmp + sizeof(Base1), pd);
然而,通常而言,咱们不能保证bmp不是0,所以必须特别留意之:
//内部转换 //防范bmp == 0 fun1(bmp ? bmp + sizeof(Base1) : 0, pd);