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.
- Como ligar e desligar código com
#ifdef DEBUG…#endif. - Como definir um nome na hora de compilar, com
gcc -DDEBUGegcc -DNIVEL=2, sem mexer no código. - Como escolher entre vários caminhos com
#if,#elife#else, e para que servedefined. - Como dar um valor padrão com
#ifndef. - O que é um include guard, por que todo
.htem um, e o atalho#pragma once. - Como descobrir em que sistema o programa está sendo compilado (
__linux__,_WIN32). - O
#elifdefdo 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 quando | quando o programa roda | antes de compilar |
| Pode testar | variáveis, o que o usuário digitou… | só macros e números fixos |
| O trecho falso | está no programa, só não roda | nem 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
DEBUGestiver 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
#ifdefprecisa 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 valor1(é 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
NIVELnã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
#elifquiser. - #else
- "Senão": o caminho de quando nenhum dos anteriores valeu. Aqui ele para tudo com um
#error. - #endif
- Um só
#endiffecha 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.
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
- Primeira vez que o
produto.hé colado:PRODUTO_Hainda não existe, então o#ifndefé verdadeiro. A primeira coisa lá dentro é#define PRODUTO_H, e o resto do arquivo entra normalmente. - Segunda vez (por dentro do
estoque.h): agoraPRODUTO_Hjá 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
| Nome | Definido quando o programa é feito para… |
|---|---|
__linux__ | Linux |
_WIN32 | Windows (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 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
| Escrita | O que faz |
|---|---|
#ifdef X … #endif | O trecho só existe se a macro X estiver definida |
#ifndef X … #endif | O trecho só existe se X não estiver definida |
#if expressão | Compara 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 … #endif | Desliga um bloco inteiro |
gcc -DX | Define X (valendo 1) antes de ler o arquivo |
gcc -DX=2 | Define X valendo 2 |
gcc -Wundef | Avisa quando um #if usa um nome não definido |
gcc -dM -E - < /dev/null | Lista as macros prontas do compilador |
#ifndef ARQ_H / #define ARQ_H / #endif | Include guard |
#pragma once | Include 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:
2. Include guard
Complete o include guard do arquivo 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
ifcomum é decidido quando o programa roda. #ifdef X/#ifndef Xperguntam se a macro existe.#ifcompara números, com#elif,#elseedefined(X). Tudo termina com#endif.gcc -DDEBUGegcc -DNIVEL=2definem 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 valordá um valor padrão que o-Dpode trocar. Nome desconhecido no#ifvale 0 (-Wundefavisa).- Todo
.htem um include guard (#ifndef ARQ_H/#define ARQ_H/#endif) ou#pragma once, para poder ser incluído várias vezes sem erro. __linux__,_WIN32e__APPLE__dizem o sistema;gcc -dM -E - < /dev/nulllista todas as macros prontas.#elifdef(C23) é curto, mas o gcc com-std=c17o ignora em silêncio: em código antigo, use#elif defined(X).