Há uma série de definições que podem coexistir em diversos módulos de um mesmo programa: valores constantes que devem ser compartilhados, definição de estruturas, definições de nomes de tipos, protótipos de funções, etc. Seria tedioso e muito sujeito a erros se estas definições tivessem de ser inseridas em cada módulo.
A linguagem C oferece mecanismos que permitem manter definições unificadas que são compartilhadas entre diversos arquivos. A base destes mecanismos é o pré-processamento de código, a primeira fase na compilação do programa. Nesta fase, por exemplo, comentários são substituídos por espaços antes do código ser passado para a fase de compilação. Essencialmente, o pré-processador é um processador de macros para uma linguagem de alto nível.
O programador se comunicar com o pré-processador inserindo diretivas em um código fonte de forma a facilitar a manutenção do programa. As diretivas para o pré-processador C podem ser reconhecidas pelo símbolo # na primeira coluna da linha onde ocorrem. Estas diretivas não são expressões C, de forma que as linhas onde elas ocorrem não são terminadas por ponto e vírgula.
A diretiva #include permite que um arquivo (geralmente com definições e declarações de protótipos) possa ser incluído em um módulo. Por convenção, tais arquivos -- chamados de arquivos de cabeçalho -- recebem a extensão .h (de header).
A linguagem C oferece um conjunto de rotinas de suporte. Para essas rotinas não é necessário que o usuário entre sempre com os protótipos de cada função que ele irá utilizar: esta informação já está contida em arquivos de cabeçalho usados pelo compilador. Por exemplo, protótipos para rotinas de ordenação e busca estão em um arquivo de cabeçalho do sistema de nome stdlib.h. Para usar essas deinições e outras que eventualmente estejam nesse arquivo, o programador inclui no início do arquivo a linha
#include <stdlib.h>
O nome do arquivo de cabeçalho foi incluído entre <...>. Isto
indica que este arquivo é um arquivo de cabeçalho fornecido pelo
compilador, que será incluído a partir de um diretório padrão do
sistema -- geralmente o diretório /usr/include em um sistema
Unix.
Existe uma forma alternativa de inclusão que permite que o programador crie seus próprios arquivos de cabeçalho e os inclua em seus módulos. Esses arquivos deverão ser localizados em algum diretório do usuário. Neste caso, o nome do arquivo de cabeçalho deve ser incluído entre aspas.
O uso da diretiva #include facilita a leitura do código C ao abstrair detalhes de definições para uma outra etapa e, principalmente, favorece a coerência entre módulos que devem compartilhar as mesmas definições.
Outra importante diretiva para o pré-processador C é a diretiva #define, que permite definir constantes simbólicas e macros que serão substituídas no código fonte durante a compilação. Com o uso desta diretiva torna-se mais simples manter o código envolvendo constantes correto. Ela também simplifica a compreensão do código ao usar nomes simbólicos que indicam o papel de constantes no módulo.
Essa diretiva foi usada na seção anterior para definir os tamanhos dos campos de uma linha de instrução:
#define LINSIZE 80
#define LABSIZE 8
#define OPCSIZE 10
#define OPRSIZE 20
#define CMTSIZE (LINSIZE-(LABSIZ+OPCSIZE+OPRSIZE))
A vantagem em se usar essas constantes simbólicas é que qualquer
modificação nessas definições -- por exemplo, mudar o tamanho da
linha para 100 caracteres ao invés de 80 -- pode ser realizada em um
único local. O restante do código permanece inalterado.
Um outro uso da diretiva #define é a definição de macro-instruções. Uma macro tem sintaxe de uso similar a uma chamada de função; entretanto, o código da macro é substituído no código fonte durante o pré-processamento. Macros não geram chamadas de funções, não têm variáveis na pilha e não fazem verificação de tipos de argumentos. Em geral, são utilizadas para substituir expressões complexas de forma eficiente.
Considere uma função max, que retorna o maior de dois valores passados como argumentos. Esta função tem um código que poderia ser expresso em uma linha com o uso do operador condicional. Entretanto, como função ela está restrita ao uso com variáveis inteiras. Para obter o maior valor entre duas variáveis do tipo double, outra função deveria ser escrita tendo exatamente o mesmo corpo -- apenas o tipo de retorno e tipo dos argumentos seriam modificados.
Uma definição de macro pode simplificar este problema. A forma geral de definição de macros é
#define nome_macro(lista_argum) (corpo_macro)
O par de parênteses em torno do corpo da macro não é necessário, mas é
usualmente incluído para evitar problemas de mudança de precedência de
operadores após a expansão da macro no código fonte.
A macro para obter o máximo de dois valores poderia ser escrita como
#define max(a,b) (a<b ? b : a)
e utilizada da mesma forma que funções:
int i, j, k;
double x, y, z;
...
k = max(i,j);
z = max(x,y);
Após a fase de pré-processamento, o código efetivamente repassado para a
fase de compilação seria:
int i, j, k;
double x, y, z;
...
k = (i<j ? j : i);
z = (x<y ? y : x);
A definição de uma macro pode se estender por mais de uma linha. Nestes
casos, cada linha a ser continuada deve ser terminada por uma contrabarra
(\). Por exemplo, a mesma macro max poderia ter sido
definida como
#define max(a,b) \
( a<b ? \
b : \
a )
O uso da diretiva #define também facilita a manutenção de código. Tais diretivas são usualmente incluídas como parte de arquivos de cabeçalho quando suas definições são compartilhadas entre diversos módulos. Por exemplo, a macro max exemplificada acima já é geralmente incluída em um arquivo padrão de cabeçalho, macros.h.
O pré-processador também entende a diretiva #undef, que permite eliminar a definição de um identificador. Por exemplo,
#undef TRUE /* esquece qualquer definicao anterior */
#define TRUE 1 /* nova definicao */
Em algumas situações, pode ser interessante incluir ou excluir alguns trechos de código em um programa -- por exemplo, para incluir testes e mensagens de depuração durante o desenvolvimento do programa e excluí-los na versão final. Para programas de porte razoável, a manutenção manual deste tipo de trechos de programa pode se tornar uma tarefa complexa. Um mecanismo que pode facilitar esta tarefa é a utilização de diretivas de compilação condicional.
A diretiva básica para a compilação condicional é #if ... #endif:
#if expr_constante
/* codigo incluido quando expr_constante != 0 */
...
#else
/* codigo incluido quando expr_constante == 0 */
...
#endif
O trecho #else é opcional, podendo ser omitido. Um exemplo de uso destas diretivas é a verificação se uma constante já foi definida, como em
...
x = malloc(n);
#if defined(DEBUG)
printf("malloc: %d bytes alocados a partir de %p\n",
n, x);
#endif
...
(A seqüência de conversão %p apresenta uma variável
apontador.) A função printf acima será invocada apenas quando o
identificador DEBUG tiver sido previamente definido, como em
#define DEBUG
Observe que nem é necessário que um valor seja associado ao identificador
neste caso; basta que ele esteja definido. Assim, durante a fase de
desenvolvimento a definição acima seria incluída em módulos sendo
depurados, sendo posteriormente removida para a geração do programa final.
A forma #if defined(...) ocorre tão frequentemente que há uma forma abreviada de diretiva, #ifdef. O exemplo acima poderia ser reescrito como
...
x = malloc(n);
#ifdef DEBUG
printf("malloc: %d bytes alocados a partir de %p\n",
n, x);
#endif
...
Um dos principais usos da compilação condicional é evitar a reinclusão de arquivos de cabeçalho. Em alguns casos, um arquivo de cabeçalho pode já incluir definições de outro arquivo de cabeçalho. A questão é: como evitar erros de redeclaração por causa de uma outra inclusão explícita de um arquivo já incluído implicitamente?
Por exemplo, suponha que a definição da estrutura linha estivesse em um arquivo de cabeçalho montador.h, por ser uma construção que será compartilhada por vários módulos:
/*
* montador.h
*/
#define LINSIZE 80
#define LABSIZE 8
#define OPCSIZE 10
#define OPRSIZE 20
#define CMTSIZE (LINSIZE-(LABSIZ+OPCSIZE+OPRSIZE))
typedef struct linha {
char rotulo[LABSIZ];
char opcode[OPCSIZ];
char operand[OPRSIZ];
char comment[CMTSIZ];
} Linha;
A compilação condicional traz a solução para o problema associado à reinclusão de arquivos, gerando erros de redefinição de estruturas. Quando um arquivo de cabeçalho é incluído pela primeira vez, ele pode definir um identificador associado apenas àquele arquivo. Quando se tenta incluir novamente o arquivo de cabeçalho, um teste é realizado -- caso o identificador já esteja definido, então o conteúdo do arquivo não é incluído.
No caso acima, o símbolo poderia ser por exemplo _H_MONTADOR, e o conteúdo do arquivo seria
/*
* montador.h
*/
#if ! defined(_H_MONTADOR)
#define _H_MONTADOR
/* conteudo original aqui */
#endif
Além de #if, #ifdef, #else e #endif, as diretivas #ifndef (se não definido) e #elif (else-if) são suportadas.
O pré-processador C oferece ainda diversos outros recursos. O operador # permite substituir na macro a grafia de um argumento. Por exemplo, o programa
#define path(prof,curso) \
"/home/faculty/" #prof "/courses/" #curso
main() {
printf ("path: %s\n",path(ricarte,progc));
}
iria resultar na seguinte saída quando executado:
path: /home/faculty/ricarte/courses/progc
No exemplo acima, o recurso de concatenação de strings adjacentes (outra tarefa desempenhada pelo pré-processador) é utilizado.
Concatenação é suportada através do operador ##. Quando este operador é usado em uma macro entre dois outros símbolos, os símbolos são inicialmente expandidos e então o símbolo do operador e quaisquer espaços em volta dele são eliminados. Por exemplo, a macro
#define sport(a) a ## bol
poderia ser usada para criar identificadores em um programa, como
int sport(fute), sport(basquete);
...
futebol = 1; /* criado por sport(fute) */
basquetebol = 2; /* criado por sport(basquete) */
...
Há também macros que são pré-definidas e que podem ser usadas em qualquer programa C, que são:
Por exemplo, considere o seguinte programa criado em um arquivo de nome predefin.c:
main() {
printf ("%s:%d (%s %s)\n",
__FILE__, __LINE__, __DATE__, __TIME__);
}
Após compilado e executado, este programa apresentaria o seguinte resultado:
predefin.c:2 (May 17 1995 13:27:29)
Outras diretivas do pré-processador incluem:
Por exemplo, considere que o arquivo predefin.c do exemplo anterior seja modificado como se segue:
main() {
#ifdef TST
# line 100 "arq_teste"
#else
# error Esqueceu de definir TST!
#endif
printf ("%s:%d (%s %s)\n",
__FILE__, __LINE__, __DATE__, __TIME__);
}
A linha de comando
cc predefin.c -o predefingeraria a seguinte resposta do compilador:
"predefin.c", line 5.0: 1506-205 (S) Esqueceu de definir TST!
É possível definir um símbolo na linha de comando de compilação através do uso da chave -D. A linha de comando
cc -DTST predefin.c -o predefinproduziria um programa executável predefin que quando executado geraria a seguinte mensagem:
arq_teste:103 (May 17 1995 14:10:27)
Deve ser ressaltado que o uso do pré-processador C não está restrito exclusivamente a programas fonte C, uma vez que ele existe como um programa independente (cpp). Assim, outras aplicações podem usar essas mesmas funcionalidades. Por exemplo, o compilador idltojava da Sun, que mapeia especificações de interface expressas em Interface Description Language (padrão especificado pelo Object Management Group para a arquitetura CORBA) para programas na linguagem Java, requer o uso de um pré-processador C para sua operação.