C Compilação Condicional

Às vezes você quer que um pedaço do código exista ou não exista no programa, dependendo de como ele foi compilado: mensagens de depuração só enquanto você testa, um trecho para Linux e outro para Windows, uma versão "de teste" e uma "de verdade". Em vez de apagar e reescrever linhas, você pede ao pré-processador para recortar o que não serve. Isso se chama compilação condicional, e é feita com #ifdef, #if e companhia.

Você vai aprender
  • Como ligar e desligar código com #ifdef DEBUG … #endif.
  • Como definir um nome na hora de compilar, com gcc -DDEBUG e gcc -DNIVEL=2, sem mexer no código.
  • Como escolher entre vários caminhos com #if, #elif e #else, e para que serve defined.
  • Como dar um valor padrão com #ifndef.
  • O que é um include guard, por que todo .h tem um, e o atalho #pragma once.
  • Como descobrir em que sistema o programa está sendo compilado (__linux__, _WIN32).
  • O #elifdef do C23 e uma armadilha dele nos padrões antigos.

Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/condicional, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Antes desta, leia Pré-processador: aqui usamos #define e #error o tempo todo.


A ideia: a receita com trechos marcados

Lembra do assistente da cozinheira, que prepara a receita antes de ela ler? Agora a receita tem trechos marcados a lápis:

  • "só se for para festa: decore com confeitos";
  • "se o forno for a gás, 180 graus; se for elétrico, 200 graus".

Antes de entregar a receita, o assistente pergunta "é para festa?" e recorta o trecho dos confeitos se não for. A cozinheira nem fica sabendo que aquele trecho existia. O pré-processador faz o mesmo com o seu código: o trecho recortado não é compilado, não entra no programa e não ocupa nenhum byte. É bem diferente de um if comum, que entra no programa e é decidido quando ele roda.

if (C)#if (pré-processador)
Decidido quandoquando o programa rodaantes de compilar
Pode testarvariáveis, o que o usuário digitou…só macros e números fixos
O trecho falsoestá no programa, só não rodanem existe no programa

1. #ifdef: código que só existe se um nome estiver definido

A Ana está somando os preços de uma compra e quer ver a conta passo a passo enquanto testa. Depois de pronto, essas mensagens devem sumir. Em vez de apagá-las, ela as cerca com #ifdef DEBUG:

#include <stdio.h>

int main(void) {
    int total = 0;
    int precos[] = {12, 2, 5};

    for (int i = 0; i < 3; i++) {
        total += precos[i];
#ifdef DEBUG
        printf("[debug] i = %d, total parcial = %d\n", i, total);
#endif
    }
    printf("Total: %d\n", total);
    return 0;
}
#ifdef DEBUG
"Se DEBUG estiver definido" (if defined). Só pergunta se o nome existe como macro, não importa o valor.
printf("[debug] …")
O trecho condicional. Pode ter quantas linhas quiser.
#endif
"Fim do if": fecha o trecho. Todo #ifdef precisa do seu #endif.
DEBUG
O nome é escolha sua. DEBUG (de debugging, "tirar bugs", depurar) é o costume.

As diretivas podem ficar no começo da linha, mesmo dentro de um bloco recuado: o recuo é só para os humanos. Compile do jeito normal:

$ gcc -std=c23 -Wall -Wextra debug.c -o debug && ./debug
Total: 19

Ninguém definiu DEBUG, então o printf de depuração foi recortado. Para ligá-lo, a Ana poderia escrever #define DEBUG no topo do arquivo. Mas há um jeito melhor, que não mexe no código.

-D: definir um nome na linha de comando

A opção -D do gcc faz o mesmo que um #define no começo do arquivo:

$ gcc -std=c23 -Wall -Wextra -DDEBUG debug.c -o debug && ./debug
[debug] i = 0, total parcial = 12
[debug] i = 1, total parcial = 14
[debug] i = 2, total parcial = 19
Total: 19
-D
"Define": cria uma macro antes de o arquivo ser lido.
DEBUG
O nome, colado no -D. Sem valor, o gcc dá o valor 1 (é como #define DEBUG 1).
-DNIVEL=2
Com valor: como #define NIVEL 2. Você vai usar isso logo abaixo.

O mesmo arquivo vira dois programas diferentes, conforme o comando. É assim que projetos grandes têm uma versão de "depuração" e uma de "lançamento" (release): no Makefile ou no CMake, uma linha decide se vai -DDEBUG ou não.

No editor do guia: data-flags

No editor do guia não há linha de comando para digitar, mas cada caixa pode vir com as opções de compilação já escolhidas. A caixa abaixo foi preparada para compilar com -DDEBUG: repare na linha $ clang … -DDEBUG … que aparece no terminal dela quando você roda. (Para quem escreve o guia, isso é o atributo data-flags da caixa.)

Experimente: esta caixa compila com -DDEBUG. Rode e veja as linhas [debug]. Depois troque #ifdef DEBUG por #ifdef DEPURAR e rode de novo: como ninguém definiu DEPURAR, as linhas somem.

#include <stdio.h>

int main(void) {
    int total = 0;
    int precos[] = {12, 2, 5};

    for (int i = 0; i < 3; i++) {
        total += precos[i];
#ifdef DEBUG
        printf("[debug] i = %d, total parcial = %d\n", i, total);
#endif
    }
    printf("Total: %d\n", total);
    return 0;
}

#if 0: desligar um bloco inteiro

Um truque muito comum: #if 0 é sempre falso, então tudo até o #endif some. Serve para "desligar" um bloco de código por um tempo, mesmo que ele tenha comentários /* */ dentro (que não podem ser aninhados) ou nem compile mais:

#include <stdio.h>

int main(void) {
    printf("Versão nova do cálculo\n");
#if 0
    printf("Versão antiga: %d\n", conta_antiga());   /* nem existe mais */
#endif
    return 0;
}
$ gcc -std=c23 -Wall -Wextra zero.c -o zero && ./zero
Versão nova do cálculo

A função conta_antiga não existe, e nem assim deu erro: o compilador nunca viu aquela linha.


2. #if, #elif e #else: escolher um caminho

O #ifdef só pergunta "existe?". O #if faz contas e comparações com números, como um if comum, e tem os parceiros #elif ("else if", senão se) e #else ("senão"). A Ana quer três níveis de mensagens no programa da lojinha:

#include <stdio.h>

#ifndef NIVEL
#define NIVEL 1
#endif

int main(void) {
#if NIVEL == 0
    printf("Modo silencioso\n");
#elif NIVEL == 1
    printf("Modo normal\n");
#elif NIVEL == 2
    printf("Modo detalhado\n");
#else
#error "NIVEL deve ser 0, 1 ou 2"
#endif
    printf("NIVEL = %d\n", NIVEL);
    return 0;
}
#ifndef NIVEL
"Se NIVEL não estiver definido" (if not defined). O contrário do #ifdef.
#define NIVEL 1
Então defina com 1. Isto é um valor padrão: vale 1, a não ser que alguém já tenha definido outro valor antes (com -D, por exemplo).
#if NIVEL == 0
Compara números. Vale ==, !=, <, >, &&, ||, ! e contas, como no C (só com inteiros).
#elif NIVEL == 1
"Senão, se NIVEL for 1". Pode haver quantos #elif quiser.
#else
"Senão": o caminho de quando nenhum dos anteriores valeu. Aqui ele para tudo com um #error.
#endif
Um só #endif fecha a escada inteira.

Agora o mesmo arquivo, compilado de quatro jeitos:

$ gcc -std=c23 -Wall -Wextra nivel.c -o nivel && ./nivel
Modo normal                 « sem -D: vale o padrão, 1
NIVEL = 1
$ gcc -std=c23 -Wall -Wextra -DNIVEL=2 nivel.c -o nivel && ./nivel
Modo detalhado
NIVEL = 2
$ gcc -std=c23 -Wall -Wextra -DNIVEL=0 nivel.c -o nivel && ./nivel
Modo silencioso
NIVEL = 0
$ gcc -std=c23 -Wall -Wextra -DNIVEL=7 nivel.c -o nivel
nivel.c: In function ‘main’:
nivel.c:15:2: error: #error "NIVEL deve ser 0, 1 ou 2"
   15 | #error "NIVEL deve ser 0, 1 ou 2"
      |  ^~~~~

Com -DNIVEL=7, nenhum #if/#elif valeu, o #else foi escolhido e o #error impediu a compilação, com uma mensagem que explica o que fazer. Esse é o uso típico do #else com #error: recusar uma configuração que não existe.

defined: #ifdef dentro de um #if

#ifdef X é o mesmo que #if defined(X), e #ifndef X é o mesmo que #if !defined(X). A forma com defined é mais longa, mas permite juntar condições:

#if defined(DEBUG) && NIVEL >= 2
    printf("[debug] detalhes extras\n");
#endif

Nome não definido no #if vale zero

Cuidado: no #if, um nome que não é macro vale 0, em silêncio. Se a Ana esquecer de definir DESCONTO, ou errar uma letra, o #if DESCONTO > 0 simplesmente dá falso. A opção -Wundef (não incluída no -Wall) avisa:

$ gcc -std=c23 -Wall -Wextra undef.c -o undef && ./undef
Preço normal
$ gcc -std=c23 -Wall -Wextra -Wundef undef.c -o undef
undef.c: In function ‘main’:
undef.c:4:5: warning: ‘DESCONTO’ is not defined, evaluates to ‘0’ [-Wundef]
    4 | #if DESCONTO > 0
      |     ^~~~~~~~

"DESCONTO não está definido, vale 0."

Missão: esta caixa compila sem -DDEBUG. Ligue a depuração escrevendo uma linha no topo do arquivo, antes do #ifdef, sem mexer no resto. Devem aparecer as três linhas [debug].

#include <stdio.h>

int main(void) {
    int total = 0;
    int precos[] = {12, 2, 5};

    for (int i = 0; i < 3; i++) {
        total += precos[i];
#ifdef DEBUG
        printf("[debug] i = %d, total parcial = %d\n", i, total);
#endif
    }
    printf("Total: %d\n", total);
    return 0;
}
#define DEBUG
#include <stdio.h>

int main(void) {
    int total = 0;
    int precos[] = {12, 2, 5};

    for (int i = 0; i < 3; i++) {
        total += precos[i];
#ifdef DEBUG
        printf("[debug] i = %d, total parcial = %d\n", i, total);
#endif
    }
    printf("Total: %d\n", total);
    return 0;
}

3. Include guard: incluir só uma vez

O uso mais comum de todos do #ifndef está em todo arquivo .h que você vai ver. Para entender, veja o problema primeiro. A Ana tem um produto.h com o tipo struct produto (aula Structs) e uma pequena função de desconto:

struct produto {
    char nome[30];
    int preco;
};

static inline int com_desconto(int preco) {
    return preco - preco / 10;
}

Ela tem também um estoque.h, que precisa do struct produto e por isso inclui o produto.h:

#include "produto.h"

int total_estoque(struct produto lista[], int n);

E o main.c inclui os dois:

#include <stdio.h>
#include "produto.h"
#include "estoque.h"

int main(void) {
    struct produto p = {"caderno", 12};
    printf("%s: R$ %d\n", p.nome, p.preco);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra main.c -o main
In file included from estoque.h:1,
                 from main.c:3:
produto.h:6:19: error: redefinition of ‘com_desconto’
    6 | static inline int com_desconto(int preco) {
      |                   ^~~~~~~~~~~~
In file included from main.c:2:
produto.h:6:19: note: previous definition of ‘com_desconto’ with type ‘int(int)’
    6 | static inline int com_desconto(int preco) {
      |                   ^~~~~~~~~~~~

Leia de cima para baixo: "no arquivo incluído por estoque.h linha 1, que foi incluído por main.c linha 3: redefinição de com_desconto". E a nota: "definição anterior, no arquivo incluído por main.c linha 2". O produto.h foi colado duas vezes no main.c: uma pelo #include direto, outra por dentro do estoque.h. E a mesma função não pode ser definida duas vezes.

E a struct, que também apareceu duas vezes? Até o C17, ela também dava erro: error: redefinition of ‘struct produto’ (compile com -std=c17 para ver). O C23 passou a aceitar a mesma struct repetida, desde que seja idêntica. Funções, variáveis e outras coisas continuam não podendo se repetir, e por isso o include guard continua obrigatório.

O conserto: #ifndef + #define + #endif

Cerque o conteúdo inteiro do .h assim:

#ifndef PRODUTO_H
#define PRODUTO_H

struct produto {
    char nome[30];
    int preco;
};

static inline int com_desconto(int preco) {
    return preco - preco / 10;
}

#endif
  1. Primeira vez que o produto.h é colado: PRODUTO_H ainda não existe, então o #ifndef é verdadeiro. A primeira coisa lá dentro é #define PRODUTO_H, e o resto do arquivo entra normalmente.
  2. Segunda vez (por dentro do estoque.h): agora PRODUTO_H já existe. O #ifndef é falso e tudo até o #endif é recortado. É como se o arquivo estivesse vazio.

O nome da macro é livre, mas o costume é o nome do arquivo em maiúsculas, com _ no lugar do ponto: produto.h → PRODUTO_H. Cada .h precisa de um nome diferente; se dois arquivos usarem o mesmo, o segundo some sem aviso.

#pragma once: o atalho

Quase todos os compiladores (gcc, Clang, o da Microsoft) aceitam uma linha só no lugar das três:

#pragma once

struct produto {
    char nome[30];
    int preco;
};

#pragma é a diretiva para "ordens especiais do compilador", e once quer dizer "uma vez": "inclua este arquivo uma vez só". É mais curto e não tem o risco de dois arquivos usarem o mesmo nome. A desvantagem: não faz parte da norma do C, nem no C23. Os dois funcionam no gcc e no Clang; projetos que querem a portabilidade máxima usam o #ifndef. Escolha um e use em todos os seus .h. Mais na aula Arquivos .h e .c.

Missão: o programa não compila, porque o produto.h é incluído duas vezes. Ponha um include guard no produto.h (aba do meio) e rode. Não mexa nas outras abas.

#include <stdio.h>
#include "produto.h"
#include "estoque.h"

int main(void) {
    struct produto p = {"caderno", 12};
    printf("%s: R$ %d\n", p.nome, p.preco);
    printf("Com desconto: R$ %d\n", com_desconto(p.preco));
    return 0;
}
struct produto {
    char nome[30];
    int preco;
};

static inline int com_desconto(int preco) {
    return preco - preco / 10;
}
#include "produto.h"

int total_estoque(struct produto lista[], int n);
#ifndef PRODUTO_H
#define PRODUTO_H

struct produto {
    char nome[30];
    int preco;
};

static inline int com_desconto(int preco) {
    return preco - preco / 10;
}

#endif

4. Em que sistema estou?

Cada compilador já define, sozinho, alguns nomes que dizem para qual sistema o programa está sendo feito. Com eles, o mesmo arquivo pode ter um trecho para cada sistema:

#include <stdio.h>

int main(void) {
#if defined(_WIN32)
    printf("Rodando no Windows\n");
#elif defined(__APPLE__)
    printf("Rodando no macOS\n");
#elif defined(__linux__)
    printf("Rodando no Linux\n");
#else
    printf("Sistema desconhecido\n");
#endif
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sistema.c -o sistema && ./sistema
Rodando no Linux
NomeDefinido quando o programa é feito para…
__linux__Linux
_WIN32Windows (de 32 ou 64 bits; _WIN64 só no de 64)
__APPLE__macOS e iOS
__unix__sistemas parecidos com o Unix (Linux, BSD…)
__GNUC__compilado com o gcc (o Clang também define, para ser compatível)
__clang__compilado com o Clang

Quer ver todas as macros que o gcc define sozinho? A opção -dM, junto com -E, lista os #define em vez do código (o - < /dev/null pré-processa um arquivo vazio):

$ gcc -dM -E - < /dev/null | wc -l
414
$ gcc -dM -E - < /dev/null | grep -i linux
#define __linux 1
#define __gnu_linux__ 1
#define linux 1
#define __linux__ 1
$ gcc -dM -E - < /dev/null | grep STDC_VERSION
#define __STDC_VERSION__ 202311L

414 macros prontas. Os nomes linux e __linux, sem os sublinhados do fim, são antigos; use __linux__.

Experimente: rode o mesmo programa no editor do guia. Ele responde "Sistema desconhecido"! O editor não é Linux: o Clang dele gera um programa para WebAssembly, que roda dentro do navegador. Acrescente um #elif defined(__wasm__) antes do #else, com um printf("Rodando no navegador (WebAssembly)\n");, e rode de novo.

#include <stdio.h>

int main(void) {
#if defined(_WIN32)
    printf("Rodando no Windows\n");
#elif defined(__APPLE__)
    printf("Rodando no macOS\n");
#elif defined(__linux__)
    printf("Rodando no Linux\n");
#else
    printf("Sistema desconhecido\n");
#endif
    return 0;
}

5. #elifdef e #elifndef C23

Assim como #ifdef X abrevia #if defined(X), o C23 trouxe #elifdef X para abreviar #elif defined(X) (e #elifndef X para #elif !defined(X)):

#include <stdio.h>

int main(void) {
#ifdef VERSAO_PRO
    printf("Lojinha Pro\n");
#elifdef VERSAO_TESTE
    printf("Lojinha (versão de teste)\n");
#else
    printf("Lojinha básica\n");
#endif
    return 0;
}
$ gcc -std=c23 -Wall -Wextra elifdef.c -o elifdef && ./elifdef
Lojinha básica
$ gcc -std=c23 -Wall -Wextra -DVERSAO_TESTE elifdef.c -o elifdef && ./elifdef
Lojinha (versão de teste)

Agora a armadilha. Compile o mesmo arquivo com o padrão C17 estrito:

$ gcc -std=c17 -Wall -Wextra -DVERSAO_TESTE elifdef.c -o elifdef && ./elifdef
Lojinha básica              « VERSAO_TESTE foi definido, mas o #elifdef foi ignorado!
$ gcc -std=gnu17 -Wall -Wextra -DVERSAO_TESTE elifdef.c -o elifdef && ./elifdef
Lojinha (versão de teste)

Com -std=c17, o gcc 15 não reconhece o #elifdef. Como ele está num trecho que já estava sendo recortado (o #ifdef VERSAO_PRO era falso), a linha desconhecida é pulada sem erro e sem aviso, e o programa cai no #else. Nem -pedantic avisa. Com -std=gnu17 (C17 com as extensões do GNU), funciona. O Clang, com -std=c17, aceita e avisa: use of a '#elifdef' directive is a C23 extension.

Qual usar? Em código C23, #elifdef é mais curto. Em código que pode ser compilado com um padrão antigo, use #elif defined(X), que funciona em qualquer versão do C desde 1989.


Quando dá errado

error: unterminated #ifdef
$ gcc -std=c23 -Wall -Wextra aberto.c -o aberto
aberto.c: In function ‘main’:
aberto.c:4: error: unterminated #ifdef
    4 | #ifdef DEBUG
aberto.c:3:1: error: expected declaration or statement at end of input
    3 | int main(void) {
      | ^~~

O que significa: "#ifdef não terminado": faltou o #endif. Sem ele, o recorte vai até o fim do arquivo e leva junto o } do main, daí o segundo erro ("esperava uma declaração ou comando no fim da entrada").

Como consertar: feche cada #if/#ifdef/#ifndef com o seu #endif. Em arquivos longos, ajuda escrever o nome num comentário: #endif /* DEBUG */.

error: ‘#endif’ without ‘#if’
$ gcc -std=c23 -Wall -Wextra sobra.c -o sobra
sobra.c: In function ‘main’:
sobra.c:5:2: error: ‘#endif’ without ‘#if’
    5 | #endif
      |  ^~~~~

O que significa: sobrou um #endif, sem um #if aberto. Acontece quando você apaga um #ifdef e esquece o par dele (o mesmo vale para #else e #elif sozinhos).

Como consertar: apague o #endif que sobrou, ou recoloque o #if que faltou.

error: redefinition of ‘…’ com In file included from

O que significa: um .h foi colado duas vezes no mesmo .c, quase sempre porque um .h inclui outro. As linhas In file included from mostram o caminho de cada inclusão.

Como consertar: ponha um include guard (#ifndef X_H / #define X_H / #endif) ou #pragma once em todo .h, desde o primeiro dia.

O trecho nunca aparece (ou sempre aparece), sem mensagem nenhuma

O que significa: o nome testado está escrito diferente do definido (-DDEBUG × #ifdef DEBGU), ou o #if usa um nome que não existe e vale 0, ou um #elifdef foi compilado com -std=c17.

Como consertar: compile com -Wundef para pegar nomes desconhecidos em #if. Para ver o que sobrou depois do recorte, use gcc -E -P com as mesmas opções -D.


Referência

EscritaO que faz
#ifdef X … #endifO trecho só existe se a macro X estiver definida
#ifndef X … #endifO trecho só existe se X não estiver definida
#if expressãoCompara números e macros (nome desconhecido vale 0)
#elif expressão"Senão, se…"
#else"Senão"
#elifdef X / #elifndef X"Senão, se X (não) estiver definida" C23
defined(X)Dentro de #if: 1 se X existe, 0 se não
#if 0 … #endifDesliga um bloco inteiro
gcc -DXDefine X (valendo 1) antes de ler o arquivo
gcc -DX=2Define X valendo 2
gcc -WundefAvisa quando um #if usa um nome não definido
gcc -dM -E - < /dev/nullLista as macros prontas do compilador
#ifndef ARQ_H / #define ARQ_H / #endifInclude guard
#pragma onceInclude guard numa linha (fora da norma, mas aceito por todos)
__linux__, _WIN32, __APPLE__Sistema para o qual se compila

Exercícios

1. Ligar sem mexer no código

Complete o comando para compilar loja.c com a macro DEBUG definida:

gcc -std=c23 -Wall -Wextra loja.c -o loja

2. Include guard

Complete o include guard do arquivo cliente.h:

CLIENTE_H
#define CLIENTE_H
/* … conteúdo … */

3. O arquivo tem #ifndef TAXA / #define TAXA 5 / #endif e foi compilado com gcc -DTAXA=8 …. Quanto vale TAXA?

4. Qual a diferença entre um trecho dentro de #if 0 e um trecho dentro de if (0)?

5. Sem nenhum #define e sem -D, o que acontece em #if PROMOCAO?

Missão final: a lojinha em dois idiomas

Missão: esta caixa compila com -DIDIOMA=2. Hoje o programa só sabe o idioma 1 e, com 2, não mostra a saudação. Complete a escada: um #elif IDIOMA == 2 que mostra Welcome to Ana's shop! e um #else com um #error para qualquer outro idioma.

Welcome to Ana's shop!
Caderno: R$ 12
#include <stdio.h>

int main(void) {
#if IDIOMA == 1
    printf("Bem-vindo à lojinha da Ana!\n");
#endif
    printf("Caderno: R$ 12\n");
    return 0;
}
#include <stdio.h>

int main(void) {
#if IDIOMA == 1
    printf("Bem-vindo à lojinha da Ana!\n");
#elif IDIOMA == 2
    printf("Welcome to Ana's shop!\n");
#else
#error "IDIOMA deve ser 1 (português) ou 2 (inglês)"
#endif
    printf("Caderno: R$ 12\n");
    return 0;
}

Resumo

  • Compilação condicional recorta trechos antes de compilar. O trecho recortado nem existe no programa; um if comum é decidido quando o programa roda.
  • #ifdef X / #ifndef X perguntam se a macro existe. #if compara números, com #elif, #else e defined(X). Tudo termina com #endif.
  • gcc -DDEBUG e gcc -DNIVEL=2 definem macros na linha de comando: o mesmo arquivo vira programas diferentes. No editor do guia, a caixa traz as opções prontas.
  • #ifndef X + #define X valor dá um valor padrão que o -D pode trocar. Nome desconhecido no #if vale 0 (-Wundef avisa).
  • Todo .h tem um include guard (#ifndef ARQ_H / #define ARQ_H / #endif) ou #pragma once, para poder ser incluído várias vezes sem erro.
  • __linux__, _WIN32 e __APPLE__ dizem o sistema; gcc -dM -E - < /dev/null lista todas as macros prontas.
  • #elifdef (C23) é curto, mas o gcc com -std=c17 o ignora em silêncio: em código antigo, use #elif defined(X).