本文主要基于Stanley.B.Lippman的《Inside the C++ Object Model》(侯捷译)而做的一些归纳(文字大多按照译文,进行了适当精简)
前言
在C中,“数据”和“处理数据的操作(函数)”是分开声明的,也就是说,语言本身并没有支持“数据和函数之间”的关联性。这种程序写法称作“程序性的(procedural)”,即所谓面向过程。
抽象数据类型(Abstract data type,ADT): 抽象数据类型 = 逻辑结构 + 抽象运算,也就是说抽象数据类型是建立在逻辑结构上的一些抽象的运算,比如C++中的string class,其支持 = ,+等运算。
数据抽象:用ADT描述程序处理的实体时,强调的是其本质的特征、其所能完成的功能,以及它和外部用户的接口。我们需要注意的是:这里在描述数据时忽略了数据的形态,而是一种数据抽象,且不会涉及到高级程序语言中的具体实现和存储。
数据封装:将实体的外部特征和内部实现细节分离,并且对外部用户隐藏其内部实现细节(即抽象数据类型内部的各种定义),对于用户来说只需要使用即可,不需要知道内部的具体实现细节。
加上封装后的布局成本,并没有增加!C++在布局以及存取时间上的主要额外负担是由virtual机制引起的,包括virtual function机制和virtual base class。
一、C++对象模式
在C++中,有两种类数据成员,即static(静态成员变量)和nonstatic(非静态成员变量),以及三种类成员函数,static(静态成员函数)、nonstatic(非静态成员函数)、virtual(虚函数)。
简单对象模型:最基本的模型,为了尽量减低C++编译器的设计复杂度而开发,赔上空间和运行期效率。此模型中,一个object是一些列的slots,每一个slots指向一个members,members按其声明顺序,各被指定一个slot。每个数据成员或者成员函数都有一个自己的slot。这个模型并未应用于实际产品,但关于索引及slot个数的观念被应用于C++“指向成员的指针”观念中。
表格驱动对象模型:为了对所有类的所有object都有一致的表达形式,这种模型把所有与成员相关的信息抽取出来,放在一个成员函数表和一个成员数据表内。Class object本身则内含指向这两个表格的指针。成员函数表是一系列的slots(槽),每个slots对应一个成员函数,而数据成员表则直接持有数据本身。此模型虽然没有用于真正的C++编译器上,但成员函数表成为了虚函数的一个支持方案。
C++对象模型:最初设计的(目前仍有优势)C++对象模型是从简单对象模型派生来的。并对内存空间和存取时间做了优化。此模型中,nonstatic data members被配置与每一个class object之内,static data members则存放在个别的class object 之外。static和nonstatic function members也被放在class object之外。虚函数则以两个步骤支持之------1、每一个类产生一堆指向虚函数的指针,放在表格中,被称作虚函数表(vtbl) ; 2、每个class object被安插一个指针,指向相关的虚函数表,这个指针被称作vptr。
不同的对象模型,会导致“现有的程序代码必须修改“以及”必须加入新的程序代码“两个结果。
二、关键词所带来的差异
这里引入一个问题,什么时候一个人应该实用struct代替class?
实际上,class的真正特性是由声明本身所决定的。
策略性正确的struct:C的设计技巧有时候可能成为C++的陷阱!比如把单一元素的数组放在一个struct的末尾,于是每个struct object可以拥有可变大小的数组:
Struct muble{
Char pc[1]
};
处于同一个access section的数据,必定保证以其声明顺序出现在内存布局中。而放置在多个access section内的数据,排列顺序就不一定了。同理,基类和派生类的数据成员布局也没有谁先谁后的强制规定,因此也不保证前述C的技巧有效。虚函数的存在也会给C的技俩的有效性成为一个问号。所以最好是不那么做!
C struct在C++中的一个合理用途,是当要传递“一个复杂的class object的全部或者部分“到某个C函数时,struct 声明可以将数据封装起来,并保证与C兼容得空间布局。然而这只在复合得情况下才存在。这种做法时,从C struct 中派生C++的部分:
Struct C_point{……};
Class point : public C_pont {…};
三、对象的差异
这里列举一些比较常见的范式:函数化程序设计、逻辑程序设计、语意数据模型、几何计算、数值计算、面向对象设计、原型设计、自然语言。
C++程序设计模式直接支持三种程序设计范式(programming paradigms):1.程序模型(procedural model) 2.抽象数据类型模型(abstract data type model) 3.面向对象模型(object-oriented model).
纯粹以一种范式写程序,有助于整体行为的良好稳固,但如果混合了多种范式,可能造成意想不到的结果。比如,虽然可以直接或间接处理继承体系中的一个基类对象,但只有通过指针或引用的间接处理,才能支持面向对象程序设计所需的多态性。在面向对象设计范式中,程序员要处理一个未知实例,它的类型虽然有所界定,却有无穷可能,原则上,被指定的对象的真实类型在每一个特定的执行点前,是无法解析的!在C++中,只有通过指针和引用的操作才能够完成,相反,在ADT范式中,程序员处理的实例是一个拥有固定而单一类型的实例,其在编译期就完全定义好了!
比如:
Librar_materials *px = retrieve_some_material();
Librar_materials &rx = *px;
Librar_materials dx = *px;
在这里,无法确定px或者rx到底指向何种类型的对象,只能说要么是一个Librar_maerials类的对象或者其子类对象,但是,可以确定dx只能是一个Librar_materials对象。
C++通过以下方式支持多态:
经由一组隐式转化操作,比如把一个派生类指针转化为一个指向其public base type的指针
Shape *ps = new circle();
经由虚函数机制
Ps->rorate();
经由dynamic_cast和type_id运算符
If(circle *pc = dynamic_cast<circle *>(ps))…
多态的主要用途式经由一个共同接口来影响指针的封装。这个接口通常被定义于一个抽象的基类中。
这里,引入一个大家可能比较关注的问题,即需要多少内存才能表现一个class object?一般而言要有:
- 其nonstatic data members 的总和大小
- 加上任何由于alignment的需求而填补上的空间(alignment就是将数值调整到某数的倍数。在32位机上,通常为4bytes,以使得bus的“运输量”达到最高效率)
- 加上为了支持virtual而由内部产生的任何额外负担(一个指针,不管指向任何类型,大小都是固定的)
指针的类型:指向不同类型之各指针间的差异,既不在于其指针表示法不同,也不在于其内容(仅存储地址)不同,而是在其所寻址出来的object类型不同,也就是说,指针类型会教导编译器如何解释某个特定地址中的内存内容及其大小。
比如,一个指向地址1000的整数指针,其在32位机上,将涵盖地址空间1000~1003(int是4位)。如果String是传统的8-bytes(包括一个4-bytes的字符指针和一个用来表示字符串长度的整数),那么一个ZooAnimal指针将横跨地址空间1000~1015(4位int,8位string,4位虚表指针)。那么,一个类型是void的指向地址1000的指针,将涵盖怎样的地址空间呢?这是未知的,所以这就是为什么一个类型为void的指针只能拥有一个地址,而不同通过这个地址操作所指之object!
加上多态后:现在,定义一个Bear,作为一种ZoomAnimal(假设ZoomAnimal对象大小为16bytes)。
假设Bear object放在地址1000处,那么一个ZoomAnimal的指针与Bear的指针区别是什么呢?---------它们每个都指向Bear Object的第一个byte,差别是,Bear的指针对应的地址涵盖整个Bear object,而ZoomAnimal的指针涵盖范围是Bear Object中的ZoomAnimal subobject(即属于基类的成分,注意,虚表指针也属于基类的成分!)。
但如果不是以指针或者引用的方式来获取Bear object,那么会引发一些问题。
Bear b;
ZoomAnimal za = b;
Za.rotate; //这里调用的不是bear的rorate而是zoomAnimal的
在这里za也是一个Bear,而是个ZoomAnimal!!!!因为多态并不支持“直接存取object”这事之上(需要使用指针或者引用)。但如果以这样的方式:ZoomAnimal *za =Bear b就可以使用多态了!
一个指针或者一个引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作”,会受到改变的,只有它们所指向的内存的“大小和内容解释方式”而已。C++通过指针或者引用来支持多态,这种程序设计风格就叫做面向对象!
C++也支持具体的ADT程序风格,如今被称为object-based(OB),例如string class就是一种非多态的数据类型。一个OB设计可能比一个对等的OO设计速度更快且空间紧凑。速度快事因为所有的函数调用操作都在编译器完成(一般编译期所完成的工作份额比例越大,速度越快),对象构建起来不需要设置virtual机制;空间紧凑则是因为每一个class object不需要负担传统上为了支持virtual机制而需要的额外负荷,不过OB设计比较缺少弹性。程序设计往往在弹性(OO)和效率(OB)间做抉择。