Estruturas de dados
had
Aula 16b:
ƒ
ADTs revisitados
Tal como os tipos pré-definidos da linguagem C (int, float, char, ...), um ADT é um tipo de dados,
(conjunto de valores e uma colecção de operações sobre esse valores). No entanto o ADT além de
ser um tipo de dados definido pelo utilizador é abstracto. O que quer dizer que a representação dos
valores não é transparente para o cliente. Diz-se pois que a interface do ADT é opaca, por esconder
a representação desses valores.
⎧
⎧conjunto de valores
⎪Tipo de dados = ⎨
ADT (abstract data type) = ⎨
⎩colecção de operações
⎪abstracto (representação dos valores escondida do cliente)
⎩
Deste modo os clientes são obrigados a manipular os ADTs apenas através das funções definidas no
seu interface o ficheiro *.h. De modo que para cada função deve estar indicado:
- Descrição em termos dos argumentos de entrada e de saída
- Pré-condições (validações de variáveis que devem ser efectuadas pelo cliente)
- Pós-condições (estado do programa - variáveis alteradas - após execução das funções)
- Excepções
Como o cliente só pode manipular os valores dos ADTs através das funções do interface, não pode
intencionalmente ou inadvertidamente deixar o ADT num estado inválido, ou seja corromper a
informação do ADT. Outra vantagem é que se for necessário (por exemplo por questões de
eficiência) alterar a representação dos valores dos dados ou a implementação de uma qualquer
função, não é necessário efectuar alterações no código cliente (desde que não se altere os
protótipos das funções).
Como um ADT é um tipo de dados, embora abstracto, deve-se comportar como qualquer tipo de
dados. Em particular deve suportar múltiplas instâncias, isto é num mesmo programa deverão
poder-se declarar múltiplas variáveis do ADT, tal como é possível para os tipos pré-definidos.
Pode-se mesmo definir algumas directrizes para desenvolver um ADT múltipla instância:
a) Se bem que não seja obrigatorio para a definição de um ADT, deve ser garantido que o
interface, e de um modo geral todos os headers (ficheiros *.h), não são incluídos mais do
que uma vez quando da compilação, usando um conjunto de directivas de compilação:
#ifndef _ADT_
#define _ADT_
/* Todo o código do interface */
#endif
Por convenção o nome da macro definida na segunda linha é o nome do ficheiro sem
extensão em maiúsculas entre underscores.
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 1/10
Estruturas de dados
had
b) Deve-se esconder do cliente o mais possível sobre a implementação, de modo que no
interface só deve estar o absolutamente necessário, mesmo em termos de definição de
macros ou de inclusão de ficheiros.
c) No interface (adt.h) existirá um ponteiro para uma estrutura que guarda os valores de cada
instância. Esta estrutura estará declarada no ficheiro de implementação (adt.c), e portanto
inacessível a manipulação por parte do cliente, mesmo que este saiba como está
definida! Os membros de dados dessa estrutura também são chamados variáveis de
instância.
d) Deverá existir uma função para criar uma nova instância bem como uma função para a
eliminar da memória. Por convenção, estas funções tem como parte do nome init (ou new)
e free (ou del).
e) De resto, por convenção todas as funções do ADT iniciam-se pelo seu nome ou por um
acrónimo em maiúsculas, por exemplo: ADTinit ( .... ).
f) Como os clientes dos ADTs não sabem e não podem aceder directamente à representação
dos valores das suas instância, é comum que os ADTs disponibilizem funções no seu
interface para se escreverem num stream (que poderá estar associado a um ficheiro ou ao
monitor - stdout) em modo de texto formatado e legível e em binário para poupar memória.
Template para ADT múltipla instância
Seguidamente é apresentado um resumo de um template que poderá ser seguido para programar
ADTs múltipla instância. A versão completa e documentada encontra-se em anexo:
interface
/* Breve descrição do ADT e identificação do autor (adt.h)
*/
#ifndef _ADT_ /* por convenção o nome do ficheiro sem extensão */
#define _ADT_
#include <stdio.h>
/* include de outros headers (*.h) ABSOLUTAMENTE necessários. */
/* Definição de Macros ABSOLUTAMENTE necessárias. */
/* ============================
ponteiro para instância do ADT
============================*/
typedef struct ADTstruct* adt; /*por conv. o nome do ponteiro em minúsculas*/
/* ============================
Declaração das funções do interface (colecção de operações)
============================*/
/*aloca nova instância do ADT*/
adt ADTnew(/* parâmetros: int x, float .... */);
/*elimina instância p*/
void ADTdel(adt p);
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 2/10
Estruturas de dados
had
/* Escreve a instância do adt apontada por p no stream f em modo texto */
int ADTprint(adt p, FILE* f);
/* Escreve a instância do adt apontada por p no stream f em modo binário */
int ADTwrite(adt p, FILE* f);
/* Retorna uma nova instância do adt lida do stream f em modo binário */
adt ADTread(FILE* f);
/* Declaração de outras funções necessárias ao ADT
...
*/
#endif
implementação
/* Breve descrição do ADT e identificação do autor (adt.c)
*/
#include "adt.h" /* include header (interface) do próprio adt */
#include <stdlib.h>
/* include de outros headers (*.h) necessários. */
/* Definição de Macros necessárias */
/* ============================
Definição da estrutura do ADT
============================*/
struct ADTstruct {
/* variáveis de instância do adt.
Podem ser de qualquer tipo (mesmo outros ADTs).
int a;
char s[35];
*/
};
/* Se necessário definir funções privadas ao ficheiro adt.c. p. ex:
static int func(int 4, ... ) {
....
}
*/
/* ============================
Definição das funções do interface
============================*/
adt ADTnew(/* parâmetros necessários: int x, float .... */) { ... };
void ADTdel(adt p) { ... };
int ADTprint(adt p, FILE* f) { ... };
int ADTwrite(adt p, FILE* f) { ... };
adt ADTread(FILE* f) { ... };
/* Implementação de outras funções declaradas no interface
...
*/
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 3/10
Estruturas de dados
had
Implementação do ADT complexo de acordo com o template
Como exemplo de implementação de um ADT segundo o template acima apresenta-se a seguir o
ADT número complexo, onde para simplificar a única operação aritmética definida é a soma de dois
complexos.
interface
/* ADT Complex interface (complex.h)
2004, 2005 had
*/
#ifndef _COMPLEX_
#define _COMPLEX_
#include <stdio.h>
/* ============================
ponteiro para instância do ADT
============================*/
typedef struct COMPstruct* complex;
/* ============================
Declaração das funções do interface (colecção de operações)
============================*/
/* Aloca memória para nova instância do ADT Complex e retorna ponteiro para ela.
Excepção: Senão houver memória suficiente para o ADT retorna NULL
*/
complex COMPinit(float re, float im);
/* Liberta memória reservada para ADT Complex
PRE: c != NULL
*/
void COMPfree(complex c);
/* Escreve a instância de complexo apontada por c no stream f
em modo texto. Retorna 1.
PRE: c != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int COMPprint(complex c, FILE* f);
/* Escreve a instância de complexo apontada por c no stream f
em modo binário. Retorna 1.
PRE: c != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int COMPwrite(complex c, FILE* f);
/* Retorna uma nova instância de complexo lida do stream f em modo binário
PRE: f != NULL
Excepção: Se ocorrer erro de leitura ou se não houver memória suficiente
para a nova instância retorna NULL
*/
complex COMPread(FILE* f);
/* Retorna o complexo soma dos argumentos c0 e c1
PRE: c0 != NULL && c1 != NULL
Excepção: Se não houver memória suficiente para a nova instância
retorna NULL
*/
complex COMPadd(complex c0, complex c1);
#endif
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 4/10
Estruturas de dados
had
implementação
/* ADT Complex (complex.c)
2004, 2005 had
*/
#include <stdlib.h>
#include "complex.h"
/* ============================
Definição da estrutura do ADT Complex
============================*/
struct COMPstruct { float re, im; };
/* ============================
Definição das funções do interface
============================*/
/* Aloca memória para nova instância do ADT Complex e retorna ponteiro para ela.
Excepção: Senão houver memória suficiente para o ADT retorna NULL
*/
complex COMPinit(float re, float im) {
complex tmp=(complex) malloc (sizeof *tmp);
if (tmp) {
tmp->re=re;
tmp->im=im;
}
return tmp;
};
/* Liberta memória reservada para ADT Complex
PRE: c != NULL
*/
void COMPfree(complex c) {
free(c);
};
/* Escreve a instância de complexo apontada por c no stream f
em modo texto. Retorna 1.
PRE: c != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int COMPprint(complex c, FILE* f){
int n0=1, n1=!EOF, n2=1;
if (c->re != 0) n0=fprintf (f, "%f", c->re);
if (c->im > 0) n1=fputc('+', f);
if (c->im != 0) n2=fprintf (f, "%fj", c->im);
return (n0<0 || n1==EOF || n2<0) ? 0 : 1;
};
/* Escreve a instância de complexo apontada por c no stream f
em modo binário. Retorna 1.
PRE: c != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int COMPwrite(complex c, FILE* f){
return fwrite (c, sizeof (*c), 1, f);
};
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 5/10
Estruturas de dados
had
/* Retorna uma nova instância de complexo lida do stream f em modo binário
PRE: f != NULL
Excepção: Se ocorrer erro de leitura ou se não houver memória suficiente
para a nova instância retorna NULL
*/
complex COMPread(FILE* f){
int n;
complex c=COMPinit(0,0);
/* inicializa instância vazia */
if (!c) return NULL;
n=fread (c, sizeof (*c), 1, f); /* lê-a do stream f */
if (n) return c;
else {COMPfree(c); return NULL;} /* e retorna-a ou NULL se erro de leitura*/
};
/* Retorna o complexo soma dos argumentos c0 e c1
PRE: c0 != NULL && c1 != NULL
Excepção: Se não houver memória suficiente para a nova instância
retorna NULL
*/
complex COMPadd(complex c0, complex c1){
return COMPinit(c0->re + c1->re, c0->im + c1->im);
};
cliente
/* testcomp.c
*/
#include "complex.h"
int main()
{
complex a = COMPinit(1,2);
complex b = COMPinit(1,3);
complex c;
FILE *f;
c=COMPadd (a, b);
/* 2+5j */
f=fopen("file.dat", "w"); COMPwrite(c, f);
f=fopen("file.dat", "r"); a=COMPread(f);
COMPprint(a, stdout);
putchar ('\n');
/*
printf("%f\n", c->re;)
c->im = 4.5;
*/
COMPfree (a); COMPfree (b); COMPfree (c);
getchar();
return 0;
}
fclose(f);
fclose(f);
Pode-se mostrar que a interface do ADT é opaca, isto é separa efectivamente a implementação do
cliente, se se tentar a partir do cliente aceder directamente aos membros de dados de uma instância
do ADT complex (as suas variáveis de instância).
Assim descomentando qualquer das linhas a vermelho o compilador retorna um erro quando tenta
compilar:
comptest.c:16: error: dereferencing pointer to incomplete type
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 6/10
Estruturas de dados
had
De facto em tempo de compilação o cliente incluí o interface (#include "complex.h") onde está
declarado o ponteiro complex para a estrutura COMPstruct. No entanto esta está definida no
ficheiro complex.c que não é incluído no cliente (é sim linkado posteriormente).
Deste modo, em tempo de compilação não é possível determinar a definição da estrutura
COMPstruct. Em particular não é conhecido que esta tem dois membros de dados tipo float,
chamados re e im, logo o compilador retorna o erro acima quando se tenta aceder a um deles
através do ponteiro, p. ex:
c->im
Aliás apenas a tentativa de saber o tamanho da variável apontada pelo ponteiro c:
sizeof (*c)
resulta no mesmo erro. Já:
sizeof (c)
retorna 4 num compilador de 32 bits, pois neste caso está-se a determinar o tamanho do ponteiro e
não da variável por este apontada.
Uma forma de aceder às variáveis de instância dos ADTs, consiste em definir funções chamadas
acessores. Estas funções são definidas no ficheiro de implementação (*.c) do ADT e declaradas no
seu interface (*.h) permitindo aceder (ler ou escrever) aos membros de dados de cada instância do
ADT.
Podia-se por exemplo ter um acessor de escrita para a parte real do complexo, a variável re:
void COMPSetReal (complex c, float r) {
c->re = r;
}
e um acessor de leitura para a mesma variável:
float COMPGetReal (complex c) {
return c->re;
}
No entanto tal deve ser evitado, e só efectuado se absolutamente necessário, pois reduz a opacidade
do interface, abrindo a implementação ao código cliente, o que de certa forma levanta a abstracção
do ADT.
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 7/10
Estruturas de dados
had
Anexo A - Template para ADT múltipla instância
Seguidamente é apresentado um template que poderá ser seguido para programar ADTs múltipla
instância:
interface
/* Breve descrição do ADT e identificação do autor (adt.h)
*/
#ifndef _ADT_ /* por convenção o nome do ficheiro sem extensão */
#define _ADT_
#include <stdio.h>
/* include de outros headers (*.h) ABSOLUTAMENTE necessários. */
/* Definição de Macros ABSOLUTAMENTE necessárias. p. ex:
#define N 10
...
*/
/* ============================
ponteiro para instância do ADT
============================*/
/* por convenção o ponteiro declara-se em minúsculas */
typedef struct ADTstruct* adt;
/* ============================
Declaração das funções do interface (colecção de operações)
============================*/
/* Aloca uma nova instância do ADT e retorna ponteiro para ela.
Excepção: Senão houver memória suficiente para o ADT retorna NULL
*/
adt ADTnew(/* parâmetros necessários: int x, float .... */);
/* Disponibiliza memória alocada a uma instância do ADT
PRE: p != NULL
*/
void ADTdel(adt p);
/* Escreve a instância do adt apontada por p no stream f
em modo texto. Retorna 1.
PRE: p != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int ADTprint(adt p, FILE* f);
/* Escreve a instância do adt apontada por pdate no stream f
em modo binário. Retorna 1.
PRE: p != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int ADTwrite(adt p, FILE* f);
/* Retorna uma nova instância do adt lida do stream f em modo binário
PRE: f != NULL
Excepção: Se ocorrer erro de leitura ou se não houver memória suficiente
para a nova instância retorna NULL
*/
adt ADTread(FILE* f);
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 8/10
Estruturas de dados
had
/* Declaração de outras funções necessárias ao ADT
...
*/
#endif
implementação
/* Breve descrição do ADT e identificação do autor (adt.c)
*/
#include "adt.h" /* include header (interface) do próprio adt */
#include <stdlib.h>
/* include de outros headers (*.h) necessários.*/
/* Definição de Macros necessárias. p. ex:
#define STR "abcdef"
*/
/* ============================
Definição da estrutura do ADT
============================*/
struct ADTstruct {
/* variáveis de instância do adt.
Podem ser de qualquer tipo (mesmo outros ADTs).
int a;
char s[35];
*/
};
/* Se necessário definir funções privadas ao ficheiro adt.c
p. ex:
static int func(int 4, ... ) {
....
}
*/
/* ============================
Definição das funções do interface
============================*/
/* Aloca uma nova instância do ADT e retorna ponteiro para ela.
Excepção: Senão houver memória suficiente para o ADT retorna NULL
*/
adt ADTnew(/* parâmetros necessários: int x, float .... */) {
adt p = (adt) malloc (sizeof *p);
if (p) {
/*
Inicializa variáveis da nova instância. p. ex.:
p->a=x;
... */
}
return p;
};
/* Disponibiliza memória alocada a uma instância do ADT
PRE: p != NULL
*/
void ADTdel(adt p) {
/* outras operações necessárias como p. ex:
libertação de estruturas de dados da memória
fecho de ficheiros, ... */
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 9/10
Estruturas de dados
had
free (p);
};
/* Escreve a instância do adt apontada por p no stream f
em modo texto. Retorna 1.
PRE: p != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int ADTprint(adt p, FILE* f){
/* usa funções como:
fprintf (FILE*, const char *, ...);
fputf (int, FILE*);
*/
return 1; /* ou 0 se erro escrita*/
};
/* Escreve a instância do adt apontada por pdate no stream f
em modo binário. Retorna 1.
PRE: p != NULL && f != NULL
Excepção: Se ocorrer erro de escrita retorna 0
*/
int ADTwrite(adt p, FILE* f) {
/* Em grande parte dos casos basta:
*/
return fwrite (p, sizeof (*p), 1, f);
};
/* Retorna uma nova instância do adt lida do stream f em modo binário
PRE: f != NULL
Excepção: Se ocorrer erro de leitura ou se não houver memória suficiente
para a nova instância retorna NULL
*/
adt ADTread(FILE* f) {
/* Em grande parte dos casos basta:
*/
adt p=ADTnew(/*...*/);
/* inicializa instância vazia */
if (!c) return NULL;
int n=fread (p, sizeof (*p), 1, f); /* lê-a do stream f */
if (n) return p;
else { ADTdel(p); return NULL; } /* e retorna-a ou NULL se erro de leitura*/
};
/* Implementação de outras funções declaradas no interface
...
*/
http://w3.ualg.pt/~hdaniel/ed
ED 2004/2005 T16b - 10/10
Download

Aula 16b: ADTs revisitados Tal como os tipos pré