您的位置:首页 > 编程语言 > C语言/C++

《深度探索C++对象模型》笔记(3)

2020-01-15 08:09 323 查看

Preface

这章主要是根据第一章的内容,进一步讲解object的存储方式,更详细的阐述内存布局。详细说了在继承、virtual时的data members的存放。(对于template类的静态成员不同)

和第一章说的一样,C++对象模型,针对内存的布局以及相应的操作行为,主要都是为了virtual这个关键词而做的包括虚函数以及虚拟继承,尤其是针对虚拟继承。

指向类成员的指针对已有的理解有很大冲击。。一直以为指针存的都是内存的地址,可以通过指针直接找到地方,原来存的并不是都是地址。。。还可能存offset

第三章 Data语义学

class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
sizeof(X) => 1
sizeof(Y) => 8
sizeof(Z) => 8
sizeof(A) => 12
//vc编译器优化后的
sizeof(X) => 1
sizeof(Y) => 4
sizeof(Z) => 4
sizeof(A) => 8

一个空的类,理论上不占任何内存,但为保证其存在性,编译器会增加一个char进去,保证不同类及objects之间在内存中地址不重复

对于类Y和Z:这两个类同时虚继承一个类X,大小受三个因素影响:

  1. 语言本身造成的overhead(虚指针):当语言支持虚基类时,导致一些额外负担。派生类存在一个指针,它指向virtual base class subobject(虚基类子对象)或者一个存放前者的相关表格,表格里存放着虚基类子对象的地址或者其偏移位置offset。在例子中Y和Z这部分指针大小是4bytes(32位机器上,类的虚指针vptr一般是4bytes)。
  2. 编译器对于特殊情况提供的优化处理:因为Y和Z也是空类,和X一样,也有1byte的char,放在派生类的固定部分的尾端。(编译器对empty virtual base class的处理,不同编译器有不同实现。)针对YZ做优化,因为已经有了overhead占用空间,此时可以不增加1byte,这样对第三项的对其就不用+1直接达到整数倍,就是4bytes。
  3. Alignment的限制:聚合的结构体大小会受到alignment对齐的限制,32位计算机alignment是4bytes使得bus运输量达到最高效率。因此类空间会被补全到alignment的整数倍,padding大小为3bytes。因此这两个类的大小:4+1+3=8bytes。

对于A,其virtual base class subobject只会在dervied class中寸一份实例,实际大小是X的1+Y的5减去重复的X的1为4+同理Z的4+补全3=12。如若编译器优化,整个A的object是有实际空间大小的,则不需要对X进行一个char的补充,总体是8不需要补全。

我的理解subobject:文中提到的object表示这一个对象完整的内存占用空间,而subobject只这个空间的一部分,只在继承时出现,比如A的object实际上就有Y和Z的subobject,同时还有唯一的X subobject,最后是A object=X subobject+Y subobject+Z subobject+A独有的空间

一个object大小取决于:所有nonstatics data member+针对virtual的增加内容+内存alignment边界调整需要

Data member的绑定

对成员函数本身的分析(evaluate)会直到整个类的声明都出现了才开始。所以类的成员函数可以引用声明在后面的成员,C 语言做不到。由于函数内的分析时类声明已经完成,对于全局变量、成员变量、局部变量均已被声明,可按照正常作用域范围实现绑定(早期的问题:成员变量声明在函数后会绑定到全局变量)

对于类型定义typedef:在成员函数的参数使用被定义类型时,不会等到函数class声明完成,若class前声明了typedef内容,成员函数后在class内同样声明了内容,那么成员函数的参数的实际类型,按照外部的声明执行,并不会等class完全声明后再解析。

对于typedef声明,若全局的声明在class外,若class内部的如与全局重名需声明在class开头。或一切类内typedef均写在开头。

Data member布局

同一个access section(即private、public、protected等区段)中,members的排列只需要符合“较晚出现的members在object中有较高的地址”即可。各个members不一定连续排列,members之间可能由于alignment需要填补bytes。

同一个access下的所有members按照其声明顺序存储。

数据成员存取

静态

static data members被视为一个global变量,只在class生命范围内可见,且只有一个实例,存在全局域中。会被编译器提取到class外。每次操作static member时都会被内部转化为对该唯一extern实例的直接参考操作。

通过一个指针和通过一个对象来存取static member结论完全相同,都会按照类名::静态数据成员=XX调用。包括通过函数的返回值.静态数据成员也会被处理成:函数();类名::静态数据成员;

对于不同class具有同名static data members,会导致在全局域重名,编译器会进行name-mangling,此操作保证:

  1. 一个算法,推导出独一无二的名称。
  2. 万一编译系统需要和使用者交谈,那些独一无二的名称可以轻易地推导回原来的名称。

非静态

非静态数据成员在每一个class object内,只能通过显式或隐式的class object来获取。在成员函数中直接处理非静态成员,就回通过隐式对象(this指针表达)来完成读写,不写this->时就是隐式。

对非静态数据的读写,编译器通过把class object的起始地址,加上data member的偏移地址offset来获取。每一个非静态数据成员的offset在编译时期即可获知,即使member属于base class subobject也一样。因此,存取一个非静态成员,其效率和存取一个C struct member或nonderived class的member是一样的。

对于偏移offset,A a;a.x=0.0;被翻译成&a+(&A::x-1);。指向datamember的指针,其offset值总是被加上1,这样可以使编译系统区分出“一个指向data member的指针,用以指出class的第一个member”和“一个指向data member的指针,没有指出任何member”两种情况。

也就是当offset值为0的时候指向的就是“没有指出任何member”,为1的时候就是第一个class member,被-1操作以后分别是-1和0

对于通过指针进行存取,如果被操作成员是一个struct member、class member、单一继承、多重继承的情况下完全相同。当被操作对象是虚拟继承中的成员时会有变化,此时无法确定指针具体是那个class type,需要在执行期进行判断。

继承与数据成员

有继承,无多态

也就是无virtual。

出现在derived class中的base class subobject有其完整原样性。

对于一个object来说,其内每个subobject都是独立个体,分别进行内存的对其补全,也就是一个baseclass只有一个char mamber,其占用空间为4,当前也有一个char,总占用就是8,不会将不同subobject空间压缩。

为了避免:子类对象=父类对象,这样的赋值过程中如果出现压缩,可能会导致父类对象中补全空间的值覆盖掉子类对象中“相同位置已经被压缩填充其他subobject内容”的空间

有多态

有虚函数

父类有虚函数,子类继承。此时布局结构是 父类subobject中包含vptr和data member以及 子类的subobject只包含自己的datamember构成整体的object。一共一个vptr。

涉及到vptr放到哪的问题,放在table首位。首部则无法支持对c struct的继承,尾部可以。在父类无虚函数、子类有虚函数的情况下,如果vptr无论何时都在object的首部,将子类类型指针改为父类时需要编译器介入,处理当前object头部为vptr,而父类无虚函数的问题。

多重继承

按照集成顺序存放subobject,若多个base均有vptr,则最后的object就有多个vptr,不会整合vptr。仍然是保证“出现在derived class中的base class subobject有其完整原样性。”

对于base中数据成员的存取不需要额外的成本,仍然是一次offset运算,因为上述整合结构在编译器已经确定,每个member均有固定位置。

有些编译器做了优化,若第一个base无vptr,则会将后续有vptr的提前。

虚拟继承virtual inheritance

虚拟继承本质是在多重集成情况下,考虑到继承串链中涉及到重复的base class,以多重集成方式避免在object中出现重复的subobject

将object分为两个部分,一个不变区域,一个共享区域。不变区域存储常规的subobject,共享区域存储涉及到虚拟继承的subobject。对于不变区域符合前面的操作,对于共享区域需要有专用的存取方法,主要问题:

  1. 每一个object对其每一个virtual base class提供一个指针,这样object的大小会因虚拟父类数量而变化。
  2. 同时虚拟继承串联的增加会导致间接存取层次增加,因为每个objcet只存直接到虚拟继承产生的指针。

微软:每个object当有虚拟继承时增加一个指针,指向增加虚拟基类表,虚拟机类表存储每个虚拟基类的指针,解决第一个问题。

Sun:把虚拟基类的offset值存到vtbl虚函数表中。相应的vtbl的索引区分正负数,整数索引为虚函数,负数索引为虚拟基类offset。也解决了第一个问题,保证object大小一致。

virtual base class最有效的运用形式:一个抽象的virtual base class 没有任何data members。

上述策略中,对虚拟继承导致的存取,只体现在对每个虚拟基类的数据成员的操作上,没有数据成员就可避免成本。

指向类成员的指针

float Point3d::*p1 = &Point3d::x;

此时p1指向了Point3d类中x成员,但是p1存的值是x的实际位置相对于Point3d的偏移值,嗯,后面还有个加一。

假如没有vptr,x作为一个成员,那么这个值就是1。假如有vptr,且被编译器放在首位,这个值就是5(32位下一个vptr占用了4)

vc++对这个+1做了特殊处理,所以看到的是0和4。

对于加一的问题:
float Point3d::*p2 = 0;
如果不加1,那么p1和p2都是x的偏移值,无法识别是0还是第一个member

所以指向类成员的指针存的是相对于类的(偏移值+1),并不是内存的实际地址。当然仔细理解也是正常,因为获取的是class的他并没有实例化成object,也就是并没有实际空间,给出偏移就可以在实际空间存在后进行操作:

Point3d *pPointObject;pPointObject->*p1;

Techie亮博客,转载请注明:Coologic » 《深度探索C++对象模型》笔记(3)

  • 点赞
  • 收藏
  • 分享
  • 文章举报
Techie亮 发布了155 篇原创文章 · 获赞 2 · 访问量 1161 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: