深度探索C++对象模型之第一章:关于对象之对象的差异
一、三种程序设计范式:
C++程序设计模型支持三种程序设计范式(programming paradiams).
- 程序模型(procedural model)
char boy[] = "ccpang"; 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)
此模型的抽象是和一组表达式(public接口)一起提供,那时其运算定义仍然隐而未明的。
1 String girl = "Anna" 2 String daughter; 3 4 //String ::operator(); 5 daughter = girl; 6 7 //String::operator == (); 8 if(girl ==daughter) 9 take_to_disneyland(girl);
- 面向对象模型(object-orlented model)
此模型中有一些彼此相关的类型,通过一个抽象的基类(用以提供共同接口)被封装起来。Library_materials class就是一个例子,真正的子类例如Book、Video、等均可以从那里派生而来。
1 void check_in (Library_materials *pmat) 2 { 3 if(pmat ->late()) 4 pmat->fine(); 5 pmat->check_in(); 6 7 if(Lender *plend = pmat->reserved()) 8 pmat->notify(plend); 9 }
只以一种程序设计范式写代码,有助于整体行为的良好稳固。如果混合了不同的范式,就可能会带来让人吃惊的后果。常出现的问题如下所示:
以一个基类的具体实例来完成某种多态(polymorphism)情况是时:
1 Library_materials thing1; 2 3 //class Book : public Library_materials{....}; 4 Book book; 5 6 //thing1不是一个book,book被裁减了(sliced) 7 thing1 = book; //调用赋值运算符,只对基类部分进行操作,派生类的其他部分将被忽略。 8 9 //调用的是Library_materials::check_in() 10 thing1.check_in(); 11 12 //通过基类的指针或引用来完成多态局面: 13 Library_materials &thing2 = book; //基类的指针或引用可以指向派生类 14 15 //现在使用的是Book::check_in() 16 thing2.check_in();
虽然你可以直接或间接(赋值或引用)处理继承体系中的一个基类对象,但是只有通过指针或引用的间接处理,才支持面向对象程序设计所需的多态性质。thing2的定义和运用符号面向对象编程的良好习惯。而thing1的定义和运用则不是面向对象的习惯,它反映的是一个抽象数据类型范式的良好习惯。thing1的行为是好是坏,取决于程序员的意图。
在面向对象编程范式中,需要处理的一个未知实例,虽然它的类型有所界定,但是却存在无数种可能,它的类型受限于其继承体系,然而该体系理论上没有深度和广度的限制。原则上,被指定的对象的真实类型在每一个特定执行点之前,是无法解析的。在C++中,只有通过指针和引用操作才能完成。相反,在抽象数据类型范式中,程序员处理的是一个拥有固定而单一类型的实例,它在编译时期就已经完全定义好了。例如下面:
1 //描述对象:不确定类型 2 3 //基类的一个指针或引用,可能指向一个基类对象或者基类对象的派生类(子类型) 4 Librar_materials *px = retrieve_some_material(); 5 Librar_materials &rx = *px; 6 7 //dx是一个基类对象,它的值是px所指向的值。赋值运算符。 8 Librar_materials dx = *px;
值得说明的是:这样的行为虽然或许未如你所预期,却是良好的行为。虽然对于对象的多态操作,要求此对象必须可以由一个指针或引用来存取,但是有指针和引用并不是多态。
在C++中,多态只存在于公有类的继承体系中(public class),px只能指向某个类型的基类对象,或是根据public继承关系派生而来的一个子类型。非公有的派生行为以及类型为void*的指针可以说是多态的,但是它们并没有被语言所明确的支持,也就是它们必须由程序员通过显式的转换操作来管理。
二、C++支持多态的方法:
- 隐式转换操作
1 //例如将一个派生类指针转换为一个指向其基类型的指针 2 shape *ps = new circle();
- 由虚函数机制
1 ps->rotate();
- 由dynamic_cast和typeid运算符
1 if(circle *pc = dynameic_cast<circle*>() ps)
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的基类中。
1 void rotate(X datum,cosnt X *pointer,cosnt X &reference) 2 { 3 //在执行期之前,无法决定到底调用哪一个rotate()实例 4 (*pointer).rotate(); 5 reference.rotate(); 6 7 // 8 //datum.rotate(); 9 10 } 11 12 13 main(){ 14 Z z; //Z是X的一个子类型 15 rotate(z,&z,z); 16 return 0; 17 }
在上例中,经pointer 和reference完成的两个函数调用操作会被动态完成,而经过datum完成的函数调用操作则不经过virtual机制。
三、表示一个类对象需要多少内存:
- 非静态数据成员的总和大小
- 加上由于alignment的需求而填补上去的空间。(alignment就是将数值调整到某数的倍数)
- 加上为了支持virtual而由内部产生的任何额外负担(overhead)
四、指针的类型:
一个指向ZooAnimal的指针和一个指向整数的指针和一个指向模板数组的指针有什么不同呢?
1 ZooAnimal *px; 2 int *pi; 3 Array<String> *pta;
答案是没有什么不同。指向不同类型的指针间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象不同。也就是说,指针类型会教导编译器如何解释某个特定的地址中的内存内容及其大小。
举个例子:以下是一个ZooAnimal的声明:
1 class ZooAnimal { 2 public: 3 ZooAnimal(); 4 virtual ~ZooAnimal(); 5 6 //。。。 7 virtual void rotate(): 8 9 protected: 10 int loc; 11 String name; 12 }; 13 14 ZooAnimal za("pig"); 15 ZooAnimal *pza = &za;
ZooAnimal在内存中的布局:
那么一个指向地址1000而类型为void*的指针,将涵盖怎样的地址空间呢?答案是不知道,这就是为什么一个void*指针只能够持有一个地址而不能通过它操作所指对象。
综上得到,转换只是一种编译器指令,大部分情况下它并不改变一个指针所含的真正地址,它只会影响被指出的内存的大小和其内容。
五、加上多态之后:
如下所示一个Bear类,它继承自ZooAnimal:
class Bear :public ZooAnimal{ public; Bear(); ~Bear(); void rotate(): virtual void dance(); protected: enum Dances{...}; Dances dances_know; int cell_blook: }; Bear b("panda"); Bear *Pb = &b; Bear &rb = *pb;
不管是指针(pointer)或引用(reference)都只需要一个word(在32位机器上是4-bytes)的空间。Beard对象需要24个bytes,也就是ZooAnimal的16个bytes再加上Bear所带来的8bytes.如下图所示:
如果把一个Bear对象放在地址1000处,那么一个Bear指针和一个ZooAnimal指针有什么不同? 这就是将一个派生类赋给基类的指针
Bear b; ZooAnimal *Pz = &b; //pz是一个指向派生类Bear的基类指针 Bear *pb = &b; //pb是一个指向Bear的Bear指针
答案是它们都会指向Bear对象的第一个字节,但是pb所涵盖的地址包含整个Bear对象,而pz所涵盖的地址只包含Bear对象中的ZooAnimal实例。(将Bear的部分截取掉了)
除了ZooAnimal实例中出现的成员,你不能用pz来直接处理Bear的任何members。唯一例外是通过virtual机制。
1 pz ->cell_block;//这是不合法的,虽然我们知道pz指向一个Bear对象,但是cell_block不是ZooAnimal的一个成员; 2 3 //经过一个显示转换操作就可以了 4 (static_cast<Bear*>(pz))->cell_block; 5 6 //下面这样更好 7 if(Bear *pb2 = dynamic_cast<Bear*>(pz)) 8 pb2->cell_block;
当我们使用pz->rotate();时,pz的类型将在编译时期决定以下两点:
- 固定的可用接口,也就是说,pz只能调用ZooAnimal的public接口
- 该接口的access level。
在每一个执行点,pz所指向的对象类型可以决定rotate()所调用的实例。类型信息的封装并不是维护在pz上,而是维护在link之中,这个link存在于对象的vptr和vptr所指的虚函数表之间。
六、面向对象程序设计并不支持对对象的直接处理:
{ ZooAnimal za; ZooAnimal *pza; Bear b; Panda *pp = new Panda; pza = &b; }
其内存布局可能如下图所示:
注意:new开辟的是Heap Memory(堆内存),其他都是在栈上开辟的。
将za或b的地址或pp所指的内容(也是地址),指定给pza,显然不是问题。一个指针或引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作(type-dependent commitment)”,会受到改变的只有它们所指向的内存的“大小和内容解释方式而已”
然而如果将一个Bear对象附给一个ZooAnimal,会违法其定义中受契约保护的“资源需求量”。将一个Bear对象指定给za,则会溢出它所分配得到的内存。(za = Bear)
而当一个基类对象被初始化为一个派生类对象时(会引起切割),派生类对象就会被切割以塞入较小的基类内存中,派生类型将没有留下任何痕迹,多态将不再呈现。
总而言之,多态允许你继一个抽象的public接口之后,封装相关的类型。但是要付出的代价就是额外的间接性——不论是在“内存的获得”,还是类型的决断上。C++通过类的指针和引用来支持多态,这种程序设计风格就被称为面向对象。
C++也支持具体的抽象数据类型风格,如今被称为基于对象(object-based(OB)),例如String,一种非多态的数据类型。String class可以展示封装的非多态形式:它提供一个public接口和一个private实现品:包括数据和算法,但是不支持类型的扩充。一个OB设计可能比OO设计速度更快且空间更紧凑:速度快是因为所有的函数调用都是编译时期完成的,对象构建起来并不需要设置virtual机制;空间紧凑是因为每一个类对象不需要负担传统上为了支持virtual机制而需要的额外负担,不过OB没有弹性。