INF01203 - Estruturas de Dados - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



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.
  • TAD: modelo que descreve dados e operações, sem especificar a implementação.
  • Interface (.h): o que o TAD oferece — tipos e protótipos de funções.
  • Implementação (.c): como o TAD funciona internamente — detalhe oculto de quem o utiliza.
  • Encapsulamento: os dados só devem ser manipulados através das funções do TAD, nunca diretamente.
  • Um mesmo TAD pode ter diversas estruturas de dados como implementação, cada uma com vantagens e desvantagens próprias.

Exercícios

  1. Explique, com suas próprias palavras, por que um TAD especifica o que uma operação faz, mas não como ela é implementada.
  2. 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.
  3. 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?
  4. 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?
  5. 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.
  6. Implemente um novo arquivo de cabeçalho contador.h e sua implementação contador.c para um TAD Contador, com as operações contadorCriar() (retorna um contador zerado), contadorIncrementar(Contador *c) (soma 1 ao contador) e contadorValor(Contador c) (retorna o valor atual). Escreva também um pequeno main.c que 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 Ponto nã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.