- Pré-processador C – Parte 1
- Pré-processador C: Compilação condicional – Parte 2
- Pré-processador C: Diagnóstico – Parte 3
- Pré-processador C: X macros – Parte 4
Olá, caro leitor! Continuando o assunto sobre o pré-processador C, vamos discutir neste artigo os recursos de compilação condicional. Vamos lá!
Compilação condicional if-else do pré-processador
Um recurso poderoso do pré-processador C é o de compilação condicional. Você certamente já se deparou com alguma dessas diretivas de compilação condicional:
1 2 3 4 5 6 |
#if #ifdef #ifndef #else #elif #endif |
Essas diretivas instruem o pré-processador C se um determinado trecho de código deve ser compilado ou não, sendo esta condição avaliada conforme uma expressão condicional.
Utilizando a estrutura de decisão
A forma geral da estrutura de decisão é:
1 2 3 4 |
#if <expressão> //... comandos //... comandos #endif |
Toda estrutura de decisão deve ser finalizada por #endif. A cláusula #else é utilizada da mesma maneira que na linguagem C, fornecendo alternativa caso a condição testada seja falsa.
1 2 3 4 5 6 7 |
#if <expressão> //... comandos //... comandos #else //... comandos //... comandos #endif |
Ao estruturar o código com expressões condicionais, o código final dependerá das condições avaliadas durante o pré-processamento. Convém observar que expressões avaliadas devem ser constantes e somente os comandos e instruções que fazem parte de uma condição verdadeira serão compilados.
As estruturas de decisão também podem ser aninhadas como o exemplo abaixo:
1 2 3 4 5 6 7 8 9 10 |
#if <expressão> //... comandos //... comandos #elif <expressão> //... comandos //... comandos #else //... comandos //... comandos #endif |
Apresentadas as diretivas vamos para algumas aplicações!
Verificação de macros
O pré-processador fornece algumas diretivas e operadores que permitem verificar se uma macro já foi definida. As diretivas #ifdef e #ifndef podem ser utilizadas para esse fim. Veja:
1 2 3 4 5 6 7 |
#ifdef MACRO //... comandos //... comandos #else //... comandos //... comandos #endif |
Outra forma de fazer esse procedimento é utilizando o operador defined, ou ainda utilizando a negação !defined (#ifndef):
1 2 3 4 5 6 7 |
#if defined MACRO //... comandos //... comandos #else //... comandos //... comandos #endif |
Considere o exemplo abaixo, no qual a macro DEBUG é utilizada na verificação condicional:
1 2 3 4 5 6 7 8 9 10 |
int main() { #if defined(DEBUG) printf("DEBUG - Versão de teste!\n"); #else printf("Versão final\n"); #endif return 0; } |
Esse programa utiliza a macro DEBUG para definir qual mensagem será exibida no início da aplicação. Cabe frisar que a avaliação das expressões condicionais são realizadas em tempo de compilação! Assim o programa final contém apenas uma das chamadas para a função printf().
Se a macro DEBUG for definida, o código final será:
1 2 3 4 5 6 |
int main() { printf("DEBUG - Versão de teste!\n"); return 0; } |
Caso contrário:
1 2 3 4 5 6 |
int main() { printf("Versão final\n"); return 0; } |
Header Guards
Vimos no artigo anterior, Pré-processador C – Parte 1, que a diretiva #include instrui o compilador a ler e processar um arquivo-fonte. Especificamente o conteúdo do arquivo incluído é copiado para linha onde a diretiva foi declarada.
Para exemplificar, considere um arquivo timer.h, o qual define uma struct chamada timer.
1 2 3 4 5 6 |
struct timer { int tm_sec; int tm_min; int tm_hour; } |
E que em um determinado arquivo a biblioteca timer.h é incluída duas vezes.
1 2 3 4 5 6 7 |
#include "timer.h" #include "timer.h" int main() { return 0; } |
Consequentemente o conteúdo de timer.h será copiado duas vezes conforme mostra o código abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct timer { int tm_sec; int tm_min; int tm_hour; } struct timer { int tm_sec; int tm_min; int tm_hour; } int main() { return 0; } |
Nesse caso, o processo de compilação será encerrado devido à duplicação da struct timer. Para evitar esses e outros erros que podem ocorrer com a inclusão de arquivos, adicionamos uma estrutura condicional chamada Header Guards.
Basicamente esse mecanismo utiliza uma macro para verificar se o arquivo já foi incluído. Uma prática comum é utilizar o nome do arquivo na macro que será definida.
Alterando o arquivo timer.h:
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef TIMER_H_INCLUDED #define TIMER_H_INCLUDED struct timer { int tm_sec; int tm_min; int tm_hour; } #endif |
Agora, quando o arquivo é incluído, ocorre a verificação da macro TIMER_H_INCLUDED. Se for a primeira vez que o arquivo é incluído, então a macro TIMER_H_INCLUDED será definida.
O mesmo exemplo de dupla inclusão agora com Header Guards é mostrado abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#ifndef TIMER_H_INCLUDED #define TIMER_H_INCLUDED struct timer { int tm_sec; int tm_min; int tm_hour; } #endif #ifndef TIMER_H_INCLUDED #define TIMER_H_INCLUDED struct timer { int tm_sec; int tm_min; int tm_hour; } #endif int main() { return 0; } |
Nesse caso o código seria compilado corretamente, pois no primeiro #include a macro ainda não foi definida. No segundo #include a macro já foi definida e a condição de teste será falsa. Assim, se timer.h for incluído em mais de um arquivo, ele será “processado” somente uma vez.
Inclusão condicional
A compilação condicional também é muito utilizada no desenvolvimento de frameworks onde arquivos-fonte podem ser compilados para diferentes plataformas. Dessa forma os arquivos incluídos no processo de compilação dependem de alguma configuração previamente definida no projeto. No exemplo abaixo os arquivos que são incluídos na compilação dependem do valor definido na macro DEVICE.
1 2 3 4 5 6 7 8 9 10 11 12 |
#define DEVICE_1 1 #define DEVICE_2 2 #define DEVICE_3 3 #define DEVICE DEVICE_1 #if DEVICE == DEVICE_1 # include "device_1.h" #elif DEVICE == DEVICE _2 # include "device_2.h" #elif DEVICE == DEVICE _3 //... #endif |
Esse mecanismo permite estruturar a aplicação conforme os dispositivos e/ou plataformas utilizadas, ou ainda conforme uma versão da aplicação sem a necessidade de alterar a estrutura do código.
Encerrando o processo de compilação
Em alguns momentos é necessário instruir o compilador para abortar o processo de compilação. A diretiva #error instrui o pré-processador a gerar uma mensagem de erro e encerrar o processo de compilação.
Considere, por exemplo, que o dispositivo utilizado na aplicação não foi definido.
1 2 3 4 5 6 7 8 9 10 |
#define DEVICE_1 1 #define DEVICE_2 2 #if DEVICE == DEVICE_1 #include "device_1.h" #elif DEVICE == DEVICE _2 #include "device_2.h" #else #error O dispositivo não foi definido! #endif |
Nesse caso, se o dispositivo não é válido, a aplicação não é compilada.
Conclusão
Nesse artigo foram apresentados, de maneira introdutória, alguns recursos do pré-processador C que podem ser empregados para realizar a compilação condicional de código. As diretivas de compilação condicional podem ser utilizadas em qualquer parte do projeto, permitindo estruturar e organizar o código que é compilado.
Na terceira parte desta série serão apresentadas outras funções do pré-processador C e algumas técnicas de expansão de macros.
Saiba mais: Série “Pré-processador C”
– Pré-processador C: Compilação condicional – Parte 2
Parabéns pelo artigo.
Obrigado, Cesar!
muito bom o artigo.
Este é um tópico muito legal, útil e capcioso em desenvolvimento de software em C/C++
Obrigado, Rafael!
Com certeza, o pré-processador é muito útil para desenvolver aplicações com certa flexibilidade, manutenibilidade e possibilidade de extensão. Mas como você mesmo disse, é capcioso, uma vez que a utilização incorreta pode causar sérias consequências para a estrutura do projeto.
o uso incorreto pode não trazer só conseqüências para o projeto… Pode trazer conseqüências para a sanidade dos desenvolvedores envolvidos.
haha certamente! Entender o que foi feito já pode ser um problema, encontrar e resolver algum erro então…..