《深度探索C++对象模型》chapter3:Data语意学

1 虚继承

class X {};
class Y: public virtual X {};
class Z: public virtual X {};
class A: public Y, public Z {};

继承关系:
在这里插入图片描述

  1. X是空类,占1bytes内存,编译器安插进的一个char
  2. 在Visual C++中,Y和Z派生类中都只有一个4bytes的指针,指向virtual base class X(不同编译器对X这样的empty virtual base class所做的处理不同,Y和Z大小也可能会是8bytes)

在这里插入图片描述

2 Data Member的布局

class Point3d {
public:
    //...
private:
    float x;
    static List<Point3d*> *freelist;
    float y;
    static const int chunksize = 250;
    float z;
};

非静态数据成员在类对象中的排列顺序与其声明顺序相同,任何中间介入的静态数据成员例如freelist和chunksize都不会被放进对象布局中,因此只需要考虑非静态数据成员在对象所占用存储空间的分布。

对于内含虚函数的类对象,vptr传统上被放在所有显式声明的成员最后,也可能会放在一个类对象的最前端。

class Point3d {
public:
    //...
private:
    float x;
    static List<Point3d*> *freelist;
private:
    float y;
    static const int chunksize = 250;
private:
    float z;
};

对于上述程序,编译器会将上述access sections连锁在一起,依照声明的顺序形成一个连续的区块。access sections的多寡不会造成额外负担。

3 Data Member的存取

3.1 static data member

  1. 每一个静态数据成员都只有一个实例,存放在程序的数据段中。程序在取用静态成员时,就会被内部转化为对该唯一extern实例的直接参考操作:
Point3d origin, *pt = &origin;
//origin.chunksize == 250;
Point3d::chunksize == 250;
//pt->chunksize == 250;
Point3d::chunksize == 250;
  1. 对于从复杂继承关系中继承而来的静态数据成员,其存取路径依然如上述直接,因为程序中对于静态成员还是只有唯一实例。
  2. 若取一个静态数据成员的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为静态成员并不内含在类对象中:
&Point3d::chunksize;
/*
会得到如下类型内存地址:
const int*
*/
  1. name-mangling:对于在不同类中重名的静态成员,编译器所采取的编码技巧。

3.2 nonstatic data member

Nonstatic data members直接存放在每一个类对象中,除非经由显式的(explicit)或隐式的(implicit)类对象,否则无法进行存取。

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;
}

如果需要对一个非静态数据成员进行存取操作,编译器需要把类对象的起始地址加上数据成员的偏移位置(offset):

origin._y = 0.0;
//地址&origin._y等于:
&origin + (&point3d::y - 1)

注意-1操作,指向数据成员的指针,其offset值总是被加1

每一个非静态数据成员的偏移位置(offset)在编译时期即可获知,甚至在member属于一个base class subobject(派生自单一或多重继承串链)也是一样的。

虚拟继承:

虚拟继承将为“经由base class subobject存取class members”导入一层新的间接性:

Point3d *pt3d;
pt3d->_x = 0.0;

执行效率在_x是一个struct member、一个class member、单一继承、多重继承的情况下都完全相同。

但_x是一个virtual base class的member时,存取速度会稍慢一点。

4 “继承”与data member

在c++继承模型中,派生类表现出的东西是其自己的members加上其base class(es) members的总和。在大部分编译器中,其排列顺序都是base class members先出现,但属于virtual base class的除外。

4.1 只要继承不要多态

采用具体继承相对于虚拟继承不会增加空间或存取时间上的额外负担

在这里插入图片描述

优点:

  1. 将管理成员变量的程序代码局部化
  2. 表现出两个抽象类的紧密关系

缺点:

  1. 经验不足的开发者可能会重复设计一些相同操作的函数
  2. 把一个class分解为两层或更多层,可能会为了“表现class体系之抽象化”而膨胀所需的空间

在这里插入图片描述

上述做法的原因是为了保证当父类指针指向子类对象时,父类指针可使用的内存空间都是父类的,而不会内含子类成员。

4.2 加上多态

引入多态(提供virtual函数接口)来实现面向对象程序设计的弹性,同时造成class在空间和存取时间上的负担:

  1. 导入一个与class相关的virtual table,用来存放它所声明的每一个virtual function的地址
  2. 在每一个类对象中导入一个vptr,提供执行期的链接,使每一个对象能够找到对应的virtual table
  3. 加强constructor,使它能够为vptr设定初值,使它指向class所对应的virtual table
  4. 加强destructor,使它能够抹消“指向class相关virtual table”的vptr

vptr放置在class object中的位置:

vptr放置在class object的尾端:可以保留base class C struct的对象布局,保证c语言兼容性。

vptr放置在class object的前端:对于“在多重继承下,通过指向class members的指针调用virtual function有帮助”;但也丧失了c兼容性。

在这里插入图片描述

4.3 多重继承

上述单一继承提供了一种“自然多态”形式,即基类和派生类的对象都是从相同的地址开始,因此在将派生类对象指定给基类(不管继承深度有多深)的指针或引用,不需要编译器去调停或修改地址,而是可以较为自然的发生。

上述例外:若vptr放置在类对象的起始处,且基类没有虚函数而派生类有时,那么单一继承的自然多态会被打破。这种情况下,在将派生类对象转换为基类型是需要编译器介入以调整地址

多重继承的复杂即在于派生类与其上一个基类及上上一个基类之间的“非自然”关系。

在这里插入图片描述

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 = pv3d ? (Vertex*)(((char*)pv3d) + sizeof(Point3d)) : 0

4.4 虚拟继承

在这里插入图片描述
class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割为两个部分:一个不变区域和一个共享区域。

不变区域中的数据,不论后继如何变化,总是拥有固定的offset,所以这一部分数据可以被直接存取

共享区域,所表现的virtual base class subobjects,这部分的数据的位置会因为每次的派生操作而有变化,所以只能采取间接存取

cfront编译器存取class共享部分的办法:在每一个派生类对象中安插一些指向virtual base class的指针。要存取继承得来的virtual base class members,可以通过相关指针间接完成。

上述方法的缺点:

  1. 每一个对象针对其每一个virtual base class背负一个额外的指针
  2. 虚拟继承串链的增长,导致间接存取层次的增加

针对问题1:

  1. Microsoft编译器引入virtual base class table。在有一个或多个virtual base classes的class object中插入指针指向virtual base class table,table中存放虚基类指针
  2. 在虚函数表vtable中存放virtual base class的offset,正值索引到虚函数,负值索引到virtual base class的offset

在这里插入图片描述
针对问题2:

通过拷贝操作将所有的nested virtual base class指针放到派生类对象中,付出空间代价换取“固定存取时间”

5 指向data members的指针

&Point3d::z;

取数据成员的地址,将得到该成员在类对象中的偏移位置,传回的值往往会比我们通常认知的要多1

member offset加1的操作是为了区分指向数据成员的指针和没有指向数据成员的指针。

因此:

&Point3d::z; //取一个nonstatic data member的地址,将会得到它在class中的offset
&origin.z;   //取一个绑定于类对象身上的数据成员的地址会得到该成员在内存中的真正地址,将该地址减去z的偏移值,并加1就会得到origin的起始地址

注意:在多重继承下,若要将第二个(或后继)基类的指针和一个与派生类对象绑定的成员结合起来,就需要编译器在内部加入offset以进行内部转换以存取正确的数据成员。

struct Base1 { int val1; };
struct Base2 { int val2; };
struct Derived : Base1, Base2 { ... };
void func1(int Derived::*dmp, Derived *pd) {
    pd->*dmp;	//若dmp为指向基类成员的指针??
}

void func2(Derived *pd) {
    int Base2::*bmp = &Base2::val2;	//val2在Base2类中的偏移为1
    func1(bmp, pd);					//val2在Drived类中的偏移为5
}

所以需要经过编译器的内部转化

func1(bmp + sizeof(Base1), pd);
//由于不能保证bmp是否为指向数据成员的指针,即有可能为0,故需要防范
func1(bmp ? bmp + sizeof(Base1) : 0, pd);

6 效率问题

6.1 对象成员的效率

对于数据成员的存取操作,一旦进行优化后,“封装”就不会带来执行期的效率成本。

在继承模型下的数据存取:

  1. 单一继承不会影响测试效率,因为数据成员会被连续存储在派生类对象中,并且其offset在编译时期就可知道
  2. 虚拟继承效率低,因为编译器未能辨识出对于继承而来的数据成员的存取是通过非多态对象进行的,而由此引入的执行期的间接存取操作大大降低了程序的执行效率(但是虚拟继承层数增加,并不会影响效率)

6.2 指向成员的指针的效率

将直接存取与通过指向已绑定的成员的指针、指向数据成员的指针进行的存取操作进行比较,由于间接性的增加,执行时间会翻倍

同样,引入继承模型不会影响程序执行的效率,但额外的间接性会降低“把所有的处理都搬移到寄存器中执行”的优化能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值