Librerie di funzioni
I file header sono molto utili quando si vuole partizionare un programma in
piu` moduli, tuttavia la potenza dei file header si esprime meglio quando si
vuole realizzare una libreria di funzioni.
L'idea e` quella di separare l'interfaccia della libreria dalla sua implementazione:
nel file header vengono dichiarati (ed eventualmente definiti) gli identificatori
che devono essere visibili anche a chi usa la libreria (costanti, funzioni,
tipi...), tutto cio` che e` privato (implementazione di funzioni non inline,
variabili...) viene invece messo in un altro file che include l'interfaccia.
Vediamo un esempio di semplicissima libreria per gestire date (l'esempio vuole
essere solo didattico); ecco il file header:
// Date.h
struct Date {
unsigned short dd; // giorno
unsigned short mm; // mese
unsigned yy; //
anno
unsigned short h; // ora
unsigned short m; // minuti
unsigned short s; // secondi
};
void PrintDate(Date);
ed ecco come sarebbe il file che la implementa:
// Date.cpp
#include "Date.h"
#include < iostream >
using namespace std;
void PrintDate(Date dt) {
cout << dt.dd << '/' << dt.mm << '/' <<
dt.yy;
cout << " " << dt.h << ':' << dt.m;
cout << ':' << dt.s;
}
A questo punto la libreria e` pronta, per distribuirla basta compilare il file
Date.cpp e fornire il file oggetto ottenuto ed il file header Date.h .
Chi deve utilizzare la libreria non dovra` far altro che includere nel proprio
programma il file header e linkarlo al file oggetto contenente le funzioni di
libreria. Semplicissimo!
Esistono tuttavia due problemi, il primo e` illustrato nel seguente esempio:
// Modulo1.h
#include < iostream >
using namespace std;
/* altre dichiarazioni */
// Modulo2.h
#include < iostream >
using namespace std;
/* altre dichiarazioni */
// Main.cpp
#include < iostream >
using namespace std;
#include < Modulo1.h >
#include < Modulo2.h >
int main(int, char* []) {
/* codice funzione */
}
Si tratta cioe` di un programma costituito da piu` moduli, quello principale
che contiene la funzione main() e altri che implementano le varie
routine necessarie. Piu` moduli hanno bisogno di una stessa libreria, in particolare
hanno bisogno di includere lo stesso file header (nell'esempio iostream) nei
rispettivi file header.
Per come funziona il preprocessore, poiche` il file principale include (direttamente
e/o indirettamente) piu` volte lo stesso file header, il file che verra` effettivamente
compilato conterra` piu` volte le stesse dichiarazioni (e definizioni) che daranno
luogo a errori di definizione ripetuta dello stesso oggetto (funzione, costante,
tipo...). Come ovviare al problema?
La soluzione ci e` fornita dal precompilatore stesso ed e` nota come compilazione
condizionale; consiste cioe` nello specificare quando includere o meno determinate
porzioni di codice. Per far cio` ci si avvale delle direttive #define
SIMBOLO , #ifndef SIMBOLO e #endif : la prima
ci permette di definire un simbolo, la seconda e` come l'istruzione condizionale
e serve a testare un simbolo (la risposta e` positiva se SIMBOLO
non e` definito, negativa altrimenti), l'ultima direttiva serve a capire dove
finisce l'effetto della direttiva condizionale. Le ultime due direttive sono
utilizzate per delimitare porzioni di codice; se #ifndef e verificata
il preprocessore lascia passare il codice (ed esegue eventuali direttive) tra
l'#ifndef e #endif , altrimenti quella porzione di
codice viene nascosta al compilatore.
Ecco come tali direttive sono utilizzate (l'errore era dovuto all'inclusione
multipla di iostream):
// Contenuto del file iostream.h
#ifndef __IOSTREAM_H
#define __IOSTREAM_H
/* contenuto file header */
#endif
si verifica cioe` se un certo simbolo e` stato definito, se non lo e` (cioe`
#ifndef e` verificata) si definisce il simbolo e poi si inserisce
il codice C/C++, alla fine si inserisce l'#endif . Ritornando all'esempio,
ecco cio` che succede quando si compila il file Main.cpp :
- Il preprocessore inizia a elaborare il file per produrre un unico file compilabile;
- Viene incontrata la direttiva
#include < iostream > e
il file header specificato viene elaborato per produrre codice;
- A seguito delle direttive contenute inizialmente in iostream, viene definito
il simbolo
__IOSTREAM_H e prodotto il codice contenuto tra #ifndef
__IOSTREAM_H e #endif ;
- Si ritorna al file
Main.cpp e il precompilatore incontra #include
< Modulo1.h > e quindi va ad elaborare Modulo1.h ;
- La direttiva
#include < iostream > contenuta in Modulo1.h
porta il precompilatore ad elaborare di nuovo iostream, ma questa volta il
simbolo __IOSTREAM_H e` definito e quindi #ifndef __IOSTREAM_H
fa si` che nessun codice venga prodotto;
- Si prosegue l'elaborazione di
Modulo1.h e viene generato l'eventuale
codice;
- Finita l'elaborazione di
Modulo1.h , la direttiva #include
< Modulo2.h > porta all'elaborazione di Modulo2.h
che e` analoga a quella di Modulo1.h ;
- Elaborato anche
Modulo2.h , rimane la funzione main()
di Main.cp p che produce il corrispondente codice;
- Alla fine il precompilatore ha prodotto un unico file contenete tutto il
codice di
Modulo1.h , Modulo2.h e Main.cpp
senza alcuna duplicazione e contenente tutte le dichiarazioni e le definizioni
necessarie;
- Il file prodotto dal precompilatore e` passato al compilatore per la produzione
di codice oggetto;
Utilizzando il metodo appena previsto in tutti i file header (in particolare
quelli di libreria) si puo` star sicuri che non ci saranno problemi di inclusione
multipla. Tutto il meccanismo richiede pero` che i simboli definiti con la direttiva
#define siano unici.
|