Le direttive del preprocessore
Il processo che dal file di testo scritto in linguaggio C porta alla creazione del file eseguibile è suddiviso in vari passaggi; il primo di questi consiste nel chiamare un programma, il preprocessore , il cui compito è quello di:
(1) rimuovere i commenti dal codice
(2) interpretare le direttive per il preprocessore
Il file di testo prodotto dal preprocessore, viene quindi passato al compilatore per il controllo del codice. Questo significa che il preprocessore NON controlla la correttezza del codice.
Che cosa faccia il preprocessore per adempiere al compito descritto al punto (1) è abbastanza intuitivo, ovvero esso rimuove tutto il testo contenuto all'interno dei commenti (ossia il testo che sta dopo i simboli '//' oppure il testo compreso tra i simboli '/*' e '*/')
Per spiegare quanto descritto al punto (2) bisogna specificare cosa sia una direttiva per il preprocessore. Con questo termine si intende un comando, preceduto dal simbolo # , come ad esempio #include. Limitiamoci per ora a capire cosa significa questo comando, in seguito nel corso esamineremo il significato delle altre direttive.
La direttiva #include
La direttiva include prevede un parametro e tale parametro può essere compreso tra i simboli '<' e '>' oppure tra doppi apici; il parametro rappresenta il nome di un file. Quando il preprocessore incontra questa direttiva si comporta in questo modo: cerca il file specificato e NE COPIA IL CONTENUTO (senza interpretarlo) nel file di testo che viene poi passato al compilatore. I criteri con cui il preprocessore cerca il file sono i seguenti:
(a) se il nome del file è compreso tra i simboli '<' e '>' la ricerca avviene all'interno della directory in cui si trovano tutti i file di intestazione della libreria standard (nei sistemi operativi tipo Linux o Unix la directory è /usr/include, mentre nei sistemi operativi di tipo windows è qualcosa del tipo C:\Programmi\Nome_compilatore\include)
(b) se il nome del file è compreso tra doppi apici la ricerca avviene, di norma, all'interno della directory in cui sta avvenendo la compilazione, ovvero la directory in cui si trova il file di testo contenente il codice C
Gli appassionati della compilazione da "linea di comando" forse preferiscono usare la notazione a '<' '>' ed inserire un'opzione a quelle già utilizzate, in modo da inserire la directory di lavoro all'interno del percorso di ricerca del preprocessore
gcc -Wall -I./ -o prova filesorgente.c
di cui la versione più generale è
gcc -Wall -Inome_directory_di_ricerca -o prova filesorgente.c
Prima di capire quale sia la potenzialità di questa direttiva, proviamo a vedere come opera il preprocessore quando la incontra.
Esempio
Inizio codice sorgente
#include <stdio.h>
int main()
{char a; // questo e' un carattere
double b; // questa variabile e' di tipo double
a = 'y';
b = 6.41;
printf(" stampo i valori delle variabili %c %e",a,b);
return 0;}
Fine codice sorgente
Il preprocessore viene automaticamente attivato nel momento in cui chiamiamo il programma gcc o, in generale, un qualsiasi compilatore C. Il preprocessore, quindi, è integrato nel compilatore.
Qui di seguito è possibile osservare, per passi, il risultato dell'azione del preprocessore:
(1) passo 1, rimozione dei commenti. L'output del preprocessore diventa
Qui inizia l'output /passo 1
#include <stdio.h>
int main()
{char a;
double b;
a = 'y';
b = 6.41;
printf(" stampo i valori delle variabili %c %e",a,b);
return 0;}
Qui termina l'output /passo1
(2) Passo 2, ricerca del file stdio.h e sua inclusione nel testo. A questo punto l'output del preprocessore diventa:
Qui inizia l'output /passo2
...
int fgetpos(FILE *_stream, fpos_t *_pos);
char * fgets(char *_s, int _n, FILE *_stream);
FILE * fopen(const char *_filename, const char *_mode);
int fprintf(FILE *_stream, const char *_format, ...);
int fputc(int _c, FILE *_stream);
int fputs(const char *_s, FILE *_stream);
size_t fread(void *_ptr, size_t _size, size_t _nelem, FILE *_stream);
FILE * freopen(const char *_filename, const char *_mode, FILE *_stream);
int fscanf(FILE *_stream, const char *_format, ...);
int fseek(FILE *_stream, long _offset, int _mode);
int fsetpos(FILE *_stream, const fpos_t *_pos);
long ftell(FILE *_stream);
size_t fwrite(const void *_ptr, size_t _size, size_t _nelem, FILE *_stream);
int getc(FILE *_stream);
int getchar(void);
char * gets(char *_s);
void perror(const char *_s);
int printf(const char *_format, ...);
int putc(int _c, FILE *_stream);
int putchar(int _c);
int puts(const char *_s);
int remove(const char *_filename);
int rename(const char *_old, const char *_new);
void rewind(FILE *_stream);
int scanf(const char *_format, ...);
void setbuf(FILE *_stream, char *_buf);
int setvbuf(FILE *_stream, char *_buf, int _mode, size_t _size);
int sprintf(char *_s, const char *_format, ...);
int sscanf(const char *_s, const char *_format, ...);
FILE * tmpfile(void);
char * tmpnam(char *_s);
...
int main()
{char a;
double b;
a = 'y';
b = 6.41;
printf(" stampo i valori delle variabili %c %e",a,b);
return 0;}
Qui termina l'output /passo2
I puntini di sospensione indicano ulteriore testo che, per brevità non viene copiato. A questo punto il compito del preprocessore termina, non essendo presenti nel codice ulteriori direttive. Ora può iniziare l'azione del compilatore. I passi che qui, per comodità, sono stati analizzati separatamente vengono in realtà effettuati contemporaneamente .
Grazie a questo esempio possiamo perciò capire che, grazie al preprocessore, il compilatore, durante il controllo della sintassi del codice C (file sorgente) preparato dall'utente si trova automaticamente le dichiarazioni di tutte le funzioni utilizzate all'interno del file sorgente. E' chiaro che è compito dell'utente inserire i file di intestazione appropriati per fare in modo che il preprocessore inserisca le dichiarazioni di TUTTE le funzioni (in modo tale che poi, il compilatore, possa correttamente interpretare il codice).
Il mancato inserimento di tutti i file di intestazione necessari al corretto funzionamento del sorgente fa sì che il compilatore produca il seguente messaggio d'errore:
implicit declaration of nome_funzione
NOTA : la dichiarazione di una funzione deve essere effettuata PRIMA di utilizzare la funzione stessa all'interno del file sorgente; quindi, in linea di principio, basterebbe inserire la direttiva #include appena prima di utilizzare una delle funzioni dichiarate nel file che si intende includere.
Ad esempio il codice che segue è sintatticamente corretto, ma nel caso in cui il sorgente cominci a superare il centinaio di righe l'inserimento di direttive in più punti del programma rischia di rendere poco leggibile il codice.
#include <stdio.h>
int main()
{char a;
double b;
a = 'y';
b = 6.41;
#include <stdio.h>
printf(" stampo i valori delle variabili %c %e",a,b);
return 0;}
Per visualizzare il risultato dell'azione del preprocessore, inserite l'opzione -E sulla linea di comando.
gcc -Wall -E -o prova nomefile.c
|