Menu

Compilação condicional em C: #ifdef, #ifndef, #if e -D

Compile código diferente para builds diferentes com #ifdef, #ifndef, #if, #elif e #else - chaves de depuração, ramos por plataforma, definir macros pela linha de comando com -D e usar #if 0 para desativar código.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

A compilação condicional permite que um único arquivo-fonte vire vários programas diferentes. O pré-processador decide, antes de o compilador rodar, quais blocos de texto sobrevivem - então o código dentro de um ramo que falha não é apenas pulado em tempo de execução, ele é apagado. Ele nunca é analisado, então não contribui em nada para o executável e pode até conter construções que o compilador atual rejeitaria.

Essa é a diferença em relação a um if comum. Os dois escolhem entre caminhos; só um deles escolhe antes de o programa existir.

#ifdef e #ifndef

A pergunta mais simples é se uma macro está definida:

Apague a linha #define DEBUG e todos os prints de depuração somem do build - não desativados, sumidos. Execute uma vez como está, depois remova essa linha e execute de novo.

#ifndef é a negação: mantenha o bloco só se a macro não estiver definida. Seu uso mais comum é a guarda de inclusão, coberta em arquivos de cabeçalho, e é também como você fornece um padrão que quem chama pode sobrescrever:

Todo #ifdef e #ifndef precisa ser fechado por um #endif. Anotar o fechamento em arquivos longos - #endif /* DEBUG */ - economiza tempo de verdade depois.

#if, #elif, #else

#if recebe uma expressão inteira constante e mantém o bloco quando ela é diferente de zero. Isso torna possíveis comparações de versão e de nível:

Mude o LOG_LEVEL para 0 ou 3 e execute de novo para ver o próprio programa mudar de forma.

A expressão é avaliada pelo pré-processador, o que significa que ela só pode usar constantes inteiras, operadores aritméticos e de comparação, e macros que expandem para isso. Ela não enxerga sizeof, valores de enum, variáveis ou qualquer coisa que precise do compilador. Uma macro não definida dentro de um #if avalia como 0 em vez de ser um erro, o que é conveniente e às vezes surpreendente:

#if FEATURE_X        /* FEATURE_X nunca é definida em lugar nenhum -> 0 -> bloco descartado */

defined(NOME) transforma a pergunta do #ifdef em algo utilizável dentro de um #if, de modo que você pode combinar testes:

#if defined(DEBUG) && !defined(NDEBUG)
    /* build de depuração, e as asserções estão ligadas */
#endif

#if defined(LINUX) || defined(BSD)
    /* qualquer sistema do tipo unix */
#endif

#if defined(X) e #ifdef X significam a mesma coisa; recorra ao primeiro quando precisar de &&, || ou !.

Definir macros pela linha de comando

O verdadeiro poder dessas chaves é que o código-fonte não precisa mudar em nada. gcc -D define uma macro para toda a compilação:

gcc -DDEBUG program.c -o program        # DEBUG definida como 1
gcc -DLOG_LEVEL=3 program.c -o program  # valor específico
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNOME sozinho equivale a #define NOME 1. -U NOME remove a definição de uma, o que importa quando um cabeçalho define algo que você quer desligado.

Então o arranjo usual é: o código-fonte contém padrões protegidos por #ifndef, e o comando de build seleciona uma configuração.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* quieto por padrão */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# build de desenvolvimento
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# build de produção
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG é padronizada: defini-la desativa todo assert() do programa, porque o próprio <assert.h> é escrito com compilação condicional. Esse é o padrão em miniatura - um cabeçalho que compila para coisas diferentes dependendo do que o build definiu.

Chaves de plataforma

Os compiladores predefinem macros que identificam o sistema alvo, de modo que um único arquivo-fonte pode chamar a API certa em cada um:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Plataforma não suportada"
#endif

As mais comuns: _WIN32 (definida tanto no Windows de 32 quanto no de 64 bits), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. A identidade do compilador tem seu próprio conjunto - __GNUC__, __clang__, _MSC_VER - e a arquitetura também - __x86_64__, __aarch64__.

Uma versão executável que informa em que ambiente foi compilada:

O #error vale conhecer por si só: ele interrompe a compilação com a sua mensagem. Terminar uma cadeia de plataformas com #else / #error "Plataforma não suportada" transforma uma compilação errada e silenciosa numa falha clara no topo do log de build.

Para listar todas as macros que seu compilador predefine:

gcc -dM -E - < /dev/null

Essa saída é a resposta definitiva para "o que eu posso testar nesta máquina".

#if 0 para desativar código

Comentar um bloco com /* ... */ falha no momento em que o bloco contém um comentário próprio, porque os comentários em C não aninham - o primeiro */ interno encerra o comentário externo e tudo depois dele vira código solto. #if 0 não tem esse problema:

#if 0
    /* Toda esta região é removida, comentários e tudo. */
    legacy_init();
    int n = old_calculation(42);  /* até este comentário está tranquilo */
    report(n);
#endif

Troque para #if 1 para trazer o código de volta. Os editores continuam realçando como C, e você pode aninhar um #if 0 dentro de outro #if.

É uma ferramenta de depuração, não um sistema de armazenamento. Código parado num #if 0 nunca é compilado, então ele apodrece em silêncio - não vai compilar quando alguém finalmente virar a chave. Use enquanto estiver bissecionando um problema, depois apague o bloco e deixe o controle de versão se lembrar dele.

Onde a compilação condicional dá errado

Três modos de falha respondem pela maior parte da dor.

Código que nunca é compilado nunca é verificado. Um erro de digitação dentro de um ramo #ifdef inativo fica invisível até alguém compilar aquela configuração - possivelmente meses depois, possivelmente na CI, numa plataforma que você não tem. Se um projeto tem ramos de plataforma interessantes, compile todos eles regularmente.

Intercalar #ifdef com o fluxo de controle fica ilegível rapidinho. Uma coisa desse tipo é difícil de raciocinar:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Chaves abertas em um ramo e fechadas em outro são legais e horríveis. Prefira condicionais que envolvam funções inteiras e selecione entre elas:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

Um if em tempo de execução costuma ser melhor. Se os dois ramos compilariam em qualquer lugar, um if (debug_enabled) comum mantém os dois caminhos com os tipos verificados, testáveis e alternáveis sem recompilar. Reserve o pré-processador para o que só ele pode fazer: código que genuinamente não consegue compilar na outra plataforma.

Para o padrão que combina tudo isso - um cabeçalho de padrões, um -D por build e ramos de plataforma envolvidos em guardas de inclusão - veja arquivos de cabeçalho; para o #define em si, macros.

Perguntas frequentes

O que é compilação condicional em C?

É usar diretivas de pré-processamento para manter ou descartar blocos de código-fonte antes de o compilador rodar. Código dentro de um #ifdef que falha é apagado do texto por completo - nunca é analisado, nunca é compilado e nunca aparece no executável. É assim que um único arquivo-fonte suporta várias plataformas ou tipos de build.

Qual é a diferença entre #ifdef e #if?

#ifdef NOME pergunta apenas se a macro existe, seja qual for o valor dela. #if expressão avalia uma expressão inteira constante, então consegue comparar valores: #if VERSION >= 3. Use #ifdef para sinalizadores que só estão presentes ou ausentes, e #if quando o valor importa. #if defined(NOME) combina os dois e pode ser unido com && e ||.

Como defino uma macro pela linha de comando com o gcc?

Use -D: gcc -DDEBUG program.c define DEBUG como se o arquivo começasse com #define DEBUG 1, e -DMAX=50 dá a ela um valor específico. É assim que os sistemas de build ligam funcionalidades sem editar o código-fonte, e -U NOME remove a definição de uma.

Por que usar #if 0 em vez de comentar o código?

Porque os comentários /* ... */ não aninham - o primeiro */ dentro da região encerra o comentário cedo demais e o resto vira código quebrado. #if 0 ... #endif remove qualquer quantidade de código contendo qualquer número de comentários, mantém o realce de sintaxe funcionando e é trivialmente revertido para #if 1.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR