以下有三种数据类型(也还可以包含第四种模版化的数据类型)
从struct Point3d到class Point3d加上封装后的布局成本增加了多少?
答案是class Point3d并没有增加成本。
三个数据成员直接内含在每一个类对象之中,就像C struct的情况一样。成员函数虽然含在class的声明之中,但不出现在每个对象中。每个非内联成员函数只会诞生一个函数实体。至于每一个“拥有零个或一个定义”的内联函数则会在每一个使用者(模块)身上产生一个函数实体。Point3d支持封装,这一点并未带给它任何空间或执行期的不良回应。
C++在布局以及存取时间上主要的额外负担
C++ 在布局以及存取时间上主要的额外负担是由virtual引起,包括:
virtual function 机制:用以支持一个有效率的“执行期绑定”
virtual base class:用以实现“多次出现在继承体系中的base class,有一个单一而被共享的实体”
还有一些多重继承下的额外负担,发生在“一个派生类和其第二或后继的基类的转换”之间。
C++对象模型
class Point {
public:
Point(float xcal);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostrem&os) const;
float _x;
static int _point_count;
};
简单对象模型
目的:减低C++编译器的设计复杂度
缺点:空间和执行期的效率低
每个对象是一系列的slot,每个slot指向一个成员。成员按其声明次序,各被指定一个slot。每个数据成员或成员函数都有自己的一个slot。
如下图:
对象中的_x索引为6,_point_count的索引为7,所以类对象的大小很容易算出来:“指针大小,乘以类中声明的成员数”。
表格驱动对象模型
目的:为了对所有类的所有对象都有一致的表达方式
把所有与成员有关的信息抽出来,根据类型分别放在一个数据成员表和成员函数表之中。而类对象本身则内含指向这两个表格的指针。
虽然这个模型没有实际应用于真正的C++编译器身上,但成员函数表这个观念却成为支持虚函数的一个有效方法。
这个模型又称之为双表模型。
C++对象模型
在此模型中,非静态数据成员被配置于每个类对象之内,而静态数据成员则被存放在所有的类对象之外。成员函数(包括静态和非静态)也被放在所有的类对象之外。
虚函数则以两个步骤支持之:
1、 每个类产生出一堆指向虚函数的指针,将这些指针放在一个表格之中。这个表格被称为virtual table(vtbl)。
2、 每个类对象被添加了一个指针,指向相关的virtual table。通常这个指针被称为vptr。vptr的设定和重置都由每个类的构造函数、析构函数和复制赋值运算符自动完成。每个类所关联的type_info对象(用来支持RTTI)也经由virutal table被指出来,通常是放在表格的第一个slot处。
优点:空间和存取时间的效率
缺点:如果应用程序代码本身未曾改变,但所用到的类对象的非静态数据成员有所修改(如增加、移除或更改),那么这些应用程序代码同样得重新编译。
base class table模型
class iostream:publicistream,public ostream
{ … };
class istream:virtualpublic ios { … };
class ostream:virtualpublic ios { … };
base class table被产生出来时,表格中的每个slot内含一个相关的基类地址,这很像virutal table内含每一个虚函数地址一样。每个类对象内含一个bptr,它会被初始化,指向其base class table。
优点:
1、 在每个类对象中对于继承都有一致的表现方式:每一个类对象都应该在某个固定位置上安放一个base table指针,与基类的大小和数目无关。
2、 不需要改变类对象本身,就可以放大、缩小或更改base class table。
缺点:由于间接性而导致空间和存取时间上的额外负担
C++最初采用的继承模型并不运用任何间接性:基类对象的数据成员被直接放置于派生类对象中。这样能对基类成员最直接的存取,缺点就是基类成员的改变会导致用到此基类或此派生类的对象都必须重新编译。
虚基类的原始模型是在类对象中为每一个有关联的虚基类加上一个指针。其他演化出来的模型则要么就是导入一个virtual base class table,要么就是扩充原已存在的virtualbase,以便维护每一个虚基类的位置。
对象模型如何影响程序
不同的对象模型,会导致“现有的程序代码必须修改”以及“必须加入新的程序代码”两个结果。如下面这个函数,其中class X定义了一个复制构造函数,一个虚析构函数和一个虚函数foo:
X foobar()
{
X xx;
X *px=new X;
// foo() 是一个虚函数
xx.foo();
px->foo();
delete px;
return xx;
};
这个函数有可能在内部被转化为:
// 可能的内部转换结果
// 虚拟C++码
void foobar(X&_result)
{
// 构造 _result
// _result 用来取代局部 xx …
_result.X::X();
// 扩展 X *px=new X;
px=_new(sizeof(X));
if(px!=0)
px->X::X();
// 扩展 xx.foo() 但不使用虚拟机制
// 以 _result 取代xx
foo(&_result);
// 使用虚拟机制扩展px->foo()
( *px->vbtl[2] ) (px)
// 扩展delete px;
if(px!=0) {
(*px->vtbl[1]) (px); // 调用析构函数
_delete(px);
}
// 不需要返回
// 不需要摧毁局部对象xx
return;
};
用下图解释上述代码,由于X有两个虚函数,一个是析构函数,一个是foo,所以X对象布局如下:
前述代码中的 px->_vbtl[0] 指向X的type_info对象,px->_vbtl[1]指向X::~X(),px->_vbtl[2]指向X::foo()。数据以其声明次序出现在内存布局当中。
关键词带来的差异
struct和class有什么区别?
在C++中这两个关键词本身没有差异,只是观念上有差异,认为struct主要是数据集合体,实现的是C的数据萃取观念,而class实现的是C++的ADT观念,事实上都导入了ADT。
C程序员的巧计有时候却称为C++程序员的陷阱。例如把单一元素的数组放在一个struct的尾端,于是每个struct对象可以拥有可变大小的数组:
struct mumble {
char pc[1];
};
// 从档案或输入装置中取得一个字符串,然后为struct本身和该字符串配置足够的内存
struct mumble *pmumbl=(struct mumble*)malloc(sizeof(structmumble)+strlen(string)+1);
strcpy(pmumbl->pc,string);
如果改用class来声明,而该class是:
指定多个accesssections,内含数据;
从另一个类派生而来
定义有一个或多个虚函数;
那么或许可以顺利转化,但也许不行。
C++ 中凡处于同一个access section的数据,必须保证以其声明次序出现在内存布局当中。然而被放置在多个access section中的各批数据,排列次序就不一定了。同样的道理,基类和派生类的数据成员的布局也没有谁先谁后的强制规定,因为也就不保证前述的C伎俩一定有效。
如果一个程序员迫切需要一个相当复杂的C++class的某部分数据,使他拥有C声明的那种样子,那么那一部分最后抽取出来称为一个独立的struct声明。将C与C++组合在一起的做法就是,从C struct中派生出C++的部分:
struct C_point { … };
class Point : public C_point { … };
这种习惯用法已不再被推荐,因为某些编译器(如MicrosoftC++)在支持virtualfunction的机制中对于类的继承布局做了一些改变。组合,而非继承,才是把C和C++结合在一起的唯一可行方法(转换运算符提供了一个十分便利的萃取方法):
struct C_point { … };
class Point {
public:
operatorC_point() { return _c_point; }
// …
private:
C_point_c_point;
//…
};
C struct在C++的一个合理用途,是当你要传递“一个复杂的类对象的全部或部分”到某个C函数中去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合的情况下才存在。如果是继承而不是组合,编译器会决定是否应该有额外的数据成员被安插到基类对象之中。
多态
classLibrary_materials { … };
class Book : publicLibrary_materials { … };
Library_materialsthing1;
Book book;
// thing1不是一个Book,book被裁切了,不过thing1仍保有一个Library_materials
thing1=book;
// 调用的是Library_materials::check_in()
thing1.check_in();
// 描述对象:不确定的类型
Library_materials*px=retrieve_some_material();
Library_materials*rx=px;
// 描述已知物:不可能有令人惊讶的结果产生
Library_materialsdx=*px;
无法知道px或rx到底指向何种类型的对象,只能够说它要不是Library_materials对象,要不是后者的一个子类型。不过,可以确定的是,dx只能是Library_materials类的一个对象。
“对于对象的多态操作”要求此对象必须可以经由一个指针或引用来存取,然而C++的指针或引用的处理却不是多态的必要结果。
// 没有多态(因为操作对象不是类对象)
int *pi;
// 没有语言所支持的多态(因为操作对象不是类对象)
void *pvi;
// ok:类x视为一个基类(可以有多态的效果)
x *px;
在C++,多态只存在与一个个的public class体系中。举个例子,px可能指向某个类型,也可以指向根据public继承关系而衍生的一个子类型(请不要把不良的转型操作考虑在内)。Nonpublic的派生行为以及类型为void*的指针可以说是多态,但它们并没有被语言明白地支持,也就是说它们必须由程序员通过明白的转型操作来管理(你或许可以说它们并不是多态对象的一线选手)。
C++ 以下列方法支持多态:
1、 经由一组隐含的转化操作。例如把一个派生类指针转化为一个指向其public base type的指针:
shape *ps=new circle();
2、 经由 virtualfunction 机制:
ps->rotate();
3、 经由dynamic_cast 和 typeid 运算符:
if(circle *pc=dynamic_case<circle*>(ps)) …
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base class中。
需要多少内存才能够表现一个类对象?
一般而言要有
l 其非静态数据成员的总和大小;
l 加上任何由于边界对齐的需求而填补上去的空间(可能存在与成员之间,也可能存在于集合体边界)。
l 加上为了支持virtual而由内部产生的任何额外负担(overhead)。
指针的类型
class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
// …
virtual void rotate();
protected:
int loc;
String name;
};
ZooAnimal za(“Zoey”);
ZooAnimal*pza=&za;
类对象za和指针pza的可能布局如下图:
一个指向ZooAnimal的指针是如何地与一个指向整数的指针或一个指向template Array的指针有所不同呢?
以内存需求的观点来说,没有什么不同,都是使用足够的内存来放置一个机器地址(通常是个word)。
“指向不同类型的各类指针”之间的差异即不在其指针表示法不同,也不再其内容(代表一个地址)不同,而是在其寻址出来的对象类型不同。
也就是说,“指针类型”会教导编译器如何解释某个特定地址的内存内容及其大小。
转型(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方法。
加上多态之后的指针类型
class Bear : public Zooanimal {
public:
Bear();
~Bear();
//...
voidrotate();
virtualvoid dance();
//…
protected:
enumDances { … };
Dancesdances_kown;
intcell_block;
};
Bear b(“Yogi”);
Bear *pb=&b;
Bear &rb=*pb;
b、pb、rb的内存布局如下:
Bear b;
ZooAnimal *pz=&b;
Bear *pb=&b;
pz->cell_block; // 不合法,cell_block不是ZooAnimial的成员
((Bear*)pz)->cell_block; //ok:经过显式的downcast操作就没有问题
// 下面这样更好,但它是一个run-timeoperation(成本较高)
if(Bear *pb2=dynamic_case<Bear*>(pz))
pb2->cell_block;
// ok:因为cell_block是Bear的一个成员
pb->cell_block;
当我们写:
pz->rotate();
时,pz的类型将在编译时期决定以下几点:
l 固定的可用接口。也就是说,pz只能够调用ZooAnimal的public接口。
l 该接口的accesslevel(例如rotate() 是ZooAnimal的一个public成员)。
在每一个执行点,pz所指向的对象类型可以决定rotate() 所调用的实体。类型信息的封装并不是维护于pz之中,而是维护于link之中,此link存在于“对象的vptr”和“vptr所指向的virtual table”之间。
Bear b;
ZooAnimal za=b; // 会引起切割
// 调用ZooAnimal::rotate()
za.rotate();
为什么za的vptr不指向Bear的virtual table?
编译器在初始化和指定(assignment)操作之间做出了仲裁。编译器必须确保如果某个对象(这里指za)含有一个或一个以上的vptr,那些vptr的内容不会被源对象(这里即b)初始化或改变。
为什么rotate() 所调用的是ZooAnimal而不是Bear实体?
za并不是(也绝不会是)一个Bear,它是(并且只能是)一个ZooAnimal。多态的特性不能实际发挥在“直接存取对象”这件事情上。有一个似是而非的观念:OO程序设计并不支持对对象的直接处理。
其可能的内存布局如下图: