Direttive al Preprocessore - STB1019/SkullOfSummer GitHub Wiki

Direttive al Preprocessore

Come già visto nelle altre pagine della wiki, il preprocessore è un componente ausiliario al compilatore che rende la fase di scrittura del codice piu agevole ai programmatori. Di fatto permette di direzionare il compilatore verso il risultato desiderato. Sono alla base della macro-programming e si rivelano molto utili, specie all'aumentare della complessità, dimensione e dispersione del codice.

#include

Questa è la direttiva fondamentale che permette di includere file header all'interno del vostro sorgente:

#include <stdio.h>
#include <limits.h>
#include "myHeader.h"

particolare attenzione ai file header non-predefiniti, magari nella stessa cartella del vostro progetto C. I file header sono utilizzati per dichiarare prototipi di funzioni, macro numeriche, logiche o condizionali, allo scopo di rendere più snello il sorgente contenente l'Entry Point, oppure semplicemente per utilizzare codice scritto da altri(ad esempio l'enorme libreria libgc). Se ogni header file contiene dichiarazioni e prototipi, ad ognuno di essi è associato un sorgente .c che le implementa(e che include il vostro sorgente)

//file myHeader.h
#define Number_Three 3
int returnThree();


//file myHeader.c
#include "myHeader.h"
int returnThree()
{
 return Number_Three;
}

Se i concetti di Header e Sorgenti sono diversi agli occhi di un programmatore, non lo sono dal punto di vista del compilatore dato che è una pura convenzione; il preprocessore si limita a un copy-paste del contenuto degli header prima della compilazione effettiva. Attenzione alla sintassi della direttiva:

  • nel caso di #include <stdio.h> GCC cercherà l'header specificato nelle cartelle di sistema (su bash è disponibile il comando locate stdio.h, solitamente /usr/include) e, nel caso in cui non lo trovi, cercherà nei percorsi include definiti dalla flag -I e nella CWD;
  • #include "myHeader.h" cercherà invece direttamente nella working directory e nei percorsi definiti da -I;

#define

Sono presenti esempi della direttiva #define nelle altre pagine della wiki, un breve ripasso:

#define PROGRAM int main(){ printf("Hello World");}

Quando questa direttiva viene richiamata in un sorgente, il preprocessore si occuperà di sostituire il contenuto al nome richiamato. Di seguito un esempio di una macro con parametri:

#define SQUARE(x) ((x)*(x))

Di fondamentale importanza l'attenzione all'uso, se si dovesse sostituire

#define MAX(x,y) x>y?x:y;

in una condizione if

...
if(MAX(var1,var2)>57)
...

si incapperebbe in un errore di compilazione.

Come visto le direttive iniziano sempre per "#", inoltre terminano alla prima sequenza "\n" trovata anche se è possibile espanderle su più linee mettendo a fine riga un carattere "\"

#undef

Può presentarsi l'esigenza di eliminare o ridefinire una macro:

#undef MYMACRO
#define MYMACRO 59

Questo esempio autoesplicativo ridefinisce la MYMACRO come 59

Compilazione condizionale

Altri esempi di direttive sono quelle che permettono la compilazione condizionale: può infatti presentarsi l'esigenza di compilare il proprio programma sotto determinate condizioni ad esempio la presenza o meno di macro dichiarate o altre dipendenze specifiche. #ifndef, #if e le rispettive #else, #elif (else if) sono specifiche per questo compito.

#ifdef(MYMACRO)
 #undef MYMACRO
 #define MYMACRO 59
#else
 #define MYMACRO 59
#endif

Quando la condizione all'interno della direttiva non si verifica, il preprocessore informa il compilatore di saltare il blocco di codice che segue la direttiva fino ad #endif. Un'altro esempio per verificare che una macro sia definita usando la direttiva #define è

#if !defined (MYMACRO)
 #define MYMACRO "Now I'm Defined!"
#endif

Guardie

Si supponga di definire uno o più header gerarchicamente o logicamente conessi fra loro come:

//file myheader.h
#define NUMBER 59

//file anotherHeader.h
#include "myheader.h"
Int aFunc();

//file anotherHeader.c che implementa aFunc()

//file mySource.c
#include "myheader.h"
#include "anotherHeader.h"

si genera un errore di compilazione dovuto al fatto che l'header myHeader è stato incluso due volte. L' header myHeader.h qui sopra deve essere corretto in:

#ifndef MYHEADER_H
 #define MYHEADER_H
//corpo di myHeader.h
#endif

che è un esempio di quelle che in C sono dette guardie, pattern utile per evitare questa condizione di copia multipla. Nello specifico la guardia testa con un ifndef la definizione di una macro che verrà definita alla prima inclusione di myHeader.h . Se tutta la definizione di myHeader è racchiusa nel blocco #ifndef, quando verrà incluso anotherHeader.h, il preprocessore skipperà automaticamente fino al successivo #endif, evitando appunto di copiare due volte lo stesso header.

Direttive Pragma

Le direttive #PRAGMA definiscono azioni specifiche per il compilatore, come ad esempio

#pragma startup __attribute__((constructor)) fun1;
#pragma exit __attribute__((constructor)) fun2;

void fun1();
void fun2();

La prima funzione verrà eseguita per prima all'avvio del programma(ad esempio un prompt in caso argv sia null), la seconda verrà eseguita prima del termine del programma,ad esempio per assicurare un salvataggio.

la notazione _ _ attribute _ _((constructor)) è necessaria per compilare con GCC, ma vi sono alcune #PRAGMA supportate su più compilatori, per esempio

#PRAGMA once

che è l'equivalente di una guardia, supportata su Clang(Apple) GCC e Pelles(Windows). Per altre, come le due sopra, é necessaria una adeguata sintassi rintracciabile nelle documentazioni ufficiali del proprio compilatore.

GCC: insight del flag -E

Come già visto nelle altre sezioni della wiki è possibile richiamare il proprio compilatore per visionare l'output del preprocessore: nel caso di gcc

gcc -E mySource.c >> preprocessorOutput__mySource.c

restituisce il sorgente c che verrà usato in fase di compilazione, con tutte le macro sostituite e gli header inclusi(in dipendenza dagli header aggiunti potrebbe risultare più o meno complicato, si provi a includere <stdio.h>)

Di seguito un semplice esempio completo:

//file header.h
#ifndef number    
#define number 23
int returnThree();


//file headerImplementation.c
#include "header.h"
int returnThree(){return 3;}

//file aSimpleProgram.c
#include "header.h"
void main(){return number+returnThree();}

il comando da eseguire è

gcc -E aSimpleProgram.c headerImplementation.c

e l'output sarà

# 1 "aSimpleProgram.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "aSimpleProgram.c"
# 1 "header.h" 1


int returnThree();
# 2 "aSimpleProgram.c" 2

void main()
{
 return 23 +returnThree();
}
# 1 "headerImplementation.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "headerImplementation.c"
# 1 "header.h" 1


int returnThree();
# 2 "headerImplementation.c" 2

int returnThree()
{
 return 3;
}

Come esercizio,si consiglia di leggere l'output del preprocessore del classico programma helloWorld.

Macro Predefinite, direttiva #error e #line

Vi sono alcune macro predefinite utilizzabili per una futura verifica di informazioni: ad esempio

__FILE__

ritorna il nome del file sorgente da cui è stato creato il programma.

__TIME__
__DATE__

ritornano data (formato MMM:DD:YYYY) e ora (formato HH:MM:SS) di traduzione e

__LINE__

la presunta linea di codice eseguita in quel momento. Infine sono disponibili alcune direttive che ritornano la costante "1" in base al tipo di standard utilizzato. La macro

__STDC__

è definita "1" se il compilatore stà utilizzando lo standard ANSI. Per una lista esaustiva, nella sezione Fonti è presente lo standard ISO/IEC 9899 e la sezione interessata.

Continuando la direttiva

#error @msg

stampa un messaggio (@msg) su stderr, viene utilizzata in fase di debug, o in generale per segnalare errori in punti del programma. Quando il preprocessore incontra questa direttiva, stoppa il processo di preprocessamento e stampa il messaggio, cui forma e stile dipendono dal compilatore utilizzato

Infine la direttiva

#line 45

forza l'espansione della macro predefinita __LINE__ al valore 45. Questa direttiva è usata nel caso in cui si voglia un maggior controllo sul prompting dei messaggi di errore, oppure nel caso in cui si voglia modificare il punto di copia di un'header nell'output preprocessato(ad esempio se si necessita che firstHeader venga copiato dopo secondHeader si dovrà aggiungere in un punto opportuno la #line.

Fonti

https://www.tutorialspoint.com/cprogramming/c_preprocessors.htm;

https://stackoverflow.com/questions/5904530/how-do-header-and-source-files-in-c-work

https://stackoverflow.com/a/21276389/1887602

Sezione 6.10: preprocessor directives http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

⚠️ **GitHub.com Fallback** ⚠️