A transformação de um programa C em um arquivo executável é um processo complexo, com várias etapas. Ele está ilustrado na figura a seguir:
#include
, etc)O preprocessador C (CPP - C PreProcessor) é uma ferramenta de substituição de texto invocada automaticamente pelo compilador C/C++ no início do processo de compilação. Apesar de não fazer parte formal da sintaxe da linguagem C, seu uso é praticamente indispensável para a estruturação de programas C, mesmo os mais simples.
Todos os comandos do preprocessador começam com o símbolo #
no início de uma linha (podem haver espaços e tabs antes).
A saída do preprocessador C, que será enviada ao compilador propriamente dito, pode ser obtida através do flags -E
:
gcc -E *.c
O preprocessador é muito usado para incluir arquivos externos em um código-fonte:
#include <stdio.h> #include "mylib.h" int main() { printf ("Hello, world\n") ; return (0) ; }
No exemplo acima, o preprocessador irá substituir cada linha #include …
pelo conteúdo do respectivo arquivo, gerando um único arquivo temporário que será entregue ao compilador C.
Há uma diferença importante entre as duas formas de inclusão:
#include <…>
: o arquivo indicado será buscado nos diretórios default do compilador, geralmente /usr/include/*
nos sistemas Unix.#include “…”
: o arquivo indicado será buscado primeiro no diretório corrente (onde está o arquivo que está sendo compilado), e depois nos diretórios default do compilador.
O preprocessador é frequentemente usado para a definição de constantes, através do comando #define
:
#define VETSIZE 64 int vetor[VETSIZE] ; for (i=0; i<VETSIZE; i++) vetor[i] = 0 ;
Após a definição, todas as ocorrências da string VETSIZE
no arquivo serão substituídas pelo valor 64, antes da compilação, resultando no seguinte código-fonte:
int vetor[64] ; for (i=0; i<64; i++) vetor[i] = 0 ;
Algumas constantes são definidas previamente pelo sistema:
__DATE__
: data atual (formato “MMM DD YYYY”)__TIME__
: horário atual (formato “HH:MM:SS”)__FILE__
: nome do arquivo corrente.__LINE__
: número da linha corrente do código-fonte.Além destas, muitas outras constantes podem estar disponíveis, dependendo da plataforma:
Uma constante pode ser definida sem ter um valor específico, funcionando como um flag que pode ser testado pelo preprocessador através de comandos específicos:
#define DEBUG ... #ifdef DEBUG printf ("Valor de i: %d\n", i) ; #endif
No exemplo acima, a linha com o printf
só estará presente no código enviado ao compilador se a constante DEBUG
estiver definida.
Deve-se observar que constantes podem ser definidas no código-fonte usando o comando #define
(como nos exemplos acima), mas também podem ser definidas na linha de comando, ao invocar o compilador:
gcc -DVETSIZE=64 arquivo1.c gcc -DDEBUG arquivo2.c
Um uso frequente da compilação condicional é a construção de include guards, ou seja código para evitar múltiplas inclusões do mesmo arquivo:
#ifndef _THIS_HEADER_FILE_ #define _THIS_HEADER_FILE_ ... #endif
Da mesma forma, pode-se evitar redefinir constantes que já estejam definidas:
#ifndef NULL #define NULL (void *) 0 #endif
Além do ifdef
, existem outros operadores condicionais, como o if
- elif
- else
:
#if DEBUG_LEVEL > 5 // print all debug messages ... #elif DEBUG_LEVEL > 3 // print relevant debug messages ... #elif DEBUG_LEVEL > 1 // print prioritary debug messages ... #else // print no debug messages #endif
O operador defined
permite testar se uma macro está definida:
#if defined (__arm__) // macro definida em sistemas com ARM #warning "Generating code for ARM processor." // code for ARM processors ... #elif defined (__i386__) // idem, x86 #warning "Generating code for x86 processor." // code for Intel 32-bit processors ... #else // abort compilation #error "Unknown architecture, aborting." #endif
#warning
e #error
no programa acima.
O preprocessador é usado com frequência para construir macros, que são funções simples com parâmetros:
#define SQUARE(x) x*x ... printf ("O quadrado de 5 é %d\n", SQUARE(5)) ;
O código acima, ao ser tratado pelo preprocessador, será transformado em:
printf ("O quadrado de 5 é %d\n", 5*5) ;
Observe que a macro SQUARE
não computou o resultado de 5*5, apenas fez a substituição do parâmetro 5 pela expressão que ela define (5*5).
Deve-se tomar muito cuidado ao definir macros com parâmetros, pois eles podem ser avaliados de forma errada pelo compilador. O código abaixo apresenta um erro dessa natureza:
#define SQUARE(x) x*x ... printf ("O quadrado de 2+3 é %d\n", SQUARE(2+3)) ;
Após o preprocessador teremos:
printf ("O quadrado de 2+3 é %d\n", 2+3*2+3) ;
O resultado da expressão deveria ser 25 (o quadrado de 2+3) mas será 11, por causa da precedência entre os operadores aritméticos. Para evitar esse erro, a macro deve ser declarada usando parênteses:
#define SQUARE(x) (x)*(x) ... printf ("O quadrado de 2+3 é %d\n", SQUARE(2+3)) ;
Que resulta em:
printf ("O quadrado de 2+3 é %d\n", (2+3)*(2+3)) ;
O preprocessador C tem operações avançadas que vão muito além do escopo desta breve introdução. Para uma visão mais completa e profunda consulte este documento: The C Preprocessor.