第一章 关于对象(Object Lessons)
C++类加上封装后的成本:这里直接给出答案,C++的封装并没有增加许多成本,事实上C++的布局以及存取时间主要的额外负担是由virtual引起的。包括:
- virtual function机制,用以支持一个有效率的“执行期绑定”。
- virtual base class 用以实现“多次出现在继承体系中的base class,有一个单一而被共享的实例”。
一般而言,没有理由说C++程序比C兄弟庞大。
上面的结论会在之后的解释慢慢清晰明了。
1.1 C++对象模式(The C++ Object Model)
假设我们在C++中声明了如下一个类,其包括静态和非静态的数据成员及成员函数,以及virtual函数
class Point{
public:
Point(float xval);
virtual ~Point();
float x() const;
static in PointCount();
protected:
virtual ostream& print(ostream& os)const;
float _x;
static in _point_count;
};
我们来看看Point在机器中怎么被表现:
1.1.1 简单对象模型(A simple Object Model)
该简单模型很简单,一个object是一系列的slots,每一个slots指向一个members,每一个类成员都有自己的slot.在简单模型中members本身并不在object内,只有指向members的指针在object内,这么做避免了member是不同类型而需要不同大小空间的问题。同时简单模型的对象大小是容易计算的:指针大小乘以members个数。
要说明的是该模型没有用于实际产品,但是关于slot个数的概念倒是被引用到C++的“指向成员的指针”的概念中。
1.1.2 表格驱动模型(A Table-driven Object Model)
为了对所有classes的所有objects都有统一表达方式,该对象模型是把所有与members相关的信息抽取出来放在一个data members table和一个members function table中,class object本身则内含指向这两个表的指针。
要说明的是该模型也没有用于实际产品,但是member function table这个观念成为支持virtual funtionc的一个有效方案。
1.1.3 C++对象模型(The C++ Object Model)
该模型由上面两个模型演化而来,在该模型中:Nostatic data members被置于每一个class objects中,static data members则被放在class object之外,static 和 Nostatic funtion members也被放在class object之外。virtual funtion则由以下两个步骤支持:
- 每一个class产生一堆指向virtual functions的指针,放在表格之中,该表被称为virtual table(vtbl)
- 每一个class object被安插一个指针,指向相关的virtual table。这个指针被称为vptr。每一个class所关联的type_info object(用以支持 runtime type identification,RTTI)也经由virtual table被指出来,通常放在第一个slot.
1.1.4 加上继承(Adding Inheritance)
C++同时支持单重继承和多重继承,甚至也可以指定为虚拟继承(保证base class在继承链中永远只有一个实例),例如下面的iostream就只有一个virtual ios base class的实例。
我们来看看对象模型是如何支持这个继承关系的:
-
简单对象模型中:每个base class可以被每个drived class object内的一个slot指出。由于间接性指引,优点是class object的大小不会因为其base classes的改变而受影响。
-
类似virtual table的思想:每个base class table被产生出来时,表格中每一个slot内含一个相关地base class地址,每个class object内含一个bptr,它会被初始化指向其base class table。这使所有class object对于继承都有统一的表达方式(初始都只有一个指针),另外base calss table的改变不受class object的影响。
对于上述两种思想都会因为间接性的加上带来一些效率问题,但是想一想不用间接指引会发生什么?对于任何一个base class 的修改都会直接影响其他相关class导致代码需要重新编译。当然C++具体实现模型比这复杂地多,这里只是给一个直观感受。
1.1.5 对象模型如何影响程序(How the Object Model Effects Programs)
我们通过下面的示例性程序来看对象模型影响:
X foobar(){
X xx;
X *px= new X;
//foo是 一个virtual function
xx.foo();
px->foo();
delete px;
return xx;
}
这个函数可能在内部被转化为:(注意这里只是伪代码,看不懂没关系的,了解大概就行)
//可能的内部转换结果
//虚拟的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 statment
//无须摧毁local object xx
return;
}
我们用图片来稍微解释以下,有个大概直观感受
由于X有两个virtual functions,一个是destructor,一个是foo.所以X object的布局如上。上述代码中的px->vtbl[0]指向X的type_info object,
px->vtbl[1]指向X::~X(), px->vtbl[2]指向X::foo()。
1.2 关键词所带来的差异(A Keyword Distinction)
如果C++不是为了兼容C兄弟,C++要远比现在简单得多。
C++中绝不要以struct取代class,原因学完本书会了解。struct在C++中的作用大多是为了向旧C代码传递一个组合数据集。
1.3 对象的差异(An Object Distinction)
范式:一种环境设计和方法论的模型或范例;系统和软件以此模型来开发和运行。
C++直接支持3种程序设计范式:
-
程序模型(procedural model),就像C一样,C++当然很好的支持该范式,字符串的处理就是一个例子:
//procedural model式伪代码 char boy[] = "Danny"; char *p_son; ... p_son = new char[strlen(boy)+1]; strcpy(p_son,boy); ... if(!strcmp(p_son,boy)) take_to_disneyland(boy);
-
抽象数据类型模型(Abstract data type model,ADT),抽象和一组表达式是一起提供的,那时其运算定义可能仍不清晰,string Class就是一个例子:
//ADT式伪代码 String girl="Anna"; String daughter; ... //String::operator=(); daughter=girl; ... //String::operator==(); if(girl==daughter) take_to_disnneyland(girl);
-
面向对象模型(object-oriented model)在此模型中有一些彼此相关的类型,通过一个抽象的base class被封装起来,面向对象大家都知道,例子不给了。
纯粹的以某种范式写程序,有助于整体行为的良好稳固。但是混用不同范式可能会发生一些不同的错误。我们用下面示例代码来引入:
//完成多态时用到的两种范式
//ADT范式
Library_b thing1;
//Book:public Library_b
Book book;
thing1=book;//thing1不是一个Book,thing1被切割了,其保有Linrary_b
thing1.check_in();//调用的是Library_b的check_in()
//OO范式
Library_b &thing2=book;
thing2.check_in();//调用的是Book的check_in()
在ADT范式中,程序员处理的是一个固定而单一类型的实例,它在编译期就完全定义好了。
在OO范式中,程序处理的是一个未知实例,其类型受限于继承体系,原则上每个object的类型在某一个特定执行点前都是无法解析的,C++中通过pointer和reference操作来完成,再看一个例子印证上面的话:
//描述objects:不确定类型
Library_b *px=ret_some_book();
Library_b &rx=*px;
//描述已知事物:不可能有令人惊讶的结果
Library_b dx=*px;
//解释:我们没有办法说出px或rx具体是那种类型,只能说它要么是Library_b要么是其子类;
//不过我们可以确定dx一定是Library_b类型
当然C++中并不是所有pointer或reference都带来多态结果,例如:
//没有多态,因为操作对象不是class object
int* pi;
//没有多态
void* pi;
//有多态,操作对象是class object
X *px;
C++有以下方法支持多态:
-
经过一系列隐式转化,例如derived class的指针指向base class
shape *ps=new circle();
-
经由virtual function机制:
ps->rotate();
-
经过动态转换(cast)和typeid运算符:
if(circle *pc==dynamic_cast<circle* >(ps))...
本小节最后我们来看一个多态的例子:
class Z:public X;
void rotate(X d,const X *p,const &r){
//执行期前无法知道到底调用哪一个rotate()实例
*p.rorate();
r.rorate();
//总是调用X::rotate()
d.rotate();
}
main(){
Z z;
rotate(z,&z,z);//那个参数调用哪一个rotate()实例应该能清楚了吧?
return 0;
}
最后补充一个常识:指针大小是固定的,无论它指向什么对象。
1.3.1 指针的类型(The Type of a Pointer)
接上文,既然指针都一样大,我们怎么区分对待不同类型的指针呢?
C++中编译器会根据指针类型解释出某个特定的地址(包括其大小和内容)。所以例如:
-
一个指向地址1000的整数指针,在32位机器上将涵盖地址1000~1003;
-
如果String是传统的8bytes(包括4bytes的字符指针和一个用来表示字符串长度的整数),那么一个ZooAnimal指针将横跨地址空间 1000~1015(4+8+4);
class ZooAnimal{ public: ZooAnimal(); virtual ~ZooAnimal(); //... virtual void rotate(); protected: int loc; String name; }; ZooAnimal za("Zoey");//假设存在地址1000处 ZooAnimal *pza = &za;
上述两条语句指针布局:
那么,一个void *指针涵盖的空间大小是多少呢?诚然,我们并不知道!这就是为什么一个void * 指针只能够持有一个地址而不能通过它操作指向的对象的原因。
所以C++的转换(cast)其实是一种编译器指令,它通常不改变一个指针涵盖的真正地址,而是去影响对指针“所指内存大小和内容”的解释方式。
1.3.2 加上多态之后(Adding Polymorphism)
现在我们看看加上多态后的指针布局
class Bear:ZooAnimal{
public:
Bear();
~Bear();
//...
void rotatea();
virtual void dance();
//...
protected:
enum Dances{...};
Dances dances_known;
int cell_block;
};
Bear b("Yogi");//假设存在地址1000处
Bear *pb = &b;
Bear &rb = *pb;
上述代码指针的内存布局可能会是这样的:
知道了上述的内存布局,那么我们来看看下面的两个指针有什么区别:
Bear b;
ZooAnimal *pz = &b;
Bear *pd = &b;
他们都指向Bear object(b)的第一个byte.差别是,pb涵盖的地址是包含整个Bear object,而pz包含的地址只包括Bear objct中ZooAnimal subobject。
所以说除了ZooAnimal中出现的members,你不能用pz来直接处理Bear中的任何members.唯一的例外是通过virtual机制:
//不合法:cell_block不是ZooAnimal的一个member
//虽然我们知道pz目前指向一个Bear object
pz->cell_block;
//ok:经过显示的downcast操作就没问题!
(static_cast<Bear*>(pz))->cell_block;
//这样更好,但这是一个runtime operation(注:操作成本较高)
if(Bear* pb2 = dynamic_cast<Bear*>(pz))
pb2->cell_block;
//ok:因为cell_block是Bear的一个member
pb->cell_block;
//当我们写:
pz->rotate();
//编译器又会怎么做呢?
pz的类型将在编译时期决定以下两点:
- 固定可用的接口。也就是说pz只能调用ZooAnimal的public接口。
- 该接口的access level。(例如rotate()是ZooAnimal的public member)。
在每一个执行点,pz所指向的object类型可以决定rotate()所调用的实例。要说明的是类型信息并不是由pz维护,而是由维护在link中(后续章节会解释link)
现在我们再来看下面的情况:
Bear b;
ZooAnimal za = b;//引起切割
//调用ZooAnimal::rotate();
za.rotate();
对此我们提出两个问题并一一解答:
- 为什么调用的是ZooAnimal::rotate()? 答:za是一个ZooAnimal对象而不是(也绝不可能是)一个Bear对象。
- 为什么assignment操作时(za=b),za的vptr不指向bear的virtual table? 答:编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vpters的内容不会被base class object初始化或改变。
这里引入一个观念:OO程序设计并不支持对object的直接处理。来看下面的代码:
class Panda:public Bear{...};
ZooAnimal za;
ZooAnimal *pza;
Bear b;
Panda *pp = new Panda;
pza = &b;
代码的内存布局可能如下图:
这里将za或b或pp所含的内容(也是地址)指定给pza显然是没有问题的。pointr或reference之所以支持多态是因为他们并不引发内存中任何“与类型有关的内存委托操作。”然而如果任何人企图改变object za 的大小就会违反资源分配的要求。
所以当一个base class object被初始化为一个derived class object,derived class object就会被切割以塞入更小的base class object,多态不再呈现。
总而言之多态是一个强大的机制,C++通过pointer和reference来支持多态!
第一章到这里就结束了,可能会云里雾里,但是坚持就会有 收获哦。。下一章继续努力,yeah!!!