Olá, caro leitor! Este artigo tem o intuito de expor, de maneira introdutória, alguns recursos do pré-processador C. Desse modo, o artigo foi estruturado para apresentar alguns exemplos básicos e algumas aplicações para você, leitor, entender melhor o assunto. Vamos lá!
Afinal, o que é o Pré-processador C?
Durante o processo de compilação de um programa você pode instruir o compilador sobre como realizar determinada tarefa. Uma das formas de fazer isso no próprio código-fonte é utilizando os recursos do pré-processador C. Especificamente, o pré-processador C é uma ferramenta utilizada antes do estágio de compilação para realizar uma transformação textual no código-fonte.
O Pré-processador C
As instruções dadas para o compilador são definidas a partir das diretivas do pré-processador. O padrão C ANSI define as seguintes diretivas:
#if #ifdef #ifndef #else #elif #endif #include #define #undef #line #error #pragma
Essas diretivas possibilitam instruir o compilador a realizar as seguintes tarefas:
- Incluir arquivos de cabeçalho (.h – header);
- Compilação condicional;
- Definir constantes e/ou macros;
- Expansão de macros;
- Controle e diagnóstico do processo de compilação.
Cabe frisar que essas diretivas não fazem parte da linguagem C, contudo fazem parte de uma ferramenta muito útil para o desenvolvimento de programas em C.
Formato das diretivas do Pré-processador C
Convém observar que todas as diretivas do pré-processador C são iniciadas com o símbolo ‘#’ seguida do nome da diretiva e devem, obrigatoriamente, ser declaradas em linhas diferentes.
Como regra para utilização das diretivas temos que:
- O símbolo # deve ser o primeiro caractere (exceto o espaço em branco) da linha;
- Toda diretiva é terminada ao final de cada linha, a não ser que a linha seja finalizada pelo símbolo backslash “\”;
- Com exceção da diretiva #pragma todas as diretivas podem ser utilizadas em qualquer parte do programa.
Incluindo arquivos – #include
Modularizar e reutilizar códigos são práticas comuns e de grande importância. Para isso é necessário instruir o compilador a incluir arquivos-fonte no processo de compilação. No pré-processador C essa tarefa é realizada pela diretiva #include.
A diretiva #include instrui o compilador a ler e compilar um arquivo-fonte. De forma geral o #include determina o nome do arquivo que será processado. Por exemplo, considere o famoso Hello World:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello world!\n");
return 0;
}
Ao processar o arquivo, a diretiva #include instrui o compilador a ler e compilar o arquivo stdio.h. Mas afinal, onde está localizado o arquivo stdio.h?
Para responder essa pergunta é necessário antes verificar a sintaxe da diretiva. Basicamente existem duas maneiras de incluir um arquivo. Uma é utilizando os símbolos ‘<’ e ‘>’, já a outra é utilizando aspas. Veja:
#include <stdio.h> #include “stdio.h”
Ao utilizar os símbolos de maior e menor você instrui o compilador a procurar o arquivo nos diretórios do sistema, neste caso, definido pelo compilador. Já na outra abordagem, o arquivo é procurado no diretório do código-fonte (diretório de trabalho).
Definindo macros – #define
A diretiva #define é utilizada para definir um nome (identificador, denominado macro) que mantém um valor associado. No estágio inicial da compilação o identificador é substituído pelo seu valor.
Considere o exemplo básico para piscar um LED no Arduino:
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin 13 as an output.
pinMode(13, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(13, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(13, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
No código de exemplo, o valor 13 refere-se ao pino de saída que aciona um LED. É possível observar também que esse valor é repetido em outras partes do programa.
Uma aplicação prática do #define é para reduzir a repetição de valores e assim evitar possíveis erros também. Neste caso é válido criar um #define que possui o valor 13, como abaixo:
#define LED_PIN 13
#define LED_ON HIGH
#define LED_OFF LOW
#define BLINK_DELAY 1000
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin as an output.
pinMode(LED_PIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_PIN, LED_ON); // turn the LED on (HIGH is the voltage level)
delay(BLINK_DELAY);
digitalWrite(LED_PIN, LED_OFF); // turn the LED off by making the voltage LOW
delay(BLINK_DELAY);
}
Ao processar o arquivo o identificador LED_PIN é substituído pelo seu valor, neste caso, o valor 13. Desse modo se o projeto é alterado de forma que o LED seja acionado por outro pino, bastaria alterar o valor da macro.
Observe também que é possível expandir macros em vários níveis como no caso da definição:
#define LED_ON HIGH
Nesse caso, LED_ON tem como valor a macro HIGH que, por sua vez, é definida como na biblioteca Arduino.h:
#define HIGH 0x1
As macros não ficam limitadas apenas em relacionar nomes e valores. A partir das macros também é possível definir trechos de código e até mesmo receber parâmetros.
Definindo macros com parâmetros – #define
Para explicar esse tópico vou utilizar o exemplo da rotina MAX. A rotina MAX é utilizada para definir o maior valor entre dois valores informados:
int maior, N1 = 5, N2 = 10;
if (A > B)
{
maior = A;
}
else
{
maior = B;
}
ou ainda, no modo if-inline:
int maior, N1 = 5, N2 = 10; maior = (N1 > N2) ? N1 : N2;
O trecho de código pode ser convertido em uma macro que recebe os dois valores:
#define MAX(A,B) ( (A) > (B) ? (A):(B))
E sua aplicação no código fica assim:
int maior, N1 = 5, N2 = 10; maior = MAX(N1,N2);
Por fazer simplesmente a substituição da macro pelo seu valor, alguns cuidados devem ser tomados. Convém observar que os parâmetros devem sempre ser utilizados entre parênteses. Considere que MAX tivesse sido definido como:
#define MAX(A,B) A > B ? A : B
e:
maior = MAX(N1+2,N2);
Durante a reposição da macro pelo seu valor, o código gerado seria:
maior = N1+2 > N2 ? N1 + 2 : N2;
Seria gerado o valor errado!
Definindo macros com várias linhas – #define
Para definir uma macro em mais de uma linha o símbolo ‘\’ (backslash) deve ser utilizado no final da linha. Esse símbolo índica que a definição da macro continua na próxima linha.
#define ABORT(status)\
printf("...");\
exit(status);
Geralmente as macros com mais de uma instrução são criadas como blocos de código estruturados por um do {} while (0). Considere o exemplo abaixo.
if (<condição>)
ABORT(10);
else
{
}
O processo de compilação seria encerrado com a seguinte mensagem:
error: 'else' without a previous 'if'.
Isso ocorre porque a substituição da macro transforma o código sem utilizar os delimitadores de bloco {}.
if (<condição>)
printf("...");
exit(status);
else
{
}
Assim uma macro que utiliza o formato do {} while (0) compilaria corretamente.
#define ABORT(status) \
do {\
printf("...");\
exit(status);\
} while(0)
Removendo definições – #undef
Para remover uma definição é necessário utilizar a diretiva #undef. A remoção torna-se necessária para que seja possível definir uma outra macro com o mesmo identificador.
#define LED_PIN 13 //… //… #undef LED_PIN
Operadores # e ##
O pré-processador C ainda inclui recursos para converter parâmetros da macro em uma string constante ou ainda realizar a concatenação de tokens.
O processo de conversão para uma string constante é chamado de Stringication e é realizado dentro de uma macro a partir do símbolo cerquilha (#). No exemplo abaixo a macro MSG_FORMAT converte o parâmetro type em uma string.
#define MSG_FORMAT(type, arg) #type ": " arg
#define ERROR 1
#define WARNING 2
#define INFO 3
int main()
{
// a string resultante será: "ERROR"": ""teste1"
printf("%s\r\n", MSG_FORMAT(ERROR, "teste1"));
// a string resultante será: "WARNING "": ""teste2"
printf("%s\r\n", MSG_FORMAT(WARNING, "teste2"));
// a string resultante será: "INFO "": ""teste3"
printf("%s\r\n", MSG_FORMAT(INFO, "teste3"));
return 0;
}
Já a concatenação é realizada a partir da sequência ##. No exemplo abaixo a macro MSG_TYPE é utilizada para definir os elementos de uma enumeração. O parâmetro type é concatenado no final do texto _MSG.
#define ERROR 1
#define WARNING 2
#define INFO 3
#define MSG_TYPE(type) MSG_##type = type
typedef enum
{
MSG_TYPE(ERROR), //a conversão da macro resulta em MSG_ERROR = 1
MSG_TYPE(WARNING), //a conversão da macro resulta em MSG_WARNING = 2
MSG_TYPE(INFO), //a conversão da macro resulta em MSG_INFO = 3
} MessageType;
Conclusão
Neste artigo foi apresentado de maneira introdutória alguns recursos do pré-processador C. Como se pode ver o pré-processador expande os recursos do ambiente de programação tornando-se uma ferramenta essencial para o desenvolvimento de programas em linguagem C.
Na segunda parte deste artigo serão apresentados os recursos do pré-processador que possibilitam realizar a compilação condicional de trechos de código.
Saiba mais: Série “Pré-processador C”
– Pré-processador C: Compilação condicional – Parte 2
– Pré-processador C: Diagnóstico – Parte 3
– Pré-processador C: X macros – Parte 4
Referências
Livro: C, completo e total – 3ª edição revista e atualizada. Herbert Schildt.
Fonte da imagem destacada: https://listamaze.com/top-10-programming-languages-for-job-security/





muito bom!
Só tem um pequeno erro no começo:
“Toda diretiva é terminada ao final de cada linha, a não ser que a linha seja finalizada pelo símbolo backslash (/);”
o backslach é o “”.
Tanto que você o utiliza no decorrer do texto.
Olá, Rafael.
obrigado pela correção!
Fernando,
Muito bom seu artigo. Vou recomendá-lo para meus alunos.
Olá, Leandro.
Fico feliz que tenha gostado. Espero que seja proveitoso para os seus alunos!
Obrigado pelo retorno!
Muito bom o artigo Fernando.
São detalhes que fazem a diferença…
Sempre tive dificuldade em quando usar certas coisas, mas com esse artigo começa a clarear a aplicação de cada coisa…
Agradeço por compartilhar seu conhecimento, no aguardo do próximo
Olá, Rafael.
Realmente os detalhes fazem a diferença. Muitas vezes apenas utilizamos algo mas não sabemos o significado. Nos próximos artigos vou mostrar alguns recursos que podem ajudar bastante no desenvolvimento de aplicações.
Obrigado pelo retorno!
Simples e objetivo, muito bom
Obrigado, Marcos!
Excelente artigo Fernando.
Aguardando os próximos!
Olá, Haroldo.
Muito obrigado pelo retorno!
Ótimo conteúdo caro Fernando ! no aguardo da segunda parte ! muito grato !
Olá, Lenio.
Obrigado pelo retorno!
Fico feliz que tenha gostado.
Parabéns pelo texto. No aguardo das outras partes.
Obrigado, Cesar.
Espero que goste dos próximos!