O Pré-processador: #define, Macros e Inclusões

O Pré-processador: #define, Macros e Inclusões

O quadrado de 2 + 3 dá 11 quando a macro esquece os parênteses — o pré-processador não entende C, apenas recorta e cola texto. Dessa cegueira saem as duas formas do include, o que separa #define de const, a compilação condicional, e a guarda que impede um cabeçalho de entrar duas vezes.
Linguagem C

10 min de leitura

Lá na primeira aula, quando dissecamos o caminho de um programa até virar executável, encontramos uma etapa que roda antes do compilador: o pré-processador. Ele é uma espécie de editor de texto automático, que percorre seu código e resolve tudo que começa com #, produzindo um novo arquivo que só então é compilado. Você já vem usando-o desde a linha um — todo #include é uma ordem ao pré-processador. Hoje vamos entendê-lo por completo, porque dominá-lo é indispensável para o próximo passo: dividir um projeto em vários arquivos.

Uma verdade importante: o pré-processador não entende C

Antes de qualquer coisa, grave isto: o pré-processador não sabe programar. Ele não entende tipos, funções ou expressões. Ele faz apenas substituição de texto — recorta, cola e troca trechos, cegamente. Essa é a fonte tanto do seu poder quanto de suas armadilhas. Tudo o que veremos hoje é, no fundo, manipulação de texto que acontece antes de o compilador entrar em cena.

#include: colando arquivos

A diretiva que você mais usou. #include simplesmente cola o conteúdo de outro arquivo no lugar da diretiva:

#include <stdio.h>   // cola o conteúdo do cabeçalho padrão stdio.h
#include "meu.h"     // cola o conteúdo de um cabeçalho seu

A diferença entre as duas formas é onde o pré-processador procura o arquivo. Com colchetes angulares (<stdio.h>), ele busca nos diretórios de cabeçalhos do sistema — use-os para bibliotecas padrão e externas. Com aspas ("meu.h"), ele procura primeiro no diretório do seu projeto — use-as para os seus próprios cabeçalhos. Lá na primeira aula, quando rodamos gcc -E e vimos o arquivo explodir de tamanho, foi exatamente isto acontecendo: o #include <stdio.h> foi substituído pelo conteúdo inteiro daquele cabeçalho.

#define: criando constantes e apelidos de texto

A diretiva #define cria uma macro — um nome que o pré-processador substitui por um texto, em todo lugar onde o nome aparecer. Sua forma mais simples cria constantes simbólicas:

#include <stdio.h>

#define PI 3.14159
#define MAX_ALUNOS 30
#define SAUDACAO "Bem-vindo ao curso de C"

int main(void) {
    double area = PI * 5 * 5;   // PI vira 3.14159 antes de compilar
    printf("%s\n", SAUDACAO);
    printf("Área: %.2f\n", area);
    printf("Turma de até %d alunos\n", MAX_ALUNOS);
    return 0;
}

Antes da compilação, o pré-processador troca cada PI por 3.14159, cada MAX_ALUNOS por 30 e cada SAUDACAO pela string. O compilador nunca vê os nomes — vê apenas os textos substituídos. Por convenção, nomes de macros são escritos em MAIÚSCULAS, justamente para sinalizar ao leitor que aquilo é uma macro, não uma variável comum.

Lembra que, na aula de variáveis, comparamos #define com const? Agora fica claro por quê const costuma ser preferível para constantes: const double PI = 3.14159; cria uma variável real, com tipo e verificação do compilador, enquanto #define PI 3.14159 é só uma troca de texto sem tipo. Ainda assim, #define tem usos legítimos e insubstituíveis, como veremos a seguir.

Macros com argumentos: funções de texto

O #define pode receber argumentos, funcionando como uma pequena "função" que também é pura substituição de texto:

#include <stdio.h>

#define QUADRADO(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    printf("%d\n", QUADRADO(5));   // vira ((5) * (5)) = 25
    printf("%d\n", MAX(10, 20));   // vira ((10) > (20) ? (10) : (20)) = 20
    return 0;
}

Repare na profusão de parênteses em ((x) * (x)). Eles não são exagero — são uma defesa essencial, e entender por quê é a lição mais importante desta aula.

A armadilha das macros: por que os parênteses são vitais

Como a macro é substituição cega de texto, ela pode produzir resultados desastrosos se você não a proteger com parênteses. Veja o que acontece com uma versão ingênua, sem parênteses:

#define QUADRADO_RUIM(x) x * x

int main(void) {
    int r = QUADRADO_RUIM(2 + 3); // vira: 2 + 3 * 2 + 3
    // por precedência: 2 + 6 + 3 = 11, e não 25!
    printf("%d\n", r); // 11 — resultado errado!
}

O pré-processador substituiu x por 2 + 3 literalmente, produzindo 2 + 3 * 2 + 3. Como a multiplicação tem precedência sobre a soma, o resultado é 11, não os 25 esperados. Com os parênteses da versão correta, ((2 + 3) * (2 + 3))25, como deveria. A regra é inflexível: em macros com argumentos, envolva cada argumento e o resultado inteiro em parênteses. Há armadilhas ainda mais sutis (como passar i++ a uma macro que usa o argumento duas vezes, incrementando duas vezes), e é por isso que, no C moderno, funções são geralmente preferíveis a macros — elas têm tipos, avaliam cada argumento uma só vez e não pregam essas peças. Use macros de argumento apenas quando houver um motivo real.

Compilação condicional: incluindo ou excluindo código

O pré-processador pode incluir ou remover trechos de código conforme condições, com as diretivas #if, #ifdef, #ifndef, #else e #endif. Isso é útil para código de depuração ou para adaptar o programa a diferentes ambientes:

#include <stdio.h>

#define DEBUG 1

int main(void) {
    int x = 42;

#if DEBUG
    printf("[debug] x = %d\n", x); // só aparece se DEBUG for verdadeiro
#endif

    printf("Resultado: %d\n", x);
    return 0;
}

Se DEBUG for 1, a linha de depuração é incluída na compilação; se for 0, o pré-processador a remove antes de compilar — ela nem chega a existir no executável. Isso permite manter mensagens de diagnóstico no código-fonte sem que elas pesem na versão final. A diretiva #ifdef (e sua oposta #ifndef) testa se uma macro foi definida, independentemente do valor:

#ifdef DEBUG
    printf("modo debug ativo\n");
#endif

O uso mais importante: guardas de inclusão

Aqui está a razão pela qual esta aula precede a próxima. Quando dividirmos o programa em vários arquivos, um mesmo cabeçalho poderá acabar incluído duas vezes (por exemplo, dois arquivos que se incluem mutuamente), o que causaria erros de "redefinição". A defesa universal contra isso é a guarda de inclusão (include guard), montada com #ifndef:

#ifndef MEU_CABECALHO_H
#define MEU_CABECALHO_H

// ... conteúdo do cabeçalho: structs, protótipos, defines ...

#endif // MEU_CABECALHO_H

A lógica é engenhosa. Na primeira vez que o arquivo é incluído, MEU_CABECALHO_H ainda não está definido, então o #ifndef deixa passar; a linha seguinte o define, e o conteúdo é incluído. Numa segunda inclusão, MEU_CABECALHO_H está definido, então o #ifndef bloqueia tudo até o #endif — o conteúdo é ignorado, evitando a duplicação. Todo cabeçalho C bem escrito começa com uma guarda dessas. Guarde firmemente este padrão: ele é obrigatório na próxima aula, quando finalmente espalharmos nosso código em módulos.

O pré-processador não conhece C: ele manipula texto antes de o compilador entrar em cena, e quase toda surpresa com macros vem de esquecer isso. Daí a insistência nos parênteses — uma macro sem eles se expande no meio de uma expressão e muda a precedência sem avisar. A guarda de inclusão pertence à mesma família de cuidados: sem ela, incluir o mesmo cabeçalho por dois caminhos duplica declarações e o erro aparece longe da causa.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Defina macros #define TAXA 0.1 e #define EMPRESA "Acme". Escreva um programa que calcule 10% de um valor usando TAXA e imprima uma mensagem com EMPRESA. Depois, rode gcc -E no arquivo e localize onde as substituições aconteceram.

Ver resposta

✓ Resposta:

#include <stdio.h>
#define TAXA 0.1
#define EMPRESA "Acme"
int main(void) {
    double valor = 200.0;
    printf("%s cobra: %.2f\n", EMPRESA, valor * TAXA); // Acme cobra: 20.00
    return 0;
}

Ao rodar gcc -E arquivo.c, você verá, no final do arquivo expandido, que TAXA foi trocado por 0.1 e EMPRESA por "Acme" diretamente no corpo de main — o compilador nunca vê os nomes das macros, só os textos substituídos.

Exercício 2

Escreva uma macro DOBRO(x) corretamente protegida com parênteses. Teste-a com DOBRO(3 + 4) e confirme que o resultado é 14, não 11. Explique o que aconteceria sem os parênteses.

Ver resposta

✓ Resposta:

#include <stdio.h>
#define DOBRO(x) (2 * (x))
int main(void) {
    printf("%d\n", DOBRO(3 + 4)); // (2 * (3 + 4)) = 14
    return 0;
}

Sem os parênteses em torno de x, a macro 2 * x viraria 2 * 3 + 4, que por precedência é 6 + 4 = 10 (ou, dependendo de como escrita, outro valor errado). Os parênteses garantem que o argumento inteiro seja multiplicado, preservando (3 + 4) como uma unidade.

Exercício 3

Escreva uma macro EH_PAR(n) que resulte em verdadeiro (não-zero) se n for par. Teste com alguns números. Cuidado com os parênteses.

Ver resposta

✓ Resposta:

#include <stdio.h>
#define EH_PAR(n) ((n) % 2 == 0)
int main(void) {
    printf("%d\n", EH_PAR(4)); // 1 (verdadeiro)
    printf("%d\n", EH_PAR(7)); // 0 (falso)
    return 0;
}

Os parênteses em (n) protegem contra argumentos compostos: EH_PAR(6 + 1) vira ((6 + 1) % 2 == 0), avaliando 7 % 2 corretamente, e não 6 + 1 % 2.

Exercício 4

Use #ifdef DEBUG para incluir uma linha de diagnóstico que só aparece quando a macro DEBUG está definida. Compile duas vezes: uma com gcc -DDEBUG (que define a macro pela linha de comando) e outra sem. Descreva a diferença na saída.

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    int x = 42;
#ifdef DEBUG
    printf("[debug] valor de x = %d\n", x);
#endif
    printf("x = %d\n", x);
    return 0;
}

Compilando com gcc -DDEBUG arquivo.c (a flag -D define a macro DEBUG), a linha de diagnóstico é incluída e a saída mostra tanto a linha [debug] quanto a linha final. Compilando sem -DDEBUG, a macro não está definida, o #ifdef remove a linha de diagnóstico antes da compilação, e a saída mostra apenas x = 42. A linha de debug nem existe no segundo executável.

Exercício 5

Escreva um cabeçalho figuras.h com uma guarda de inclusão completa, contendo a definição de uma struct Circulo e o protótipo de uma função double area_circulo(double raio);. Explique, linha a linha, como a guarda impede a inclusão dupla.

Ver resposta

✓ Resposta:

#ifndef FIGURAS_H
#define FIGURAS_H

typedef struct {
    double raio;
} Circulo;

double area_circulo(double raio);

#endif // FIGURAS_H

Funcionamento linha a linha: #ifndef FIGURAS_H pergunta "a macro FIGURAS_H ainda não foi definida?". Na primeira inclusão, ela não foi, então o pré-processador entra no bloco. #define FIGURAS_H define a macro imediatamente, marcando que este cabeçalho já foi visto. Em seguida vêm a struct e o protótipo. #endif fecha o bloco. Se o mesmo arquivo for incluído uma segunda vez (direta ou indiretamente), o #ifndef FIGURAS_H agora encontra a macro já definida e pula todo o conteúdo até o #endif — evitando redefinir Circulo e o protótipo, o que causaria erro de compilação. O nome da macro guarda (FIGURAS_H) deve ser único por cabeçalho, tipicamente derivado do nome do arquivo.

Comentários

Mais em Linguagem C

O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas
O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas

Nove fases, da primeira linha impressa na tela ao programa que cria processos…

Árvores Binárias de Busca
Árvores Binárias de Busca

Inserir 1, 2, 3, 4, 5 numa árvore de busca produz uma lista encadeada…

Programação Defensiva e Contratos
Programação Defensiva e Contratos

Ponteiro nulo onde a função exige um válido é bug e pede assert; arquivo que…