Quello che e`stato visto fin'ora costituisce sostanzialmente il
sottoinsieme C del C++ (salvo l'overloading, i reference e altre piccole aggiunte),
e` tuttavia sufficiente per poter realizzare un qualsiasi programma.
A questo punto, prima di proseguire, e` necessario soffermarci per esaminare il
funzionamento del linker C++ e vedere come organizzare un grosso progetto in piu`
file separati.
Linkage
Abbiamo gia` visto che ad ogni identificatore e`
associato uno scope e una lifetime, ma gli
identificatori di variabili, costanti e funzioni possiedono anche un linkage.
Per comprendere meglio il concetto e` necessario sapere che in C e in C++ l'unita`
di compilazione e` il file, un programma puo` consistere di piu` file che vengono
compilati separatamente e poi linkati (collegati) insieme per ottenere un file
eseguibile. Quest'ultima operazione e` svolta dal linker e possiamo pensare al
concetto di linkage sostanzialmente come a una sorta di scope dal punto
di vista del linker. Facciamo un esempio:
// File a.cpp
int a = 5;
// File b.cpp
extern int a;
int GetVar() {
return a;
}
Il primo file dichiara una variabile intera e la inizializza, il secondo
(trascuriamone per ora la prima riga di codice) dichiara una funzione che ne
restituisce il valore. La compilazione del primo file non e` un problema, ma nel
secondo file GetVar() deve utilizzare un nome dichiarato
in un altro file; perche` la cosa sia possibile bisogna informare il compilatore che
tale nome e` dichiarato da qualche altra parte e che il riferimento a tale nome deve
essere risolto dal linker (il compilatore non e` in grado di farlo perche` compila
un file alla volta), tale dichiarazione viene effettuata tramite la keyword
extern. In effetti la riga extern int a;
non dichiara un nuovo identificatore, ma dice "La variabile intera a
e` dichiarata da qualche altra parte, lascia solo lo spazio per risolvere il
riferimento".
Se la keyword extern fosse stata ommessa il compilatore avrebbe interpretato
la riga come una nuova dichiarazione e avrebbe risolto il riferimento in
GetVar() in favore di tale definizione; in fase di linking comunque
si sarebbe verificato un errore perche` a sarebbe stata definita
due volte (una per file).
Naturalmente extern si puo` usare anche con le funzioni:
// File a.cpp
int a = 5;
int f(int c) {
return a+c;
}
// File b.cpp
extern int f(int);
int GetVar() {
return f(5);
}
Si noti che e` necessario che extern sia seguita dal prototipo completo della
funzione, al fine di consentire al compilatore di generare codice corretto e di
eseguire i controlli di tipo sui parametri e il valore restituito.
Come gia` detto, il C++ ha un'alta compatibilita` col C, tant'e` che e` possibile
interfacciare codice C++ con codice C; anche in questo caso l'aiuto ci viene dalla
keyword extern. Per poter linkare un modulo C con un modulo C++ e` necessario
indicare al compilatore le nostre intenzioni:
// Contenuto file C++
extern "C" int CFunc(char *);
extern "C" char * CFunc2(int);
// oppure per risparmiare
extern "C" {
void CFunc1(void);
int * CFunc2(int, char);
char * strcpy(char *, const char *);
}
La presenza di "C" serve a indicare che bisogna adottare le
convenzioni del C sulla codifica dei nomi.
Un altro uso di extern e` quello di ritardare la definizione di una variabile
o di una funzione all'interno dello stesso file, ad esempio per realizzare funzioni
mutuamente ricorsive:
extern Func2(int);
int Func1(int c) {
if (c==0) return 1;
return Func2(c-1);
}
int Func2(int c) {
if (c==0) return 2;
return Func1(c-1);
}
I nomi che sono visibili all'esterno di un file (come la variabile a)
sono detti avere linkage esterno; tutte le variabili globali hanno linkage
esterno, cosi` come le funzioni globali non inline; le funzioni inline, tutte
le costanti e le dichiarazioni fatte in un blocco hanno invece linkage interno
(cioe` non sono visibili all'esterno del file); i nomi di tipo non hanno alcun
linkage, ma devono riferire ad una unica definizione:
// File 1.cpp
enum Color { Red, Green, Blue };
extern void f(Color);
// File2.cpp
enum Color { Red, Green, Blue };
void f(Color c) { /* ... */ }
Una situazione di questo tipo e` illecita, ma molti compilatori potrebbero non
accorgersi dell'errore.
Per quanto concerne i nomi di tipo, fanno eccezione quelli definiti tramite
typedef in quanto non sono veri tipi, ma solo abbreviazioni.
E` possibile forzare un identificatore globale ad avere linkage interno utilizzando
la keyword static:
// File a.cpp
static int a = 5; // linkage interno
int f(int c) { // linkage esterno
return a+c;
}
// File b.cpp
extern int f(int);
static int GetVar() { // linkage interno
return f(5);
}
Si faccia attenzione al significato di static: nel caso di variabili locali
static serve a modificarne la lifetime (durata), nel caso di nomi globali
invece modifica il linkage.
L'importanza di poter restringere il linkage e` ovvia; supponete di voler realizzare
una libreria di funzioni, alcune serviranno solo a scopi interni alla libreria e non
serve (anzi e` pericoloso) esportarle, per fare cio` basta dichiarare static
i nomi globali che volete incapsulare.
File header
Purtroppo non esiste un meccanismo analogo alla keyword
static per forzare un linkage esterno, d'altronde i nomi di tipo non hanno
linkage (e devono essere consistenti) e le funzioni inline non possono avere linkage
esterno. Esiste tuttavia un modo per aggirare l'ostacolo: racchiudere tali
dichiarazioni e/o definizioni in un file header (file solitamente con
estensione .h) e poi includere questo nei files che utilizzano tali
dichiarazioni; possiamo anche inserire dichiarazioni e/o definizioni comuni in modo
da non doverle ripetere.
Vediamo come procedere. Supponiamo di avere un certo numero di file che devono
condividere delle costanti, delle definizioni di tipo e delle funzioni inline;
quello che dobbiamo fare e` creare un file contenente tutte queste definizioni:
// Esempio.h
enum Color { Red, Green, Blue };
struct Point {
float X;
float Y;
};
const int Max = 1000;
inline int Sum(int x, int y) {
return x + y;
}
A questo punto basta utilizzare la direttiva #include "NomeFile" nei
moduli che utilizzano le precedenti definizioni:
// Modulo1.cpp
#include "Esempio.h"
/* codice modulo */
La direttiva #include e` gestita dal precompilatore che e` un programma che
esegue delle manipolazioni sul file prima che questo sia compilato; nel nostro caso
la direttiva dice di copiare il contenuto del file specificato nel file che vogliamo
compilare e passare quindi al compilatore il risultato dell'operazione.
In alcuni esempi abbiamo gia` utilizzato la direttiva per poter eseguire
input/output, in quei casi abbiamo utilizzato le parentesi angolari (< >)
al posto dei doppi apici (" "); la differenza e` che utilizzando i
doppi apici dobbiamo specificare (se necessario) il path in cui si trova il file
header, con le parentesi angolari invece il preprocessore cerca il file in un
insieme di directory predefinite.
Un file header puo` contenere in generale qualsiasi istruzione C/C++, in particolare
anche dichiarazioni extern da condividere tra piu` moduli:
// Esempio2.h
// dichiarazioni extern comuni ai moduli
extern int a;
extern double * Ptr;
extern void Func();
extern "C" {
int CFunc1(int, float);
void CFunc2(char *);
}
Librerie di funzioni
L'uso dei file header visto prima e` molto utile 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);
/* altre funzioni */
ed ecco come sarebbe il file che la implementa:
// Date.cpp
#include < Date.h >
#include < iostream.h >
void PrintDate(Date dt) {
cout << dt.dd << '/' << dt.mm << '/' << dt.yy;
cout << " " << dt.h << ':' << dt.m;
cout << ':' << dt.s;
}
/* implementazione di altre funzioni */
A questo punto la libreria e` pronta, per distribuirla basta compilare il file
Date.cpp e fornire il file oggetto ottenuto insieme al 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!
C'e` tuttavia un problema illustrato nel seguente esempio:
// Modulo1.h
#include < iostream.h >
/* altre dichiarazioni */
// Modulo2.h
#include < iostream.h >
/* altre dichiarazioni */
// Main.cpp
#include < iostream.h >
#include < Modulo1.h >
#include < Modulo2.h >
void main() { /* 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.h) 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` 1 se
SIMBOLO non e` definito, 0 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 restituisce 1 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.h):
// 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 restituisce 1) 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.h >
e il file header specificato viene elaborato per produrre codice;
- A seguito delle direttive contenute inizialmente in iostream.h,
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.h > contenuta in
Modulo1.h porta il precompilatore ad elaborare di nuovo
iostream.h, 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.cpp 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.
Pagina precedente - Pagina successiva