3.1 Data Member的绑定
1.对于内联函数函数体内的data member绑定操作是在整个class声明完成之后才发生的,所以对于:
extern float x;
class Point3d
{
public:
Point3d(float, float, float);
float X() const { return x; }
private:
float x, y, z;
};
函数X()中的x是Point3d的成员x,而不是那个extern的x。
2.但是对于成员函数的参数列表来说,其绑定操作是在第一次遇见他们是就发生了的,也就是在class没有被声明完之前,如果
typedef int length;
class Point3d
{
public:
void mumble(length val){ _val = val; }
length mumble(){ return _val; }
private:
typedef float length;
length _val;
};
上述类中的成员函数中的length是int而不是float
为了避免这种情况,要把类中用到的类型别名的声明放在类的起始处。
3.2 Data Member的布局
C++standard要求,在同一个access section(也就是private,public,protected等区段)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件。不同access section没有规定。
3.3Data member的存取
对于下面两种存取操作的区别
Point3d origin,*pt=&origin;
origin.x=0.0;
pt->x = 0.0;
static Data Member
上面两种存取操作对于static Data member 没有区别,不管该member是否是自身的或者是通过复杂的继承关系继承而来的。因为static data member位于class之外,并被视为一个global变量,存取static member不需要通过class object,通过'.'运算符进行存取,如上述的origin.x,只是语法上的一种便宜行事。
取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不内含在一个class object之中。
对于不同class中同名的static data member,编译器会对每一static member 编码(name-mangling),以获得独一无二的名称。
Nonstatic Data Members
nonstatic data member 直接存放在每一个class object之中,只有两种方式可以存取它们,一种是implicit,也就是通过this指针,一种是explict,也即是通过对象或指针。
对nonstatic data member 进行存取操作需要把class object 的起始地址加上data member的偏移量。对于member来说其偏移量在编译期即可获得,甚至是该member派生自单一或多重继承串链,都一样。因此存取一个nonstatic data member 其效率和存取一个C struct member或一个nonderived class 的member是一样的 。
但是对于虚拟继承来说就会出现不同,如下,如果x是从虚基类继承而来的,用对象进行存取和用指针进行存取
Point3d origin,*pt=&origin;
origin.x=0.0;
pt->x = 0.0;
由于original的类型是确定的为Point3d,所以x的偏移地址也即是确定的,但是对于pt,其真正指向的类型要在执行期才能确定,所以x的偏移地址(相对于pt指向的类型)也在执行期才能获得,要根据虚表指针所指向的虚表来确定。
3.4“继承”与Data member
只要继承不要多态
对于一个类
无继承时
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;
};
内存布局与C struct完全一样
有继承无多态时
class Point2d
{
public:
Point2d(float x = 0.0, float y = 0.0)
:_x(x), _y(y){};
float getx(){ return _x; }
float gety(){ return _y; }
void setx(float newX){ _x = newX; }
void sety(float newY){ _y = newY; }
void operator+=(Point2d& rhs)
{
_x += rhs.getx();
_y += rhs.gety();
}
//.....more members
private:
float _x;
float _y;
};
class Point3d :public Point2d
{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
:Point2d(x, y), _z(z){};
float getz(){ return _z; }
void setz(float newZ){ _z = newZ; }
void operator+=(Point3d& rhs)
{
Point2d::operator+=(rhs);
_z += rhs.getz();
}
private:
float _z;
};
布局为
在设计继承时容易犯两个错误。
1.重复设计一些相同的操作
2.把class分解为两层或更多层,有可能会为了“表现class体系之抽象化”而膨胀所需空间。由于C++语言保证“出现在Derived class中的 base class subobject有其完整原样性”,所以导致了在成员变量存在内存对齐时,对class进行分层,可能会导致空间膨胀,举例如下
class Concrete
{
public:
//...
private:
int val;
char c1;
char c2;
char c3;
};
现在假设将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;
};
这就是Derived class中,base class subobject保持原样性。不会出现,在继承时,派生类占据基类部分的填补空间的情况。
加上多态
1.会生成一个virtual table,用来存放所声明的每一个virtual function的地址。这个table的元素数目一般是所声明的虚函数的数目,在加上一个或两个slots(用以支持runtime type identification )
2.会导入一个vptr,提供执行期链接,使得每一个object能够找到相应的virtual table
3.加强constructor,使它能够为vptr设定初值,让它指向class所有的virtual table
4.加强destructor,使它能够抹消“指向class之相关virtual table”的vptr
多重继承
class Point2d
{
public:
virtual void fun1();
protected:
float _x, _y;
};
class Point3d :public Point2d
{
public:
//...
protected:
float _z;
};
class Vertex
{
public:
virtual void fun2();
protected:
Vertex *next;
};
class Vertex3d :public Point3d, public Vertex
{
public:
//...
protected:
float mulble;
};
如上面的类,继承关系为
在vs2013中布局为
对于一个多重派生的对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况和单一继承时相同,因为二者指向相同的起始地址,需付出的成本只有地址指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过:加上或减去介于中间的base class subobject(s)大小。
如果要存取第二个或后继base class中的一个data member,member的位置在编译时就固定了,因此存取member只是一个简单的offset运算,就像单一继承一样简单,不管是经由一个指针、一个reference或是一个object来存取。
虚拟继承
编译器在实现虚拟继承时一般使用如下方法,class 如果内含一个或多个virtual base class subobject,如:
对于istream,将被分割为两部分:一个不变局部和一个共享局部,不变局部中的数据,不管后续如何衍化,总拥有固定的offset(从object的头算起),所以这一部分数据可以直接被存取。至于共享局部,所表现的就是virtual base class subobject.这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们只能被间接存取(编译器会安插指针,利用指针完成存取·)。
在vs2013中,是通过引入virtual baseclasstable,在对象的布局中会被安插一个指针指向该表,通过该表访问virtual base class
class Point2d
{
public:
protected:
float _x, _y;
};
class Point3d :public virtual Point2d
{
public:
protected:
float _z;
};
class Vertex :public virtual Point2d
{
public:
protected:
Vertex *next;
};
class Vertex3d :public Vertex,public Point3d
{
public:
protected:
float mulble;
};
对于上面这种继承关系,vs2013 中的布局为
两个指针所指向的虚表如下,分别表示了virtual base class Point2d相对于Point3d和Vertex,在Vertex3d对象布局中地址的偏移
现在再来看之前的解释
对于虚拟继承来说就会出现不同,如下,如果x是从虚基类继承而来的,用对象进行存取和用指针进行存取
Point3d origin,*pt=&origin;
origin.x=0.0;
pt->x = 0.0;
由于original的类型是确定的为Point3d,所以x的偏移地址也即是确定的,但是对于pt,其真正指向的类型要在执行期才能确定,所以x的偏移地址(相对于pt指向的类型)也在执行期才能获得,要根据虚表指针所指向的虚表来确定。