3.4 继承 与 data member
在 C++ 继承模型中,一个derived class object 所表现出来的东西, 是其自己的 members 加上其 base class members 的总和。
至于 derived class members 和 base class members 的排列次序并未在 C++ Standard 中强制指定;理论上编译器可以自由安排。在大部分编译器中,base class members 总是先出现,但 virtual base class 的例外。
假如我为 2D 或 3D 坐标点提供两个抽象数据类型如下:
//supporting abstract data types
class Point2d
{
public:
//constructors
//operations
//access functinos
private:
float _x, _y;
};
class Point3d
{
public:
//constructors
//opsrations
//access functions
private:
float _x, _y, _z;
};
和C struct 布局完全一样:

以下探讨4种情况:
1. 单一继承且不含 virtual functions
2. 单一继承含 virtual functions
3. 多重继承
4. 虚拟继承
单一继承且不含 virtual functions
从 Point2d 派生出一个 Point3d, 于是 Point3d 将继承 _x 和 _y 坐标的一切。影响则是可以共享数据 和 对数据的处理方法。
具体继承(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 x){ _x = x; }
void Y(float y){ _y = y; }
void operator+=(const Point2d &rhs)
{
_x += rhs.X();
_y += rhs.Y();
}
//more operations
protected:
float _x, _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 Z(){return _z;}
float Z(float z){_z = z;}
void operator+=(const Point3d &rhs)
{
Point2d::operator+=(rhs);
_z += rhs.z();
}
//more operations
protected:
float _z;
};

把一个 class 分解为多层, 有可能为了表现 class 体系的抽象化,却导致空间膨胀(内存对齐)。
class Concrete
{
public:
private:
int val;
char c1;
char c2;
char c3;
}
// concrete object
-----------------
| 4 int val |
| 1 char c1 |
| 1 char c2 |
| 1 char c3 |
| 1 padding |
-----------------
// 共 8 个字节大小
class Concrete1
{
public:
private:
int val;
char c1;
};
class Concrete2:public Concrete1
{
public:
private:
char c2;
};
class Concrete3:public Concrete1
{
public:
private:
char c3;
};

而更可怕的结果是地址的覆盖, 比如我声明一组指针:
Concrete2 *p2;
Concrete1 *p1_1, *p1_2;
其中 p1_1 和 p1_2 都可以指向上述三种 classes objects, 而以下的的这个操作:
*p1_2 = *p1_1;
应该执行一个 memberwise 的复制操作, 对象则是被指的 object 的 Concrete1 那部分, 那如果有以下操作:
p1_1 = p2;
//error! derived subobject 被覆盖掉
*p1_2 = *p1_1; // p1_2 对象现在有了一个不确定的数值(同类型对象拷贝)

2. 单一继承含 virtual functions
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.X();
}
//...more operations
protected:
float _x, _y;
};
这样的好处就是可以让不同维度之间的点进行运算的弹性,代价是空间和存取时间的额外负担:
- 导入一个和 Point2d 有关的 virtual table,用来存放它所声明的每一个 virtual functions 的地址。table 元素数目一般而言是被声明的 virtual functions 的数目, 再加上一个或两个 slots(以支持 runtime type identification)
- 在每一个 class object 中导入一个 vptr,提供执行期的链接,使每一个 object 能够找到相应的 virtual table。
- 加强 constructor,使它能够为 vptr 设定初值,让它指向 class 所对应的 virtual table。 这可能意味着在 derived class 和每一个 base class 的 constructor 中,重新设定 vptr 的值,其情况视编译器而定。
4.加强 destructor,能够抹掉指向 class 的相关 virtual table 的 vptr。因为 vptr 很可能已经在 derived class destructor 中被设定为 derived class 的 virtual table 地址。值得注意的是,destructor 的调用顺序是反向的,与栈的行为类似。一个积极的优化编译器可以压抑那些大量的制定操作。
这些额外负担的收益视被处理的 Point2d objects 的数目和生命期而定。
//新版本的 Point3d 声明
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 z){ _z = z; }
void operator+=(const Point3d &rhs)
{
Point2d+=(rhs);
_z += rhs.Z();
}
//...more members
protected:
float _z;
};
C++ 编译器的领域里有一个主要讨论:把 vptr 放置在 class object 的哪里会最好?cfront 编译器把它被放在 class object 的尾端, 用以支持以下的继承类型:
struct no_virts{ int d1, d2; };
class has_virts: public no_virts
{
public:
virtual void Foo();
private:
int _d3;
}
no_virts *p = new has_virts;

把 vptr 放在编译器尾端,可以保留 base class C struct 的对象布局,因而允许在 C 程序代码中也能使用。
随着 OOP 的兴起, 某些编译器开始把 vptr 放到 class obejct 的开头处,这对于在多重继承下,通过指向 class members 的指针调用 virtual function会带来一些帮助。否则, 不仅从 class object 起始点开始量起的 offset 必须在执行期备妥,甚至与 class vptr 之间的 offset 也必须备妥。代价就是放弃了与 C 的兼容性。
放在开头的数据布局:

3、多重继承
单一继承提供了一种自然多态的形式,是关于 class 体系中的 base type 和derived type 之间的转换,base class 和 derived class 的 objects 都是从相同的地址开始, 其间差异只在于 derived object 比较大, 用以多容纳它自己的 nonstatic data members。
Point3d *p3d;
Point2d *p = &p3d;
把一个 derived class 指定给一个 base class 的指针或 reference, 该操作并不需要编译器去调停或修改地址,很自然地发生,并提供了最佳执行效率。
之前的一种编译器把 vptr 放在 class object 的起始处, 如果 base class 没有 virtual function 而 derived class 有,那么单一继承的自然多态就会被打破,这种情况下, 把一个 derived object 转换为其 base 类型就需要编译器的介入,用以调整地址(vptr 插入之故)。在既是多重继承又是虚拟继承的情况下,编译器的介入更为重要。
多重继承不像单一继承,也不容易画出其模型,多重继承的复杂度在于 derived class 和其上一个 base class 乃至于上上个 base class 之间的非自然联系, 例如:
class Point2d
{
public:
//...
protected:
float _x, _y;
};
class Point3d: Point2d
{
public:
//...
protected:
float _z;
};
class Vertex
{
public:
//...
protected:
Vertex *next;
};
class Vertex3d:public Point3d, public Vertex
{
public:
//...
protected:
float mumble;
};

多重继承的问题主要发生于 derived class objects 和其第二或后继的 base class objects 之间的转换:
extern void mumble(const Vertex&);
Vertex3d v;
...
// 将一个 Vertext3d 转换为一个 Vertex。
mumble(v);
对一个多重派生对象,将其地址指定给第一个 base class 的指针,情况和单一继承时相同,因为二者都指向相同的起始地址,付出的成本只有地址的指定操作。至于第二个 base class 的地址制定操作,则需要修改地址:加上或减去介于中间的 base class subobject(s)大小:
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
pv = &v3d; //这个操作,内部的转化:
pv = (Vertex*)( (char*)&v3d + sizeof(Point3d) );
// 下面的指定操作,就只需要简单的拷贝其地址:
p3d = &v3d;
p2d = &v3d;
但如果有两个指针如下:
Vertex3d *pv3d;
Vertex *pv;
下面的指定操作,不能够只是简单的转换:
pv = pv3d;
pv = (Vertex*)((char*)pv3d) + sizeof(Point3d);
因为如果 pv3d == 0,之前的代码中 pv = (Vertex*)(sizeof(Point3d)),不成立。
因为此pv3d == nullptr;访问权限冲突。
所以这就需要一个条件测试:
pv = pv3d ? (Vertex*)((char*)pv3d + sizeof(Point3d)) : 0;

原始编译器根据声明顺序排列,完成多重base classes 的布局。
4. 虚拟继承
多重继承的一个语义上的副作用就是, 它必须支持某种形式的 shared subobject 继承, 一个典型的例子是最早的 iostream library:
// pre-standard iostream implement
class ios{ ... };
class istream: public ios{ ... };
class ostream: public ios{ ... };
class iostream:
public istream, public ostream{ ... };

istream 和 ostream,都内含一个 ios subobject,然而在 iostream 的对象布局中,只需要一份 ios subobject 就好,所以在语言层面的解决方法就是虚拟继承。
虚拟继承的实现难点在于要找到一个有效的方法,将 istream 和 ostream 各自维护的 ios subobject 折叠成为一个单一的 ios subobject, 并且还可以保存 base class 和 derived class 的指针(以及 reference) 之间的多态指定操作。
一般实现的方法如下所述,class 如果含有一个或多个 virtual base class subobject, 像 istream 那样,将被分割成两部分:不变局部 和 共享局部。
不变局部:不变局部中的数据,不管后继如何衍化,总是有固定的 offset(从 object 的开头算起),所以这一部分数据可以被直接存取。
共享局部:所表现的就是 virtual base class subobject,这一部分的数据,其位置会因为每次的派生操作而变化,所以它们只能被间接存取。
各编译器实现技术之间的差异就在于间接存取的方法不同,以下是三种主流策略:
//虚拟继承的层次结构
class 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;
};

一般的布局策略是先安排好 derived class 的不变部分,然后再建立起共享部分。
然而,这中间存在一个问题:如何能够存取 class 的共享部分呢?cfront 编译器会在每一个 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;
这样的模型有两个缺点:
1. 每一个对象必须针对其每一个 virtual base class 背负一个额外的指针,然而我们希望 class object 有固定的负担(只有一个虚基类指针vbptr),不因为其 virtual base class 的数目而有变化。
2. 由于虚拟继承的串联的加长,导致间接存取层次的增加。意思是,如果有三层虚拟衍化,就需要三次间接存取。但是我们希望有固定的存取时间,不会因为虚拟衍化的深度而改变。
其实就是这个策略开销不稳定
许多的编译器都使用原始实现模型来解决第二个问题,它们经由拷贝操作得到的所有的 nested virtual class 指针, 放到 derived class object 之中, 这就解决了固定存储时间的问题。 代价是空间。
第二个问题,以指针指向base class的实现模型:

对于第一个问题, 一般而言有两种解决办法:
Microsoft 编译器引入所谓的 virtual base class table。 每一个 class object 如果有一个或多个 virtual base classes, 就会由编译器安插一个指针, 指向 virtual base class table。 至于真正的 virtual base class 指针, 就放在该表格中:

经由一个非多态的 clas object 来存取一个继承而来的 virtual bass class 的 member, 可以被优化为一个直接存取操作,就好像一个经由对象调用的 virtual function 调用操作可以在编译时期被决议完成一样。在这次存取和下次存取之间, 对象的类型不可以改变, 所以 virtual base class subobjects 的位置会变化的问题在这之下就不存在了。
Point3d origin;
origin._x;

浙公网安备 33010602011771号