The C++ Object Model
在C++ 中,有两种class data members:static和nonstatic,以及三种class member functions:static、nonstatic和virtual。
A Simple Object Model
简单模型中,member本身并不存放在object中,object中存放的是指向member的指针,这么做可以避免“members"有不同的类型的问题(不同类型的对象需要不同大小的存储空间),Object中的members是以slot的索引来进行寻址。
这个简单对象模型没有被应用于实际产品上,但是这里关于索引或slot数目的观念被应用到了C++的”指向成员的指针(pointer-to-member)“观念中。
A Table-driven Object Model
这个模型给对象有了一个统一的表达方式,将data member和member function分别存放到data member table和member function table中,一个object中就只存放这两个table的ptr。
这个模型也没有实际应用到真正的C++编译器身上,但是member function table这个观念对于virtual function的实现有着很大的借鉴作用。
The C++ Object Model
C++对象模型是从简单对象模型衍生而来,只不过对内存空间和存取时间做了优化。
Nonstatic data members:存放在object中,object之间各存各的,有几个object就会存几份
Static data members:存放在object之外,仅存一份
Static/Nonstatic function members:存放在object之外,仅存一份
Virtual functions:每个class会给每个virtual function都产生一个ptr,并将这些指向函数的ptr存放到一个表格中,这个表格就是vtbl(虚表)。每个class object也会被添加一个ptr,这个ptr指向vtbl,而这个ptr我们也称其为vptr,vptr的设定和重置都由每个class中的constructor、destructor和copy assignment运算符自动完成。每个class所关联的type_info object也经由virtual table被指出来,通常被存放在vtbl的第一个slot处。
该方法相比table-driven object model来说降低了一层间接性,nonstatic data members直接存放在object中,这使得存取时间降低,但是如果nonstatic data members有所修改,那么那些应用程序代码得重新编译,而表格驱动的模型就不需要。
Adding Inheritance
加上继承机制后,以上三种模型又该如何设计实现
Simple Object Model
简单对象模型中,每个base class可以被derived class object内的一个slot指出,该slot内含base class subobject的地址
缺点:存在间接性,导致空间和存取时间上有额外负担
优点:class object的大小不会因为base classed的改变而受影响
Table-driven Object Model
表格驱动模型中,在原来两个data member table和member function table的基础上新加一个base class table,这个新加的base class table中每个slot内含有一个相关的base class地址,很像vtbl内含每个virtual funciton的地址一样。
缺点:存在间接性,导致空间和存取时间上有额外负担
优点:每个class object对于继承都有一个统一一致的表现方式,都是在object中新增一个ptr指向base class table。此外,base class table的修改调整并不会对class objects本身有任何影响
以上两种模型的继承都存在间接性,而间接性的级数会随着继承的深度而增加。
C++ Object Model
C++最初采用的继承模型并不会运用任何间接性:base class subobject和nonstatic data members一样直接放置在derived class object中。
优点:这提供了对base class members最紧凑而且最有效率的存取
缺点:base class members的任何改变都会使得用到该base class或其derived class的部分,都必须重新编译
C++2.0起新导入了virtual base class,实现了具备间接性的base class表现方法,virtual base class的原始模型是在class obejct中为每一个有关联的virtual base class加上一个指针。
How the Object Model Effects Programs
下面举例来说明,class X中定义了一个copy constructor,一个virtual destructor,和一个virtual function foo
X foobar() { X xx; X *px = new X; // foo()是一个virtual funciton xx.foo(); px->foo(); delete px; return x; }; // 可能的内部转换结果 // 虚拟C++代码 void foobar(X& _result) { // 构造_result // _result 取代local xx _result.X::X(); // 扩展 X *px = new X; px = _new(sizeof(X)); if (px != 0) px->X::X(); // 扩展 xx.foo() 但不使用virtual机制 // 以 _result来取代 xx foo(&_result); // 使用virtual机制扩展px->foo() (*px->vtbl[2])(px) // 扩展delete px if (px != 0) { (*px->vtbl[1](px)); // destructor _delete(px); } // 不需使用named return statement // 不需要摧毁local object xx return; }
从上面的代码可以看到,我们自己实现的函数会被编译器进行转化,差异十分巨大。
An Object Distinction
C++程序设计模型直接支持三种程序设计典范programming paradigms
1、程序模型:就像C一样,C++也支持,例如字符串处理
char boy[] = "Danny"; char *p_son; ...; p_son = new char[strlen(boy) + 1]; strcpy(p_son, boy); ...;
2、抽象数据类型模型(abstract data type ADT):该模型所谓的“抽象”是和一组表达式一起提供。例如:
String girl = "Anna"; String daughter; daughter = girl; if (girl == daughter) ***;
3、面向对象模型(object-oriented OO):类型之间存在关联,通过一个base class来进行封装,就是我们理解中的多态。我们一般在base class中定义好方法,其余子类中进行实现,我们都是通过base class object来调用方法。
最好只使用一种paradigm来写程序,如果混合不同的paradigm就可能会带来不好的后果,举例来说:
一个base class Library_materials和该类的子类Book
Library_matrials thing1; Book book; thing1 = book; thing1.check_in();
我们会发现thing1调用的是base class的方法,而非Book的方法。thing1的定义和运用并非是了OO的习惯,它体现的是一个ADT paradigm的设计准则。
只有通过ptr或reference的间接处理才支持OO程序设计中的多态性。
Library_materials& thing2 = book; thing2.check_in(); // 这就能够调用Book类中的方法
在OO paradigm中,程序员处理的是一个未知的对象,这个对象的类型虽然有被界定,受限于这个类的继承体系,但是object的真实类型在每一个特定的执行点之前是无法解析的,在C++中,只有通过ptr和references操作才能够完成。而在ADT paradigm中程序员处理的是一个拥有固定而单一类型的实体,在编译期间就决定了这个实体的信息。
// 描述objects,并不确定其类型 Library_materials *px = retrieve_some_material(); Library_materials& rx = *px; // 描述一个确定的实体 Library_materials dx = *px;
个人理解:指针的类型可以随着程序的运行发生变化;而一个实体,它的类型其实在编译阶段就已经确定,sizeof的大小,类型都是固定的,都是不可改变的。而引用本质和指针是一致的。
多态通过一个共同的接口来实现封装,这个接口就是定义在base class中,这个共享的接口通过virtual function机制来引发,在执行期间根据Object的真正类型来调用指定的函数实体。
优势:例如我们在设计Library_materials时,base class为Library_materials,subtype为Book、Video、Puppet等,当我们后续新增类型Magzine时,如果没有多态的方法,我们就需要对代码进行调整,使得所有类型之间能够互通,确保兼容。
一个class object需要多少内存?
1、nonstatic data member的总和
2、加上任何需要alignment的需求而进行填充的空间
3、支持virtual而内存产生的额外负担
指针的类型
指针的类型就是教导编译器如何取解释某个特定地址中的内容和大小。
比如int*就表明取该地址中(32位机器上)4byte范围中的内容,类型为整型;String是传统的8bytes,包含一个字符指针和表示字符串长度的整数。
Add Polymorphsim加上多态后
我们定义ZooAnimal作为base class,而Bear作为derived class。
class Bear : public ZooAnimal { public: Bear(); ~Bear(); // ... void rotate(); virtual void dance(); // ... protected: enum Dances {...}; Dances dances_known; int cell_block; }; int main() { Bear b("Yogi"); Bear *pb = &b; Bear& rb = *pb; ZooAnimal *pz = &b; }
首先pointer和reference都只需要一个word的空间,32位的机器就是4bytes,因此pb和rb都占据4bytes空间,而b需要使用24bytes,ZooAnimal类中nonstatic的member data和vptr以及自身的nonstatic member data
那么一个采用多态方法,类型为ZooAnimal的指针pz指向Bear对象和本身类型为Bear的指针指向Bear对象有何差别?
也就是pb和pz指针解析出的内容的差别,差别在于pb能够解析出整个Object中24bytes的信息,而pz只能解析出Bear Object中ZooAnimal subobject部分的信息。
我们不能通过pz来对Bear Object中的dances_known和cell_block成员变量进行查看或是修改操作。唯一例外的就是virtual机制。
pz->rotate();这行代码在编译时期,决定了以下两点:
1、固定可用接口,pz只能够调用ZooAnimal的Public接口
2、该接口的access level,rotate是ZooAnimal的一个public member
pz所指的object类型可以决定rotate所调用的实体。类型信息并不是维护在pz中,而是维护在link中,这个link在object的vptr所指向的virtual table中。
Bear b; // 这里会直接引起切割sliced ZooAnimal za = b; // 这里调用的是ZooAnimal::rotate() za.rotate();
这里我们可以分析两个问题
1、为什么za.rotate调用的是ZooAnimal实体而非Bear实体?
因为za只可能是ZooAnimal类型的,它的类型不可能发生变化。直接存取objects的方式并不具备多态性的前提,这个object可能是一个以上的类型。
2、如果初始化函数将一个object内容完全拷贝到另一个object中去,为什么za的vptr不指向Bear的virtual table?
ZooAnimal za = b;这里其实可以分成两步骤:
首先是za作为ZooAnimal类型的初始化,后续是将b中的值赋值给za,C++编译器在两个操作之间进行了仲裁,编译器必须确保这个object中 含有的一个或一个以上的vptr的内容不会被改变。也就是说za存在初始化过程,vptr已经被确定,后续的赋值操作并不会进行vptr的拷贝。
{ ZooAnimal za; ZooAnimal* pza; Bear b; Panda *pp = new Panda; pza = &b; }
我们看到这里将za或b或是pp所含的内容指定为pza,这不会有任何问题,因为pza只是一个指针,一个ptr或refernce之所以支持多态,是因为它们并不会引发内存中任何“与类型有关的内存委托操作(type-dependent commitment)”;指针类型发生改变只会改变它们指向的内存的“大小和内容的解释方式”而已。
将b赋值给object za,那么就违反了object中限定的类型约束,占用的内存大小不一致,复制给za的数据量超出了za类型指定的数据量大小,那么就会发生切割sliced的现象,执行结果自然就会出问题。
多态的实现要求一个Object需要在执行阶段根据指向内存的虚表来确定调用的方法是什么,但是一个严格的编译器也可以通过在编译时期分析出该object的调用操作方法具体是什么,回避virtual机制,将virtual function定义inline的,这可以让性能得到很大的提升。但是目前并不支持虚函数为inline,回避virtual机制这是编译器的优化工作。