「ANSI-C によるオブジェクト指向プログラミング」の第 1 章 (抽象データ型 - 情報の隠蔽)

 

第 1 章抽象データ型 - 情報の隠蔽  

1.1データ型

       データ型は、あらゆるプログラミング言語に不可欠な部分です。ANSI-C (標準化されたC ) には、 intdoublecharなどの基本的なデータ型があります限られたデータ型ではプログラマの要件を満たすことはほとんどできないため、プログラミング言語は、プログラマがこれらの基本的な事前定義されたデータ型を使用して新しいデータ型を構築できるようにするメカニズムを提供します。簡単なアプリケーションは、配列、構造体、共用体などのコレクションを構築することです。そして、 CAR Hare氏の言葉によれば、「このステップからは決して回復できない可能性があります」というポインタ セットを使用すると、本質的に無限に複雑なデータを記述および操作できるようになります。

       実際のデータ型は何ですか? 私たちはさまざまな意見を表明することができます。データ型は値のコレクションです。char (文字)データ型には256の異なる値があり、int (整数) データ型にはより多くの異なる値が等間隔であり、数学における自然数や整数に似た動作をします。また、double (浮動小数点データ型)型には、数学における小数部のある数値と同様に、より多くの取り得る値があります

       オプションで、値のコレクションと何かを行うための一連の操作としてデータ型を定義できます。通常、これらの定義された値はコンピューター対応の表現であり、そのような操作は機械命令に変換できます。この点で、int 型は標準Cではあまりうまく機能しません。これらのデータ集合の値の範囲はマシンによって異なり、演算方法は算術の右シフト演算に似ており、表現形式も異なる場合があります。

       複雑すぎる例は、効果的に説明できないことがよくあります。通常、線形リスト内の要素は次のように構造として定義できます。

typedef  構造体ノード {

構造体ノード *次へ;

…情報…

}ノード;

そして、このリストに対する操作では、次のようにリストの先頭を指定します。

ノード * ヘッド (ノード * elt 、 const  ノード *  テール);

       ただし、このようなアプリケーションは非常に冗長であり、適切なプログラミング規則では、データ項目の表現を非表示にして、操作メソッドのみを宣言するように指示されています。

1.2抽象データ型

       このデータ型の表現をユーザーに提示しない場合、このデータ型を抽象データ型と呼びます。理論的な観点からは、可能な演算を含む数式でデータ型のプロパティを指定する必要があります。たとえば、前に追加したキューから要素を削除すると、追加した要素を同じ順序でキューから取得できます。

       抽象データ型はプログラマに大きな利便性をもたらします。なぜなら、式は定義の一部ではないからです。私たちはそれを達成するために、よりシンプルでより効果的な方法を自由に選択できます。必要な情報を正しく分離できれば、データ型の使用と実装は完全に独立したものになります。

       抽象データ型は、「情報隠蔽」と「分割統治」という優れたプログラミング規則を満たします。たとえば、データ項目式— ユーザーではなく、抽象データ型の実装者など、知る必要がある人にのみ提供します。抽象データ型を使用することで、実装者とユーザーのさまざまなタスクを明確に分離できます。また、大規模なシステムを小さなモジュールに非常にうまく分解できます。

1.3 - 集める

       では、抽象データ型を実装するにはどうすればよいでしょうか? コレクション内の要素の操作を例として、add (増加)、find (検索)、drop (削除) の操作メソッドを使用します。これらのメソッドは、コレクションおよびコレクション内の要素に対して使用されます。addメソッドは、コレクションに要素を追加し、追加される要素を返します。findメソッド、コレクションから指定された要素を検索し、指定された要素がコレクション内にあるかどうかを判断する実装に使用できます。drop メソッドは、コレクションから要素を削除します。

       このように、 set ( collection )は抽象データ型であることがわかります。次に、ヘッダー ファイルSet.hから始めて、何をしたいかを宣言します

#ifndef __USR_SET_H__

#define __USR_SET_H__

extern const void * オブジェクト;

void* add(void* セット,const void* 要素);

void* find(const void* セット,const void *element);

void* ドロップ(void *set,const void * 要素);

int contains(const void* セット,const void* 要素);

#endif

 

最初の 2 つの文の機能により、コンパイラはこのステートメントを保護できます。ヘッダー ファイルSet.h が何回インクルードされても、Cコンパイラーはこの宣言を 1 回だけコンパイルします。このようなヘッダー ファイルの宣言方法は標準であり、GNU Cプリプロセッサによって認識されており、ガード シンボル (上記の__USR_SET_H__ )が定義されている場合、ガード ゾーンによって宣言されたコードが再入力されないことが保証されます。

 

Set.hは完成しましたが、本当に便利でしょうか? 私たちがその欠点を検出したり想像したりすることはほとんど不可能です。Set (コレクション)もちろんインスタンスを表し、このインスタンスを使用して多くのことを行うことができます。Add()メソッドは要素を渡し、それをコレクションに追加し、追加された要素またはコレクション内の既存の要素を返します。find() は指定されたコレクション内の要素を検索し、見つかった要素を返します。見つからない場合は NULL (空) を返します。drop ( )要素を検索し、コレクションから要素を削除し、削除された要素を返します。contains()の本質は、 find()メソッドの結果を「true」値に変換することです。

       通用指针类型 void* 的应用贯穿全文。一方面它使得我们想发现集合到底是什么东西成为不可能。但是另一方面它允许我们向如add() 和其他方法中传递任意类型的数据。并不是每件事都会拥有像集合和集合中的元素一样的表现形式——在信息隐藏的乐趣中我们牺牲了类型的安全性。然而,我们可以在第八章看到这样的应用会非常之安全。

1.4 内存管理

       也许我们已经瞥见了某些东西:怎样的获得一个集合呢?Set(集合)是一个指针,并不是被typedef 关键字定义的类型;因此我们不能把Set定义成一个局部或全局的类型。相反的我们只是使用指针来引用集合和集合中的元素,并且建立一个文件new.h,并声明如下:

void * new (const void * type, ...);

void delete (void * item);

 

就像Set.h 文件一样的做法,文件被预处理器符号NEW_H保护起来。以后只列出感兴趣的部分,所有的源代码和所有实例的代码均能在光碟中找到。

       new() 接收一个像Set的描述符,传递更多可能的参数用于初始化操作,返回一个指向新数据项的携带描述符信息的指针。delete() 接受一个由new() 所原先产生的指针,并回收关联的资源。

       new() delete() 可看成类似于标准C函数calloc() free() 。如果的确是,描述符得能够指示出至少需要申请多大的内存空间。

1.5 Object)对象      

       如果我们想搜集在集合感兴趣的东西,我们需要另外一个抽象数据类型Object ,在头文件Object.h 中有如下描述:

extern const void * Object; /* new(Object); */

int differ (const void * a, const void * b);

 

differ() 是用来做对象比较的:即若两个对象不相等则返回真,否则返回假。这样的描述为C语言函数strcmp() 留有余地:因为在某些比较中我们也许选择返回一个整数或负数的值来指示排列的次序(正序或倒叙)。

       现实生活的对象需要更多的功能去做有用的事情。此刻,我们约束我们自己只对集合中的成员(必须品)操作而已。如果我们建立一个更大的类库,我们将看到所谓的集合 实际,包括其他所有东西 均是一个对象。从这个观点出发,很多的对象包含的功能其实都是无条件存在的。

1.6 应用

       包含头文件,和库信息,抽象数据类型,我们能够写一个main.c 的应用程序如下所示:

#include <stdio.h>

#include "New.h"

#include "Object.h"

#include "Set.h"

 

int main()

{

       void* s=new(Set);

       void* a=add(s,new(Object));

       void* b=add(s,new(Object));

       void* c=new(Object);

 

       if(contains(s,a)&&contains(s,b)){

              puts("ok");

       }

 

       if(contains(s,c)){

              puts("contains?");

       }

 

       if(differ(a,add(s,a))){

              puts("differ?");

       }

 

       if(contains(s,drop(s,a))){

              puts("drop?");

       }

 

       delete(drop(s,a));

       delete(drop(s,a));

 

       return 0;

}

 

我们创建了一个结合并给集合中添加了两个新建的对象。如果不出意外,我们可以发现对象会在集合中,并且我们不会再发现其他的新对象。程序的运行会简单得打印出 ok

       differ() 的调用会证明出这样的语义:数学上的集合只包含集合 a 的一份拷贝;对一个元素的重复添加必须返回已经加入的对象,因此上述程序的 differ() 。相似的,一旦我们从一个结合删除一个元素,它将不会再存在于这个集合中。

       从一个集合中删除一个不存在的元素将返回一个空的指针传递给 delete() 。现在,我们已经指示出 free() 的语义且必须是合情合理可接受的。

1.7 一种实现机制——Set(集合)

       main.c 会编译成功,但在连接和执行之前,我们必须实现其中的抽象数据类型和内存管理。如果一个对象不存储信息,且每个对象最多属于一个结合,我们可以把每个对象和集合当成,小的,独立的,正整数的值。可在 heap[] 中通过数组下标来索引到。如果一个对象(这里的对象为数组元素的地址)是一个集合的成员,则数组元素的值代表这个集合。对象指向包含它的集合。

       首先的解决方案是非常简单的,即我们把其他模块与Set.c 相结合。集合对象集有相似的呈现方式。因此,对于 new() 无需关注类型描述。它仅仅从数据 heap[] 中返回非零元素。

void * new (const void * type, ...)

{

    int *p ;    /*&heap[1...]*/

    for(p=heap+1;p<heap+MANY;++p){

        if(!*p){/*heap 中的某个元素为0,则返回这个指针,并把值设为MANY*/

            break;

        }

    }

    assert(p<heap+MANY);

    *p=MANY;   

    return p;

}

 

我们是用0来标记 heap[] 中有效的元素;因此不能返回heap[0] 的引用——如果它是一个集合,而集合的元素可以是索引值为0的对象。

       在一个对象被添加到集合当中之前, 我们让它包含无效的索引MANY,以便于new() 不会再次返回它,请不能误解 MANY 是集合的一个成员。

       new() 能够使用完内存。这是很多“致命性错误”的其中一个。我们可以简单的使用标准化C语言宏的宏 assert() 来标记这些错误。一个更理想的实现方式是至少会打印合理的错误信息或使用用户可重写的错误处理机制的通用功能。这也是我们的目的中,编码技术完整性的一部分。在第13章,我们会介绍一种通用异常处理的技术。

       delete() 必须得严加防范空指针的传入。通过设置其元素的值为0 来进行 heap[] 中元素的回收。

void delete (void * _item)

{

    int* item=_item;

    if(item){

        assert(item>heap && item,heap+MANY);

        *item=0;

    }

}

 

我们需要统一的处理通用指针;因此,给每个通用指针的变量的前面加上下划线前缀,然后仅仅使用它初始化指定类型的局部变量。

       一个集合被它的对象所表示。集合中的每个元素指向它的集合。如果元素包含 MANY ,则它可以被添加到一个集合中。否则它已经属于一个集合的元素了。因为我们不允许一个对象属于多个集合。

void* add(void* ­_set,const void* _element);

{

    int * set=_set;

    const int *element=_element;

 

    assert(set>heap && set<heap+MANY);

    assert(*set==MANY);

    assert(element>heap&& element<heap+MANY);

 

    if(*element==MANY){

        *(int*)element=set-heap;

    }

    else{

        assert(*element==set-heap);

    }

    return (void*) element;

}

assert() 在这里稍微显得逊色:我们只关注在 heap[] 内的指针和集合不属于其他部分的集合,等等,数组元素的值应该为 MANY

       其他的功能都是很简单的。find() 只查找元素的值为集合索引的元素。若找到,返回元素,否则返回NULL

void* find(const void* _set,const void * _element)

{

    const int* set=_set;

    const int* *element=_element;

    assert(set>heap && set<heap+MANY);

    assert(*set==MANY);

    assert(element>heap && element<heap+MANY);

    assert(*element);

    return *element==set-heap?(void*)element:0;

}

contains() find() 的结果转换为真值:

int contains(const void* _set,const void* _element)

{

    return find(_set,_element)!=0;

}

drop() 依赖于find() 的结果,若在集合中查找到,则把此元素的值标记为MANY,并返回此元素:

void* drop(void * _set,const void * _element)

{

    int* element=find(_set,_element);

    if(element){

        *element=MANY;

    }

    return element;

}

 

如果我们深入挖掘,一定会坚持被删除的元素要不包含于其他集合中。在这种情况下,毫无疑问会在 drop() 中复制更多 find() 的代码。

       我们的实现是很非传统的。在实现一个集合时似乎不需要 differ() 。我们仍然提供它,因为我们的程序要使用这个函数。

int differ (const void * a, const void * b)

{

    return a!=b;

}

当数组中对象的索引不同时,这个对象必然是不同的,也就是索引值就能区分它们的不同,但一个简单的指针比较已经足够了。

       我们已经做完了——对于这个问题的解决我们还没有使用描述符 Set Object ,但是不得不定义它以使我们的编译器能通过。

const void * Set;

const void * Object;

我们在 main() 函数中使用上述指针来创建集合和对象。

 

1.8 另一种实现——包

       不需要改变Set.h 中的接口,我们来改变接口的实现方式。这次使用动态内存分配,使用结构体来表示集合和对象:

struct Set{

       unsigned count;

};

struct Object{

       unsigned count;

       struct Set* in;

};

count 用于跟踪集合中的元素的计数个数。对于一个元素来说,count 记录这个元素被集合添加的次数。如果我们想递减count 值,可调用 drop() 方法。一旦一个元素的count 值为0,我们就可以删除它,我们拥有一个,即,一个集合,集合中的元素拥有一个对count 的引用。

       因为我们使用动态内存分配机制去表示集合集和对象集,所以需要初始化Set Object 描述符,以便于new() 能够知道需要分配多少内存:

static const size_t _Set=sizeof(struct Set);

static const size_t _Object=sizeof(struct Object);

 

const void * Set=&_Set;

const void * Object=&_Object;

new() 方法现在更加简单:

void * new (const void * type,...)

{

       const size_t size= *(const size_t*)type;

       void* p=calloc(1,size);

       assert(p);

       return p;

}

delete() 可直接把参数传递给 free()——标准化C语言中 一个空的指针可以传进 free() 。如下:(如意调用)

void delete (void * _item)

{

       free(_item);

}

 

add() 方法多多少少对它的指针自变量比较信任。它会增加元素的引用计数和集合的引用计数。

void* add(void* _set,const void* _element)

{

       struct Set *set=_set;

       struct Object* element=(void*)_element;

 

       assert(set);

       assert(element);

 

       if(!element->in){

              element->in=set;

       }

       else{

              assert(element->in==set);

       }

 

       ++element->count;

       ++set->count;

 

       return element;

}

 

find() 方法仍然会检查,一个元素是否指向一个适当的集合:

void* find(const void* _set,const void * _element)

{

       const struct Object* element=_element;

 

       assert(element);

 

       return element->in==_set?(void*)element:0;

}

contains() 方法基于find() 方法来实现,仍然保持不变。

       drop() 在集合中找到它要操作的元素,它将递减元素的引用计数和元素在集合中的计数。如果引用计数减为0,这个元素即被从集合中删除:

void* drop(void * _set,const void * _element)

{

       struct Set* set=_set;

       struct Object* element=find(set,_element);

 

       if(element){

              if(--element->count==0){

                     element->in=0;

              }

              --set->count;

       }

       return element;

}

       现在我们可以提供一个新的方法,用来获取集合中的元素个数:

unsigned count(const void* _set)

{

       const struct Set* set=_set;

 

       assert(set);

       return set->count;

}

       当然啦,直接让程序通过读 对象 .count 显得比较简单,但是我会坚持不去披露集这样的实现。与应用程序重写临界值的危险性相比上述功能的调用的开销是可忽视的。

       的表现与集合是不同的。一个元素可被添加多次;当一个元素的删除次数等于其被添加的次数时,这个元素被从集合中删除,contains() 方法仍然能够找着它。测试程序的运行结果如下:

ok

drop?

 

1.9 总结

       对于抽象数据类型,我们完全隐藏了其实现的细节,例如应用程序代码中数据项的描述。

       程序代码只访问头文件,在头文件中描述符指针表示数据类型,对数据类型的操作作为一种方法被声明,此方法接收和返回通用指针。

       描述符指针被传进通用方法 new() 中去获得一个指向数据项的指针,这个指针被传进通用方法 delete() 中去回收关联的资源。

       通常情况,每个抽象数据类型被在单独的源文件中实现。理想情况下,它不对其他数据类型描述。这个描述符指针正常情况下至少指向一个固定大小的值来指示需要的数据项空间大小。

 

1.10 练习

       略。

おすすめ

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