- 3.1 Data Member的绑定(The Binding of a Data Member)
- 3.2 Data Member的布局(Data Member Layout)
- 3.3 Data Member的存取
- 3.4 “继承”于Data Member
- 3.5 对象成员的效率(Object Member Efficiency)
- 3.6 指向Data Members的指针(Poniter to Data Members)
- 一个空的class如:
1
2
3
4
5// sizeof X == 1
class X { }; // sizeof X == 1
class Y : public virtual X {}; // sizeof X == 8
class Z : public virtual X {}; // sizeof X == 8
class A : public Y, public Z {};// sizeof X == 12事实上并不是空的,它有一个隐藏的1byte大小,那是编译器安插进去的一个char。有机器上Y、Z得出大小是8。这个值的大小和机器有关,也和编译器有关:1
2
3
4
5graph BT
Y --- X
Z --- X
A --- Y
A --- Z- 语言本身所造成的额外负担(overhead)。当语言支持virtual base classes时,会导致额外负担。在derived中,反映在某种形式的指针身上,它或者指向virtual base class subobject,或指向一个相关表格;表格中是virtual base class subobject地址。
- 编译器对于特殊情况所提供的优化处理。
- Allgnment的限制。大部分机器结构体大小会收到alignment的限制(内存对齐),使它们能够更有效率在内存中被存取。alignment就是将数值调整到某数的整数倍。
- Empty virtual base class已经成为C++OO设计的一个特有术语了。他提供一个virtual interface,没有定义任何数据。
- 对于class A竟然大小为12这个结果。记住,一个virual base class sbobject只会在derived class中存在一份实例,不管它在继承体系中出现多少次。它的大小由以下决定:
- 被大家共享的唯一一个Class X,大小为1byte。
- Base class Y,减去因virtual base class X二配置的大小,结果是4bytes。
- class A自己的大小:0byte。
- class A的alignment数量。
- C++对象模型尽量以空间优化和存取速度优化的考虑来表现nonstatic data members,并且保持和C语言struct数据配置的兼容性。至于static data members,则被放置在程序的一个global data segment中,不会影响个别的class object大小。在程序之中,不管class被产生出多少个objects(经由直接产生或间接产生),static data members永远只存在一份实例。但是一个template class的static data members的行为稍有不同。
- 每一个class object必须有足够的大小容纳所有的nonstatic data members,因为它可能比你想象的还大,原因是:
- 编译器自动加上额外的data members,用以支持某些特性(virtual)。
- 因为alignment的需要。
3.1 Data Member的绑定(The Binding of a Data Member)
- 早期C++有两种防御性程序设计风格:
- 把所有data members放在class声明处,以确保正确的绑定:
1
2
3
4
5
6
7class Point3d
{
float x, y, z;
public:
float X() const { return x; }
// ...etc
}; - 把所有的inline functions,不管大小都放在class声明之外: 但这种设计风格在C++2.0之后就不存在了。这个古老的语言规则称为“member rwriting rule”。大概意思是“一个inline函数实体,在整个class声明未被完全看见之前,不会被评估求值(evaluated)的”。也就是说:
1
2
3
4
5
6
7
8
9
10
11
12
13class Point3d
{
public:
Point3d();
float X() const;
void X(float) const;
// ...etc
};
inline float Point3d::X() const
{
return x;
}因此一个inline member function躯体之内的data member绑定操作,会在整个class声明完成之后才发生。但这对member function的argument list并不是真的。argument list中的名称会在第一次遇到的时候被适当resolved完成。因此extern和nested type names之间的非直觉绑定操作还是会发生。1
2
3
4
5
6
7
8
9
10extern int x;
class Point3d
{
public:
// 对于函数本体的分析延迟,直到class声明右大括号出现才开始
float X() cons { return x; }
private:
float x;
};
// 分析在这里进行上述的语言状况,仍然需要某种防御性程序风格:请总是把“nested type 声明”放在class的起始处。这样做才能保证非直觉绑定的正确性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15typedef int length;
class Point3d
{
public:
// length is resolved for global
// _val is resolved for Point3d::_val
void mumble(length val) { _val = val; }
length mumble() { return _val; }
private:
// length必须在class对他第一个参考操作前被看见
// 这样的声明使之前操作不合法
typedef float length;
length _val;
// ...
};
- 把所有data members放在class声明处,以确保正确的绑定:
3.2 Data Member的布局(Data Member Layout)
- 已知一组data members: Nonsatic data members在class object中的排列顺序和其被声明的顺序一样。任何中介的static data members都不会放进对象布局之中。上述例子中,每个Point3d对象是由三个float组成的。static data members存放在程序的data segment中,和个别class objects无关。
1
2
3
4
5
6
7
8
9
10class Point3d {
public:
// ...
private:
float x;
staic List<Point3d*> *freeList;
float y;
static const int chunkSize 250;
float z;
};
C++ Standard要求,在同一个access section(private、public、protected等区段)中,members的排列只需要符合“较晚出现的members在class object中有较高的地址”这一条件就可以了。下面这个template funciton,接受两个data members,然后判断谁先出现在class object中。如果两个members都是不同的access sections中的第一个被声明者,函数就会判断哪个section先出现:1
2
3
4
5
6
7
8
9template<class class_type, class data_type1, class data_type2>
char* access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2)
{
assert (mem1 != mem2);
return mem1 < mem2 ? "Member 1 occurs first" : "member 2 occurs first";
}
// call
access_order(&Point3d::z, &Point3d::y);
3.3 Data Member的存取
- 已知这段代码: 通过origin存取和通过pt存取有什么重大差异吗?
1
2
3
4Point3d origin, *pt = &origin;
// 存取data members, like this:
origin.x = 0.0;
pt->x = 0.0;
Static Data Members
- Static data members,按字面意义,被编译器提出class之外,并被视为global变量。每个member的存取许可,以及class的关联,都不会招致任何空间上或执行时间上的额外负担。每个static data member只有一个实例。
- 但如果static data members是一个从复杂继承关系中继承而来的,它仍然只有唯一一个实例,其存取路径仍然那么直接。如果static data member经由函数调用,或其他某些语法存取呢?例如: 若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是指向其class member的指针,因为static member并不内含在一个class object之中:
1
2
3
4
5foobar().chunkSize = 250;
// 可能的转化
(void) foobar();
Point3d.chunkSize = 250;1
2
3&Point3d::chunkSize;
// 会得到如下类型的内存地址
const int* - 如果有两个classes,每个都声明了一个static member freeList,那么当它们放在程序的data segment时,会导致名称冲突。编译器的解决方式是暗中对每个static data member编码(name-mangling),以获得一个独一无二的程序识别代码。任何name-mangling做法都有两个重点:
- 一个算法,推导出独一无二的名称。
- 万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称。
Nonstatic Data Members
- Nonstatic data members直接放在每个class object中。除非经由显式(explcit)或隐式的(implicit),否则没有办法直接存取它们。只要在member funcion中直接处理一个nonstatic data member,implicit class object就会发生。例如: 表面上x、y、z直接存取,事实上是经由implicit class object(由this指针表述)完成的。这个函数的参数是:
1
2
3
4
5Point3d Point3d::translate(const Point3d &pt) {
x += pt.x;
y += pt.y;
z += pt.z;
}1
2
3
4
5
6// 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)。例如: 每一个nonstatic data member的偏移位置(offset)在编译时期即可获知,甚至如果派生自单一或多重继承串链也是一样。
1
2
3origin._y = 0.0;
// 相当于
&origin + (&Point3d::_y - 1); - 再来看看虚拟继承。虚拟继承将为经由base class subobject存取class members导入一层间接性。 从origin存取和从pt存取有什么重大差异?答案是当Pointe3d是一个derived class,而其继承结构中有一个virtual base class,并且被存取member是一个从该virtual base class继承而来的member就会有重大差异。这时候,我们无法在编译时期直到member真正的offset位置。这个存取操作必须延迟到执行期。如果使用origin就不会有这个问题。
1
2origin.x = 0.0;
pt->x = 0.0;
3.4 “继承”于Data Member
- 如果为2D和3D坐标点提供两个抽象数据类型:
1
2
3graph BT
Point2d
Point3d这和提供两层或三层继承结构,每一层(代表一个维度)是一个class,派生自较低维层次有什么不同?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// supporting abstract data types
class Point2d {
public:
// constructor(s)
// operations
// access functions
private:
float x, y;
};
class Point3d {
public:
// constructor(s)
// operations
// access functions
private:
float x, y, z;
};
只要继承不要多态(Inheritance without Polymorphism)
- 我们可能希望不论是2D或3D坐标点,能共享同一个实例,但又能继续使用于类型性质相关的实例。以上的设计策略。带来的影响则是可以共享数据本身以及数据处理方法,并将它局部化。一般而言,具体继承(concrete inheritance,相对于虚拟继承virtual inheritance)并不会增加空间或存取时间上的额外负担。
1
2graph BT
Point3d --- Point2d这样的设计的好处是可以把管理x和y坐标的程序代码局部化。也表现处两个类之间的紧密关系。但把原本两个独立不相干的classes凑成一堆”type/subtype“,并带有继承关系,会有什么易犯的错误呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class 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 Piont2d &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 oeprator+=(const Point3d &rhs) {
Point2d::operator+=(rhs);
_z += rhs.z();
}
// ... more members
protected:
float _z;
};- 经验不足的人可能会重复设计一些相同操作的函数。以例子中的constructor和operator+=为例,它们并没有被做成inline函数。
- 第二是,把class分解成两层或更多层,可能会为了”表现class继承体系的抽象化“而膨胀所需的空间。在32位机器中,每个Concrete class object大小是8bytes,细分如下:
1
2
3
4
5
6
7
8
9class Concrete {
public:
// ...
private:
int val;
char c1;
char c2;
char c3;
};
- val占用4bytes;
- c1、c2和c3各占用1bytes;
- alignment(调整到word边界)需要1bytes。
1
2
3graph BT
Concrete2 --- Concrete1
Concrete3 --- Concrete2现在Concrete3 object的大小是16bytes,比原先的设计多了100%。(p106)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Concrete1 {
public:
// ...
private:
int val;
char bit1;
};
class Concrete2 : public Concrete1 {
public:
// ...
private:
int val;
char bit2;
};
class Concrete3 : public Concrete2 {
public:
// ...
private:
int val;
char bit3;
};1
2
3
4
5
6
7Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;
// 如果C++把derived class members和Concrete1 subobject捆绑在一起,去除填补空间
pc1_1 = pc2; // pc1_1指向Concrete2对象
// derived class subobject被覆盖掉
// 于是bit2 member现在有了一个并非预期的数值
*pc1_2 = *pc1_1;
加上多态(Adding Polymorphism)
- 如果在继承关系中提供要给virtual function接口: 这个设计有个好处是可以把operator+=运用在一个Pointe3d对象和一个Point2对象身上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class Point2d {
public:
Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) {};
// 之前的存取操作
// 加上z的保留空间(目前什么也不做)
virtual float z() { return 0.0; }
virtual void z(float) {}
// 谁当以下运算符为virtual
virtual void operator+=(const Point2d &rhs) {
_x += rhs.x();
_y += rhs.y();
}
// ... more members
protected:
float _x, _y;
};
class Point3d : public Point2d {
pblic:
Point3d(float x = 0.0, float y = 0l0, float z = 0.0) : Ponit2d(x, y), _z(z) {};
float z() { return _z; }
void z(float newZ) { _z = newZ; }
vodi operator+=(const Piont2d &rhs) {
Point2d::oeprator+=(rhs);
_z += rhs.z();
}
// ... more members
protected:
float _z;
};虽然class的声明语法没变,但事情不一样了:z() member function和operator+=()运算符都成了虚函数;每个Point3d class object内含一个额外的vptr member和一个Piont3d virtual table;此外每个virtual member function的调用也复杂了。1
2
3Point2d p2d(2.1, 2.2);
Point3d p3d(3.1,, 3.2, 3.3);
p3d += p2d; - C++编辑器领域主要讨论的问题是把vptr放置在class object哪里最好?cfont编译器中,被放在class object的尾端:
1
2
3
4
5
6
7
8
9
10
11
12
13struct no_virts {
int d1, d2;
};
class has_virts : public no_virts {
public:
virtual void foo();
// ...
private:
int d3;
};
no_virts *p = new has_virts;struct no_virts nv; class has_virts :
public no_virts hv;int d1 int d1 int d2 int d2 int d3 __vptr__has_virts 把vptr放在object尾端,可以保留base class C struct的对象布局,因而允许C程序代码也能使用,这做法出现在C++问世时。 到了C++2.0,开始支持虚继承以及抽象基类,某些编译器开始把vptr放到class object前端: struct no_virts nv; class has_virts :
public no_virts hv;:-: :-: int d1 __vptr__has_virts int d2 int d1 int d2 int d3 vptr放在class object前端,对于多继承下通过指向class members的指针调用virtual function会有一些帮助。否则,不仅从class object起始点开始量起的offset必须在执行器备妥,class vptr之间的offset也必须备妥。 以下是Point2d和Point3d加上了virtual function之后的继承布局(单一继承): class Point2d p2d; class Point3d :
public Point2d pt3d;:-: :-: float _x float _x float _y float _y __vptr__Point2d __vptr__Point2d float _z
多重继承(Multiple Inheritance)
- 单一继承提供了一种自然多态(natural polymorphism)形式,是关于base type和derived type之间的转换。base 它们的object都是从相同的地址开始。差异只在derived object比较大,用以容纳它自己的nonstatic data members。一个derived class指定给base class的指针或reference。这个操作并不需要编译器调停或修改地址。可以很自然地发生,而且提供了最佳执行效率。
- 多重继承不像单一继承,也不容易模塑处模型。它的复杂度在于derived class和其base class之间的非自然关系:
1
2
3
4graph BT
Point3d --- Point2d
Vertex3d --- Point3d
Vertex3d --- Vertex多重继承的问题主要发生在derived class objects和后继base class objects之间的转换,不论是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Point2d {
public:
// ...有virtual接口,会有vptr
protected:
float _x, _y;
};
class Point3d : public Point2d {
public:
// ...
protected:
float _z;
};
class Vertex {
public:
// ... vptr
protected:
Vertex *next
};
class Vertex3d : public Point3d, public Vertex {
public:
// ...
protected:
float mumble;
};或是经由所支持的virtual function机制做转换。多继承对象符出的成本在于地址的指定操作而已:1
2
3
4extern void mumble(const Vertex&);
Vertex3d v;
...
mumble(v); // 不自然以下为多重继承(Multiple Inheritance):1
2
3
4
5
6
7
8
9
10
11Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
// 指定操作
pv = &v3d;
// 需要内部转化
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
// 如果v3d为指针类型,即pv3d,则:
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0; // 防止pv3d为0class Point2d pt2d; float _x float _y __vptr__Point2d class Point3d :
public Point2d pt3d;:-: float _x float _y __vptr__Point2d float _z Vertex v; :-: Vertex *next __vptr__Vertex class Vertex3d :
public Point3d,
public Vertex
{}v3d;:-: float _x float _y __vptr__Point2d float _z Vertex *next __vptr__Vertex float mumble
虚拟继承(Virtual Inheritance)
- 多重继承的一个语意上的副作用是,必须支持某种形式上的”shared subobject继承“。如早期的iostream library: 下图可表现iostream的继承体系图。第一个为多重继承,第二个为虚拟多重继承:
1
2
3
4
5// pre-standard iostream implementation
class ios { ... };
class istream : public ios { ... };
class ostream : public ios { ... };
class iostream : public istream, public ostream { ... };1
2
3
4
5graph BT
istream --- ios1[ios]
ostream --- ios2[ios]
iostream --- istream
iostream --- ostream在iostream对象布局中,只需要一份就好:1
2
3
4
5graph BT
istream --- ios
ostream --- ios
iostream --- istream
iostream --- ostream上述iostream的实现挑战在于:**一个有效的方法,将istream和ostream各自维护ios subobjet,折叠成由iostream维护的单一ios subobject,并且保存base class和derived class的指针(reference)之间的多态指定操作(polymorphism assignments)**。1
2
3
4class ios { ... };
class istream : public virtual ios { ... };
class ostream : public virtual ios { ... };
class iostream : public istream, public ostream { ... }; - 一般的实现方法是。Class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割两部分:一个不变区域和一个共享区域。不变区域中的数据,总是有固定的offset,所以这里可以直接存取。而共享区域,就是virtual base class subobject。它们只能被间接存取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Point2d {
public:
...
protected:
float _x, _y;
};
class Vertex : public virtual Point2d {
public:
...
protected:
Vertex *next;
};
class Point3d : public virtual Point2d {
public:
...
protected:
float _z;
};
class Vertex3d : public Vertex, public Point3d {
public :
...
protected:
float mumble;
};这中间存在一个问题:如何能够存取class共享部分呢?cfont编译器是在derived class object中插指针,每个指向virtual base class。需要完成操作都是通过指针间接完成的。1
2
3
4
5
6graph BT
Vertex -- _x,_y--- Point2d
Point3d -- _z --- Point2d
Vertex3d -- next --- Vertex
Vertex3d -- _z --- Point3d
none[ ] -- mumble --- Vertex3d这样实现模型有两个主要缺点:1
2
3
4
5
6
7
8
9
10
11void Point3d::operator+=(const Point3d &rhs)
{
_X += rhs._x;
_y += rhs._y;
_z += rhs._z;
};
// 转化为虚构代码vbc为virtual base class
__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
__vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
_z += rhs._z;- 每个对象针对其每个virtual base class背负一个额外的指针。而我们希望class object负担是稳定的。
- 虚继承链加长会导致存取层次增加。我们呢希望有着固定的存取时间。
- 对于第二个问题。MetaWare和其他编译器到今天还是用cfont原始模型:它们经由拷贝操作取得所有nested virtual base class指针,放到derived class object中,虽然有空间代价。
- 至于第一个问题,有两个解决方法。Microsoft编译器引入virtual base class table。每个class object如果有一个以上virual base classes,编译器就安插指针,指向virtual base class table,真正的vptr则被放在该表格中。第二个解决方法,是在virtual function table中放置virtual base class的offset(而非地址)。
class Vertex3d :
public Vertex,
public Point3d
{…} v3d;Vertex *next Point2d *pPoint2d __vptr__Vertex float _z Point2d *pPoint2d __vptr__Point3d float mumble float _x float _y __vptr__Point2d 该方法把virtual base class offset和virtual function entires混杂在一起。Sum编译器中,virtual function table由正值或负值来索引。正值则索引到virtual functions;负值则索引到virtual base class offsets。 1
2
3
4
5(this + __vptr__Point3d[-1])->_x +=
(&rhs + rhs.__vptr__Point3d[-1])->_x;
(this + __vptr__Point3d[-1])->_y +=
(&rhs + rhs.__vptr__Point3d[-1])->_y;
_ += rhs._z;因此Derived class实例和base class实例之间的转换操作为: 1
2
3Point2d *p2d = pv3d;
// translation
Point2d *pt2 = pv3d ? pv3d + pv3d->__vptr__Point3d[-1] : 0;
3.5 对象成员的效率(Object Member Efficiency)
- 下面测试旨在测试聚合(aggregation)、封装(encapsulation)以及继承(inheritance)所引发的额外负荷程序。跳过。
3.6 指向Data Members的指针(Poniter to Data Members)
- 考虑下面Point3d声明。有一个virtual function,一个static data member,以及三个坐标: 每个Point3d object含有三个坐标值,依序为x、y、z,以及vptr。static data member origin放在class object之外。vptr的位置根据编译器不同而不同。不是放头就是尾。
1
2
3
4
5
6
7
8class Point3d {
public:
virtual ~Point3d();
// ...
protected:
static Point3d origin;
float x, y, z;
};&Point3d::z;
取得某个坐标成员的地址代表什么呢?代表着z坐标在class object中偏移位置(offset)。 - 一台32位机器上,每一个float是4bytes,所以期望获取地址偏移位置要么是8要么是12。然而获取的总是多1,也就是9和13(我的输出并没有+1)。
- 在多继承下,要将第二个或后继base class的指针,和一个与derived class object绑定的member结合起来,那么将会因为需要加入offset值而变得复杂。例如: 要解决这个问题,必须:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct Base1 { int val1; };
struct Base2 { int val2; };
struct Derived : Base1, Base2 { ... };
void func1(int Derived::*dmp, Derived *pd)
{
// 期望第一个参数是一个derived class的member指针,结果是base class的会怎样。
pd->*dmp;
}
void func2(Derived *pd)
{
// bmp将成为1
int Base2::*bmp = &Base2::val2;
// bmp == 1但在Derived中val2 == 5
func1(bmp, pd);
}1
2// 经由编译器内部转换
func1(bmp ? bmp + sizeof(Base1) : 0, pd);
“指向Members的指针”的效率问题
- 下面是测试数据。了解在3D坐标点的不同class表现方式下指向members的指针所带来的影响。