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 DEBUGObserve 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 ## bolpoderia 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.