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 e di definizioni globali di costanti,
variabili... ed un insieme di dichiarazioni e 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). I vecchi compilatori non standard spesso
lasciavano ampia liberta` circa il prototipo di main,
alcuni consentivano di dichiararla void, ora a norma di
standard main deve avere tipo int e se nel corpo
della funzione non viene inserito esplicitamente una istruzione
return, il compilatore inserisce automaticamente una
return 0;.
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 >
using namespace std;
int 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;
return 0;
}
|
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 sia la realizzazione che la
successiva 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 ogni
controllo statico di tipo poiche` gli errori divengono evidenti solo
quando la macro viene espansa; in C tutto sommato cio` non costituisce un
grave problema perche` il C non e` fortemente tipizzato.
Al contrario il C++ possiede un rigido sistema di tipi e l'uso di
macro costituisce un grave ostacolo allo sfruttamento di tale caratteristica. Esistono poi altri svantaggi nell'uso delle macro:
rendono difficile il debugging e non sono flessibili come le funzioni
(e` ad esempio difficile rendere fattibili macro ricorsive).
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 lo sostituisce alla chiamata della funzione; 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 comunque la cosa generalmente non e` grave, poiche` un ciclo
tipicamente richiede una quantita` di tempo ben maggiore di quello
necessario a chiamare la funzione, per cui l'espansione inline non
avrebbe portato grossi benefici.
Quando l'espansione inline della funzione non e` possibile solitamente
si viene avvisati da una warning.
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);
float Sum(float a, float b);
float Sum(float a, int b);
float Sum(int a, float b);
|
Comunque si tratta di una pratica obsoleta che infatti non e` prevista
nello standard.
Le funzioni sovraccaricate si utilizzano esattamente come le normali
funzioni:
|
#include < iostream >
using namespace std;
/* Dichiarazione ed implementazione delle varie Sum */
int main(int, char* []) {
int a = 5;
int y = 10;
float f = 9.5;
float r = 0.5;
cout << "Sum(int, int):" << endl;
cout << " " << Sum(a, y) << endl;
cout << "Sum(float, float):" << endl;
cout << " " << Sum(f, r) << endl;
cout << "Sum(int, float):" << endl;
cout << " " << Sum(a, f) << endl;
cout << "Sum(float, int):" << endl;
cout << " " << Sum(r, a) << endl;
return 0;
}
|
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 viste sopra):
|
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
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|