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

[366] 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 que vem a seguir

Hoje desvendamos o pré-processador: um manipulador de texto que roda antes do compilador, resolvendo #include (colar arquivos), #define (constantes e macros, com o cuidado dos parênteses) e a compilação condicional (#if, #ifdef). E terminamos com a guarda de inclusão, a peça que protege cabeçalhos de inclusões duplicadas. Com essas ferramentas em mãos, estamos prontos para o objetivo que vínhamos construindo: na próxima aula, vamos quebrar um programa monolítico em múltiplos arquivos, com cabeçalhos (.h) e arquivos de implementação (.c), aprendendo como um projeto C de verdade se organiza.

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

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

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

Threads com pthreads
Threads com pthreads

contador++ não é uma operação só: são ler, somar e escrever — e duas threads…

Recursão: Quando uma Função Chama a Si Mesma
Recursão: Quando uma Função Chama a Si Mesma

O fatorial recursivo é bonito; o Fibonacci recursivo é uma armadilha, porque…