User Tools

Site Tools


prog2:o_preprocessador_c

O preprocessador C

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:

 Do fonte ao executável

  • preprocessamento: tratamento das diretivas do preprocessador (#include, etc)
  • compilação: conversão de C para assembly
  • tradução: conversão de assembly para código de máquina (binário)
  • ligação: junção dos arquivos-objeto e bibliotecas para formar o arquivo executável

Preprocessamento

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

Inclusão de arquivos

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.

Definição e uso de constantes

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 ;

Para evitar confusões entre variáveis da linguagem C e constantes do preprocessador, convencionou-se definir as constantes em MAIÚSCULAS.

Constantes predefinidas

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:

Compilação condicional

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:

headers.h
#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

Observe o uso das diretivas #warning e #error no programa acima.

Macros com parâmetros

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)) ;

Operações avançadas

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.

prog2/o_preprocessador_c.txt · Last modified: 2019/02/19 17:35 (external edit)