La funzione main()
Come gia` precedentemente accennato, anche il corpo di
un programma C/C++ e` modellato come una funzione. Tale funzione ha un nome
predefinito, main, e viene invocata automaticamente dal sistema quando
il programma viene eseguito.
Per adesso possiamo dire che la struttura di un programma e` sostanzialmente la
seguente:
< Dichiarazioni globali e funzioni >
int main(int argc, char* argv[ ]) {
< Corpo della funzione >
}
Un programma e` dunque costituito da un insieme (eventualmente vuoto) di
dichiarazioni (ed eventualmente di definizioni) globali di costanti,
variabili... ed un insieme di dichiarazioni (ed eventualmente definizioni)
di funzioni (che non possono essere dichiarate e/o definite localmente ad
altre funzioni); infine il corpo del programma e` costituito dalla funzione
main, il cui prototipo per esteso e` mostrato nello schema
riportato sopra.
Nello schema main ritorna un valore di tipo int (che generalmente e`
utilizzato per comunicare al sistema operativo la causa della terminazione), ma puo`
essere dichiarata void o in teoria tornare un tipo qualsiasi. Inoltre
main puo` accettare opzionalmente due parametri: il primo e` di tipo
int e indica il numero di parametri presenti sulla riga di comando attraverso
cui e` stato eseguito il programma; il secondo parametro (si comprendera` in seguito)
e` un array di stringhe terminate da zero (puntatori a caratteri) contenente i
parametri, il primo dei quali (argv[0]) e` il nome del programma come
riportato sulla riga di comando.
#include < iostream.h >
void main(int argc, char * argv[ ]) {
cout << "Riga di comando: " << endl;
cout << argv[0] << endl;
for(int i=1; i < argc; ++i)
cout << "Parametro " << i << " = " << argv[i] << endl;
}
Il precedente esempio mostra come accedere ai parametri passati sulla riga di
comando; si provi a compilare e ad eseguirlo specificando un numero qualsiasi di
parametri, l'output dovrebbe essere simile a:
> test a b c d // questa e` la riga di comando
Riga di comando: TEST.EXE
Parametro 1 = a
Parametro 2 = b
Parametro 3 = c
Parametro 4 = d
Funzioni inline
Le funzioni consentono di scomporre in piu` parti un
grosso programma facilitandone la realizzazione (e anche la manutenzione), tuttavia
spesso si e` indotti a rinunciare a tale beneficio perche` l'overhead imposto dalla
chiamata di una funzione e` tale da sconsigliare la realizzazione di piccole
funzioni. Le possibili soluzioni in C erano due:
- Rinunciare alle funzioni piccole, tendendo a scrivere solo poche funzioni
corpose;
- Ricorrere alle macro;
La prima in realta` e` una pseudo-soluzione e porta spesso a programmi
difficili da capire e mantenere perche` in pratica rinuncia ai benefici delle
funzioni; la seconda soluzione invece potrebbe andare bene in C, ma non in C++: una
macro puo` essere vista come una funzione il cui corpo e` sostituito (espanso) dal
preprocessore in luogo di ogni chiamata. Il problema principale e` che questo sistema
rende difficoltoso se non impossibile ogni controllo statico di tipo; in C tutto
sommato cio` non costituisce un grave problema perche` il compilatore C non esegue
controlli di tipo, tuttavia il C++ e` un linguaggio fortemente tipizzato e l'uso di
macro costituisce un grave ostacolo a tali controlli.
Per non rinunciare ai vantaggi forniti dalle (piccole) funzioni e a quelli forniti
da un controllo statico dei tipi, sono state introdotte nel C++ le funzioni
inline.
Quando una funzione viene definita inline il compilatore ne memorizza il
corpo e, quando incontra una chiamata a tale funzione, semplicemente sostituisce
alla chiamata della funzione il corpo; tutto cio` consente di evitare l'overhead
della chiamata e, dato che la cosa e` gestita dal compilatore, permette di eseguire
tutti i controlli statici di tipo.
Se si desidera che una funzione sia espansa inline dal compilatore, occorre
definirla esplicitamente inline:
inline int Sum(int a, int b) {
return a + b;
}
La keyword inline informa il compilatore che si desidera che la funzione
Sum sia espansa inline ad ogni chiamata; tuttavia cio` non vuol dire
che la cosa sia sempre possibile: molti compilatori non sono in grado di espandere
inline qualsiasi funzione, tipicamente le funzioni ricorsive sono molto difficili
da trattare e il mio compilatore non riesce ad esempio a espandere funzioni
contenenti cicli. In questi casi viene generata una normale chiamata di funzione e
al piu` si viene avvisati che la funzione non puo` essere espansa inline.
Si osservi che, per come sono trattate le funzioni inline, non ha senso utilizzare
la keyword inline in un prototipo di funzione perche` il compilatore
necessita del codice contenuto nel corpo della funzione:
inline int Sum(int a, int b);
int Sum(int a, int b) {
return a + b;
}
In questo caso non viene generato alcun errore, ma la parola chiave inline
specificata nel prototipo viene del tutto ignorata; perche` abbia effetto
inline deve essere specificata nella definizione della funzione:
int Sum(int a, int b);
inline int Sum(int a, int b) {
return a + b;
} // Ora e` tutto ok!
Un'altra cosa da tener presente e` che il codice che costituisce una funzione inline
deve essere disponibile prima di ogni uso della funzione, altrimenti il compilatore
non e` in grado di espanderla (non sempre almeno!). Una funzione ordinaria puo`
essere usata anche prima della sua definizione, poiche` e` il linker che si occupa
di risolvere i riferimenti (il linker del C++ lavora in due passate); nel caso delle
funzioni inline, poiche` il lavoro e` svolto dal compilatore (che lavora in una
passata), non e` possibile risolvere correttamente il riferimento.
Una importante conseguenza di tale limitazione e` che una funzione puo` essere
inline solo nell'ambito del file in cui e` definita, se un file riferisce ad una
funzione definita inline in un altro file (come, lo
vedremo piu` avanti), in questo file (il primo) la funzione non potra` essere
espansa; esistono comunque delle soluzioni al problema.
Le funzioni inline consentono quindi di conservare i benefici delle funzioni anche
in quei casi in cui le prestazioni sono fondamentali, bisogna pero` valutare
attentamente la necessita` di rendere inline una funzione, un abuso potrebbe portare
a programmi difficili da compilare (perche` e` necessaria molta
memoria) e voluminosi in termini di dimensioni del file eseguibile.
Overloading delle funzioni
Il termine overloading (da to overload)
significa sovraccaricamento e nel contesto del C++ overloading delle funzioni
indica la possibilita` di attribuire allo stesso nome di funzione piu` significati.
Attribuire piu` significati vuol dire fare in modo che lo stesso nome di funzione
sia in effetti utilizzato per piu` funzioni contemporaneamente.
Un esempio di overloading ci viene dalla matematica, dove con spesso utilizziamo lo
stesso nome di funzione con significati diversi senza starci a pensare troppo, ad
esempio + e` usato sia per indicare la somma sui naturali che quella sui
reali...
Ritorniamo per un attimo alla nostra funzione Sum; per come e` stata
definita, Sum funziona solo sugli interi e non e` possibile
utilizzarla sui float. Quello che vogliamo e` riutilizzare lo stesso nome,
attribuendogli un significato diverso e lasciando al compilatore il compito di
capire quale versione della funzione va utilizzata di volta in volta. Per fare cio`
basta definire piu` volte la stessa funzione:
int Sum(int a, int b); // per sommare due interi...
float Sum(float a, float b); // per sommare due float...
float Sum(float a, int b); // per la somma di un
float Sum(int a, float b); // float e un intero
Nel nostro esempio ci siamo limitati solo a dichiarare piu` volte la funzione
Sum, ogni volta con un significato diverso (uno per ogni possibile
caso di somma in cui possono essere coinvolti, anche contemporaneamente, interi e
reali); e` chiaro che poi da qualche parte deve esserci una definizione
per ciascun prototipo (nel nostro caso tutte le definizioni sono identiche a quella
gia` vista, cambia solo l'intestazione della funzione).
In alcune vecchie versioni del C++ l'intenzione di sovraccaricare una funzione
doveva essere esplicitamente comunicata al compilatore tramite la keyword
overload:
overload Sum; // ora si puo` sovraccaricare Sum:
int Sum(int a, int b); // per sommare due interi...
float Sum(float a, float b); // per sommare due float...
float Sum(float a, int b); // per la somma di un
float Sum(int a, float b); // float e un intero
Comunque si tratta di una pratica obsoleta che non va piu` utilizzata (se possibile!).
Le funzioni sovraccaricate si utilizzano esattamente come le normali funzioni:
#include < iostream.h >
int a = 5;
int Y = 10;
float f = 9.5;
float r = 0.5;
cout << "Sum utilizzata su due interi" << endl;
cout << Sum(a, Y) << endl;
cout << "Sum utilizzata su due float" << endl;
cout << Sum(f, r) << endl;
cout << "Sum utilizzata su un intero e un float" << endl;
cout << Sum(a, f) << endl;
cout << "Sum utilizzata su un float e un intero" << endl;
cout << Sum(r, f) << endl;
E` il compilatore che decide quale versione di Sum utilizzare, in base
ai parametri forniti; infatti e` possibile eseguire l'overloading di una funzione
solo a condizione che la nuova versione differisca dalle precedenti almeno nei tipi
dei parametri (o che questi siano forniti in un ordine diverso, come mostrano le
ultime due definizioni di Sum):
void Foo(int a, float f);
int Foo(int a, float f); // Errore!
int Foo(float f, int a); // Ok!
char Foo(); // Ok!
char Foo(...); // OK!
La seconda dichiarazione e` errata perche`, per scegliere tra la prima e la seconda
versione della funzione, il compilatore si basa unicamente sui tipi dei parametri
che nel nostro caso coincidono; la soluzione e` mostrata con la terza dichiarazione,
ora il compilatore e` in grado di distinguere perche` il primo parametro anzicche`
essere un int e` un float. Infine le ultime due dichiarazioni non sono
in conflitto per via delle regole che il compilatore segue per scegliere
quale funzione applicare; in linea di massima e secondo la loro priorita`:
- Match esatto: se esiste una versione della funzione che richiede
esattamente quel tipo di parametri (i parametri vengono considerati a uno a
uno secondo l'ordine in cui compaiono) o al piu` conversioni banali (tranne
da T* a const T* o a volatile T*, oppure da
T& a const T& o a volatile T&);
- Mach con promozione: si utilizza (se esiste) una versione della
funzione che richieda al piu` promozioni di tipo (ad esempio da int a
long int, oppure da float a double);
- Mach con conversioni standard: si utilizza (se esiste) una versione
della funzione che richieda al piu` conversioni di tipo standard (ad esempio da
int a unsigned int);
- Match con conversioni definite dall'utente: si tenta un matching con una
definizione (se esiste), cercando di utilizzare conversioni di tipo definite
dal programmatore;
- Match con ellissi: si esegue un matching utilizzando (se esiste) una
versione della funzione che accetti un qualsiasi numero e tipo di parametri
(cioe` funzioni nel cui prototipo e` stato utilizzato il simbolo ...);
Se nessuna di queste regole puo` essere applicata, si genera un errore (funzione non
definita!). La piena comprensione di queste regole richiede la conoscenza del
concetto di conversione di tipo per il quale si rimanda
all'appendice A; si accenna inoltre ai tipi puntatore e
reference che saranno trattati nel prossimo capitolo, infine
si fa riferimento alla keyword volatile. Tale keyword serve ad informare il
compilatore che una certa variabile cambia valore in modo aleatorio e che di
conseguenza il suo valore va riletto ogni volta che esso sia richiesto:
volatile int ComPort;
La precedente definizione dice al compilatore che il valore di ComPort
e` fuori dal controllo del programma (ad esempio perche` la variabile e` associata
ad un qualche registro di un dispositivo di I/O).
Il concetto di overloading di funzioni si estende anche agli operatori del
linguaggio, ma questo e` un argomento che riprenderemo piu` avanti.
Pagina precedente - Pagina successiva