C实现面向对象(翻译)——第二章

本文探讨了通过动态连接实现泛型函数的方法,包括构造器、解析器的概念及其在对象销毁中的作用。介绍了如何利用类型描述符实现多态,通过具体示例说明了动态连接在构造函数和解析函数中的应用。

第二章 动态连接——泛型函数

 

 

===== 2.1 Constructors and Destructors 构造器和解析器 ======

 

我们简单实现字符串类,并会加入到set中。我们使用动态内存保存字符串的文本。当删除字符串时,会回收相关的内存。

 

new方法负责创建一个对象而delete负责回收相关资源。new方法知道所创建对象的类型,因为该方法的第一个参数是对象的描述符。基于这个参数,我们可以一大串if语句区分出每个对象的创建。但这有个明显的缺点,new方法将会明确的包含每个数据类型的部分代码。

 

delete方法同样也会有个大问题,它同样要基于参数所属的数据类型作不同的处理:对字符串而言,相关文本的内存数据需要释放;对于第一章介绍的对象元素,只有对象本身需要回收;对于set而言,需要释放许多用于保存集合元素的引用的内存数据。

 

我们可以给delete方法多传递一个参数:数据类型描述符或清理方法的指针,但都不推荐。更加实用和优雅的方式:让每个对象必须知道怎么销毁该对象的相关资源。每个对象都会包含一个指向清理函数的指针,我们将这样的函数称作解析器。

 

现在是new方法有问题。它负责创建对象并且返回指针,而这些指针能够当作参数传递给delete方法。也就是说,new方法必须为每个对象指明销毁的信息。最显而易见的方式就是将指向解析器的指针成为类型描述符的一部分,而类型描述符是new方法的参数。目前为止,我们只需要像下面这样的声明:

    struct type {
        size_t size; /* size of an object */
        void (* dtor) (void *); /* destructor */
    };

    struct String {
        char * text; /* dynamic string */
        const void * destroy; /* locate destructor */
    };

    struct Set {
        ... information ...
	const void * destroy; /* locate destructor */
    };

上面的代码还是有个问题:对象需要从类型描述符中拷贝解析器函数指针用于销毁资源,而该指针在结构体的位置在不同类的对象中会有变化。

 

初始化是new方法的职责,不同的类别需要不同的初始化——new方法可能会根据类别的不同而需要不同的参数:

    new(Set); /* make a set */
    new(String, "text"); /* make a string */

为了完成初始化,我们需要另外一个特别意义的函数,我们称之为构造函数。因为构造函数和解析函数对所有对象而言都是不可或缺的,所以把这两个函数指针都加进了类型描述符中。

 

注意构造器和解析器并不负责申请内存和释放内存,那是new方法和delete方法的职责。new方法会调用构造函数,而构造函数负责初始化由new方法申请的内存。例如字符串,它确实会申请一片内存存储文本,但是String结构体本身的内存空间是由new方法申请的,这片内存是由delete方法释放的。更确切的说,delete方法首先调要解析器destructor,解析器会逆向处理构造器的工作,然后delete方法回收由new方法申请的内存。

 

 

===== 2.2 Methods,Messages,Classes and Objects 方法,消息,类和对象 ======

 

delete方法必须能在不知道对象类型的条件下,找到解析函数指针。回顾2.1节给出的声明,我们坚持将能够找到解析函数的指针放在所有能够调用delete方法的对象的表示结构的头部位置。

 

更加具体的解释,这个指针该指向什么?如果我们仅仅拥有的只是对象的地址,这个指针应该能让我们访问对所有对象有特别意义的函数,例如解析函数。我们可能还需要一些其他特别意义的函数:例如描述对象状况的函数display(个人理解为类似java的toString方法),比较对象的函数differ,或者拷贝对象的函数clone。因此,我们将这个指针指向拥有这一系列函数的一张表。

 

仔细研究,我们发现这张表应该加入到new方法的参数——类型描述符中,显而易见的办法就是用一个指针指向整个类型描述符:

    struct Class {
        size_t size;
        void * (* ctor) (void * self, va_list * app);
        void * (* dtor) (void * self);
        void * (* clone) (const void * self);
        int (* differ) (const void * self, const void * b);
    };

    struct String {
        const void * class; /* must be first */
        char * text;
    };

    struct Set {
	const void * class; /* must be first */
	...
    };

每一个对象都是以一个指向该对象类描述的指针开始,这样我们就能够找到对对象有特别意义的信息:.size方法指示new方法申请的空间大小;.ctor指向构造函数,并且会接受new方法申请的内存空间以及剩余的参数列表做参数;.dtor指向解析函数;.clone指向一个拷贝函数用于复制对象;.differ指向一个用于比较对象的函数。

 

通过观察我们发现每个函数会根据对象做处理。仅仅只有构造函数处理需要初始化的内存空间。我们将这些函数称为对象的方法。调用方法又称为消息,我们将接受消息的对象参数用self命名。我们始终使用普通的C函数,self不必是第一个参数。

 

很多对象会共享同一个类型描述符,也就是说,它们需要同样大小的内存而同样的方法也适用于这些对象。我们将所有共享的同一个类型描述符的对象归纳为一类;而单个对象称为类的实例。那么一个类,就是一个抽象数据类型,是一个集合,包含一系列可能的值,一系列的操作。也就是一个数据类型,和第一章介绍的大致相同。

 

一个对象是一个类的一个实例。对象的状态由new方法申请的内存空间上的数据表示,类的方法会更改对象的状态。更加科学的说法,一个对象是一个特别的数据类型的集合的一个值。

 

 

===== 2.3 Selectors, Dynamic Linkage, and Polymorphisms 选择器,动态连接,多态 ======

 

消息如何传递?new方法申请的内存空间在交给构造函数前,大部分时候都没有初始化:

    void * new (const void * _class, ...)
    {   const struct Class * class = _class;
        void * p = calloc(1, class -> size);

        assert(p);
        * (const struct Class **) p = class;

        if (class -> ctor)
        {   va_list ap;

            va_start(ap, _class);
            p = class -> ctor(p, & ap);
            va_end(ap);
       }
       return p;
    }

位于对象的结构体的起始位置指向struct Class的指针尤其重要。这也是为什么我们能够在new方法里初始化赋值的原因:



 

右边的类型描述符是在编译阶段初始化。而对象是在运行阶段创建的,破解号代表的指针也是运行阶段创建的,下面这句赋值语句

    * (const struct Class **) p = class;

p指向一个对象的内存数据的起始位置。我们使用强制转换把对象的开头当作一个指向struct Class的指针,用class参数赋值。

 

下一步,如果构造函数是类型描述符的一部分,就会调用构造函数,并将该调用返回的结果当作new方法的返回结果,也就是新的对象。2.6节将会介绍更高级的构造器如何处理相关内存的管理。

 

注意,只有明确可见的函数,例如new函数,可以拥有可变参数列表。访问参数列表要用va_list类型的变量(代码中的ap),使用stdarg.h中的宏va_start初始化变量。new方法可以将整个 参数列表传递给构造函数;因此,.ctor方法的声明中也会有va_list参数。之后我们可能还会用到该参数列表,我们只是将变量的地址传递给构造函数,但调用完构造函数后,ap将会指向构造函数用到的最后一个参数的后面的第一个参数。

 

delete方法假定每个对象,也就是非空指针,都指向一个类型描述符。这也是用到解析函数的地方,当然前提是解析函数存在。self作用类似前一张图片的p。

    void delete (void * self)
    {   const struct Class ** cp = self;
        
        if (self && * cp && (* cp) -> dtor)
            self = (* cp) -> dtor(self);
        free(self);
    }

解析函数,同样有机会替换掉传递给free方法的指针。如果构造函数做了某些手脚,解析函数有机会纠正,请参考2.6小节。如果一个对象不打算被释放,那么可以让解析函数返回空指针。

 

其他记录在类型描述符的方法也是类似的调用方式。用一个接受对象self做参数,通过类型描述符执行该类的方法:

    int differ (const void * self, const void * b)
    {   const struct Class * const * cp = self;
    
        assert(self && * cp && (* cp) -> differ);
        return (* cp) -> differ(self, b);
    }

关键部分就是假定我们可找到任意指针self中潜在的类型描述指针*self,当然空指针除外。我们可以在每个类型描述的起始位置放置一个魔术数字,或者将*self与任意已知的类型描述的地址比较来判断参数的正确性,不用急,在第8章我们加入严格得多的检查机制。

 

不管怎样,differ方法展示了这项调用函数的技术,又被称为动态连接或者延迟绑定:我们可以对任意对象调用differ方法,只要其他对象起始位置包含指向合适的类型描述符的指针,这个函数只在实际调用呼叫时执行。

我们将differ方法称为选择器。这是个多态的例子,也就是说,一个函数可以接受不同类型的参数,基于类型的不同而执行不同的行为。如果我们实现了更多的类,在类型描述符中均包含.differ指针,那么differ是一个泛型函数,可以被其他类的任何对象调用。

 

我们可以将选择器看作是本身不是动态连接的方法,但行为类似多态,因为选择器让动态连接函数执行了真正的工作。

 

多态函数被内置进入了许多编程语言。例如Pascal中的write过程,还有C语言中的操作符+,处理int,指针或float时有些不同。这种现象又被称为重载:参数类型和操作符名称一起决定了操作符如何处理;同一个操作符用在不同的参数时,产生不同的效果。

 

本节中并没有明显的区分:由于动态连接,differ方法有点像重载函数,而C编译器使得+表现得有点像多态。C编译器可以创建不同的返回数据类型,而differ方法必须总是返回同一类数据。

方法可以无需使用动态连接表现出多态。下面的sizeOf方法就是个例子,该方法返回任意一个对象的内存大小:

    size_t sizeOf (const void * self)
    {   const struct Class * const * cp = self;
        
        assert(self && * cp);
        return (* cp) -> size;
    }

所有对象都会有描述符,因此可以从描述符中获取大小。注意下面的不同:

    void * s = new(String, "text");
    assert(sizeof s != sizeOf(s));

sizeof是C的操作符,用于在编译阶段计算并返回相关参数需要的字节数。sizeOf是我们的多态函数,在运行阶段返回一个对象的内存大小。

 

===== 2.4 An Application 一个应用 ======

 

我们还没有实现string类,但我们可以先写一个简单的测试程序。String.h定义了该抽象数据类型:

 

    extern const void * String;

我们之前介绍的方法对所有对象都很常见;因此,将这些方法的声明加入到负责内存管理的的头文件new.h(1.4节介绍过)中:

 

    void * clone (const void * self);
    int differ (const void * self, const void * b);
    size_t sizeOf (const void * self);

前两个函数原型是选择器,由 struct Class演化而来:

 

    #include "String.h"
    #include "new.h"
    int main ()
    {   void * a = new(String, "a"), * aa = clone(a);
        void * b = new(String, "b");

        printf("sizeOf(a) == %u\n", sizeOf(a));
        if (differ(a, b))
            puts("ok");

        if (differ(a, aa))
            puts("differ?");

        if (a == aa)
            puts("clone?");

        delete(a), delete(aa), delete(b);
        return 0;
    }

我们简单的创建了两个字符串,并拷贝了其中的一份。我们获取String对象的大小而非文本的大小,我们还检查发现不同的文本产生不同的字符串,我们还得知对象的拷贝与本身相等但不相同,最后我们删除了字符串,如果一切正常,程序将会打印一下信息


  sizeOf(a) == 8
  ok

 

 

===== 2.5 An Implementation String 一个实现 字符串 ======

 

我们通过编写会被加入到类型描述符的方法来实现字符串。动态连接帮助我们清晰的区分:当实现一个新的数据类型,哪些函数需要编写。

 

构造函数获取传递给new方法的文本并保存一份动态拷贝到由new方法分配的内存struct String中:

 

    struct String {
        const void * class; /* must be first */
        char * text;
    };

    static void * String_ctor (void * _self, va_list * app)
    {   struct String * self = _self;
        const char * text = va_arg(* app, const char *);

        self -> text = malloc(strlen(text) + 1);
        assert(self -> text);
        strcpy(self -> text, text);
        return self;
    }

在构造函数中,我们只需要初始化.text,而.class在new方法中已经设值了。

 

解析函数释放由string控制的内存数据。因为delete方法会检查self参数,所以我们不必多此一举了。

    static void * String_dtor (void * _self)
    {   struct String * self = _self;

        free(self -> text), self -> text = 0;
        return self;
    }

String_clone方法拷贝一份字符串。之后,两个字符串都会调用delete方法,所以需要使用动态内存拷贝,简单的调用new方法就可以了:

 

    static void * String_clone (const void * _self)
    {   const struct String * self = _self;
        return new(String, self -> text);
    }

String_differ方法的作用是:当与对象自身比较,返回0;当与非String对象比较,返回1;当与其他String对象比较,调用strcmp方法。

 

    static int String_differ (const void * _self, const void * _b)
    {   const struct String * self = _self;
        const struct String *b=_b;

        if (self == b)
            return 0;
        if (! b || b -> class != String)
            return 1;
        return strcmp(self -> text, b -> text);
    }

类型描述符是独一无二的,这里我们用到了,判断第二个参数是不是个字符串。

 

所有这些方法都是静态方法,只会被new(),delete()或者选择器调用。这些方法通过类型描述符的方式可被选择器调用:

 

    #include "new.r"

    static const struct Class _String = {
        sizeof(struct String),
        String_ctor, String_dtor,
        String_clone, String_differ
    };

    const void * String = & _String;

String.c 导入了 String.h和new.h 的公共声明。为了初始化类型描述符,也导入了私有的头文件new.r, new.r中包含了struct Class的表示结构(参见2.2节)。

 

 

 

===== 2.6 Another Implementation Atom 另一个实现 原子 ======

 

为了说明我们在处理构造函数和解析函数接口时做了什么,我们实现了atoms。一个atom是一个独立的string对象;如果两个atoms包含同样的string,那么它们是相同的,当然也就相等。Atoms的比较方法非常简单:仅仅比较两个指针是否一致。而Atoms的构造和销毁就复杂多了:我们维护一个所有atom对象的循环列表,并统计每个atom的拷贝数:

 

    struct String {
        const void * class; /* must be first */
        char * text;
        struct String * next;
        unsigned count;
    };

    static struct String * ring; /* of all strings */

    static void * String_clone (const void * _self)
    {   struct String * self = (void *) _self;

        ++ self -> count;
        return self;
    }

由所有atoms构成的循环列表的头部标记为ring,通过.next扩展延伸,由构造函数和销毁汗函数维护。构造函数在保存文本之前,会首先检查列表中是否有同样的文本存在。将下面的代码插入到String_ctor()方法的开头处:

 

    if (ring)
    {   struct String * p = ring;

        do
            if (strcmp(p -> text, text) == 0)
            {   ++p -> count;
                free(self);
                return p;
	    }
        while ((p = p -> next) != ring);
    }
    else
        ring = self;

    self -> next = ring -> next, ring -> next = self;
    self -> count = 1;

如果发现存在相同的atom,增加相应的统计数,释放新的string对象,返回atom对象p。否则就将新string对象加入到列表中,并将统计数设为1。

 

解析函数并不会删除atom对象,除非相应的统计数为0。将下面的代码加入到String_dtor的开头处:

 

    if (-- self -> count > 0)
        return 0;

    assert(ring);
    if (ring == self)
        ring = self -> next;
    if (ring == self)
        ring = 0;
    else
    {   struct String * p = ring;

        while (p -> next != self)
        {   p=p -> next;
            assert(p != ring);
        }
        p -> next = self -> next;
    }

如果统计数为正数,返回一个空指针。否则从列表中移除字符串,并判断是否需要将列表头ring设为0。

再次执行2.4节的代码,发现拷贝的字符串与本身相等并相同。打印信息如下

  sizeOf(a) == 16

  ok
  clone?

 

 

===== 2.7 Summary 总结 ======

 

指针指向对象,动态连接允许我们找到一些特别的函数:每个对象的开始位置都存放着类型描述符指针,通过这个指针可以找到那些对一类对象通用的函数。特别是,类型描述符包含着构造函数指针——用以初始化内存数据,解析函数指针——用以在调用delete方法之前回收相关额外的内存资源。

 

我们将共享同一个类型描述符的所有对象称为一个类。一个对象是一个类的一个实例,对象的特别函数称之为方法,消息就是调用这些函数。我们使用选择器函数用于查找和调用对象的动态连接方法。

 

选择器和动态连接使得同一个函数名对不同的类起到不同的作用。这样的函数称为多态。

 

多态函数非常有用。这提供了一个抽象概念的层次:differ方法会比较任意连个对象——在任何地方,我们都不需要记住哪个具体分支的differ方法调用了。一个可行方便的调试工具就是使用多态函数store(),将对象的信息保存到文件中。

 

 

===== 2.8 Exercises 练习 ======

 

要了解多态函数如何处理,我们需要通过动态连接实现Object还有Set。Set还有一个难题,因为在集合中的元素没有记录它属于哪个集合。

 

string类应该有更多的方法:我们需要知道文本的长度,我们可以会赋予一个新的文本值,我们应该能够打印一个String。如果考虑substring呢?

 

Atom要高效的多,如果将atom对象放入哈希表。一个atom对象的值会改变吗?

 

String_clone方法有一个细微的地方:在这个函数里,String应该和self->class有相同的值。将这两参数传递给new方法时什么不同?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值