一、C++对象模型
在C++中,有两种数据成员:static和nonstatic,以及三种成员函数:static、nonstatic和virtual。已知下面这个类point声明:
怎样在机器中表现这个类point呢?
1.1、简单对象模型
在简单对象模型中,一个object是一系列的slots,每一个slot指向一个成员。成员按其声明顺序,各被指定一个slot。每一个数据成员或函数成员都有自己的一个slot。
在这个简单模型下,成员本身并不放在object中。只有"指向成员的指针"才放在object内。这么做可以避免"成员有不同的类型,因而需要不同的存储空间"所招致的问题。object中的成员是以slot的索引来寻址的。
虽然这个模型并没有被应用于实际产品上,不过关于索引或slot个数的观念,被应用到C++的"指向成员的指针"(pointer to member)观念之中。
1.2、表格驱动对象模型
为了对所有类的所有对象都有一致的表达方式,表格驱动对象模型把所有与成员相关的信息抽出来,放在一个数据成员表和一个成员函数表中,类对象本身则内含指向这两个表格的指针。
虽然这个模型也没有实际应用于真正的C++编译器身上,但成员函数表这个观念却成为支持虚函数的一个有效方案。
1.3、C++对象模型
Stroustrup当初设计(目前仍占有优势)的C++对象模型是从简单对象模型派生而来的,并对内存空间和存取时间做了优化。在此模型中,nonstatic数据成员被配置于每一个类对象之内,static数据成员则被存放在类对象之外。static和nonstatic成员函数也被放在类对象之外。虚函数则以两个步骤支持:
- 1、每一个类产生出一堆指向虚函数的指针,放在表格中,这个表格被称为virtual table(vtbl)。
- 2、每一个类对象被安插一个指针,指向相关的virtual table。通常这个指针被称为vptr。每一个类所关联的type_info 对象也经由virtual table被指出来,通常放在此表格的第一个slot。
1.4、对象模型如何影响程序
不同的对象模型,会导致"现有的程序代码必须修改"以及"必须加入新的程序代码"两个结果。例如下面这个函数,其中class X定义了一个copy constructor、一个virtual destructor和一个virtual function foo:
这个函数有可能在内部被转化为:
由于类X有两个virtual functions,一个是destructor,一个是foo,所以X对象布局如下:
在被转化的代码中,px->_vtbl[0]指向X的type_info object,px->_vtbl[1]指向X::~X(),px->_vtbl[2]指向X::foo()。
二、对象的差异
需要多少内存才能够表现一个类对象呢?一般而言要有:
- 其nonstatic数据成员的总和大小
- 加上任何由于alignment的需求而填补上去的空间(可能存在于成员之间,也可能存在于集合体边界)
- 加上为了支持virtual而由内部产生的任何额外负担
2.1、指针的类型
一个指针,不管它指向哪一种数据类型,指针本身所需的内存大小是固定的。例如,有下面的ZooAnimal声明及变量定义:
其中的类对象za和指针pza的可能布局如下图所示:
但是,一个指向ZooAnimal的指针与一个指向整数的指针或一个指向template Array的指针有何不同呢?
以内存需求的观点来说,没有什么不同!它们三个都需要有足够的内存来放置一个机器地址。"指向不同类型之各指针"间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说,"指针类型"会教导编译器如何解释某个特定地址中的内存内容及其大小:
- 一个指向地址1000的整数指针,在32位机器上,将涵盖地址空间1000~1003
- 如果String是传统的8-bytes,那么一个ZooAnimal指针将横跨地址空间1000~1015
那么,一个指向地址1000而类型为void的指针,将涵盖怎样的地址空间呢?答案是不知道!这就是为什么一个类型为void的指针只能够持有一个地址,而不能够通过它操作所指对象的缘故。
2.2、加上多态
现在,让我们定义一个Bear,作为一种ZooAnimal。有如下类型定义和变量定义:
无论是pointer或reference都只需要一个word的空间(在32位机器上是4-bytes)。b、pb、rb的布局如下图所示。Bear object需要24bytes,也就是ZooAnimal的16bytes加上Bear所带来的8bytes。
假设我们的Bear object放在地址1000处,一个Bear指针和一个ZooAnimal指针有什么不同呢?
它们每个都指向Bear object的第一个byte。其间的差别是,pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear object中的ZooAnimal subobject。
除了ZooAnimal subobject中出现的成员,不能够使用pz来直接处理Bear的任何成员。唯一例外是通过virtual机制。
多态所造成的"一个以上的类型"的潜在力量,并不能够实际发挥在"直接存取objects"这件事情上,比如,将一个派生类对象赋值给一个基类对象时,派生类对象将会被切割以塞入较小的基类类型内存中。有一个似是而非的观念:面向对象程序设计并不支持对对象的直接处理。举个例子,下面这组定义:
其可能的内存布局如下图所示:
将za或b的地址,或pp所含的内容(也是个地址),指定给pza,显然不是问题。一个pointer或一个reference之所以支持多态,是因为它们并不引发内存中任何"与类型有关的内存委托操作(type-dependent commitment)";会受到改变的,只有它们所指向的内存的"大小和内容解释方式"而已。