Pré-processador: macros, includes e guards

1. O que é o pré-processador e como ele funciona

Em Linguagem C, o processo de transformar código-fonte em um executável ocorre em etapas. O pré-processador é a primeira dessas etapas — ele age antes da compilação propriamente dita. Enquanto o compilador entende a sintaxe da linguagem, o pré-processador trabalha com substituição textual pura, guiado por diretivas que começam com #.

As etapas da compilação são:

  1. Pré-processamento: expande macros, inclui arquivos, interpreta condicionais
  2. Compilação: traduz o código C pré-processado para assembly
  3. Montagem: converte assembly para código objeto (.o ou .obj)
  4. Linkedição: combina código objeto com bibliotecas para gerar o executável

As diretivas mais comuns incluem #define, #include, #ifdef, #ifndef, #if, #else, #elif, #endif e #pragma. Cada uma delas é processada em tempo de pré-processamento, antes de o compilador ver qualquer token.

2. Macros: definição e substituição simples

A diretiva #define permite criar constantes simbólicas e macros com ou sem parâmetros. O pré-processador substitui cada ocorrência do nome da macro pelo seu texto de definição.

#define PI 3.14159
#define TAXA 0.15

int main() {
    float area = PI * raio * raio;
    float imposto = valor * TAXA;
    return 0;
}

Macros com parâmetros funcionam como "mini-funções" textuais:

#define SOMA(a, b) ((a) + (b))
#define QUADRADO(x) ((x) * (x))

int resultado = SOMA(3, 4);        // ((3) + (4)) = 7
int quad = QUADRADO(2 + 3);       // ((2 + 3) * (2 + 3)) = 25

Cuidado fundamental: sempre envolva cada parâmetro e a expressão inteira entre parênteses. Sem eles, operadores podem se comportar de forma inesperada:

#define ERRADO(x) x * x
int valor = ERRADO(2 + 3);  // 2 + 3 * 2 + 3 = 11 (e não 25)

Efeitos colaterais também são perigosos: QUADRADO(i++) expande para ((i++) * (i++)), incrementando i duas vezes.

3. Macros avançadas: operadores especiais e truques

O pré-processador oferece operadores poderosos para manipulação de tokens.

Operador # (stringificação): converte o parâmetro da macro em uma string literal.

#define PRINT_INT(x) printf(#x " = %d\n", x)

int idade = 30;
PRINT_INT(idade);  // imprime: idade = 30

Operador ## (concatenação): une dois tokens em um único token durante a expansão.

#define CRIA_VAR(nome, indice) nome ## indice

int CRIA_VAR(contador, 1) = 10;  // cria int contador1 = 10;
int CRIA_VAR(contador, 2) = 20;  // cria int contador2 = 20;

Macros variádicas com ... e __VA_ARGS__ permitem número variável de argumentos:

#define DEBUG_LOG(fmt, ...) \
    printf("[DEBUG] " fmt "\n", __VA_ARGS__)

DEBUG_LOG("Valor: %d, Nome: %s", 42, "Joao");

4. Inclusão de arquivos: #include e organização de código

A diretiva #include insere o conteúdo de outro arquivo no ponto onde é usada. Existem duas formas:

  • #include <arquivo> — busca em diretórios padrão do sistema (como /usr/include no Linux)
  • #include "arquivo" — busca primeiro no diretório atual, depois nos diretórios padrão
#include <stdio.h>    // header padrão da biblioteca C
#include <stdlib.h>   // funções de alocação de memória
#include "meu_header.h"  // header do próprio projeto

Boas práticas:
- Inclua apenas headers necessários em cada arquivo .c
- Em headers, inclua apenas o essencial para que eles compilam isoladamente
- Evite dependências circulares (header A inclui B, que inclui A)

5. Guards de inclusão: protegendo contra múltiplas definições

Quando um header é incluído mais de uma vez (diretamente ou por inclusão indireta), tipos, structs e protótipos são redefinidos, causando erro de compilação. A solução clássica é o include guard:

#ifndef MINHA_BIBLIOTECA_H
#define MINHA_BIBLIOTECA_H

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

void inicializar(Pessoa *p);

#endif  // MINHA_BIBLIOTECA_H

A primeira vez que o pré-processador encontra o arquivo, MINHA_BIBLIOTECA_H não está definida, então ele processa o conteúdo e define a macro. Nas inclusões seguintes, o #ifndef impede nova execução.

Alternativa moderna: #pragma once é mais simples e evita erros de digitação no nome do guard:

#pragma once

typedef struct {
    int x, y;
} Ponto;

Porém, #pragma once não faz parte do padrão ANSI C. Embora seja suportado por todos os compiladores modernos (GCC, Clang, MSVC), projetos que exigem portabilidade máxima preferem o guard tradicional.

6. Compilação condicional: #if, #else, #elif, #ifdef

A compilação condicional permite que partes do código sejam incluídas ou excluídas com base em condições avaliadas pelo pré-processador.

Exemplo com #ifdef para depuração:

#define DEBUG

#ifdef DEBUG
    printf("Valor de x: %d\n", x);
#endif

Se DEBUG não estiver definida, a linha com printf é removida antes da compilação.

Exemplo multiplataforma:

#ifdef _WIN32
    #include <windows.h>
    #define LIMPAR_TELA system("cls")
#elif defined(__linux__)
    #define LIMPAR_TELA system("clear")
#else
    #error "Plataforma nao suportada"
#endif

int main() {
    LIMPAR_TELA;
    return 0;
}

Expressões com #if permitem lógica mais complexa:

#define VERSAO 2

#if VERSAO >= 3
    printf("Funcionalidade nova disponivel\n");
#elif VERSAO == 2
    printf("Modo de compatibilidade\n");
#else
    printf("Versao antiga\n");
#endif

7. Macros predefinidas e padrões do compilador

O padrão C define várias macros que o pré-processador disponibiliza automaticamente:

#include <stdio.h>

int main() {
    printf("Arquivo: %s\n", __FILE__);      // nome do arquivo fonte
    printf("Linha: %d\n", __LINE__);        // número da linha atual
    printf("Data: %s\n", __DATE__);         // data da compilação
    printf("Hora: %s\n", __TIME__);         // hora da compilação
    printf("Padrao C: %d\n", __STDC__);     // 1 se compilador ANSI C
    return 0;
}

Macros de identificação do compilador:

#ifdef __GNUC__
    printf("Compilador GCC\n");
#endif

#ifdef _MSC_VER
    printf("Compilador MSVC\n");
#endif

Uso prático em logging:

#define LOG(msg) \
    printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)

LOG("Inicializacao concluida");
// imprime: [main.c:15] Inicializacao concluida

8. Boas práticas e armadilhas comuns

Evite substituir palavras-chave ou funções padrão:

#define int float   // péssima ideia! Quebra todo o código
#define malloc(n) my_malloc(n, __FILE__, __LINE__)  // perigoso, esconde comportamento

Prefira alternativas modernas:

  • Use const para constantes tipadas: const double PI = 3.14159;
  • Use enum para constantes inteiras relacionadas: enum { SUCESSO, ERRO };
  • Use funções inline para operações pequenas e seguras:
static inline int soma(int a, int b) {
    return a + b;
}

Documente macros complexas e teste efeitos colaterais. Uma macro como #define MAX(a,b) ((a) > (b) ? (a) : (b)) parece segura, mas MAX(x++, y++) ainda causa dupla avaliação.

Regra de ouro: se uma macro pode ser substituída por uma função ou constante sem perda de desempenho, prefira a função ou constante. Macros devem ser reservadas para situações onde realmente são necessárias (como logging com __FILE__ e __LINE__, ou geração de código com ##).

Referências