typedef e o Design de Tipos Legíveis

[365] typedef e o Design de Tipos Legíveis

Esconder um asterisco dentro de um apelido faz PInt a, b; declarar dois ponteiros sem que ninguém perceba — é o único uso que a tradição desaconselha. Fora dele, o typedef só rende: apaga o struct repetido das declarações, limpa assinaturas de função e prepara o nó autorreferente que a lista encadeada vai pedir.
Linguagem C

9 min de leitura

Você já reparou como é repetitivo escrever struct toda vez que declara uma variável de um tipo que você mesmo criou? struct Pessoa p; struct Data d; — a palavra struct aparece sem parar. O C oferece uma ferramenta para eliminar essa repetição e, mais do que isso, para dar aos seus tipos nomes limpos e expressivos: o typedef. É uma mudança pequena na sintaxe, mas grande no conforto de leitura, e um passo essencial antes de organizarmos código em projetos maiores.

O que o typedef faz

O typedef cria um apelido (um sinônimo) para um tipo existente. Ele não cria um tipo novo — apenas dá um segundo nome, mais curto ou mais claro, a um tipo que já existe:

#include <stdio.h>

typedef unsigned long long Grande;

int main(void) {
    Grande populacao = 8000000000ULL; // em vez de "unsigned long long"
    printf("%llu\n", populacao);
    return 0;
}

Aqui, Grande passa a significar unsigned long long. Toda vez que você escreve Grande, o compilador entende unsigned long long. É puramente uma conveniência de nomenclatura — mas conveniência que, aplicada a tipos longos ou complicados, torna o código muito mais legível.

typedef com structs: o uso mais comum

O verdadeiro brilho do typedef aparece com structs, eliminando a repetição da palavra struct. Compare as duas formas:

#include <stdio.h>

// Sem typedef: exige "struct" em toda declaração
struct DataAntiga {
    int dia, mes, ano;
};

// Com typedef: o tipo passa a se chamar apenas "Data"
typedef struct {
    int dia, mes, ano;
} Data;

int main(void) {
    struct DataAntiga d1;   // precisa do "struct"
    Data d2;                // limpo: só "Data"

    d1.dia = 15;
    d2.dia = 20;
    printf("%d %d\n", d1.dia, d2.dia);
    return 0;
}

Na segunda definição, a struct não tem nome próprio (é uma struct anônima), mas o typedef lhe dá o apelido Data. A partir daí, você declara Data d2; sem a palavra struct. Essa é a forma que você verá na maioria do código C moderno — ela deixa os tipos definidos pelo usuário tão naturais de usar quanto os tipos nativos.

A forma com nome duplo

Há também uma variação que dá tanto um nome de struct quanto um apelido de typedef — útil quando a struct precisa se referir a si mesma (como veremos nas listas encadeadas):

typedef struct Pessoa {
    char nome[50];
    int idade;
} Pessoa;

Isso cria dois nomes para o mesmo tipo: struct Pessoa e Pessoa, que passam a ser intercambiáveis. Parece redundante, mas é necessário num caso específico: quando um campo da struct precisa ser um ponteiro para a própria struct. Dentro da definição, o apelido Pessoa ainda não existe (ele só fica pronto ao final), então o campo recursivo precisa usar struct Pessoa:

typedef struct No {
    int valor;
    struct No *proximo;  // aqui "No" (o apelido) ainda não existe — usamos "struct No"
} No;

Guarde este exemplo com carinho: ele é, literalmente, o nó de uma lista encadeada, a estrutura que construiremos na Fase 5. O campo proximo aponta para outro nó do mesmo tipo, e é isso que permite encadear elementos. Por ora, apenas note por que precisamos do nome struct No além do apelido No.

Deixando funções e tipos mais legíveis

O typedef também melhora a legibilidade de assinaturas de funções. Uma função que recebe e devolve suas structs fica mais limpa:

#include <stdio.h>

typedef struct {
    double x, y;
} Ponto;

Ponto somar(Ponto a, Ponto b) {   // sem typedef: struct Ponto somar(struct Ponto a...)
    Ponto r = {a.x + b.x, a.y + b.y};
    return r;
}

int main(void) {
    Ponto a = {1.0, 2.0};
    Ponto b = {3.0, 4.0};
    Ponto s = somar(a, b);
    printf("(%.1f, %.1f)\n", s.x, s.y); // (4.0, 6.0)
    return 0;
}

Sem o typedef, a assinatura seria struct Ponto somar(struct Ponto a, struct Ponto b) — funcional, mas carregada. Com ele, Ponto somar(Ponto a, Ponto b) lê quase como pseudocódigo. Em programas com muitas structs e funções, esse ganho se acumula e faz diferença real na clareza.

Uma questão de bom senso

O typedef é poderoso, mas convém usá-lo com critério. Para structs, enums e tipos longos, ele é quase sempre bem-vindo. Já para tipos simples, há um debate de estilo: alguns programadores gostam de typedef int Idade; para dar significado semântico; outros acham que esconder que Idade é apenas um int prejudica a clareza. Há um consenso, porém, sobre um uso perigoso — esconder ponteiros dentro de um typedef:

typedef int *PInt;   // PInt é "ponteiro para int"

PInt a, b;  // ARMADILHA: a é ponteiro, mas b também? Sim — mas não é óbvio!

O problema é que PInt a, b; faz ambos ponteiros, mas quem lê pode facilmente pensar que só a é ponteiro. Como o asterisco fica escondido dentro do apelido, a natureza de ponteiro deixa de ser visível no ponto de uso — e ponteiros são exatamente o que você mais quer enxergar claramente em C. A recomendação geral é: use typedef livremente para structs e enums, mas evite escondê-lo sobre tipos ponteiro, salvo em casos muito bem justificados (como ponteiros para função, que veremos adiante e onde o typedef de fato ajuda).

Um exemplo consolidando a Fase 3

Vamos reunir struct, enum e typedef num único tipo bem desenhado:

#include <stdio.h>
#include <string.h>

typedef enum {
    ATIVO,
    INATIVO,
    SUSPENSO
} StatusConta;

typedef struct {
    char titular[50];
    int numero;
    double saldo;
    StatusConta status;
} Conta;

const char *nome_status(StatusConta s) {
    switch (s) {
        case ATIVO:    return "ativa";
        case INATIVO:  return "inativa";
        case SUSPENSO: return "suspensa";
        default:       return "desconhecida";
    }
}

void extrato(const Conta *c) {   // ponteiro const: lê sem copiar
    printf("Conta %d (%s): R$ %.2f — %s\n",
           c->numero, c->titular, c->saldo, nome_status(c->status));
}

int main(void) {
    Conta minha = {.numero = 1001, .saldo = 750.0, .status = ATIVO};
    strcpy(minha.titular, "Marcelo Fontes");
    extrato(&minha);
    return 0;
}

Repare como o código ficou expressivo: Conta, StatusConta, ATIVO — os nomes falam a linguagem do problema, sem struct ou enum poluindo as declarações, e a função extrato recebe um const Conta * (a técnica de leitura eficiente da aula Ponteiros para Struct e o Operador Seta). Esse é o padrão de código C limpo que buscaremos daqui em diante.

O que vem a seguir

Hoje aprendemos a dar nomes limpos aos nossos tipos com typedef, eliminando a repetição de struct e deixando declarações e funções mais legíveis — e vimos o cuidado a tomar para não esconder ponteiros. Guardamos também um exemplo que reaparecerá em breve: o nó autorreferente de uma lista. Na próxima aula, damos um passo diferente, mergulhando na primeira etapa da compilação que estudamos lá no início: o pré-processador. Vamos dominar #define, macros e as diretivas condicionais — as ferramentas que atuam antes mesmo de o compilador ver seu código.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um typedef chamado Temperatura para o tipo double. Declare uma variável desse tipo, atribua 36.5 e imprima-a. O programa se comporta diferente de usar double diretamente? Por quê?

Ver resposta

✓ Resposta:

#include <stdio.h>
typedef double Temperatura;
int main(void) {
    Temperatura t = 36.5;
    printf("%.1f\n", t);
    return 0;
}

O programa se comporta exatamente igual a usar double diretamente, porque typedef não cria um tipo novo — apenas um apelido. Temperatura é double para todos os efeitos; o compilador os trata de forma idêntica. O ganho é só de legibilidade/semântica: o nome Temperatura comunica a intenção ao leitor.

Exercício 2

Reescreva esta definição usando typedef com struct anônima, de modo que se possa declarar Retangulo r; sem a palavra struct:

struct Retangulo { double largura, altura; };
Ver resposta

✓ Resposta:

typedef struct {
    double largura, altura;
} Retangulo;

A struct fica anônima, e o apelido Retangulo permite declarar variáveis sem a palavra struct: Retangulo r;.

Exercício 3

Defina, com typedef, um tipo Aluno (struct com nome, matricula e media) e escreva uma função void imprimir(const Aluno *a) que exiba os três campos. Note como a assinatura fica mais limpa que a versão com struct.

Ver resposta

✓ Resposta:

#include <stdio.h>

typedef struct {
    char nome[50];
    int matricula;
    double media;
} Aluno;

void imprimir(const Aluno *a) {
    printf("%s (mat. %d) — média %.1f\n", a->nome, a->matricula, a->media);
}

A assinatura void imprimir(const Aluno *a) é mais limpa que void imprimir(const struct Aluno *a) — o typedef removeu a palavra struct do ponto de uso.

Exercício 4

Explique por que a linha typedef int *PInt; PInt a, b; pode enganar quem lê o código. O que exatamente a e b são?

Ver resposta

✓ Resposta: A linha engana porque, ao declarar PInt a, b;, quem lê pode pensar que apenas a é ponteiro (talvez esperando que b seja um int comum). Na verdade, ambos são ponteiros para int: como PInt já significa "ponteiro para int", tanto a quanto b recebem esse tipo. O asterisco está escondido dentro do apelido, então a natureza de ponteiro não é visível na declaração. Se escrevêssemos sem o typedef, int *a, b;, ficaria claro (e correto) que só a é ponteiro e b é int — o oposto do que o typedef sugere. Por isso se recomenda evitar esconder ponteiros em typedef.

Exercício 5

Usando a forma typedef struct Nome { ... } Nome;, defina um tipo No para o nó de uma lista encadeada, com um campo int valor e um campo que aponta para o próximo nó. Explique por que, dentro da struct, o campo de ligação precisa usar struct No * em vez de apenas No *.

Ver resposta

✓ Resposta:

typedef struct No {
    int valor;
    struct No *proximo;  // ponteiro para o próximo nó
} No;

Dentro da definição da struct, o apelido No ainda não está pronto — o typedef só o cria ao final, depois da chave de fechamento. Portanto, no momento em que declaramos o campo proximo, o nome No não existe como tipo ainda; precisamos nos referir à struct pelo seu nome de struct, struct No. É exatamente por isso que usamos a forma com nome duplo (typedef struct No { ... } No;): o nome struct No fica disponível imediatamente, permitindo a autorreferência, enquanto o apelido No fica disponível para uso externo depois. Fora da struct, você pode usar No * normalmente.

Comentários

Mais em Linguagem C

Ponteiro para Ponteiro e Argumentos de Linha de Comando
Ponteiro para Ponteiro e Argumentos de Linha de Comando

Cada asterisco a mais é um passo a mais na cadeia — e são dois deles que o…

Alocação Dinâmica: malloc, calloc, realloc e free
Alocação Dinâmica: malloc, calloc, realloc e free

Toda alocação cria uma dívida que só o free quita. Daí saem as quatro funções…

Otimização Consciente e Noções de Desempenho
Otimização Consciente e Noções de Desempenho

Programador é notoriamente ruim em adivinhar onde o próprio código é lento — e…