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)) dá 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 já 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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 4.11, The C Preprocessor
- cppreference — Preprocessor — visão completa das diretivas — https://en.cppreference.com/w/c/preprocessor
- cppreference —
#includeguards e a diretiva#pragma once— https://en.cppreference.com/w/c/preprocessor/include - Modern C, Jens Gustedt — capítulo sobre o pré-processador e macros — https://gustedt.gitlabpages.inria.fr/modern-c/
- CERT C — PRE série (PRE01-C a PRE31-C), sobre uso seguro de macros — https://wiki.sei.cmu.edu/confluence/display/c
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.