[读书笔记] - 《深度探索C++对象模型》第3章 Data语意学

Table of Contents

1.Data Memeber的绑定

2.Data Member的布局

3.Data Member的存取

4.“继承”于Data Member

4.1单继承,基类无虚函数

4.1.1 base class subobject在derived class中的原样性

4.2 单继承,基类有虚函数

4.3 多重继承

4.4 虚拟继承


一个空的class,如:

// sizeof X == 1
class X { };

事实上并不是空的,它有一个隐晦的1 byte,那是被编译器安插进去的一个char。这使得这个class的两个objects得以在内存中配置独一无二的地址。

1.Data Memeber的绑定

在下面的程序中,length的类型在两个member function signatures中都解析为global typedef,也就是int。当后续再有length的nested typedef声明出现时,C++ Standard就把稍早的绑定标示为非法:

typedef int length;

class Point3d
{
public:
    // oops: length 被 resolved 为 global
    // no problem: _val 被 resolved 为 Point3d::_val
    void mumble(length val) { _val = val; }
    length mumble() { return _val; }
    // ...
private:
    // lengthPoint3d::_val
    void mumble(length val) { _val = val; }
    length mumble() { return _val; }
    // ...
private:
    // length必须在“本class对它的第一个参考操作”之前被看见
    // 这样的声明将使先前的参考操作不合法
    typedef float length;
    length _val; 
};

这种情况需要采取防御性程序风格:请始终把“nested type声明”放在class的起始处。在上述例子中,如果把length的nested typedef定义于“在class中被参考”之前,就可以确保非直觉绑定的正确性。

2.Data Member的布局

C++ Standard要求,在同一个access section(也就是private、public、protected等区段)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件即可。也就是说,各个members并不一定得连续排列。什么东西可能会介于被声明的members之间呢?members的边界调整(alignment)可能就需要填补一些bytes。

3.Data Member的存取

Point3d origin;
Point3d* pt;
origin.x = 0.0;
pt->x = 0.0;

“从origin存取”和“从pt存取”有什么重大的差异?

答案是“当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的member(r如本例的x)是一个从该virtual base class继承而来的member时,就会有重大的差异”。这时候我们不能够说pt必然指向哪一种class type(因此我们也就不知道编译时期这个member真正的offset位置),所以这个存取操作必须延迟至执行期,经由一个额外的间接导引,才能够解决。但如果使用origin,就不会有这些问题,其类型无疑是Point3d class,而即使它继承自virtual base class, members的offset位置也在编译时期就固定了。

4.“继承”于Data Member

在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base class(es) members的总和。至于derived class members和base class(es) members的排列次序并未在C++ Standard中强制指定:理论上编译器可以自由安排之。在大部分编译器中,base class members总是先出现,但virtual base class除外(一般而言,任何一条规则一旦碰上virtual base class就没辙,这里亦不例外)。

4.1单继承,基类无虚函数

                                                                单继承且没有virtual function时的数据布局

4.1.1 base class subobject在derived class中的原样性

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的大小是16 bytes,让我们仔细观察这一继承结构的内存布局,看看到底发生了什么事。

Concrete1的两个member: val和bit1,加起来是5 bytes,加上alignment padding的3 bytes,Concrete1 object实际大小是8 bytes。有些人会以为Concrete2的member bit2会和Concrete1捆绑在一起,占用原来用来填补空间的1 byte,于是Concrete2 object的大小为8 bytes,其中2 bytes用于alignment padding。

然而Concrete2的bit2实际上却是被放在填补空间所用的3 bytes之后,于是其大小变成12 bytes,不是8 bytes。

                                                      Concrete1、Concrete2、Concrete3的对象布局

4.2 单继承,基类有虚函数

虽然class的声明语法没有改变,但每一件事情都不一样了:两个z() member functions以及operator+=运算符都成了虚拟函数;每一个Point3d class object内含一个额外的vptr member(继承自Point2d);多了一个Point3d virtual table;此外,每一个virtual member function的调用也比以前复杂了。

4.3 多重继承

单一继承提供了一种“自然多态”形式,是关于classes体系中的base type和derived type之间的转换。base class和derived class的object都是从相同的地址开始。把一个derived class object指定给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:
    // ... (拥有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 objects之间的转换;不论是直接转换如下:

extern void mumble(const Vertex&);
Vertex3d v;
...
// 将一个Vertex3d转换为一个Vertex。这是“不自然的”
mumble(v);

或是经由其所支持的virtual function机制做转换。

对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过:加上(或减去,如果downcast的话)介于中间的base class subobject(s)大小,例如:

Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

pv = &v3d;
//需要这样的内部转化:
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));

p2d = &v3d;
p3d = &v3d;
//都只需要简单地拷贝其地址就行了。

Vertex3d* pv3d;
pv = pv3d;
//不能够只是简单地被转换为:
pv = (Vertex*)(((char*)pv3d) + sizeof(Point3d));
//因为如果pv3d为0,pv将获得sizeof(Point3d)的值。这是错误的!
//对于指针,内部转换操作需要有一个条件测试:
pv = pv3d ? (Vertex*)(((char*)pv3d) + sizeof(Point3d)) : 0;

                                                                                   多重继承的数据布局

C++ Standard并未要求Vertex3d中的base classes Point3d和Vertex有特定的排列次序。原始的cfront编译器是根据声明次序来排列它们。因此cfront编译器制作出来的Vertex3d对象,将可被视为是一个Point3d subobject(其中又有一个Point2d subobject)加上一个Vertex subobject,最后再加上Vertex3d自己的部分。(但如果加上虚拟继承,就不一样了。)

如果要存取第二个(或后继)base class中的一个data member,将会是怎样的情况?需要付出额外的成本吗?不,members的位置在编译时就固定了,因此存取members只是一个简单的offset运算,就像单一继承一样简单——不管是经由一个指针、一个reference或是一个object来存取。

4.4 虚拟继承

多重继承会导致菱形继承时,最终的派生子类中包含多份最原始基类的部分。解决办法是引入虚拟继承:

class如果内含一个或多个virtual base class subobjects,class将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的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的不变部分,然后再建立其共享部分。

在virtual function table中放置virtual base class的offset(而不是地址),virtual function table可经由正值或负值来索引。如果是正值,很显然就是索引到virtual functions;如果是负值,则是索引到virtual base class offsets。

一般而言,virtual base class最有效的一种运用形式就是:一个抽象的virtual base class,没有任何data members。

                                                                 虚拟继承的数据布局

在这样的策略下,Point3d的operator+=运算符必须被转换为以下形式:

(this + __vptr__Point3d[-1])->_x += (&rhs + rhs.__vptr__Point3d[-1])->_x;

Derived class实体和base class实体之间的转换操作将变成:

Point2d* p2d = pv3d;
==> Point2d* p2d = pv3d ? pv3d + pv3d->__vptr__Point3d[-1] : 0;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值