Data 语义学
一个class的data members,一般而言,可以表现这个class在程序执行时的某种状态。Nonstatic data members放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据。
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的行为稍有不同。
Data Member的绑定(The Binding of a Data Member)
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声明之后才发生。
Data Member的布局(Data Member Layout)
已知下面一组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内。
Data Member的存取
Static Data Members
static data members,按其字面意义,被编译器提出于class之外,并被视为一个global变量(但只在class生命范围内可见)。每一个member的存取许可(译注:private、protected或public),以及与class的关联,并不会招致任何空间上或执行时间上的额外负担——不论是在个别的class objects还是在static data member本身
每一个static data member只有一个实例,存放在程序的data segment之中。每次程序参阅(取用)static member时,就会被内部转化为对该唯一extern实例的直接参考操作。例如:
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
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是一样的。
“继承”与Data 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就没有辙了,这里亦不例外)
只要继承不要多态(Inheritance without Polymorphism)
一般而言,具体继承(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,细分如下:
- val占用4bytes
- cl,c2,c3各占用1bytes
- alignment(调整到word边界)需要1bytes
现在假设,经过某些分析之后,我们决定了一个更逻辑的表达方式,把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;
加上多态(Adding Polymorphism)
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带来空间和存储时间的额外负担:
- 导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual functions的地址。这个table的元素数目一般而言是被声明的virtual functions的数目,再加上一个或两个slots(用以支持runtime type identification)
- 每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table.
- 加强constructor,使它能够为vptr设定初值,让它指向class所对应的virtual table.
- 加强destructor,使它能够抹消“指向class之相关virtual table”的vptr
多重继承(Multiple Inheritance)
单一继承提供了一种“自然多态(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来存取。
虚拟继承(Virtual Inheritance)
多重继承的一个语意上的副作用就是,它必须支持某种形式的“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;
这样的实现模型有两个主要的缺点:
- 每一个对象必须针对每一个virtual base class背负一个额外的指针。然而理想上我们希望class object有固定的负担,不因为其virtual base classes的数目而有所变化。
- 由于虚拟继承串链的加长,导致间接存取层次增加。比如,有三层虚拟衍化,就需要三次间接存取(经由三个virtual base class指针),然而理想上我们却希望有固定的存取时间,不因为虚拟衍化的深度而改变。
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
指向Data members的指针(Pointer to 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);
来源:https://www.cnblogs.com/lengender-12/p/6953351.html