Aula 2b - Tipos Abstratos de Dados (TAD)
Nesta aula apresentamos o conceito de Tipo Abstrato de Dados (TAD), um dos pilares fundamentais da disciplina de Estruturas de Dados. Um TAD permite separar a especificação do comportamento de uma estrutura de sua implementação concreta, promovendo modularidade, reusabilidade e clareza no projeto de programas.
Veremos como definir um TAD, como especificá-lo através de suas operações, e como implementá-lo em linguagem C utilizando structs e funções. Este conceito servirá de base para todas as estruturas de dados estudadas ao longo da disciplina (pilhas, filas, listas, árvores, entre outras).
O que é um Tipo Abstrato de Dados?
Nos programas mais simples, os dados são manipulados diretamente por meio de variáveis e tipos primitivos da linguagem. À medida que os problemas crescem em complexidade, torna-se necessário organizar os dados de maneira mais estruturada e controlada.
Um Tipo Abstrato de Dados (TAD) é um modelo que descreve um conjunto de dados, acompanhado das operações definidas sobre esses dados. O TAD especifica o que as operações fazem, mas não como elas são implementadas internamente.
Essa separação entre especificação e implementação traz diversas vantagens:
- Abstração: quem usa o TAD não precisa conhecer os detalhes internos da implementação.
- Modularidade: a implementação pode ser alterada sem afetar o código que usa o TAD, desde que a interface permaneça a mesma.
- Reusabilidade: o TAD pode ser utilizado em diferentes programas e contextos.
- Encapsulamento: os dados internos ficam protegidos contra acessos e modificações indevidas.
Papéis em torno de um TAD
A ideia de abstração fica mais clara quando pensamos nos diferentes "papéis" envolvidos ao redor de um programa que utiliza um TAD:
| Papel | Descrição |
|---|---|
| Usuário | Utiliza o programa final (o executável), através de sua interface (linha de comando, telas, etc.). Não tem contato nenhum com código-fonte. |
| Cliente (do TAD) | É um desenvolvedor, mas que apenas utiliza o TAD em seu próprio programa — inclui o arquivo .h e chama as funções da interface, sem conhecer (nem precisar conhecer) como elas são implementadas. |
| Programador (do TAD) | É quem efetivamente implementa o TAD — escreve o arquivo .c, decide a estrutura de dados interna e garante que as operações se comportem de acordo com a especificação. |
No exemplo do TAD Ponto desta aula, quem implementa ponto.c é o programador (ou implementador) do TAD. Quem escreve main.c, incluindo ponto.h e chamando funções como pontoDistancia e pontoMover, é o cliente (ou usuário da biblioteca), que utiliza o TAD sem conhecer sua implementação interna. Já quem apenas executa o programa compilado, sem ter acesso ao código-fonte, é o usuário final. Uma mesma pessoa pode, naturalmente, desempenhar mais de um desses papéis, mas distingui-los é importante porque cada um interage com o programa em um nível de abstração diferente.
Especificação de um TAD
A especificação de um TAD é composta por três partes principais:
| Componente | Descrição |
|---|---|
| Nome | Identificador do TAD. |
| Dados | Descrição do conjunto de informações que o TAD armazena. |
| Operações | Conjunto de operações permitidas sobre os dados, com suas entradas, saídas e efeitos. |
As operações de um TAD costumam ser classificadas em quatro categorias:
| Categoria | Descrição | Exemplos |
|---|---|---|
| Construtoras | Criam ou inicializam o TAD. | criar, inicializar |
| Modificadoras | Alteram o estado interno do TAD. | inserir, remover, atualizar |
| Consultoras | Consultam o estado sem modificá-lo. | buscar, tamanho, está vazio |
| Destrutoras | Liberam os recursos utilizados pelo TAD. | destruir, liberar |
Exemplo: TAD Ponto
Para ilustrar os conceitos, consideremos o TAD Ponto, que representa um ponto no plano cartesiano com coordenadas reais. Diferente do que se costuma ver em linguagens orientadas a objetos, não vamos criar funções do tipo criar, getX ou setX — em C, como os campos da struct já são diretamente acessíveis (não existe private), basta montar e ler um Ponto diretamente pelos seus campos. O TAD se resume às operações que de fato fazem algo com os dados:
Especificação
TAD Ponto Dados: Um par de números reais (x, y) representando coordenadas cartesianas. Operações: distancia(p1, p2) -> real : retorna a distância euclidiana entre p1 e p2. mover(p, dx, dy) : desloca o ponto p de dx em x e dy em y. imprimir(p) : exibe as coordenadas do ponto p.
Por que pontoDistancia, e não simplesmente distancia? Diferente de linguagens como C++ ou Java, a linguagem C não permite sobrecarga de funções (overloading): não é possível ter duas funções com o mesmo nome, mesmo que recebam parâmetros de tipos diferentes. O código abaixo, por exemplo, não compila em C:
double distancia(Ponto a, Ponto b); double distancia(Cidade a, Cidade b); // erro: já existe "distancia" double distancia(char *s1, char *s2); // erro: já existe "distancia" (distância de edição entre strings)Por isso, é convenção em C prefixar o nome da função com o nome do TAD ao qual ela pertence ( ponto, cidade, ...), simulando manualmente o "namespace" que a linguagem não oferece. Adotamos aqui a notação camelCase para esses nomes compostos (ex: pontoDistancia, cidadeDistancia), em vez de separar as palavras por _. Essa convenção aparece o tempo todo na própria biblioteca padrão de C: strcpy e strcmp (prefixo str, operações sobre strings), fopen e fclose (prefixo f, operações sobre arquivos) — em vez de, digamos, copy e compare genéricos, que colidiriam com qualquer outra função de mesmo nome.
|
Implementação em C
Em linguagem C, implementamos o TAD utilizando uma struct para os dados e funções para as operações. O arquivo de cabeçalho (ponto.h) expõe apenas a interface do TAD:
/* ponto.h */ typedef struct { double x; double y; } Ponto; double pontoDistancia(Ponto p1, Ponto p2); void pontoMover(Ponto *p, double dx, double dy); void pontoImprimir(Ponto p);
O arquivo de implementação (ponto.c) contém os detalhes internos, que ficam ocultos de quem utiliza o TAD:
/* ponto.c */ #include <stdio.h> #include <math.h> #include "ponto.h" double pontoDistancia(Ponto p1, Ponto p2) { double dx = p1.x - p2.x; double dy = p1.y - p2.y; return sqrt(dx*dx + dy*dy); } void pontoMover(Ponto *p, double dx, double dy) { p->x += dx; p->y += dy; } void pontoImprimir(Ponto p) { printf("(%.2f, %.2f)\n", p.x, p.y); }
O programa principal (main.c) usa o TAD sem conhecer seus detalhes internos. Como não existe uma função pontoCriar, um Ponto é montado diretamente, preenchendo seus campos:
#include <stdio.h> #include "ponto.h" int main() { Ponto a = {0.0, 0.0}; Ponto b = {3.0, 4.0}; printf("Ponto a: "); pontoImprimir(a); printf("Ponto b: "); pontoImprimir(b); printf("Distancia: %.2f\n", pontoDistancia(a, b)); pontoMover(&a, 1.0, 1.0); printf("Ponto a apos mover: "); pontoImprimir(a); return 0; }
Como o programa agora está dividido em mais de um arquivo-fonte (main.c e ponto.c), é preciso informar ambos ao compilador, para que ele gere um único executável a partir dos dois:
gcc -Wall main.c ponto.c -lm -o programa
A opção -Wall habilita os avisos (warnings) do compilador, ajudando a identificar erros comuns antes mesmo de executar o programa. Já a opção -lm liga a biblioteca matemática (math library), necessária aqui porque ponto.c utiliza a função sqrt().
Separação entre interface e implementação
Uma das práticas mais importantes ao implementar um TAD em C é a separação entre interface e implementação, feita através de arquivos .h e .c.
| Arquivo | Conteúdo | Visibilidade |
|---|---|---|
tad.h |
Declaração do tipo e protótipos das funções. | Público — incluído por quem usa o TAD. |
tad.c |
Definição das funções (implementação). | Interno — não precisa ser conhecido por quem usa o TAD. |
Quem utiliza o TAD inclui apenas o arquivo .h e não precisa conhecer nem modificar o arquivo .c. Isso garante o encapsulamento dos dados e a independência entre os módulos do programa.
Observação. Em C não existem mecanismos de controle de acesso como private ou public, presentes em linguagens orientadas a objetos. A separação entre interface e implementação em C é uma convenção de projeto: os campos da struct são tecnicamente acessíveis, mas por boas práticas devem ser manipulados somente através das funções do TAD.
|
TAD versus Estrutura de Dados
É importante distinguir dois conceitos frequentemente confundidos:
Um TAD é a especificação abstrata dos dados e das operações (o o quê), enquanto uma estrutura de dados é a forma concreta escolhida para organizar esses dados na memória (o como). Um mesmo TAD pode ter mais de uma estrutura de dados como implementação, desde que todas respeitem a mesma interface.
Resumo
Principais Conceitos.
|
Exercícios
- Explique, com suas próprias palavras, por que um TAD especifica o que uma operação faz, mas não como ela é implementada.
- No TAD Ponto visto em aula, identifique quais operações são construtoras, modificadoras, consultoras ou destrutoras. Note que nem toda categoria precisa estar presente — e explique por que este TAD não possui uma operação construtora.
- Explique a diferença entre os papéis de cliente e programador de um TAD. Por que essa distinção é importante mesmo quando as duas tarefas são feitas pela mesma pessoa?
- Por que, em C, a separação entre interface e implementação de um TAD é considerada apenas uma "convenção de projeto", e não uma restrição imposta pela linguagem?
- Dê um exemplo (diferente do "turma de alunos" visto em aula) de um TAD que poderia ser implementado por mais de uma estrutura de dados diferente, mantendo a mesma interface.
-
Implemente um novo arquivo de cabeçalho
contador.he sua implementaçãocontador.cpara um TAD Contador, com as operaçõescontadorCriar()(retorna um contador zerado),contadorIncrementar(Contador *c)(soma 1 ao contador) econtadorValor(Contador c)(retorna o valor atual). Escreva também um pequenomain.cque utilize esse TAD para contar de 0 a 5.
Sugestões de Respostas dos Exercícios
Exercício 1
Porque o objetivo do TAD é permitir que quem o utiliza (o cliente) programe contra uma interface estável, sem se preocupar com os detalhes internos. Se o TAD descrevesse também a implementação, qualquer mudança na forma de organizar os dados internamente (por exemplo, trocar a estrutura de dados usada) obrigaria a reescrever também o código de quem o utiliza — o que anularia a vantagem da abstração.
Exercício 2
- Construtora: não existe nenhuma neste TAD.
- Modificadora:
pontoMover(altera o estado interno do ponto). - Consultoras:
pontoDistancia,pontoImprimir(nenhuma delas altera o ponto). - Destrutora: não existe nenhuma nesse TAD — como
Pontonão aloca memória dinamicamente, não há recursos para liberar.
Este TAD não precisa de uma operação construtora porque, em C, os campos de uma struct são sempre diretamente acessíveis — basta escrever Ponto a = {0.0, 0.0}; para montar um Ponto, sem necessidade de chamar nenhuma função. Uma construtora só costuma ser necessária quando a inicialização exige alguma lógica além de simplesmente copiar os valores recebidos para os campos (por exemplo, alocar memória, ou calcular um valor derivado).
Exercício 3
O cliente utiliza o TAD através de sua interface (o arquivo .h), chamando suas funções sem conhecer os detalhes internos. O programador é quem escreve essa implementação (o arquivo .c), decidindo a estrutura de dados interna e garantindo que o comportamento da especificação seja respeitado.
Mesmo quando a mesma pessoa exerce os dois papéis, é importante separá-los mentalmente porque cada um deve respeitar limites diferentes: enquanto estiver "no papel" de cliente, a pessoa não deve depender de detalhes internos da implementação (por exemplo, acessar diretamente um campo da struct), pois esses detalhes podem mudar no futuro sem aviso — e um bom TAD deve poder trocar de implementação sem quebrar o código de quem o utiliza.
Exercício 4
Porque C não possui palavras-chave como private ou public que impeçam, em tempo de compilação, o acesso direto aos campos de uma struct. Nada impede tecnicamente que o código cliente escreva p.x = 10; diretamente, mesmo que x devesse ser "privado" ao TAD — a separação existe apenas porque o programador que escreve o código cliente opta por respeitá-la, seguindo a interface do .h em vez de acessar os campos diretamente.
Exercício 5
Um exemplo é o TAD Agenda de Contatos, com operações como adicionarContato, buscarContato e removerContato. Ele poderia ser implementado, por exemplo, com um vetor de contatos (mais simples, busca sequencial) ou com uma tabela hash (mais complexa, busca mais rápida) — ambas as implementações atendem à mesma especificação, mudando apenas o desempenho de cada operação.
Exercício 6
/* contador.h */ typedef struct { int valor; } Contador; Contador contadorCriar(); void contadorIncrementar(Contador *c); int contadorValor(Contador c);
/* contador.c */ #include "contador.h" Contador contadorCriar() { Contador c; c.valor = 0; return c; } void contadorIncrementar(Contador *c) { c->valor += 1; } int contadorValor(Contador c) { return c.valor; }
/* main.c */ #include <stdio.h> #include "contador.h" int main() { Contador c = contadorCriar(); while (contadorValor(c) < 5) { printf("%d\n", contadorValor(c)); contadorIncrementar(&c); } return 0; }
Compilação: gcc -Wall main.c contador.c -o programa. Note que o main.c nunca acessa c.valor diretamente — apenas através das funções contadorValor e contadorIncrementar, exatamente como esperado de um cliente do TAD.