Chapter 1 of "Object-Oriented Programming With ANSI-C" (Abstract Data Type - Information Hiding)

 

Chapter 1 Abstract Data Types—Information Hiding  

1.1 Data Types

       Data types are an integral part of every programming language. ANSI-C (Standardized C ) has some basic data types: int , double and char . Limited data types can hardly meet the requirements of programmers, so the programming language will provide a mechanism to enable programmers to use these basic predefined data types to construct new data types. A simple application is to construct collections, such as arrays, structures, and unions. And pointer sets, according to CAR Hare 's words: "From this step, we may never recover" allow us to describe and manipulate data that is infinitely complex in nature.

       What is the real data type? We can express different opinions. A data type is a collection of values— the char ( character ) data type has 256 different values, the int (integer) data type has more different values, equally spaced between them, and behaves somewhat like natural numbers or integers in mathematics, and the double ( floating point data type ) type has more possible values, similar to numbers with decimal parts in mathematics.

       Optionally, we can define a data type as a collection of values ​​plus a series of operations to do something. Typically, these defined values ​​are computer-capable representations, and such operations can be translated into machine instructions. In this regard, the int type doesn't do very well in standard C. The value range of these data collections may vary with different machines, and the operation method is like the right shift operation in arithmetic, and the expression forms may be different.

       Too complex examples often cannot be effectively explained. We can typically define the elements in a linear list as a structure as follows:

typedef  struct node {

struct node *Next;

…information…

}node ;

And, for operations on this list, we specify the head of the list, as follows:

node * head (node * elt , const  node *  tail);

       However, such an application is very redundant, and good programming rules instruct us to hide the representation of data items and only declare the operation methods.

1.2 Abstract data types

       If we do not present the representation of this data type to the user, we call this data type an abstract data type. From a theoretical point of view, we are required to specify properties of data types in mathematical expressions involving possible operations. For example, we remove an element from the queue that we added earlier, and the elements we added can be retrieved from the queue in the same order.

       Abstract data types provide programmers with great convenience. Because the expression is not part of the definition. We can freely choose a simpler and more effective way to achieve it. If we can correctly separate out the necessary information, then the use and implementation of the data type will be completely independent.

       Abstract data types satisfy the good programming rules of "information hiding" and "divide and conquer". For example data item expression - Provide only those who need to know, implementers of abstract data types, not users. By using abstract data types, we can clearly separate the different tasks of implementers and users. And it can decompose a large system into small modules very well.

1.3 Examples --gather

       So, how do we implement an abstract data type? We take the operation of elements in a collection as an example, using the operation methods, add (increase), find (find), and drop (delete). These methods are used on collections and elements in collections. The add method adds an element to the collection and returns the element to be added. The find method finds the specified element from the collection and can be used to determine whether a specified element is in the collection. The drop method deletes an element from the collection.

       In this way, you can see that set ( collection ) is an abstract data type. Now declare what we want to do, starting with a header file Set.h :

#ifndef __USR_SET_H__

#define __USR_SET_H__

extern const void * Object;

void* add(void* set,const void* element);

void* find(const void* set,const void *element);

void* drop(void *set,const void * element);

int contains(const void* set,const void* element);

#endif

 

The function of the first two sentences enables the compiler to protect this statement. No matter how many times the header file Set.h is included, the C compiler only compiles this declaration once. Such a way of declaring header files is standard, recognized by the GNU C preprocessor, and when a guard symbol ( __USR_SET_H__ above ) is defined, it is guaranteed not to re-enter the code declared by the guard zone.

 

Set.h is complete, but is it really useful? It is almost impossible for us to detect and imagine its shortcomings: Set (collection) of course represents an instance, and we can use this instance to do many things. The Add() method passes an element, adds it to the collection, and returns the added element or the existing element in the collection; find() searches for the element in the specified collection, and returns the found element, if not found, returns NULL (empty); drop() locates an element, deletes the element from the collection, and returns the deleted element; the essence of contains() is to convert the result found by the find() method into a "true" value.

       通用指针类型 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 练习

       略。

Guess you like

Origin blog.csdn.net/besidemyself/article/details/6376408