informatica |
Quello che è stato visto finora costituisce sostanzialmente il sottoinsieme C del C++ (salvo l'overloading, i reference e altre piccole aggiunte), è tuttavia sufficiente per poter realizzare un qualsiasi programma.
A questo punto, prima di proseguire, è necessario soffermarci per esaminare il funzionamento del linker C++ e vedere come organizzare un grosso progetto in più file separati.
Abbiamo già visto che ad ogni identificatore è associato uno scope e una lifetime, ma gli identificatori di
variabili, costanti e funzioni possiedono anche un linkage.
Per comprendere meglio il concetto è necessario sapere che in C e in C++ l'unità di compilazione è il file, un programma può consistere di più file che vengono compilati separatamente e poi linkati (collegati) insieme per ottenere un file eseguibile. Quest'ultima operazione è 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()
Il primo file dichiara una variabile intera e la inizializza, il secondo (trascuriamo per ora la prima riga di codice) dichiara una funzione che ne restituisce il valore. La compilazione del primo file non è un problema, ma nel secondo file GetVar() deve utilizzare un nome dichiarato in un altro file; perché la cosa sia possibile bisogna informare il compilatore che tale nome è dichiarato da qualche altra parte e che il riferimento a tale nome deve essere risolto dal linker (il compilatore non è in grado di farlo perché 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 è dichiarata da qualche
altra parte, lascia solo lo spazio per risolvere il riferimento'.
Se la keyword extern fosse stata omessa 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 perché a sarebbe stata definita due volte (una per file).
Naturalmente extern si può usare anche con le funzioni:
// File a.cpp
int a = 5;
int f(int c)
// File b.cpp
extern int f(int);
int GetVar()
Si noti che è 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 già detto, il C++ ha un'alta compatibilità col C, tant'è che è 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++ è 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'
La presenza di 'C' serve a indicare che bisogna adottare le convenzioni del C sulla codifica dei nomi.
Un altro uso di extern è 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)
int Func2(int c)
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, così come le funzioni globali non inline (che per il loro funzionamento richiedono che il codice sorgente sia sempre disponibile); le funzioni inline, tutte le costanti e le dichiarazioni fatte in un blocco hanno invece linkage interno (cioè 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 ;
extern void f(Color);
// File2.cpp
enum Color ;
void f(Color c)
Una situazione di questo tipo è 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.
È' possibile forzare un identificatore globale e forzarlo ad avere linkage interno utilizzando la keyword static:
// File a.cpp
static int a = 5; // linkage interno
int f(int c)
// File b.cpp
extern int f(int);
static int GetVar()
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 è ovvia; supponete di voler realizzare una libreria di funzioni, alcune serviranno solo a scopi interni alla libreria e non serve (anzi è pericoloso) esportarle, per fare ciò basta dichiarare static i nomi globali che volete incapsulare.
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 è creare un file contenente tutte queste definizioni:
// Esempio.h
enum Color ;
struct Point ;
const int Max = 1000;
inline int Sum(int x, int 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 è gestita dal precompilatore che è 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 già utilizzato la direttiva per poter eseguire input/output, in quei casi abbiamo utilizzato le parentesi angolari (<>) al posto dei doppi apici (' '); la differenza è 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 può contenere in generale qualsiasi istruzione C/C++, in particolare anche dichiarazioni extern da condividere tra più moduli:
// Esempio2.h
// dichiarazioni extern comuni ai moduli
extern int a;
extern double * Ptr;
extern void Func();
extern 'C'
L'uso dei file header visto prima è molto utile quando si vuole partizionare un programma in più moduli, tuttavia la potenza dei file header si esprime meglio quando si vuole realizzare una libreria di funzioni.
L'idea è 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 ciò che è 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 ;
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)
/* implementazione di altre funzioni */
A questo punto la libreria è 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 dovrà far altro che includere nel proprio programma il file header e linkarlo al file oggetto contenente le funzioni di libreria. Semplicissimo!
C'è 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()
Si tratta cioè di un programma costituito da più moduli, quello principale che contiene la funzione main() e altri che implementano le varie routine necessarie. Più 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, poiché il file principale include (direttamente e/o indirettamente) più volte lo stesso file header, il file che verrà effettivamente compilato conterrà più 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 è fornita dal precompilatore stesso ed è nota come compilazione condizionale; consiste cioè nello specificare quando includere o meno determinate porzioni di codice. Per far ciò ci si avvale delle direttive #define SIMBOLO, #ifndef SIMBOLO e #endif: la prima ci permette di definire un simbolo, la seconda è come l'istruzione condizionale e serve a testare un simbolo (la risposta è 1 se SIMBOLO non è 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 cioè se un certo simbolo è stato definito, se non lo è (cioè #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 ciò 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 è definito e quindi #ifndef __IOSTREAM_H fa sì 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 è 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 è passato al compilatore per la produzione di codice oggetto;
Utilizzando il metodo appena visto in tutti i file header (in particolare quelli di libreria) si può star sicuri che non ci saranno problemi di inclusione multipla. Tutto il meccanismo richiede però che i simboli definiti con la direttiva #define siano unici.
Privacy
|
© ePerTutti.com : tutti i diritti riservati
:::::
Condizioni Generali - Invia - Contatta