《深度探索C++对象模型》读书笔记(三)
2017-04-20 23:53
267 查看
3. The Semantics of 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
Viusal C++的结果:
sizeof(X) => 1 sizeof(Y) => 4 sizeof(Z) => 4 sizeof(A) => 8
对于类X:一个空类的大小并不是空,而是1byte,编译器安插一个char,使得同一类的两个对象在内存中可以配置独一无二的地址。
对于类Y和Z:这两个类同时虚继承一个类X,大小受三个因素影响:
语言本身造成的overhead(虚指针):当语言支持虚基类时,导致一些额外负担。派生类存在一个指针,它指向virtual base class subobject(虚基类子对象)或者一个存放前者的相关表格,表格里存放着虚基类子对象的地址或者其偏移位置offset。在例子中Y和Z这部分指针大小是4bytes(32位机器上,类的虚指针vptr一般是4bytes)。
编译器对于特殊情况提供的优化处理:因为Y和Z也是空类,和X一样,也有1byte的char,放在派生类的固定部分的尾端。(这是编译器对empty virtual base class的处理,不同编译器有不同实现。)
Alignment的限制:一般聚合的结构体大小会受到alignment对齐的限制,是的能够有效率在内存中存取。32位计算机alignment是4bytes使得bus运输量达到最高效率。因此类空间会被补全到alignment的整数倍,padding大小为3bytes。因此这两个类的大小:4+1+3=8bytes。
对于类A:(不管virtual base class subobject在class继承体系出现多少次,只会在derived class中存在一份实例)
Y和Z共享的唯一一个X实例,大小为1byte
基类Y的大小,减去“因virtual base class X而配置”的大小(应该是指针的大小),剩下4bytes。Z同理。
A自身大小0byte,对齐补全3bytes,因此得到1+4+4+3=12bytes。
注:书中多处提及的class subobject,应该是区别于class object,用来表示被其他class包含的object,如类包含中一个类的object作为其他类的成员,如类继承中基类的成员会被包含在派生类中。virtual base class subobject就是后者,指派生类中继承virtual base的那部分。(个人理解)
Empty virtual base class,只提供一个virtual interface,没有定义任何数据。新的编译器如Visual C++对此有特殊处理:空的虚基类被认为是派生类的最开头部分,不花费任何额外空间,派生类也就有了member,不需要原本为了empty class而安插的1byte大小的char。上述例子中的Y和Z就只剩下4byte虚指针的overhead,也就不需要3byte的padding来对齐,因此在Visual C++下的模型布局,Y和Z都是4个bytes。对于A,X的1byte被拿掉,只剩下Y和Z的4bytes,对齐补全的3bytes也不需要了,共计4+4=8bytes。
对于这两种编译器实现,如果虚基类X中至少有一个成员,那么两种编译器能得到完全相同的对象布局。即都没有因为空类X产生1byte的char以及对齐带来的padding。
讨论class的data members的存放模型:
nonstatic数据成员: 数据面向个别class object。C++对象模型以空间优化和存取速度优化的考虑来表现nonstatic数据成员,且保持和C语言struct的兼容性。把数据直接存放在每一个class object中,对于继承而来的非静态成员(不管是基类是否virtual)也一样。多个变量一般按声明的先后顺序存放。
static数据成员:数据面向整个class。静态数据存放在全局的data segment,不会影响具体class object的大小,不管class(直接产生或间接派生)实例化多少个object,静态数据只存在一份实例。即使该类没有任何object,它的静态数据也是存在的。(但一个模板类的静态数据成员行为有不同)
总结:一个对象的内存布局由三部分组成
nonstatic data members的总和大小;
编译器自动加上额外的数据成员的overhead,支持语言特性,主要是各种virtual特性(虚指针);
边界对齐的需要填补padding的空间。
3.1 数据成员的绑定
对成员函数本身的分析(evaluate)会直到整个类的声明都出现了才开始。所以类的成员函数可以引用声明在后面的成员,C 语言做不到。typedef int length; class Point3d{ public: void f1(length l){ cout << l << endl; } typedef string length; void f2(length l){ cout << l << endl; } }; //f1绑定的length类型是int;而f2绑定的length类型才是string。
但类中的typedef并不具备这个性质,类中的typedef会受到函数与typedef的先后顺序的影响。因此,对于typedf需要防御性的程序风格:始终把“nested type声明”即typedef放在类的起始处。
3.2 数据成员的布局
正如开头例子总结的对象数据成员layout:Nonstatic data members在class object中的排序顺序和其被声明的顺序一样,任何中间接入的static数据不会放进对象布局中。
同一个access section(即private、public、protected等区段)中,members的排列只需要符合“较晚出现的members在object中有较高的地址”即可。各个members不一定连续排列,members之间可能由于alignment需要填补bytes。
编译器会合成内部使用的data members,支持整个对象模型。如vptr,编译器会把它安插在每一个含有虚函数的类的对象内,一般在开头或者结尾。
3.3 数据成员的存取
一个例子,通过object存取和指针存取数据成员,有什么重大区别呢?Point3d origin, *pt = &origin; origin.x = 0.0; ptr->x = 0.0;
当类型Point3d是一个继承体系中有虚基类的派生类,并且 x 成员又是虚基类中的成员时,这两种写法在编译器看来就会有重大的区别了(这种情况下的 offset 计算方式不同,其它情况的offset 也可以直接算出)。
如果用origin对象来存取,就确定是Point3d class,即使它继承自虚基类,成员x的offset在编译期就确定了,可以静态通过origin存取x。但是对于ptr来说,由于无法确定ptr指向的真正类型(多态?动态绑定,运行时确定),所以只能借由一个运行时的间接来得到成员的具体地址。
静态数据成员
每一个静态数据成员只有一个实例,存放在程序的data segment之中,视为全局变量,但只在class的生命范围之内可见。每次程序读写静态成员(无论是通过对象还是通过指针),都会被内部转化为对改唯一extern实例的直接引用来操作。此时从指令执行的角度看,通过对象和通过指针读写member,没有区别。因为静态成员不在class object之中,读写静态成员并不需要通过class object。
对静态成员取地址,会得到一个指向其数据类型的指针,而不是指向其class member的指针(对非静态成员取地址就是了???)。对于class的不同object,其静态成员的地址是相同。
非静态数据成员
非静态数据成员在每一个class object内,只能通过显式或隐式的class object来获取。在成员函数中直接处理非静态成员,就回通过隐式对象(this指针表达)来完成读写。对非静态数据的读写,编译器通过把class object的起始地址,加上data member的偏移地址offset来获取。每一个非静态数据成员的offset在编译时期即可获知,即使member属于base class subobject也一样。因此,存取一个非静态成员,其效率和存取一个C struct member或nonderived class的member是一样的。
对于虚继承,如果一个非静态成员是虚基类的成员,读写该变量的速度回比它是struct member、class member、单继承或者多继承的成员要慢。一般而言,具体继承(相对于虚继承)并不会增加时间和空间上的负担,书中也强调多次:C++中的额外成本大多来自于 virtual 机制。
3.4 继承&数据成员
这里讨论C++几种继承模型下的数据成员存储分布。A. 没有多态的单继承
以及
B. 加上多态的单继承
在继承关系中提供虚函数接口,支持多态会带来的 4 个额外负担:导入vtbl用来存放每一个virtual functions的地址。这个表的元素数目一般是被声明的virtual functions数目,再加上一个或两个 slots(用以支持 RTTI)。
在每一个class object中安插一个vptr指向相应的vtbl,实现运行时的动态绑定,使得每一个object能找到相应的vtbl。
在constructor中安插代码以正确设置vptr初值,指向class对应的vtbl。
在destructor 中安插代码以正确设置vptr,使它能抹消指向class相关的vtbl。
C. 多继承
单一继承提供了“自然多态”形式,是关于类体系中base type和derived type之间的转换。base class和derived class的object都是从相同的地址开始,只是derived object还要容纳自己的非静态数据。当用基类指针或引用指向派生类对象时,这个操作不需要编译器去介入,可以很自然操作,提供最佳执行效率。当单一继承带有虚函数,即基类没有虚函数,派生类有虚函数,单继承的“自然多态”会被打破。当基类指针指向派生类时,需要编译器介入调整地址(因为插入了vptr)。
class Point2d { ... }; class Point3d : public Point2d { ... }; class Vertex { ... }; class Vertex3d : public Point3d, public Vertex { ... }; Vertex3d v3d; Point2d *p2d; Point3d *p3d; Vertex *pv;
对于上述多继承的例子,Point3d是Vertex3d的第一基类,Vertex是第二基类。
- 当用第一基类的指针p2d或p3d指向派生类的对象v3d,和单继承一样,只需要简单拷贝地址即可。
- 当用第二基类的指针pv指向派生类的对象v3d,则需要修改地址offset,加上(或减去)介于中间的base class subobject(s)的大小。
pv = &v3d; // => pv = (((Vertex*)(((char*)&v3d) + sizeof(Point3d));
多重继承中,可能会有多个 vptr 指针,视其继承体系而定:**派生类中vptr的数目最等于所有基
类的vptr数目的总和。**
D. 虚继承
iostream的继承层次有用到虚继承,实现共享继承。class ios {...}; class istream : public virtual ios { ... }; class ostream : public virtual ios { ... }; class iostream : public istream, public ostream { ... };
通过虚继承实现共享基类,这个共享必须通过编译器安插的一些指针指向virtual base classobject来间接的存取,这样才能够实现共享。
对于这个安插指针来实现共享的技术,有两种主流的做法:Pointer Strategy和Virtual Table Offset Strategy,产生的数据布局如下两图所示。
Pointer Strategy:对每个虚继承的派生类对象,安插一个指针指向基类,实现共享。如Point3d和Vertex中的指针pPoint2d,Vertex3d中的两个subobject中的两个指针pPoint2d。
Virtual Table Offset Strategy:在每个虚继承的派生类相关的vtbl中,放置virtual base class的offset(而不是地址,记录虚基类的位置 )和virtual function的地址。
相关文章推荐
- 《深度探索C++对象模型》读书笔记之Data语意学
- 读书笔记 《深度探索c++对象模型》 (5)
- 《深度探索C++对象模型》读书笔记(2)
- 《深度探索C++对象模型》读书笔记
- 《深度探索C++对象模型》读书笔记(4)
- 《深度探索C++对象模型》读书笔记第二章:构造函数语意学
- 《深度探索C++对象模型》读书笔记(2)。
- 《深度探索C++对象模型》读书笔记(一)
- 《深度探索C++对象模型》读书笔记之Function语意学
- 读书笔记 《深度探索c++对象模型》 (1)
- 读书笔记 《深度探索c++对象模型》 (2)
- 《深度探索C++对象模型》读书笔记(5)
- 《深度探索C++对象模型》读书笔记第四章:Function语意学
- [C++]《深度探索C++对象模型》读书笔记 - nontrivial default constructor
- 《深度探索C++对象模型》读书笔记2
- 《深度探索C++对象模型》读书笔记之构造、析构、拷贝语意学
- 读书笔记 《深度探索c++对象模型》 (3)
- 《深度探索C++对象模型》读书笔记(4)
- 《深度探索C++对象模型》读书笔记(5)
- 深度探索C++对象模型》读书笔记(2)