Capítulo 1 de "Programación orientada a objetos con ANSI-C" (Tipo de datos abstractos - Ocultación de información)

 

Capítulo 1 Tipos de datos abstractos: ocultación de información  

1.1 Tipos de datos

       Los tipos de datos son una parte integral de cada lenguaje de programación. ANSI-C (Standardized C ) tiene algunos tipos de datos básicos: int , double y char . Los tipos de datos limitados difícilmente pueden cumplir con los requisitos de los programadores, por lo que el lenguaje de programación proporcionará un mecanismo que permita a los programadores utilizar estos tipos de datos básicos predefinidos para construir nuevos tipos de datos. Una aplicación simple es construir colecciones, como arreglos, estructuras y uniones. Y los conjuntos de punteros, según las palabras de CAR Hare : "Desde este paso, es posible que nunca nos recuperemos" nos permiten describir y manipular datos que son de naturaleza infinitamente compleja.

       ¿Cuál es el tipo de datos real? Podemos expresar diferentes opiniones. Un tipo de datos es una colección de valores: el tipo de datos char ( carácter ) tiene 256 valores diferentes, el tipo de datos int (entero) tiene más valores diferentes, igualmente espaciados entre ellos, y se comporta de alguna manera como números naturales o enteros en matemáticas, y el tipo doble ( tipo de datos de punto flotante ) tiene más valores posibles, similar a los números con partes decimales en matemáticas.

       Opcionalmente, podemos definir un tipo de datos como una colección de valores más una serie de operaciones para hacer algo. Por lo general, estos valores definidos son representaciones con capacidad de computadora y tales operaciones se pueden traducir a instrucciones de máquina. En este sentido, el tipo int no funciona muy bien en C estándar. El rango de valores de estas colecciones de datos puede variar con diferentes máquinas, y el método de operación es como la operación de desplazamiento a la derecha en aritmética, y las formas de expresión pueden ser diferentes.

       Los ejemplos demasiado complejos a menudo no se pueden explicar de manera efectiva. Por lo general, podemos definir los elementos en una lista lineal como una estructura de la siguiente manera:

  nodo de estructura typedef {

nodo de estructura *Siguiente;

…información…

}nodo;

Y, para operaciones en esta lista, especificamos el encabezado de la lista, de la siguiente manera:

nodo * cabeza (nodo * elt , const   nodo *   cola);

       Sin embargo, tal aplicación es muy redundante, y las buenas reglas de programación nos instruyen a ocultar la representación de los elementos de datos y solo declarar los métodos de operación.

1.2 Tipos de datos abstractos

       Si no presentamos la representación de este tipo de datos al usuario, llamamos a este tipo de datos un tipo de datos abstracto. Desde un punto de vista teórico, estamos obligados a especificar las propiedades de los tipos de datos en expresiones matemáticas que involucran posibles operaciones. Por ejemplo, eliminamos un elemento de la cola que agregamos anteriormente y los elementos que agregamos se pueden recuperar de la cola en el mismo orden.

       Los tipos de datos abstractos proporcionan a los programadores una gran comodidad. Porque la expresión no es parte de la definición. Podemos elegir libremente una forma más sencilla y eficaz de lograrlo. Si podemos separar correctamente la información necesaria, entonces el uso y la implementación del tipo de datos serán completamente independientes.

       Los tipos de datos abstractos satisfacen las buenas reglas de programación de "ocultación de información" y "divide y vencerás". Por ejemplo, la expresión del elemento de datos—— Proporcione solo aquellos que necesitan saber, implementadores de tipos de datos abstractos, no usuarios. Mediante el uso de tipos de datos abstractos, podemos separar claramente las diferentes tareas de los implementadores y los usuarios. Y puede descomponer muy bien un sistema grande en pequeños módulos.

1.3 Ejemplo --recolectar

       Entonces, ¿cómo implementamos un tipo de datos abstracto? Tomamos como ejemplo la operación de elementos en una colección, utilizando los métodos de operación, agregar (aumentar), buscar (encontrar) y soltar (eliminar). Estos métodos se utilizan en colecciones y elementos en colecciones. El método add agrega un elemento a la colección y devuelve el elemento que se agregará. El método find encuentra el elemento especificado de la colección y se puede usar para implementar para determinar si un elemento específico está en la colección. El método drop elimina un elemento de la colección.

       De esta forma, puedes ver que set ( colección ) es un tipo de dato abstracto. Ahora declare lo que queremos hacer, comenzando con un archivo de encabezado Set.h :

#ifndef __USR_SET_H__

#define __USR_SET_H__

const externo vacío * Objeto;

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

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

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

int contiene(const void* conjunto,const void* elemento);

#terminara si

 

La función de las dos primeras oraciones permite al compilador proteger esta declaración. No importa cuántas veces se incluya el archivo de encabezado Set.h , el compilador de C solo compila esta declaración una vez. Esta forma de declarar archivos de encabezado es estándar, reconocida por el preprocesador GNU C , y cuando se define un símbolo de guardia ( __USR_SET_H__ arriba ) , se garantiza que no se volverá a ingresar el código declarado por la zona de guardia.

 

Set.h está completo, pero ¿es realmente útil? Es casi imposible para nosotros detectar e imaginar sus deficiencias: Set (colección) por supuesto representa una instancia, y podemos usar esta instancia para hacer muchas cosas. El método Add() pasa un elemento, lo agrega a la colección y devuelve el elemento agregado o el elemento existente en la colección; find() busca el elemento en la colección especificada y devuelve el elemento encontrado, si no lo encuentra, devuelve NULL (vacío); drop() localiza un elemento, elimina el elemento de la colección y devuelve el elemento eliminado; la esencia de contains() es convertir el resultado del método find() en un valor "verdadero".

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

       略。

Supongo que te gusta

Origin blog.csdn.net/besidemyself/article/details/6376408
Recomendado
Clasificación