第一章 抽象数据类型

1.1 数据类型

  数据类型是所有语言的组成部分,标准C有int、double和char等等。程序员很少关心哪些是有效的,编程语言中通常会提供一些列的方法从已有类型中构建新的数据类型。最简单的方法就是聚合,例如数组、结构体或者联合体。指针,根据C.A.R Hoare的说法“a step from which we may never recover”,允许我表示和操作无限复杂的数据。

  究竟什么是数据类型?我们可以有很多观点。数据类型是一堆值——char通常表示256种不同的值,int表示的更多,两者都均匀分布,或多或少的像自然数或者数学上的整数。double能表示的值更多,但是并不等同于数学上的实数。

  或者我们也可以自己定义一个值的集合,并附加上一些对这个集合的操作。计算机能表示的值和操作或多或少的影响到底层的硬件指令。标准C中的int在这一方面表现的并不好,int在不同机器上所表示的值的范围是不一样的,算术右移产生的结果也不一样。

  一些更复杂的例子也不会表现的很好。一般我们会将单链表定义如下:

typedef struct node {
    struct node * next;
    ... information ...
} node;

  用于操作链表的函数声明如下:

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

      然而这种方法十分稀疏平常。好的编程准则描述我们所关注的数据表示方法,仅提供所需的操作方法。

 

1.2 抽象数据类型

  如果不将具体表现暴露给用户,则我们称之为数据抽象。在理论上这要求我们通过数学公理来描述数据的属性和相关操作。例如,我们只能在队列里加入数据之后才能移除其中的数据,而且必须保证其位置和我加入的时候一致。

  抽象数据类型对程序员来说十分灵活。因为定义并不包含具体的实现,我们可以挑选最简单或者最优的实现。如果我们能保证必要信息的正确性,数据类型和我们选择的实现完全毫无关系。

  抽象数据类型满足良好编程准则的特性——信息隐藏和分而治之。具体的实现只需要实现者知道,用户不用关心。使用抽象数据类型,我们将实现和使用分开,这就使我们能很好的将大系统分解成为一个个小的模块。

 

1.3 例子-Set

  如何实现一个抽象数据类型呢?我们考虑一个拥有添加(add)、查找(find)、移除(drop)等操作的元素集合。这些操作应用于一个集合和一个元素,并返回已添加、查找到或者移除的元素。find操作可以用于实现包含(contains),用于查询一个元素是否包含在集合中。

  从以上观点来看,Set就是一个抽象数据类型。我们在Set.h中声明如下:

#ifndef SET_H
#define SET_H
extern const void * Set;
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

  上面的预编译语句用来保护声明:不管我们包含这个头文件多少次,C编译器只会处理一次。这是保护头文件的一种标准技术,GNU C编译器也支持这种技术,如果遇到已经定义的保护符号,甚至不会去读取完整的头文件。

  Set.h的定义是完整的,但是能用么?我们不会通过Set.h中的声明而了解到Set的具体实现。add()方法将一个元素添加到集合,并返回这个已经添加到集合的元素;find()方法在集合中查找一个元素,并返回这个在集合中的元素或者空指针;drop()方法先查找一个元素,并移除这个元素,最后返回这个已被移除的元素。contains()将find()返回的值转化为一个布尔值。

  无类型指针void *将会贯穿整书。一方面,我们隐藏了Set的具体实现,另一方面,这样做就允许我们向add()等方法传递任意的值。并不是所有传入的值都是Set或者element——我们为了是实现信息隐藏而牺牲了类型安全。然而,第八章我们会看到一种安全的方法。

 

1.4 内存管理

  我们可能忽略了什么,如何获取一个Set?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中malloc()和free()一个包装。如果是这样的话,传给new()的描述符至少要包含所需内存的大小。

 

1.5 对象

  如果我们想所有东西都能包含于集合中,我们需要另一个抽象数据类型Object,定义于头文件Object.h:

extern const void * Object; /* new(Object); */
int differ (const void * a, const void * b);

  differ()用于比较两个对象,如果相同就返回真值,如果不同就返回假值。这种定义方法给类似于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, b));
    delete(drop(s, c));
    return 0;
}

   我们创建一个集合和两个新对象。如果一切正常,我们会在这个集合中发现刚才添加的对象,并不会发现其他对象。程序仅仅会输出ok。

  differ()的调用证明了如下几点:一个集合只能存储每个元素一份;如果再向集合中添加这个元素,必然会返回这个元素,并且differ()的调用返回为false。同样的,一旦我们移除这个元素,那么集合中不会再包含这个元素。

  移除一个不再集合中的元素会导致delete()被传入空指针。我们现在认为这个可以接受的。

 

1.7 Set实现

  main.c能编译通过,但是在链接和执行本程序之前,我们得实现抽象数据类型和内存管理。如果一个对象不存储任何数据,如果一个对象只能属于一个集合,我们可以使用一个唯一的小整数来表示这个对象,并使用这个小整数作为数组heap[]的索引。如果一个对象属于一个集合,这个索引所对应的数组的值就用来表示这个集合。因此这个值指向了包含此对象的集合。

  最简单的方法是我们将所有的模块都集中到一个文件中,Set.c。集合和对象都有着同样的表示方法,所以new()不用关心类型描述。它仅仅返回一个位于heap[]中的0值。

#if ! defined MANY || MANY < 1
#define MANY 10
#endif
static int heap [MANY];
void * new (const void * type, ...)
{ 
    int * p; /* & heap[1..] */
    for (p = heap + 1; p < heap + MANY; ++ p)
        if (! * p)    
            break;
    assert(p < heap + MANY);
    * p = MANY;
    return p;
}

   我们使用0来标记来标记heap[]中可用的元素,因此我们不能返回一个指向heap[0]的引用,如果heap[0]是集合,那么属于这个集合的元素的值就是0了(元素的值表示所属集合)。

  在将一个元素加入到集合中之前,我们将元素的值设置为MANY,new()在寻找空闲的空间时不会找到这个元素,并能标记其不属于任何一个集合。

  new()可能遇到内存不够的情况。这是最常见的错误之一,不能让这种情况发生。我们使用标准C中的assert()来标记这些错误。更具体的实现至少应该打印一条合理的错误消息或者使用一个通用函数实现错误处理,用户可以重写这个函数。我们的目的是开发新的编程技术,我们希望能保持代码的整洁。在十三章我们会看到错误处理的通用方法。

  delete()得小心处理空指针。回收一个位于heap[]中的元素仅仅需要将这个元素的值设置为0。

void delete (void * _item)
{
     int * item = _item;
    if (item)
    { 
        assert(item > heap && item < heap + MANY);
        * item = 0;
    }
}

  我们得使用一种统一的方式来处理这些指针。我们使用以下划线开头的名字,只用来初始化本地变量。

  集合可以由元素表示出来,每个元素都指向所属集合。如果一个元素的值是MANY,它就可以添加到集合中,相反的,如果元素的值不为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()函数仅仅查看这个元素所属集合是否与给定集合一致。

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()函数,find()函数用于检测要移除的元素是否属于这个集合。如果属于,我们只需返回这个元素,并将这个元素的值设置为MANY。

void * drop (void * _set, const void * _element)
{
    int * element = find(_set, _element);
    if (element)
        * element = MANY;
    return element;
}

  如果我们吹毛求疵,我们可以要求被移除的元素不能属于其他的集合,只能属于给定集合。我们这个函数复用了find()函数的大部分代码。

  我们的实现并非传统方法。我们不需要differ()来实现集合。我们仍然提供这个函数,因为我们的程序用到了这个函数。

int differ (const void * a, const void * b)
{
r    eturn a != b;
}

   我们判断两个元素是否相等,只需要判断这个元素的值是否相等,这个值表示这个元素在heap数组中的索引,一个简单的比较操作足以。

  差不多完成了,我们没有用到Set和Object这两个变量,但是为了能编译通过,我们需要添加如下代码:

const void * Set;
const void * Object;

  我们使用这两个变量在main()函数中创建新的Set和Object对象。

 

 1.8 另一个实现-Bag

  在不改变set.h中的显式接口的情况下,我们可以改变它的实现。这次我们使用动态内存的数据结构来表示Set和Object对象:

struct Set { unsigned count; };
struct Object { unsigned count; struct Set * in; };

  count记录集合中元素的数量。在Object对象中,count记录这个元素在集合中的数量。在drop()函数中,将元素的count值减小,直到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()函数也可以处理空指针。

  add()函数或多或少的利用到传入的指针。函数会增加元素和集合的count值。

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(_set);
    assert(element);
    return element — > in == _set ? (void *) element : 0;
}

  contains()函数仍然基于find()函数,没什么变化:

  如果drop()函数查找到元素属于这个集合,先减少这个元素的count值,并减少集合的count值,如果元素的count值为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;
}

  我们提供一个新的count()函数,用来返回集合中元素的个数:

unsigned count (const void * _set)
{ 
    const struct Set * set = _set;
    assert(set);
    return set — > count;
}

  当然我们也可以简单的让其他代码直接访问count值,但是为了信息隐藏,我们还是不这么做。使用函数来访问数据会带来额外开销,直接访问成员也会带来值被重写的风险,两者相害取其轻,我们选择了前者。

  与前一个实现相比,一个元素可以加到一个集合中多次,只有移除次数和加入次数相同时,我们才能真正的从集合中移除这个元素。1.6中的例子将元素a加入到集合中两次。当第一次将元素a从集合中移除时,contains()函数任然会找到这个元素。这个例子的输出如下:

  ok

  drop?

 

1.9 总结

  对于抽象数据类型,我们必须完全隐藏所有的实现细节,比如说数据项的具体表示。

  应用程序只能通过头文件的定义和函数来访问数据,头文件中使用指针来表示数据类型,提供接受或者返回通用指针的函数来操作数据。

  new函数接受类型描述指针来产生一个对象指针,并将这个指针传入delete()函数用以回收相关资源。

  一般来说,每一个抽象数据类型都使用一个单独的源文件实现。很明显,没有其他的方法能访问到具体的数据。用以表示类型的指针至少要指向一个size_t的数值,用以表示对象所需内存空间大小。

译者:su47yuwenshu 2014.4.22

转载请注明出处

posted @ 2014-03-27 00:49  su47yuwenshu  阅读(1494)  评论(0)    收藏  举报