C++对象模型——对象的差异(第一章)

来源:互联网 发布:西安软件新城招聘信息 编辑:程序博客网 时间:2024/05/20 16:43

1.3    对象的差异 (An Object Distinction)

    C++程序设计模型直接支持三种programming paradigms(程序设计典范)

1.    程序模型 (procedural model)

    就像C一样,C++当然也支持它,字符串的处理就是一个例子,可以使用字符数组以及str*函数集:
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);

2.    抽象数据类型模型 (abstract data type model, ADT)

    该模型所谓的"抽象"是和一组表达式(public接口)一起提供,而其运算定义不是显式的,例如下面的String class:
String girl = "Anna";String daughter;...// String::operator=();daughter = girl;...// String::operator==();if (girl == daughter)    take_to_disneyland(girl);

3.    面向对象模型 (Object-oriented model)

    在此模型中有一些彼此相关的模型,通过一个抽象的base class(用以提供共同接口)被封装起来。Library_materials class就是一个例子,真正的subtypes例如Book、Video等都可以从那里派生而来:
void check_in(Library_materials *pmat){    if (pmat->late())        pmat->fine();    pmat->check_in();    if (Lender *plend = pmat->reserved())        pmat->notify(plend);}
    纯粹以一种paradigm写程序,有助于整体行为的良好巩固,然而如果混合了不同的paradigms,就可能会带来让人惊吓的后果,特别是在没有谨慎处理的情况下。最常见的疏忽发生在以一个base class的具体实体如:
Library_materials thing1;
    来完成某种多态(polymorphism)局面时:
class Book : Public Library_materials {...};Book book;// thing1不是一个Book,book被切割(sliced),不过thing1仍保有一个Library_materials.thing1 = book;// 调用Library_materials::check_in()thing1.check_in();
    而不是通过base class的pointer或reference来完成多态局面:
Library_materials &thing2 = book;thing2.check_in();
    虽然可以直接或间接处理继承体系中的一个base class object,但只有通过pointer或者reference的间接处理,才支持OO程序设计所需的多态性质。上个例子中的thing2的定义和运用,是OO paradigm中一个良好的例证。thing1的定义和运用则逸出了OO的习惯,它反映的是一个ADT paradigm的良好行为。thing1的行为是好是坏,视程序员的意图而定。
    虽然对于object的多态操作要求此object必须可以经由一个pointer或reference来存取,然而C++中的pointer或reference的处理却不是多态的必要结果。如下所示:
// 没有多态,因为操作对象不是class objectint *pi;// class x视为一个base class(可以有多态的效果)x *px;
    在C++,多态只存在与一个个的public class体系中,例如px可能指向自我类型的一个object,或者指向以public派生而来的一个类型。Nonpublic的派生行为以及类型为void*的指针可以说是多态,但它们并没有被语言明白地支持,也就是说它们必须由程序员通过显式的转换操作来管理。

C++以下方法支持多态:

1.    经由一组隐式的转换操作

    例如把一个derived class 指针转化为一个指向其public base type的指针:
    shape *ps = new circle();

2.    经由virtual function机制

    ps->rotate();

3.    经由 dynamic_cast和typied运算符

    if (circle *pc = dynamic_cast<circle *>(ps))
    多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base class 中。例如Library_materials class 就为Book、Vedio等subtype定义了一个接口。这个共享接口是以virtual function机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数实体被调用,经由这样的操作:
Library_material->check_out();
    代码可以避免由于"借助某一特定library的materials"而导致变动无偿,这不只使得"当类型有所增加、修改、删减时,程序代码不需改变",而且也使一个新的Library_materials subtype的供应者不需要重新编写出"对继承体系中所有类型都共通"的行为和操作.
    考虑下面这样的代码:
void rotate(X datum, const X *pointer, const X &reference) {    // 在执行期之前,无法决定到底调用哪一个rotate()实体    (*pointer).rotate();    reference.rotate();        // 下面这个操作总是调用X::rotate()    datum.rotate();}main() {    Z z;    // Z是X的一个子类型        rotate(z, &z, z);    return 0;}
    经由pointer和reference完成的两个"函数调用指针"会被动态完成。此例中它们都调用Z::rotate(),经由datum完成的"函数调用操作"则可能(可能不)经由virtual机制。不过,它总是调用X::rotate()。(这就是所谓的"编译素养"问题:不管经由datum所调用的virtual function采不采用virtual机制,从语意来说,结果都是一样的)
    需要多少内存才能表现一个class object?一般而言要有:
    其nonstatic data members的总和大小
    加上任何由于alignment(将数值调整到某整数的倍数)的需求而填补(padding)上去的空间(可能存在于members之间,也可能存在于集合体边界)
    加上为了支持virtual而由内部产生的任何额外负担(overhead)

    一个指针不管它指向哪一种数据类型,指针本身所需的内存大小是固定的,如下:
class ZooAnimal {public:    ZooAnimal();    virtual ~ZooAnimal();    // ...    virtual void rotate();protected:    int loc;    string name;};ZooAnimal za("Zoey");ZooAnimal *pza = &za;
    其中的class object za和指针pza的可能布局为loc(1000~1003),name(1004~1011),virtual(1012~1015)。

指针的类型 (The Type of a Pointer)

    一个指向ZooAnimal的指针式如何与一个指向整数的指针或者一个指向template Array的指针有所不同呢?
ZooAimal *px;int *pi;Array<string> *pta;
    以内存需求的观点来说,没有不同。它们三个都需要有足够的内存来存放一个机器地址。"指向不同类型的指针"间的差异,既不在于指针表示法不同,也不在于其内容(代表一个地址)不同,而是在于其所寻址出来的object类型不同。也就是说,"指针类型"会指示编译器如何解释某个特定地址中的内存内容以及其大小。
    1.    一个指向地址1000的整数指针,在32位机器上,将涵盖地址空间1000~1003
    2.    如果string是传统的8-bytes(包括一个4-bytes的字符指针和一个用来表示字符串长度的整数),那么一个ZooAnimal指针将横跨地址空间1000~1015(4+8+4)
    那么,一个指向1000而类型为void *的指针,将涵盖则怎样的地址空间?
    不知道。这就是为什么一个类型为void *的指针只能含有一个地址,而不能通过它操作所指的object的原因
    所以,转型(cast)其实是一种编译器命令,大部分情况下它并不改变一个指针所含的真正的地址,它只影响"被指出的内存的大小和其内存"的解释方式

加上多态之后 (Add Polymorphism)

    现在定义一个Bear,作为一种ZooAnimal,当然,经由"public继承"可以完成。
class Bear : public ZooAnimal {public:    Bear();    ~Bear();    // ...    void rotate();    virtual void dance();    // ...protected:    enum Dances {...}    Dances dances_known;    int cell_block;};Bear b("Yogi");Bear *pb = &b;Bear &rb = *pb;
    b,pb,rb会有怎样的内存需求呢?不管是pointer或reference都只需要一个word的空间(在32位机器上是4-bytes)。Bear object需要24 bytes,也就是ZooAnimal的16 bytes加上Bear所带来的8 bytes。    
    假设Bear object位于地址1000处,一个Bear指针和一个ZooAnimal指针有什么不同?
Bear b;ZooAnimal *pz = &b;Bear *pb = &b;
    它们每个都指向Bear object的第一个byte,其间的差别是pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear object中的ZooAnimal subobject。
    除了ZooAnimal subobject中出现的members,不能够使用pz来直接处理Bear的任何members。唯一的例外是通过virtual机制:
// 不合法:cell_block不是ZooAnimal的一个member,虽然pz当前指向一个Bear objectpz->cell_block;// OK:经过一个显示的downcast操作就没问题((Bear *)pz)->cell_block;// 下面这样更好,但它是一个run-time operation(成本较高)if (Bear *pb2 = dynamic_cast<Bear *>(pz))    pb2->cell_block;// OK:因为cell_block是Bear的一个memberpb->cell_block;
    如果写如下语句
pz->rotate();
    时,pz的类型将在编译时决定以下两点:
    固定的可用接口,pz只能够调用ZooAnimal的public接口
    该接口的access level(例如rotate()是ZooAnimal的一个public member)
    在每一个执行点,pz所指的object类型可以决定rotate()所调用的实体。类型信息的封装并不是维护在pz中,而是维护在link中,此link存在于"object的vptr"和"vptr所指向virtual table"之间(4.2节对于virtual functions有一个完整的讨论)
    看下面这种情况:
Bear b;ZooAnimal za = b;        // 这会引起切割(sliced)// 调用ZooAnimal::rotate()za.rotate();
    为什么rotate()所调用的是ZooAnimal实体而不是Bear实体?此外,如果初始化函数(应用于上述assignment操作发生时)将一个object内容完全拷贝到另一个object中去,为什么za的vptr不指向Bear的virtual table?
    第二个问题的答案是,编译器在初始化以及赋值(assignment)操作(将一个class object指定给另一个class object)之间做出了选择。编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化或改变。
    至于第一个问题的答案是:za并不是(而且也绝不会是)一个Bear,它是(并且只能是)一个ZooAnimal.多态所造成的"一个以上的类型"的潜在力量,并不能够实际发挥在"直接存取objects"这件事情上。有一个似是而非的观念:OO程序并不支持对object的直接处理。如下所示:
{    ZooAnimal za;    ZooAnimal *pza;    Bear b;    Panda *pp = new Panda;    pza = &b;}
    将za或b的地址,或pp的内容(也是个地址)赋给pza,显然不是问题。一个pointer或者一个reference之所以支持多态,是因为它们并不引发内存中任何"与类型有关的内存委托操作(type-dependent commitment)",会受到改变的知识它们所指向的内存的"大小和内容解释方式"而已。
    然而,如果改变object za的大小,则会违反其定义受契约保护的"资源需求量"。如果把整个Bear object赋值给za,则会溢出它所配置得到的内存,执行结果则出错。
    当一个base class object被直接初始化为(或者赋值为)一个derived class object时,derived object就会被切割(sliced),以塞入较小的base type内存中。derived type将没有留下任何蛛丝马迹。多态于是不再呈现,而一个严格的编译器可以在编译时期解析一个"通过该object而触发的virtual function调用操作",因而回避virtual机制,如果virtual function被定义为inline,则更有效率上的大收获。
    总而言之,多态是一种威力强大的设计机制,允许继承一个抽象的public接口之后,封装相关的类型。Library_materials体系就是一例。需要付出的代价就是额外的间接性——不论是在"内存的获得"或者是在"类型的判断"上,C++通过class的pointers和references来支持多态,这种程序设计风格就称为"面向对象"(Object-oriented model(OO)).
    C++也支持具体的ADT程序风格,如今被称为object-based(OB).例如string class,一种非多态的数据类型,string class 可以展示封装的非多态形式:它提供一个public接口和一个private实作品,包括数据和算法,但是不支持类型的扩充。一个OB设计可能比一个对等的OO设计速度更快而且空间更紧凑,速度快是因为所有的函数引发操作都是在编译时期解析完成,对象构建起来时不需要设置virtual机制;空间紧凑则是因为每一个class object不需要负担传统上为了支持virtual机制而需要的额外负担,不过OB设计比较没有弹性。
    在弹性(OO)和效率(OB)之间常常存在着取舍,在能够有效选择其一之前,必须清楚了解两者的行为和应用领域的需求。
0 0
原创粉丝点击