欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-14 12:45:26
...

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单继承,基类无虚函数

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

                                                                单继承且没有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。

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

                                                      Concrete1、Concrete2、Concrete3的对象布局

4.2 单继承,基类有虚函数

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

虽然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++对象模型》第3章 Data语意学

                                                                                   多重继承的数据布局

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

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

一般的布局策略是先安排好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。

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

                                                                 虚拟继承的数据布局

在这样的策略下,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;

 

相关标签: 读书笔记