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:

  1. Rinunciare alle funzioni piccole, tendendo a scrivere solo poche funzioni corpose;
  2. 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`:

  1. 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&);
  2. 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);
  3. 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);
  4. Match con conversioni definite dall'utente: si tenta un matching con una definizione (se esiste), cercando di utilizzare conversioni di tipo definite dal programmatore;
  5. 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