Come dividere un progetto in più files

Nei progetti di una certa grandezza è improponibile pretendere di poter scrivere tutto sullo stesso file sorgente.
Vediamo perciò come è possibile suddividere un progetto C++ in più files sorgenti.
La regola è quella di mantenere nel file sorgente main.cpp solo la funzione main(), mentre le altre funzioni (o classi, quando impareremo la programmazione ad oggetti) andranno su altri files sorgenti.
Infine, per utilizzare tali funzioni all'interno del nostro main() basterà includere i files sorgenti di queste funzioni.

Facciamo un semplice esempio di una funzione che stampa a schermo la scritta "hello world".
Creiamo innanzitutto il nostro progetto, all'interno del quale inseriremo tre files: main.cpp, helloworld.cpp ed helloworld.h

Il file con estensione .cpp (helloworld.cpp), conterrà la definizione della funzione:
#include <iostream>

void helloworld()
{
using std::cout;

cout << "hello world!";
}
Abbiamo messo la direttiva using all'interno della funzione, in questo modo varrà solo all'interno di essa e non per eventuali altre funzioni all'interno dello stesso files che non hanno bisogno di utilizzare l'istruzione cout.

Il file con estensione .h, detto header, conterrà il prototipo della funzione:

void helloworld();

Infine nel nostro file main.cpp andremo ad includere l'header; questo ci permetterà di utilizzare la funzione helloworld() all'interno del nostro main():
#include "helloworld.h"

int main()
{
helloworld();

return 0;
}
Grazie all'inclusione dell'header helloworld.h siamo in grado di richiamare la funzione helloworld().

Come di sicuro avrete notato, il metodo di inclusione è leggermente diverso da quello utilizzato per includere i files della libreria standard del C++ (ad esempio iostream o string).
Nel nostro caso abbiamo dovuto scrivere anche l'estensione .h del file, e abbiamo utilizzato i doppi apici al posto dei simboli di maggiore e minore.

Questo perché la libreria standard è all'interno di una directory che è riconosciuta dal compilatore, mentre per le librerie di funzioni scritte da noi o da terzi, dobbiamo specificare il nome completo del file e l'eventuale percorso, se questo non si trova nella stessa directory del file in cui è incluso. I doppi apici dicono al compilatore che si tratta di un percorso assoluto, e non della cartella di default contenente i files della standard template library.

Un'altra cosa che avrete notato è che il nostro progetto, helloworld, è diviso in due files: l'header che contiene il prototipo della funzione, e il file .cpp che contiene il corpo della funzione.
Perché non inserire tutto in un unico file? Certo, è possibile, ma la suddivisione in header e file delle definizioni ha alcuni vantaggi.
Prima di tutto, in caso di un files che contiene più funzioni, hai il vantaggio di avere un file bello pulito con tutti i prototipi delle varie funzioni, che potrai guardare per trovare il nome della funzione che ti serve, senza dover cercare all'interno del sorgente con tutte le implementazioni delle funzioni.

L'altro vantaggio è che l'interfaccia è separata dall'implementazione.
Questo è un concetto fondamentale nella programmazione ad oggetti, perciò è meglio iniziarlo ad utilizzare anche se per ora siamo limitati ad usare delle semplici funzioni.

Nella programmazione ad oggetti, ogni classe ha un'interfaccia ed un implementazione.
Lo stesso, in modo semplificato, avviene per le funzioni. Infatti come sappiamo una funzione ha un'interfaccia di ingresso (i parametri che ricevono gli argomenti) ed un'interfaccia di uscita (il tipo restituito); una funzione ha inoltre una sua implementazione, e cioé tutto il codice che lavora all'interno della funzione stessa.
Nella programmazione ad oggetti, un programmatore che utilizza una classe deve essere in grado di poterla utilizzare anche senza andare a sbirciare all'interno del codice di implementazione.
Dividendo il progetto in due file, il programmatore può limitarsi a conoscere l'interfaccia della funzione, resa nota dal suo prototipo, senza doversi preoccupare di cosa succede all'interno della funzione stessa, di come lavora e così via.

Ad esempio, se scriviamo una funzione che somma due numeri, il suo prototipo sarà:

int somma(int, int); //funzione che somma due numeri e restituisce il risultato

Al programmatore basterà aprire il file header .h e leggere il prototipo della funzione per capire come utilizzarla, e si risparmierà di dover controllare al suo interno per capire che meccanismo c'è dietro.

E' la metafora della scatola chiusa che abbiamo già visto in passato.
Quando utilizzi ad esempio un forno a microonde non ti preoccupi di controllare i vari fili e resistenze, di consultare il progetto del costruttore, di fare misurazioni interne e così via, ma ti limiti ad usare la sua interfaccia.
Hai delle manopole, inserisci i tuoi dati come la temperatura e il tempo di cottura, dopodiché il forno lavora da solo senza che tu sappia cosa succede al suo interno, sai solo che quando riceverai in uscita l'avviso che la torta è pronta, dovrai toglierla dal forno.

Addirittura esistono alcune librerie commerciali di funzioni, dove l'header .h è un file sorgente, mentre il file che contiene l'implementazione è precompilato in codice macchina (in windows queste librerie hanno l'estensione .dll) e durante la compilazione del nostro progetto il compilatore si limiterà ad inserire nel nostro programma il contenuto della libreria precompilata, senza doverla prima compilare come avviene di solito.

Questo succede perché molte librerie sono frutto del lavoro e della ricerca di aziene per le quali la programmazione è un lavoro, e distribuire in chiaro il proprio codice sorgente significherebbe renderlo disponibile gratis alla concorrenza o a chiunque lo voglia copiare o utilizzare senza pagare.

Ad esempio potrei scrivere una serie di funzioni che eseguono dei calcoli matematici complessi in poco tempo, e voglio che altri programmatori comprino le mie funzioni e le utilizzino all'interno dei loro programmi di calcolo, ma non voglio che vedano come faccio ad eseguire così velocemente quei calcoli, perciò precompilo il mio codice sorgente e loro si limiteranno a leggere l'header che contiene i prototipi delle funzioni, e utilizzeranno le mie funzioni all'interno del proprio progetto senza conoscerne il meccanismo interno.

Un problema che si può presentare quando si divide un progetto in più files è l'inclusione multipla degli headers.
Ad esempio, tornando al nostro ipotetico videogioco, immaginiamo di avere diversi files contenenti le funzioni che gestiscono i vari componenti del gioco: nemici.h, personaggio.h, bonus.h, monete.h, pareti.h, proiettili.h e così via.

Ognuno di questi files, o meglio i rispettivi files .cpp, include al suo interno il file collisioni.h per la gestione delle collisioni con altri oggetti.

Nel momento in cui, nel nostro file main.cpp, includiamo tutti gli header (nemici.h, personaggio.h ecc..), il compilatore compila prima il file main.cpp, dopodiché passa ai files linkati, all'interno dei quali è presente l'inclusione del file collisioni.h.
A questo punto il compilatore tenta di collegare (linkare) all'eseguibile finale il file collisioni.cpp, ed essendo questo presente più volte si ritrova a tentare di includere più volte lo stesso file, con le stesse funzioni, variabili e quant'altro, con il risultato di un errore di dichiarazione multipla.

Per ovviare a questo dobbiamo dire al compilatore di includere un determinato file solo se non è già stato incluso prima. Per farlo utilizziamo delle direttive del preprocessore, includendo queste direttive all'interno di ogni file header.
Ad esempio il nostro file helloworld.h diventerà:

#ifndef HELLOWORLD_H
#define HELLOWORLD_H

void helloworld();

#endif

Le prima due righe vogliono dire, in parole povere: "se il file taldeitali non è stato mai incluso, allora includilo; altrimenti non lo includere ed utilizza quello già incluso in precedenza".

La direttiva #ifndef va letta come "if not defined" e cioè "se non è definito", ed è seguita da un etichetta detta macro e scelta da noi a piacere, anche se di solito viene utilizzato il nome del file tutto in maiuscolo seguito da _H.

La direttiva #define entra definisce il contenuto di quel file sotto l'etichetta da noi scelta, in modo che sia utilizzata come identificativo dal compilatore per capire se il file è già stato incluso.

La direttiva #endif significa fine della definizione e va sempre alla fine del file.

In questo modo se il compilatore verifica che la macro non è già stata definita prima, la definisce. Se invece è già stata definita da una precedente inclusione, il compilatore ignora l'inclusione attuale. In questo modo si risolve il problema delle definizioni multiple.

Nessun commento:

Posta un commento