「ANSI-C によるオブジェクト指向プログラミング」の第 2 章 (動的リンクと関数)

第 2 章ダイナミックリンクとジェネリック関数

2.1コンストラクターとデストラクター

       以下のコレクションで使用される単純な文字列データ型を実装してみましょう。新しい文字列の場合、リテラルを保持するために動的バッファを割り当てます。この文字列が削除されると、それが占有していたメモリ バッファが再利用されます。

       new() はオブジェクトの作成を担当し、 delete() は このオブジェクトが占有しているリソースを再利用する必要があります。new() は、最初のパラメータがこのオブジェクトの説明を渡すため、作成するリソースのタイプを事前に知っています。このパラメータに基づいて、作成される単一の異なるオブジェクトを処理するかどうかの一連の判断ステートメントを使用できますこの欠点は、サポートされている各オブジェクトを処理するためのコードをnew()に完全に含める必要があることです。

       さて、new() にはさらに大きな問題があります。これは、オブジェクトを作成し、 delete()に渡されるオブジェクト ポインターを返す役割を果たします。つまり、new() は各オブジェクトに特定のデストラクター情報をインストールする必要があります。最も明白な使用法は、new()に渡される型記述子の一部である特定のデストラクターへのポインターを使用することです。これまでのところ、次のような宣言が必要です

構造体の型{

    size_t サイズ;           /*オブジェクトのサイズ*/

    void (*dtor)(void*);   /*デストラクター*/

};

構造体文字列{

    文字 * テキスト;            /*動的文字列*/

    const void* 破壊します。  /*デストラクターの位置を特定*/

};

構造体セット{

    ...情報...

    const void * destroy; /*デストラクターの位置を特定*/

};

別の問題があるようです。新しいオブジェクトでは、型記述からデストラクタ ポインタdtor をdestroyにコピーする必要があります。そして、そのようなコピーは、異なるオブジェクト クラスの異なる場所に配置されます。

       初期化作業はnew()の一部であり、型が異なると、異なる作業を実行するためにnew()が必要になります。 new()は、型が異なると処理するために異なるパラメータを必要とする場合があります。

新しい(セット);            /*セットを作る*/

new(文字列,"テキスト"); /*文字列を作成します*/

 初期化には、コンストラクターと呼ばれる、指定された型の別の関数を使用します。コンストラクターとデストラクターは型指定されており、変更する必要がないため、型の説明の一部としてnew()関数に渡します。

      コンストラクターとデストラクターは、オブジェクト自体のメモリの取得と解放を担当しないことに注意してください。これはnew() delete()の仕事です。コンストラクターは、new()によって割り当てられたメモリを初期化するためにのみnew()によって呼び出されます。文字列の場合、テキストを保存するために別のメモリが必要ですが、構造体 String自体のメモリはnew()を使用して割り当てられます。このスペースは、 delete()によって解放されますただし、delete() は最初にデストラクターを呼び出します。これは、コンストラクターによって行われる初期化の逆です。このステップが完了すると、delete() が呼び出され、 new()によって割り当てられたメモリが解放されます。

 

2.2メソッド、メッセージ、クラス、オブジェクト

       delete() は、 オブジェクトの型を知らなくてもデストラクターを見つけることができなければなりません。したがって、セクション2.1のステートメントを修正するには、デストラクターを見つけるためにポインターが使用されることを主張する必要があります。また、このポインターは、タイプに関係なく、 delete()に渡されるすべてのオブジェクトの先頭に配置する必要があります。

       このポインタは何を指すのでしょうか? オブジェクトのアドレスしか持っていない場合、オブジェクトの場合、オブジェクトのデストラクターと同様に、ポインターを使用すると、指定された型のオブジェクトに関する情報にアクセスできます。おそらく、オブジェクトを表示する関数、オブジェクトの比較関数Different()オブジェクトの正確なコピーを作成する関数clone() など、型固有の関数を作成することになると思われます。そこで、関数ポインタ テーブルへのポインタを使用します。

       よく見ると、このテーブルは new() に渡される型の説明の一部である必要があり問題の明白な解決策は、オブジェクトが型の完全な説明を指すようにすることです。

構造体クラス {

       size_t サイズ;

       void * (* ctor) (void * self, va_​​list * app);

       void * (* dtor) (void * self);

       void * (* クローン) (const void * self);

       int (* 異なる) (const void * self, const void * b);

};

構造体文字列 {

       const void * クラス;   /* 最初に指定する必要があります */

       文字 * テキスト;

};

構造体セット{

    const void* class    /*最初に指定する必要があります*/

        ...

};

每个对象将以一个指针开始,这个指针向对象的类型描述表,通过这个类型描述表,我们就可以定位一个对象的类型指定信息:.size new() 所分配的对象所占用内存的大小;.dtor 指向被delete()delete 用来销毁对象) 所调用的析构器;而 .differ 指向一个函数,这个函数用于比较对象。

继续往下看,我们会注意到,每个功能都是以对象而存在的,通过对象来选择这些功能。只有构造函数要处理部分初始化内存区域工作。我们都叫这些功能为一个对象的方法。调用一个对象的方法就是处理一则消息,我们使用参数self 来标记消息接收的对象。因为我们使用基本的C函数功能,self 是不需要作为第一个参数而传进的。

多个对象共享相同的类型描述符,也就是说,他们需要相同数量大小的内存空间,可用于相同的方法。我们称所有拥有相同的类型描述符的对象为一类;单独的对象被称为类的实例。到目前为止,一个类,一个抽象数据类型,可能的值与操作结合的集合,即,一个数据类型,这些是极其相似的。

一个对象是一个类的实例,也就是说,它拥有一个描述,这个描述被new() 所分配的内存所指示,并且这个描述被类的方法操作。普遍来说,一个对象是特殊数据类型的值。

 

2.3 选择器,动态链接,多态

谁来邮递消息呢? 构造器被 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() 中初始化它的原因:

如上图右边的类型描述 class 在编译的时候已经被初始化。对象是在运行时被创建的,接下来图中的虚线关联才被插入。在语句:

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

中,指向对象的内存区域的起始位置。我们对进行了强制类型转换,p把对象的起始位置当成一个指针,指向struct Class,即把参数class 设置为这个指针的值。

       接下来,若构造器是类型描述的一部分,我们调用它,并把其返回值做为 new() 的结果,即作为一个新的对象返回。2.6 部分列出一个很聪明的构造器,由于它聪明,所以能够对它自己的内存管理作出决策。

       注意啦,只有明确的可见函数如 new() 能拥有可变的参数列表。参数列表被 va_list 的变量 ap 所访问,ap 被一个宏 va_start() 初始化,这个宏在stdarg.h 头文件。new()仅仅能够把整个参数列表传进构造器中;因此,.ctor 也被声明成拥有 va_list 的参数,而不是它私有的参数列表。由于我们接下来要在好多函数中共享源参数列表,因此我们只传递 ap的地址到构造器中——当它返回时,ap 指向参数列表的第一个参数,而参数列表本身不会被改变。

       delete() 假设每个对象,也就是说,每个非空指针,指向一个类型描述。如果类型描述的析构器存在,则调用它。这里,self 扮演前面 p 的角色。我们使用局部变量cp 来进行强制类型转换,并从self 中获得我们所需要的信息。

void delete (void * self)

{     

    const struct Class ** cp = self;

 

       if (self && * cp && (* cp) -> dtor){

              self = (* cp) -> dtor(self);

       }

       free(self);

}

析构器,在上述delete()中,也会获得一次把他的返回值传进free() 的机会,如果构造器试着去欺骗,则析构器会有更改的机会,参看2.6部分。如果一个对象在调用delete() 的时候不想被删除,则可在他的析构器中返回一个空指针。

       所有其他的方法都存储在类型描述中,并以相似的方式被调用。在每个例子中,我们有一个单独的接收对象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下面。此时此刻,至少,我们会对空指针很警惕。在每个类型描述的起始,我们将存放一个“魔法数字”,或甚至把地址或所有已知类型的地址范围与 *self 相比较,但是,在第八章会看到,我们将做更严格的检查。

       不管怎么说,differ() 列举出了函数调用技术怎么被动态链接或后期链接调用的原因:即只要我们能够在一开始拥有一个正确的类型描述指针,那么我们就可以对任意的对象使用differ() 调用。这个函数实际上被调用的时机是尽可能的晚的——即仅仅在实际执行期间调用,而不是之前调用。

       我们可以称differ() 为一个选择器。它是多态功能的一个例子,也就是说,一个函数能够接受不同的参数类型,且表现不同,并且这种现象是基于他们的参数类型。一旦我们实现了更多的类时,这些类在他们的描述符中都包含 .differ ,则可称 differ() 为一个泛函数,且在这些类中能够被应用于任何对象。

       我们可以把这个选择器当成方法,方法自己本身不会动态链接,但仍然能够像多态函数一样的表现,因为它能让动态的连接的函数做他们真实的事情。

       多态机制实际已经嵌入到很多编程语言中,例如:如在Pascal(一种编程语言)中,write() 函数会根据参数类型不同进行不同的处理。在C++中,操作符 + 如果被不同的类型值如整型,指针,浮点指针调用,将产生不同的结果。这个现象被称作重载,即:参数类型和操作符名结合起来决定操作结果。相同的操作符与不同的参数类型结合将产生不同的响应。

       这里并没有明显的差异。因为动态连接,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 应用

然而我们还没有实现一个字符串类,我们仍然做好了一个简单的测试程序的准备。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 中声明。下面是其应用:

int main ()

{

    void * a = new(String, "a"), * aa = clone(a);

       void * b = new(String, "b");

 

       printf("sizeOf(a) == %lu/n", (unsigned long)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 实现——String

       我们通过写这些方法实现字符串,这些方法需要被放入类型描述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 因为 new() 已经建立了 .class

       析构器释放被字符串控制的动态内存。由于 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 如果我们比较同一个字符串对象,则返回假,若果我们比较两个不同的字符串对象,返回真,如果我们想比较字符串文本的差异可试着使用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);

}

类型描述符是独一无二的——这里我们要确定一个因素,即:我们的第二个参数是否为字符串文本。

       所有这些方法都应该使用关键字static 来修饰。因为这些方法只能通过 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.h 中声明String.c 中包含的公有方法,new.h 中声明new.c中包含的公有方法。以便于正确的初始化类型描述符,这里也包含了一个私有的头文件 new.r ,此文件中包含了2.2 部分定义的 struct Class 类型描述。

 

2.6 另一种实现——原子

       为了列举我们通过构造器和析构器到底能够做什么,我们实现了原子,所谓原子就是一个唯一的字符窜对象;如果两个原子包含相同的字符窜,则他们是相等的。原子是很容易比较的:如果两个参数的指针不同,则 differ() 返回真。原子的构造和销毁要付出一定的代价;我们为所有的原子维持了一个循环链表,并计数原子被克隆的次数,如下:

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;

}

所有的原子的循环链表被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;

如果我们找到了相同文本的原子,则增加它的引用计数count值,释放新的对象self 返回当前找的的原子指针p 。否则我们向循环链表中插入一个新字符串对象并设置其引用计数为count 1

       析构器防止删除引用计数为非零的原子。如下的代码被插入到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;

}

如果对引用计数的减1操作计数扔为正数,则返回一个空指针,以便于 delete() 手下留情。否则如果我们的字符串对象是最后一个对象我们清除循环链表标记符,否则从链表中删除我们的字符串。

把上述的实现加入到 2.4 的程序中,注意,对一个字符串对象的克隆此时为源字符串对象本身,运行结果如下:

sizeOf(a)==16

ok

clone?

 

2.7 总结

给一个指针指定一个对象,动态连接使我们找到了类型指定的函数功能:每个对象都会以一个描述符开始,这个描述符包含了指针,指向对象的可用函数指针表。尤其是,一个描述符包含一个指向构造器的指针,这个构造器用来初始化对象所关联的内存区域,另外这个指针还指向一个析构器,析构器会在删除对象之前回收对象所拥有的资源。

我们称所有的对象所共享的描述符为一个类。而对象是类的实例,对于对象指定类型的功能被称作对象的方法,而消息被这些功能函数所调用。对于一个对象,我们使用选择器功能去定位和调用动态连接的方法。

通过选择器和动态连接使得相同函数名对于不同的类而产生不同的结果。这样的函数被称为是多态的。

多态功能是非常有用的。他们提供了一种概念上的抽象:differ() 可比较任何两个对象——我们不需要铭记differ() 针对具体的情形是否可用。一个很容易并非常方便的调试工具就是多态函数 store() ,可在一个文件描述符上显示任何对象。

 

2.8 练习

了解了多态的功能后,我们需要使用动态连接来实现Object Set。这对于 Set 来说是比较困难的。我们不再记录一个元素属于哪个集合。

对于字符串来说,似乎有更多的方法去实现。我们需要知道字符串的长度,我们更想为一个对象从新设置它的字符串文本,我们应该能打印字符串文本。如果我们乐意去处理子串,将会更加有趣味。

原子是如此有效的,我们可使用一个哈希表来跟踪它,那么一个原子的值能否被改变呢?

String_clone() 呈现出一个微妙的问题:在这个函数中,String 的值似乎应该与self->class 相同。我们向 new() 中传递的参数会有任何变化吗?

おすすめ

転載: blog.csdn.net/besidemyself/article/details/6387915