一、什么是c++对象模型
- 语言中直接支持面向对象程序设计的部分。
- 对于各种支持的底层实现机制。
二、c++对象的布局成本
成员函数不占用成本
- member functions虽然再class的声明之内,却不在object之中。每一个non-inline member function只会诞生一个函数实例。 每一个“拥有零个或一个定义”的inline function则会在其每一个使用者(模块)身上产生一个函数实例。
c++在布局以及存取时间上主要的额外负担是由virtual引起的
- virtual function 机制用以支持一个有效的“执行期绑定”。
- virtual base class 用以实现“多次出现在继承体系中的 base class,有一个单一而被共享的实例”。
- 多重继承下的额外负担:发生在“一个derived class 和其第二或后继之base class的转换之间”
三、c++对象模式加上封装后的布局成本
在C++中,有两种class data members:static和nonstatic,以及三种class member functions:static、nonstatic和virtual。
3.1 简单对象模型
- 在这个简单模型中,一个object是一系列的slots,每一个slot指向一个members。Members按其声明顺序,各被指定一个slot。每一个data member或function member 都有自己的一个slot。
- 在这个简单模型下,members本身并不放在object之中。只有“指向member的指针”才放在object内。这么做可以避免“members有不同的类型,因而需要不同的存储空间”所招致的问题。
- Object中的members是以slot的索引值来寻址的。
- 一个class object 的大小很容易计算出来:“指针大小,乘以class中所声明的members个数”便是。
这个模型并没有被应用于实际产品上,不过关于索引或slot个数的观念,倒是被应用到C++的“指向成员的指针”(pointer-to-member)观念之中。
3.2 表格驱动对象模型
- 为了对所有classes的所有objects都有一致的表达方式,另一种对象模型是把所有与 members 相关的信息抽出来,放在一个 data member table 和一个 member function table之中,class object本身则内含指向这两个表格的指针。
- Member function table是一系列的slots,每一个slot指出一个member function;
- Data member table则直接持有data本身
虽然这个模型也没有实际应用于真正的C++编译器身上,但member function table 这个观念却成为支持virtual functions的一个有效方案。
3.3 C++对象模型
3.3.1 模型简介
Stroustrup当初设计(目前仍占有优势)的C++对象模型是从简单对象模型派生而来的,并对内存空间和存取时间做了优化。在此模型中,
- Nonstatic data members被配置于每一个class object之内,
- static data members则被存放在个别的class object之外。
- Static和nonstatic function members也被放在个别的class object之外。
Virtual functions则以两个步骤支持之: 1.每一个 class产生出一堆指向 virtual functions 的指针(即函数指针),放在表格之中。这个表格被称为 virtual table(vtbl)。 2.每一个 class object被安插一个指针,指向相关的 virtual table。通常这个指针被称为 vptr。vptr 的设定(setting)和重置(resetting)都由每一个 class的 constructor、destructor和 copy assignment运算符自动完成。每一个 class所关联的 type_info object(用以支持 runtime type identification,RTTI)也经由 virtual table被指出来,通常放在表格的第一个 slot。
3.3.2 模型优缺点
- 优点:空间和存取时间的效率;
- 缺点:如果应用程序代码本身未曾改变,但所用到的class objects的nonstatic data members有所修改(可能是增加、移除或更改),那么那些应用程序代码同样得重新编译。
3.3.3 加上继承的C++对象模型
- 继承方式
在虚拟继承的情况下,base class不管在继承串链中被派生(derived)多少次,永远只会存在一个实例(称为subobject)。 - 一个derived class 如何在本质上模塑其base clas s的实例呢?
- 在“简单对象模型”中,每一个base class可以被derived class object内的一个slot指出,该slot内含base class subobject的地址。这个体制的主要缺点是,因为间接性而导致的空间和存取时间上的额外负担,优点则是class object的大小不会因其base c lasses的改变而受到影响。
- 当然啦,你也可以想象另一种所谓的base table模型。这里所说的base class table被产生出来时,表格中的每一个slot内含一个相关的base class地址,这很像virtual table 内含每一个virtual function 的地址一样。每一个class object 内含一个bptr,它会被初始化,指向其base class table。这种策略的主要缺点是由于间接性而导致的空间和存取时间上的额外负担,优点则是在每一个class object 中对于继承都有一致的表现方式:每一个class object 都应该在某个固定位置上安放一个base table指针,与base classes的大小或个数无关。第二个优点是,无须改变class objects本身,就可以放大、缩小,或更改base class table。
不管上述哪一种体制,“间接性”的级数将因为继承的深度而增加。 C++最初采用的继承模型并不运用任何间接性:base class subobject 的 data members被直接放置于derived class object中。这提供了对base class members最紧凑而且最有效率的存取。缺点呢?当然就是:base class members的任何改变,包括增加、移除或改变类型等等,都使得所有用到“此base class或其derived class之objects”者必须重新编译。 自C++2.0起才新导入的virtual base class,需要一些间接的base class表现方法。 - Virtual base class的原始模型
添加一个类似虚表指针的虚基类指针:在class object中为每一个有关联的virtual base class加上一个指针。 - 其他演化出来的模型:
导入一个虚基类表。导入一个virtual base class table。 在虚表中扩充。扩充原已存在的virtual table,以便维护每一个virtual base class的位置。
3.3.4 对象模型如何影响程序
不同的对象模型,会导致“现有的程序代码必须修改”以及“必须加入新的程序代码”两个结果。例如下面这个函数,其中class X 定义了一个copy constructor、一个virtual destructor 和一个virtual function foo: 这个函数有可能在内部被转化为:
四、关键词struct 和class
- C++中凡处于同一个access section(public protected private 标识的段)的数据,必定保证以其声明顺序出现在内存布局当中。然而被放置在多个access sections中的各笔数据,排列顺序就不一定了。关于内存布局后续会详细说明。
- C struct在C++中的一个合理用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合(composition)的情况下才存在。如果是“继承”而不是“组合”,编译器会决定是否应该有额外的data me mbers被安插到base struct subobject之中。
- conversion 运算符提供了一个十分便利的萃取方法
五、c++三种程序范式
5.1 程序模型(procedural model)
指以函数的方式提供接口
5.2 抽象数据类型模型(abstract data type model,ADT)
指将事务抽象为一个class类型,通过设置public的访问权限提供接口
5.3 面向对象模型(object-oriented model)
在此模型中有一些彼此相关的类型,通过一个抽象的 base class(用以提供共同接口)被封装起来。
5.4 C++通过以下方法支持多态
1.经由一组隐式的转化操作。例如把一个 derived class 指针转化为一个指向其 public base type的指针 2.经由 virtual function机制 3.经由 dynamic_cast和 typeid运算符
5.5 需要多少内存才能表现一个class object?
- 其nonstatic data members 的总和大小
- 加上任何由于alignment的需求而填补上去的空间(可能存才与members之间,也可能存在于集合边界)。—— 内存对齐
- 加上为了支持virtial而由内部产生的任何额外负担。
5.6 指针的类型
“指向不同类型之各指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。 转换(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。
5.5 问题:
1.向上类型转换后虚表指针是如何处理的?按基类类型来解释指针或引用所指向内容的大小,虚表指针在派生类对象的最末端,那么基类的指针如何获取派生类的虚表指针呢? 2.对于将派生类赋值给基类时发生的截取情况:如果初始化函数将一个object内容完整拷贝到另一个object去,为什么za的vptr不指向Bear的virtual table? 编译器在初始化及指定(assignment)操作(将一个class object指定给另一个class object)之间做出了仲裁。编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化或改变。 3.将派生类赋值给基类时发生的截取情况,是如何赋值的,基类的虚表指针如何赋值?
|