A compilação condicional é um recurso poderoso do pré-processador C que permite incluir ou excluir partes do código durante o processo de compilação. Isso é feito por meio de diretivas como #ifdef, #ifndef, #if, #else e #endif. Essas diretivas são avaliadas antes da compilação propriamente dita, permitindo que o compilador veja apenas o código que atende às condições especificadas.

Esse mecanismo é essencial para escrever código portátil, que funcione em diferentes sistemas operacionais e arquiteturas, além de permitir a inclusão de funcionalidades opcionais, como depuração ou otimizações específicas. Nesta aula, veremos como usar essas diretivas corretamente, incluindo a prática comum de include guards para evitar múltiplas inclusões de arquivos de cabeçalho.

#ifdef, #ifndef, #if

As diretivas #ifdef e #ifndef são usadas para verificar se um determinado macro está definido ou não. #ifdef (abreviação de "if defined") executa o código subsequente somente se o macro especificado estiver definido. Por outro lado, #ifndef ("if not defined") executa o código se o macro não estiver definido. Essas diretivas são frequentemente usadas para incluir código específico de plataforma, como bibliotecas que só existem em determinados sistemas.

A diretiva #if é mais geral e permite avaliar expressões constantes, como #if defined(MACRO) ou #if VALOR > 10. Ela pode ser usada com operadores lógicos e aritméticos, oferecendo maior flexibilidade. É importante lembrar que as expressões avaliadas em #if devem ser constantes inteiras, pois são avaliadas pelo pré-processador.

#include <stdio.h>

#define DEBUG 1

int main() {
#ifdef DEBUG
    printf("Modo depuração ativado\n");
#endif

#ifndef RELEASE
    printf("Versão de desenvolvimento\n");
#endif

#if DEBUG == 1
    printf("Nível de depuração 1\n");
#endif

    return 0;
}

Neste exemplo, #ifdef DEBUG inclui a primeira mensagem, pois DEBUG está definido. #ifndef RELEASE inclui a segunda mensagem, pois RELEASE não está definido. #if DEBUG == 1 inclui a terceira mensagem, pois a expressão é verdadeira.

#else e #endif

As diretivas #else e #endif complementam as estruturas condicionais. #else é usada para fornecer um bloco alternativo de código quando a condição não é satisfeita. #endif marca o fim do bloco condicional. A combinação dessas diretivas permite criar ramificações completas no código, semelhante ao if-else em tempo de execução, mas em tempo de compilação.

É crucial que cada bloco #if, #ifdef ou #ifndef seja fechado com #endif correspondente. O uso incorreto pode levar a erros de compilação ou a um código que não se comporta como esperado.

#include <stdio.h>

#define SISTEMA 1  // 1 para Linux, 2 para Windows

int main() {
#if SISTEMA == 1
    printf("Compilado para Linux\n");
#else
    printf("Compilado para Windows\n");
#endif
    return 0;
}

Aqui, se SISTEMA for 1, imprime "Linux"; caso contrário, imprime "Windows". Essa técnica é comum em código multiplataforma.

Include guards

Include guards são um padrão de programação usado em arquivos de cabeçalho para evitar que o conteúdo do arquivo seja incluído mais de uma vez em uma mesma unidade de tradução. Isso é essencial para evitar definições duplicadas de tipos, variáveis e funções, que causariam erros de compilação. Um include guard usa #ifndef, #define e #endif no início e no final do arquivo.

A estrutura típica de um arquivo de cabeçalho com include guard é:

#ifndef NOME_ARQUIVO_H
#define NOME_ARQUIVO_H

// Declarações e definições aqui

#endif // NOME_ARQUIVO_H

Quando o cabeçalho é incluído pela primeira vez, NOME_ARQUIVO_H não está definido, então o bloco é processado e o macro é definido. Em inclusões subsequentes, #ifndef é falso e o conteúdo é pulado. Alternativamente, a diretiva #pragma once pode ser usada, mas não é padrão em todos os compiladores.

// exemplo.h
#ifndef EXEMPLO_H
#define EXEMPLO_H

typedef struct {
    int x;
    int y;
} Ponto;

#endif // EXEMPLO_H

Se esse cabeçalho for incluído duas vezes, a segunda inclusão será ignorada graças ao guard, evitando a redefinição de Ponto.

Casos de uso

A compilação condicional é amplamente utilizada em projetos reais. Um dos usos mais comuns é a detecção de plataforma: por exemplo, um código que precisa usar funções específicas do Windows ou do Linux pode verificar macros definidos pelo compilador, como _WIN32 ou __linux__. Outro uso é a inclusão de código de depuração apenas em versões de desenvolvimento, controlando isso com uma macro como DEBUG.

Além disso, a compilação condicional permite ativar ou desativar funcionalidades opcionais, como suporte a determinados padrões ou otimizações. Também é usada para garantir compatibilidade com diferentes versões de bibliotecas ou para adaptar o código a diferentes arquiteturas.

#include <stdio.h>

#ifdef _WIN32
    #include <windows.h>
#else
    #include <unistd.h>
#endif

int main() {
#ifdef _WIN32
    Sleep(1000);  // Windows
#else
    sleep(1);     // POSIX
#endif
    printf("Pausa concluída\n");
    return 0;
}

Esse exemplo mostra como adaptar o código para diferentes sistemas operacionais, usando a macro _WIN32 que é definida automaticamente no Windows.

Boas práticas e observações

Ao usar compilação condicional, é importante manter o código legível e organizado. Evite aninhamentos excessivos de diretivas, pois isso pode dificultar a leitura. Prefira usar macros com nomes claros e documente o propósito de cada condição. Sempre feche cada bloco com #endif e, se possível, adicione um comentário indicando qual condição foi fechada.

Outra boa prática é usar include guards em todos os arquivos de cabeçalho, mesmo os pequenos, para evitar problemas futuros. Ao usar #if, certifique-se de que as expressões sejam constantes inteiras e que não dependam de valores que só são conhecidos em tempo de execução.

Referências

Exercícios

  1. Escreva um programa que use #ifdef para imprimir "Debug ativo" se a macro DEBUG estiver definida, e "Debug inativo" caso contrário, usando #else.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
    #ifdef DEBUG
        printf("Debug ativo\n");
    #else
        printf("Debug inativo\n");
    #endif
        return 0;
    }
  2. Explique por que include guards são necessários em arquivos de cabeçalho. Dê um exemplo de erro que ocorreria sem eles.

    ✓ Resposta: Include guards evitam a inclusão múltipla de um arquivo de cabeçalho, o que causaria redefinição de tipos, variáveis e funções. Por exemplo, se um cabeçalho define uma struct e for incluído duas vezes, o compilador reclamará de redefinição de 'struct'. Sem o guard, o código não compila.
  3. Escreva um código que use #if para verificar se um macro VERSAO é igual a 2 e imprima "Versão 2" se for, e "Outra versão" caso contrário.

    ✓ Resposta:
    #include <stdio.h>
    
    #define VERSAO 2
    
    int main() {
    #if VERSAO == 2
        printf("Versão 2\n");
    #else
        printf("Outra versão\n");
    #endif
        return 0;
    }
  4. Crie um arquivo de cabeçalho config.h com um include guard e uma constante MAX_TAM definida como 100. Inclua esse cabeçalho duas vezes em um programa e mostre que não há erro.

    ✓ Resposta:
    // config.h
    #ifndef CONFIG_H
    #define CONFIG_H
    #define MAX_TAM 100
    #endif // CONFIG_H
    
    // main.c
    #include <stdio.h>
    #include "config.h"
    #include "config.h"
    
    int main() {
        printf("%d\n", MAX_TAM);
        return 0;
    }
  5. Escreva um programa que detecte se está sendo compilado em Windows (macro _WIN32) e imprima "Windows" ou "Unix/Linux" caso contrário, usando #ifdef e #else.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
    #ifdef _WIN32
        printf("Windows\n");
    #else
        printf("Unix/Linux\n");
    #endif
        return 0;
    }