Programmare in C++

« Older   Newer »
 
  Share  
.
  1. Molko92
     
    .

    User deleted


    Come e' facile intuire, il linguaggio C++ è un'estensione del linguaggio C. In particolare, il C++ conserva tutti i punti di forza del C, come la potenza e la flessibilità di gestione dell'interfaccia hardware e software, la possibilità di programmare a basso livello e l'efficienza, l'economia e le espressioni, tipiche del C. Ma, in più, il C++ introduce il dinamico mondo della programmazione orientata agli oggetti che rende tale linguaggio una piattaforma ideale per l'astrazione dei problemi di alto livello.

    Il C++ fonde, quindi, i costrutti tipici dei linguaggi procedurali standard, familiari per molti programmatori, con il modello di programmazione orientata agli oggetti, che può essere pienamente sfruttato per produrre soluzioni completamente orientate agli oggetti di un determinato problema. In pratica, una applicazione C++ riflette questa dualità incorporando sia il modello di programmazinoe procedurale che il modello di programmazione orientato agli oggetti.

    Questa guida al C++ si rivolge a chi si volge allo straordinario mondo della programmazione per la prima volta o, anche, a chi ha desiderio di rispolverare qualche concetto non troppo chiaro. L'obiettivo, in ogni caso, non è certamente quello di fornire una descrizione approfondita di tutte le potenzialità della programmazione ad oggetti ma, più semplicemente, quello di fornire al programmatore una panoramica del C++ che lo renda presto in grado di scrivere applicazioni funzionanti

    Ogni programma scritto in un qualsiasi linguaggio di programmazione prima di essere eseguito viene sottoposto ad un processo di compilazione o interpretazione (a seconda che si usi un compilatore o un interprete). Lo scopo di questo processo e` quello di tradurre il programma originale (codice sorgente) in uno semanticamente equivalente, ma eseguibile su una certa macchina. Il processo di compilazione e` suddiviso in piu` fasi, ciascuna delle quali volta all'acquisizione di opportune informazioni necessarie alla fase successiva.
    La prima di queste fasi e` nota come analisi lessicale ed ha il compito di riconoscere gli elementi costitutivi del linguaggio sorgente, individuandone anche la categoria lessicale. Ogni linguaggio prevede un certo numero di categorie lessicali e in C++ possiamo distinguere in particolare le seguenti categorie:

    Commenti;
    Identificatori;
    Parole riservate;
    Costanti letterali;
    Segni di punteggiatura e operatori;
    Analiziamole piu` in dettaglio.



    Commenti
    I commenti, come in qualsiasi altro linguaggio, hanno valore soltanto per il programmatore e vengono ignorati dal compilatore. E` possibile inserirli nel proprio codice in due modi diversi:
    secondo lo stile C ovvero racchiudendoli tra i simboli /* e */
    facendoli precedere dal simbolo //
    Nel primo caso e` considerato commento tutto quello che e` compreso tra /* e */, il commento quindi si puo` estendere anche su piu` righe o trovarsi in mezzo al codice:


    void Func() {
    ...
    int a = 5; /* questo e` un commento diviso su piu` righe */
    a = 4 /* commento */ + 5;
    ...
    }


    Nel secondo caso, proprio del C++, e` invece considerato commento tutto cio` che segue // fino alla fine della linea, ne consegue che non e` possibile inserirlo in mezzo al codice o dividerlo su piu` righe (a meno che anche l'altra riga non cominci con //):



    void Func() {
    ...
    int a = 5; // questo e` un commento valido
    a = 4 // Errore! il "+ 5;" e` commento + 5;
    e non e` possibile dividerlo su piu` righe
    ...
    }


    Benche` esistano due distinti metodi per commentare il codice, non e` possibile avere commenti annidati, il primo simbolo tra // e /* determina il tipo di commento che l'analizzatore lessicale si aspetta. Bisogna anche ricordare di separare sempre i caratteri di inizio commento dall'operatore di divisione (simbolo /):



    a + c //* commento */ su
    una sola riga


    Tutto cio` che segue "a + c" viene interpretato come un commento iniziato da //, e` necessario inserire uno spazio tra / e /*.



    Identificatori
    Gli identificatori sono simboli definiti dal programmatore per riferirsi a cinque diverse categorie di oggetti:

    Variabili;
    Costanti simboliche;
    Etichette;
    Tipi definiti dal programmatore;
    Funzioni;
    Le variabili sono contenitori di valori di un qualche tipo; ogni variabile puo` contenere un singolo valore che puo` cambiare nel tempo, il tipo di questo valore viene comunque stabilito una volta per tutte e non puo` cambiare.
    Le costanti simboliche servono ad identificare valori che non cambiano nel tempo, non possono essere considerate dei contenitori, ma solo un nome per un valore.
    Una etichetta e` un nome il cui compito e` quello di identificare una istruzione del programma e sono utilizzate dall'istruzione di salto incondizionato goto.
    Un tipo invece, come vedremo meglio in seguito, identifica un insieme di valori e di operazioni definite su questi valori; ogni linguaggio (o quasi) fornisce un certo numero di tipi primitivi (cui e` associato un identificatore predefinito) e dei meccanismi per permettere la costruzione di nuovi tipi (a cui il programmatore deve poter associare un nome) a partire da questi.
    Infine, funzione e` il termine che il C++ utilizza per indicare i sottoprogrammi.
    In effetti potremmo considerare una sesta categoria di identificatori, gli identificatori di macro; una macro e` sostanzialmente un alias per un frammento di codice. Le macro comunque, come vedremo in seguito, non sono trattate dal compilatore ma da un precompilatore che si occupa di eseguire alcune elaborazioni sul codice sorgente prima che questo venga effettivamente sottoposto a compilazione.
    Parleremo comunque con maggior dettaglio di variabili, costanti, etichette, tipi, funzioni e macro in seguito.
    Un identificatore deve iniziare con una lettera o con carattere di underscore (_) che possono essere seguiti da un numero qualsiasi di lettere, cifre o underscore; viene fatta distinzione tra lettere maiuscole e lettere minuscole. Tutti gli identificatori presenti in un programma devono essere diversi tra loro, indipendentemente dalla categoria cui appartengono.
    Benche` il linguaggio non preveda un limite alla lunghezza massima di un identificatore, e` praticamente impossibile non imporre un limite al numero di caratteri considerati significativi, per cui ogni compilatore distingue gli identificatori in base a un certo numero di caratteri iniziali tralasciando i restanti; il numero di caratteri considerati significativi varia comunque da sistema a sistema.



    Parole riservate
    Ogni linguaggio si riserva delle parole chiave (keywords) il cui significato e` prestabilito e che non possono essere utilizzate dal programmatore come identificatori. Il C++ non fa eccezione:


    asm auto bool break
    case catch char class
    const continue const_cast default
    delete do double dynamic_cast
    else enum explicit extern
    false float for friend
    goto if inline int
    long mutable namespace new
    operator private protected public
    register reinterpret_cast return short
    signed sizeof static static_cast
    struct switch template this
    trow true try typedef
    typeid typename union unsigned
    using virtual void volatile
    wchar_t while




    Sono inoltre considerate parole chiave tutte quelle che iniziano con un doppio underscore __; esse sono riservate per le implementazioni del linguaggio e per le librerie standard, e il loro uso da parte del programmatore dovrebbe essere evitato in quanto non sono portabili.


    Costanti letterali
    All'interno delle espressioni e` possibile inserire direttamente dei valori, questi valori sono detti costanti letterali. La generica costante letterale puo` essere un carattere racchiuso tra apice singolo, una stringa racchiusa tra doppi apici, un intero o un numero in virgola mobile.


    'a' // Costante di tipo carattere
    "a" // Stringa di un carattere
    "abc" // Ancora una stringa


    Un intero puo` essere:

    Una sequenza di cifre decimali, eventualmente con segno;
    Uno 0 (zero) seguito da un intero in ottale (base 8);
    0x o 0X seguito da un intero in esadecimale (base 16);
    Nella rappresentazione in esadecimale, oltre alle cifre decimali, e` consentito l'uso delle lettere da "A" a "F" e da "a" a "f".
    Si noti che un segno puo` essere espresso solo in base 10, negli altri casi esso e` sempre +:


    +45 // Costante intera in base 10,
    055 // in base 8
    0x2D // ed in base 16


    La base in cui viene scritta la costante determina il modo in cui essa viene memorizzata. Il compilatore scegliera` il tipo (Vedi tipi di dato) da utilizzare sulla base delle seguenti regole:

    Base 10:
    il piu` piccolo atto a contenerla tra int, long int e unsigned long int
    Base 8 o 16:
    il piu` piccolo atto a contenerla tra int, unsigned int, long int e unsigned long int
    Si puo` forzare il tipo da utilizzare aggiungendo alla costante un suffisso costituito da u o U, e/o l o L: la lettera U seleziona i tipi unsigned e la L i tipi long; se solo una tra le due lettere viene specificata, viene scelto il piu` piccolo di quelli atti a contenere il valore e selezionati dal programmatore:


    20 // intero in base 10
    024 // 20 in base 8
    0x14 // 20 in base 16
    0x20ul // forza unsigned long
    0x20l // forza long
    0x20u // forza unsigned


    Un valore in virgola mobile e` costituito da:

    Intero decimale, opzionalmente con segno;
    Punto decimale
    Frazione decimale;
    e o E e un intero decimale con segno;
    L'uso della lettera E indica il ricorso alla notazione scientifica.
    E` possibile omettere uno tra l'intero decimale e la frazione decimale, ma non entrambi. E` possibile omettere uno tra il punto decimale e la lettera E (o e) e l'intero decimale con segno, ma non entrambi.
    Il tipo scelto per rappresentare una costante in virgola mobile e` double, se non diversamente specificato utilizzando i suffissi F o f per float, o L o l per long double. Esempi:


    .0 // 0 in virgola mobile
    110E+4 // 110 * 10000 (10 elevato a 4)
    .14e-2 // 0.0014
    -3.5e+3 // -3500.0
    3.5f // forza float
    3.4L // forza long double




    Segni di punteggiatura e operatori
    Alcuni simboli sono utilizzati dal C++ per separare i vari elementi sintattici o lessicali di un programma o come operatori per costruire e manipolare espressioni:


    [ ] ( ) { } + - * % ! ^ &

    = ~ | \ ; ' : " < > ? , .

    Anche le seguenti combinazioni di simboli sono operatori:

    ++ -- -> .* ->* << >> <= >= == != &&

    || += -= *= <<= /= %= &= ^= |= :: >>=


    Inizieremo ad esaminare i costrutti del C++ partendo proprio dalle istruzioni e dalle espressioni, perche` in questo modo sara` piu` semplice esemplificare alcuni concetti che verranno analizzati nel seguito. Per adesso comunque analizzaremo solo le istruzioni per il controllo del flusso e l'assegnamento, le rimanenti (poche) istruzioni verranno discusse via via che sara` necessario nei prossimi capitoli.



    Assegnamento
    Il C++ e` un linguaggio pesantemente basato sul paradigma imperativo, questo vuol dire che un programma C++ e` sostanzialmente una sequenza di assegnamenti di valori a variabili. E` quindi naturale iniziare parlando proprio dell'assegnamento.
    L'operatore di assegnamento e` denotato dal simbolo = (uguale) e viene applicato con la sintassi:

    < lvalue > = < rvalue >;

    Il termine lvalue indica una qualsiasi espressione che riferisca ad una regione di memoria (in generale un identificatore di variabile), mentre un rvalue e` una qualsiasi espressione la cui valutazione produca un valore. Ecco alcuni esempi:

    Pippo = 5;
    Topolino = 'a';
    Clarabella = Pippo;
    Pippo = Pippo + 7;
    Clarabella = 4 + 25;


    Il risultato dell'assegnamento e` il valore prodotto dalla valutazione della parte destra (rvalue) e ha come effetto collaterale l'assegnazione di tale valore alla regione di memoria denotato dalla parte sinistra (lvalue). Cio` ad esempio vuol dire che il primo assegnamento sopra produce come risultato il valore 5 e che dopo tale assegnamento la valutazione della variabile Pippo produrra` tale valore fino a che un nuovo assegnamento non verra` eseguito su di essa.
    Si osservi che una variabile puo` apparire sia a destra che a sinistra di un assegnamento, se tale occorrenza si trova a destra produce il valore contenuto nella variabile, se invece si trova a sinistra essa denota la locazione di memoria cui riferisce. Ancora, poiche` un identificatore di variabile puo` trovarsi contemporaneamente su ambo i lati di un assegnamento e` necessaria una semantica non ambigua: come in qualsiasi linguaggio imperativo (Pascal, Basic, ...) la semantica dell'assegnamento impone che prima si valuti la parte destra e poi si esegua l'assegnamento del valore prodotto all'operando di sinistra.

    Poiche` un assegnamento produce come risultato il valore prodotto dalla valutazione della parte destra (e` cioe` a sua volta una espressione), e` possibile legare in cascata piu` assegnamenti:

    Clarabella = Pippo = 5;

    Essendo l'operatore di assegnamento associativo a destra, l'esempio visto sopra e` da interpretare come

    Clarabella = (Pippo = 5);

    cioe` viene prima assegnato 5 alla variabile Pippo e il risultato di tale assegnamento (il valore 5) viene poi assegnato alla variabile Clarabella.

    Esistono anche altri operatori che hanno come effetto collaterale l'assegnazione di un valore, la maggior parte di essi sono comunque delle utili abbreviazioni, eccone alcuni esempi:

    Pippo += 5; // equivale a Pippo = Pippo + 5;
    Pippo -= 10; // equivale a Pippo = Pippo - 10;
    Pippo *= 3; // equivale a Pippo = Pippo * 3;

    si tratta cioe` di operatori derivanti dalla concatenazione dell'operatore di assegnamento con un altro operatore binario.
    Gli altri operatori che hanno come effetto laterale l'assegnamento sono quelli di autoincremento e autodecremento, ecco come possono essere utilizzati:

    Pippo++; // cioe` Pippo += 1;
    ++Pippo; // sempre Pippo += 1;
    Pippo--; // Pippo -= 1;
    --Pippo; // Pippo -= 1;

    Questi due operatori possono essere utilizzati sia in forma prefissa (righe 2 e 4) che in forma postfissa (righe 1 e 3); il risultato comunque non e` proprio identico poiche` la forma postfissa restituisce come risultato il valore della variabile e poi incrementa tale valore e lo assegna alla variabile, la forma prefissa invece prima modifica il valore associato alla variabile e poi restituisce tale valore:

    Clarabella = ++Pippo;

    /* equivale a */

    Pippo++;
    Clarabella = Pippo;


    /* invece */

    Clarabella = Pippo++;

    /* equivale a */

    Clarabella = Pippo;
    Pippo++;


    Altri operatori
    Le espressioni, per quanto visto sopra, rappresentano un elemento basilare del C++, tant'e` che il linguaggio fornisce un ampio insieme di operatori.
    La tabella che segue riassume brevemente quasi tutti gli operatori del linguaggio, per completarla dovremmo aggiungere alcuni particolari operatori di conversione di tipo per i quali si rimanda all'appendice A.

    SOMMARIO DEGLI OPERATORI

    :: risolutore di scope
    .
    ->
    [ ]
    ( )
    ( )
    ++
    -- selettore di campi
    selettore di campi
    sottoscrizione
    chiamata di funzione
    costruttore di valori
    post incremento
    post decremento
    sizeof
    ++
    --
    ~
    !
    -
    +
    &
    *
    new
    new[ ]
    delete
    delete[ ]
    ( ) dimensione di
    pre incremento
    pre decremento
    complemento
    negazione
    meno unario
    piu` unario
    indirizzo di
    dereferenzazione
    allocatore di oggetti
    allocatore di array
    deallocatore di oggetti
    deallocatore di array
    conversione di tipo
    .*
    ->* selettore di campi
    selettore di campi
    *
    /
    % moltiplicazione
    divisione
    modulo (resto)
    +
    - somma
    sottrazione
    <<
    >> shift a sinistra
    shift a destra
    <
    <=
    >
    >= minore di
    minore o uguale
    maggiore di
    maggiore o uguale
    ==
    != uguale a
    diverso da
    & AND di bit
    ^ OR ESCLUSIVO di bit
    | OR INCLUSIVO di bit
    && AND logico
    || OR logico (inclusivo)
    ? : espressione condizionale
    =
    *=
    /=
    %=
    +=
    -=
    <<=
    >>=
    &=
    |=
    ^= assegnamento semplice
    moltiplica e assegna
    divide e assegna
    modulo e assegna
    somma e assegna
    sottrae e assegna
    shift sinistro e assegna
    shift destro e assegna
    AND e assegna
    OR inclusivo e assegna
    OR esclusivo e assegna
    throw lancio di eccezioni
    , virgola



    Gli operatori sono raggruppati in base alla loro precedenza: in alto quelli a precedenza maggiore. Gli operatori unari e quelli di assegnamento sono associativi a destra, gli altri a sinistra. L'ordine di valutazione delle sottoespressioni che compongono una espressione piu` grande non e` definito, ad esempio nell'espressione


    Pippo = 10*13 + 7*25;


    non si sa quale tra 10*13 e 7*25 verra` valutata per prima (si noti che comunque verranno rispettate le regole di precedenza e associativita`).

    Gli operatori di assegnamento e quelli di (auto)incremento e (auto)decremento sono gia` stati descritti, esamineremo ora l'operatore per le espressioni condizionali.

    L'operatore ? : e` l'unico operatore ternario:

    <cond> ? <expr1> : <expr2>


    La semantica di questo operatore non e` molto complicata: Cond puo` essere una qualunque espressione che produca un valore booleano (Vedi paragrafo successivo), se essa e` verificata il risultato di tale operatore e` la valutazione di Expr1, altrimenti il risultato e` Expr2.
    Per quanto riguarda gli altri operatori, alcuni saranno esaminati quando sara` necessario; non verranno invece discussi gli operatori logici e quelli di confronto (la cui semantica viene considerata nota al lettore). Rimangono gli operatori per lo spostamento di bit, ci limiteremo a dire che servono sostanzialmente a eseguire moltiplicazioni e divisioni per multipli di 2 in modo efficiente.



    Vero e falso
    Prima che venisse approvato lo standard, il C++ non forniva un tipo primitivo (vedi tipi primitivi) per rappresentare valori booleani. Esattamente come in C i valori di verita` venivano rappresentati tramite valori interi: 0 (zero) indicava falso e un valore diverso da 0 indicava vero. Cio` implicava che ovunque fosse richiesta una condizione era possibile mettere una qualsiasi espressione che producesse un valore intero (quindi anche una somma, ad esempio). Non solo, dato che l'applicazione di un operatore booleano o relazionale a due sottoespressioni produceva 0 o 1 (a seconda del valore di verita` della formula), era possibile mescolare operatori booleani, relazionali e aritmetici.
    Il comitato per lo standard ha tuttavia approvato l'introduzione di un tipo primitivo appositamente per rappresentare valori di verita`. Come conseguenza di cio`, la` dove prima venivano utilizzati i valori interi per rappresentare vero e falso, ora si dovrebbero utilizzare il tipo bool e i valori true (vero) e false (falso), anche perche` i costrutti del linguaggio sono stati adattati di conseguenza. Comunque sia per compatibilita` con il C ed il codice C++ precedentemente prodotto e` ancora possibile utilizzare i valori interi, il compilatore converte automaticamente ove necessario un valore intero in uno booleano e viceversa (true viene convertito in 1):


    10 < 5 // produce false
    10 > 5 // produce true
    true || false // produce true

    Pippo = (10 < 5) && true; // possiamo miscelare le due
    Clarabella = true && 5; // modalita`, in questo caso
    // si ottiene un booleano




    Controllo del flusso
    Esamineremo ora le istruzioni per il controllo del flusso, ovvero quelle istruzioni che consentono di eseguire una certa sequenza di istruzioni, o eventualmente un'altra, in base al valore di una espressione booleana.


    IF-ELSE

    L'istruzione condizionale if-else ha due possibili formulazioni:

    if ( <condizione> ) <istruzione1> ;

    oppure
    if ( <condizione> ) <istruzione1> ;
    else <istruzione2> ;

    L'else e` quindi opzionale, ma, se utilizzato, nessuna istruzione deve essere inserita tra il ramo if e il ramo else. Vediamo ora la semantica di tale istruzione.
    In entrambi i casi se Condizione e` vera viene eseguita Istruzione1, altrimenti nel primo caso non viene eseguito alcunche`, nel secondo caso invece si esegue Istruzione2.

    Si osservi che Istruzione1 e Istruzione2 sono istruzioni singole (una sola istruzione), se e` necessaria una sequenza di istruzioni esse devono essere racchiuse tra una coppia di parentesi graffe { }, come mostra il seguente esempio (si considerino X, Y e Z variabili intere):


    if ( X==10 ) X--;
    else { // istruzione composta
    Y++;
    Z*=Y;
    }


    Ancora alcune osservazioni: il linguaggio prevede che due istruzioni consecutive siano separate da ; (punto e virgola), in particolare si noti il punto e virgola tra il ramo if e l'else; l'unica eccezione alla regola e` data dalle istruzioni composte (cioe` sequenze di istruzioni racchiuse tra parentesi graffe) che non devono essere seguite dal punto e virgola (non serve, c'e` la parentesi graffa).

    Per risolvere eventuali ambiguita` il compilatore lega il ramo else con la prima occorrenza libera di if che incontra tornando indietro (si considerino Pippo, Pluto e Topolino variabili intere):


    if (Pippo) if (Pluto) Topolino = 1;
    else Topolino = 2;


    viene interpretata come



    if (Pippo)
    if (Pluto) Topolino = 1;
    else Topolino = 2;


    l'else viene cioe` legato al secondo if.


    WHILE & DO-WHILE
    I costrutti while e do while consentono l'esecuzione ripetuta di una sequenza di istruzioni in base al valore di verita` di una condizione.
    Vediamone la sintassi:

    while ( <condizione> ) <istruzione> ;

    Al solito, Istruzione indica una istruzione singola, se e` necessaria una sequenza di istruzioni essa deve essere racchiusa tra parentesi graffe.
    La semantica del while e` la seguente: prima si valuta Condizione e se essa e` vera (true) si esegue Istruzione e poi si ripete il tutto; l'istruzione termina quando Condizione valuta a false.

    Esaminiamo ora l'altro costrutto:
    do <istruzione;> while ( <condizione> ) ;

    Nuovamente, Istruzione indica una istruzione singola, se e` necessaria una sequenza di istruzioni essa deve essere racchiusa tra parentesi graffe.
    Il do while differisce dall'istruzione while in quanto prima si esegue Istruzione e poi si valuta Condizione, se essa e` vera si riesegue il corpo altrimenti l'istruzione termina; il corpo del do while viene quindi eseguito sempre almeno una volta.
    Ecco un esempio:


    // Calcolo del fattoriale tramite while
    if (InteroPositivo) {
    Fattoriale = InteroPositivo;
    while (--InteroPositivo)
    Fattoriale *= InteroPositivo;
    }
    else Fattoriale = 1;

    // Calcolo del fattoriale tramite do-while
    Fattoriale = 1;
    if (InteroPositivo)
    do
    Fattoriale *= InteroPositivo;
    while (--InteroPositivo);




    IL CICLO FOR
    Come i piu` esperti sapranno, il ciclo for e` una specializzazione del while, tuttavia nel C++ la differenza tra for e while e` talmente sottile che i due costrutti possono essere liberamente scambiati tra loro.
    La sintassi del for e` la seguente:

    for ( <inizializzazione> ; <condizione> ; <iterazione> )
    <istruzione> ;

    Inizializzazione puo` essere una espressione che inizializza le variabili del ciclo o una dichiarazione di variabili (nel qual caso le veriabili dichiarate hanno scope e lifetime limitati a tutto il ciclo); Condizione e` una qualsiasi espressione booleana; e Iterazione e` una istruzione da eseguire dopo ogni iterazione (solitamente un incremento). Tutti e tre gli elementi appena descitti sono opzionali, in particolare se Condizione non viene specificata si assume che essa sia sempre verificata.
    Ecco la semantica del for espressa tramite while (a meno di una istruzione continue contenuta in Istruzione):
    <inizializzazione> ;
    while ( <condizione> ) {
    <istruzione> ;
    <iterazione> ;
    }

    Una eventuale istruzione continue (vedi di seguito) in Istruzione causa un salto a Iterazione nel caso del ciclo for, nel while invece causa un salto all'inizio del ciclo.
    Ecco come usare il ciclo for per calcolare il fattoriale:




    for (Fatt = IntPos? IntPos : 1; IntPos > 1; /* NOP */)
    Fatt *= (--IntPos);



    Si noti la mancanza del terzo argomento del for, omesso in quanto inutile.


    BREAK & CONTINUE
    Le istruzioni break e continue consentono un maggior controllo sui cicli. Nessuna delle due istruzioni accetta argomenti. L'istruzione break puo` essere utilizzata dentro un ciclo o una istruzione switch (vedi paragrafo successivo) e causa la terminazione del ciclo in cui occorre (o dello switch). L'istruzione continue puo` essere utilizzata solo dentro un ciclo e causa l'interruzione della corrente esecuzione del corpo del ciclo; a differenza di break quindi il controllo non viene passato all'istruzione successiva al ciclo, ma al punto immediatamente prima della fine del corpo del ciclo (pertanto il ciclo potrebbe ancora essere eseguito):


    Fattoriale = 1;
    while (true) { // all'infinito...
    if (InteroPositivo > 1) {
    Fattoriale *= InteroPositivo--;
    continue;
    }
    break; // se InteroPositivo <= 1
    // continue provoca un salto in questo punto
    }



    SWITCH
    L'istruzione switch e` molto simile al case del Pascal (anche se piu` potente) e consente l'esecuzione di uno o piu` frammenti di codice a seconda del valore di una espressione:

    switch ( <espressione> ) {
    case <valore1> : <istruzione> ;
    /* ... */
    case <valoren> : <istruzione> ;
    default : <istruzione> ;
    }

    Espressione e` una qualunque espressione capace di produrre un valore intero; Valore1...ValoreN sono costanti a valori interi; Istruzione e` una qualunque sequenza di istruzioni (non racchiuse tra parentesi graffe).
    All'inizio viene valutata Espressione e quindi viene eseguita l'istruzione relativa alla clausola case che specifica il valore prodotto da Espressione; se nessuna clausola case specifica il valore prodotto da Espressione viene eseguita l'istruzione relativa a default qualora specificato (il ramo default e` opzionale).
    Ecco alcuni esempi:

    switch (Pippo) {
    case 1 :
    Topolino = 5;
    case 4 :
    Topolino = 2;
    Clarabella = 7;
    default :
    Topolino = 0;
    }

    switch (Pluto) {
    case 5 :
    Pippo = 3;
    case 6 :
    Pippo = 5;
    case 10 :
    Orazio = 20;
    Tip = 7;
    } // niente caso default



    Il C++ (come il C) prevede il fall-through automatico tra le clausole dello switch, cioe` il controllo passa da una clausola case alla successiva (default compreso) anche quando la clausola viene eseguita. Per evitare cio` e` sufficiente terminare le clausole con break in modo che, alla fine dell'esecuzione della clausola, termini anche lo switch:

    switch (Pippo) {
    case 1 :
    Topolino = 5; break;
    case 4 :
    Topolino = 2;
    Clarabella = 7; break;
    default :
    Topolino = 0;
    }




    GOTO
    Il C++ prevede la tanto deprecata istruzione goto per eseguire salti incondizionati. La cattiva fama del goto deriva dal fatto che il suo uso tende a rendere obiettivamente incomprensibile un programma; tuttavia in certi casi (tipicamente applicazioni real-time) le prestazioni sono assolutamente prioritarie e l'uso del goto consente di ridurre al minimo i tempi. Comunque quando possibile e` sempre meglio evitarne.
    L'istruzione goto prevede che l'istruzione bersaglio del salto sia etichettata tramite un identificatore utilizzando la sintassi

    <etichetta> : <istruzione>
    che serve anche a dichiarare Etichetta.
    Il salto ad una istruzione viene eseguito con
    goto <etichetta> ;
    ad esempio:

    if (Pippo == 7) goto PLUTO;
    Topolino = 5;
    /* ... */
    PLUTO : Pluto = 7;


    Si noti che una etichetta puo` essere utilizzata anche prima di essere dichiarata. Esiste una limitazione all'uso del goto: il bersaglio dell'istruzione (cioe` Etichetta) deve trovarsi all'interno della stessa funzione dove appare l'istruzione di salto.


    Ad eccezione delle etichette, ogni identificatore che il programmatore intende utilizzare in un programma C++, sia esso per una variabile, una costante simbolica, di tipo o di funzione, va dichiarato prima di essere utilizzato. Ci sono diversi motivi che giustificano la necessita` di una dichiarazione; nel caso di variabili, costanti o tipi:

    consente di stabilire la quantita` di memoria necessaria alla memorizzazione di un oggetto;
    determina l'interpretazione da attribuire ai vari bit che compongono la regione di memoria utilizzata per memorizzare l'oggetto, l'insieme dei valori che puo` assumere e le operazioni che possono essere fatte su di esso;
    permette l'esecuzione di opportuni controlli per determinare errori semantici;
    fornisce eventuali suggerimenti al compilatore;
    nel caso di funzioni, invece una dichiarazione:
    determina numero e tipo dei parametri e il tipo del valore restituito;
    consente controlli per determinare errori semantici;
    Le dichiarazioni hanno anche altri compiti che saranno chiariti in seguito.



    Tipi primitivi
    Un tipo e` una coppia < V, O >, dove V e` un insieme di valori e O e` un insieme di operazioni per la creazione e la manipolazione di elementi di V.
    In un linguaggio di programmazione i tipi rappresentano le categorie di informazioni che il linguaggio consente di manipolare. Il C++ fornisce sei tipi fondamentali (o primitivi):


    bool
    char
    wchar_t
    int
    float
    double


    Abbiamo gia visto (vedi Vero e falso) il tipo bool e sappiamo che esso serve a rappresentare i valori di verita`; su di esso sono definite sostanzialmente le usuali operazioni logiche (&& per l'AND, || per l'OR, ! per la negazione...) e non ci soffermeremo oltre su di esse, solo si faccia attenzione a distinguerle dalle operazioni logiche su bit (rispettivamente &, |, ~...).
    Il tipo char e` utilizzato per rappresentare piccoli interi (e quindi su di esso possiamo eseguire le usuali operazioni aritmetiche) e singoli caratteri; accanto ad esso troviamo anche il tipo wchar_t che serve a memorizzare caratteri non rappresentabili con char (ad esempio i caratteri unicode).
    int e` utilizzato per rappresentare interi in un intervallo piu` grande di char.
    Infine float e double rappresentano entrambi valori in virgola mobile, float per valori in precisione semplice e double per quelli in doppia precisione.

    Ai tipi fondamentali e` possibile applicare i qualificatori signed (con segno), unsigned (senza segno), short (piccolo) e long (lungo) per selezionare differenti intervalli di valori; essi tuttavia non sono liberamente applicabili a tutti i tipi: short si applica solo a int, signed e unsigned solo a char e int e infine long solo a int e double. In definitiva sono disponibili i tipi:


    bool
    char
    wchar_t
    short int
    int
    long int
    signed char
    signed short int
    signed int
    signed long int
    unsigned char
    unsigned short int
    unsigned int
    unsigned long int
    float
    double
    long double




    Il tipo int e` per default signed e quindi e` equivalente a tipo signed int, invece i tipi char, signed char e unsigned char sono considerate categorie distinte. I vari tipi sopra elencati, oltre a differire per l'intervallo dei valori rappresentabili, differiscono anche per la quantita` di memoria richiesta per rappresentare un valore di quel tipo (che pero` puo` variare da implementazione a implementazione). Il seguente programma permette di conoscere la dimensione di alcuni tipi come multiplo di char (di solito rappresentato su 8 bit), modificare il codice per trovare la dimensione degli altri tipi e` molto semplice e viene lasciato per esercizio:


    #include < iostream >
    using namespace std;

    int main(int, char* []) {
    cout << "bool: " << sizeof(bool) << endl;
    cout << "char: " << sizeof(char) << endl;
    cout << "short int: " << sizeof(short int) << endl;
    cout << "int: " << sizeof(int) << endl;
    cout << "float:" << sizeof(float) << endl;
    cout << "double: " << sizeof(double) << endl;
    return 0;
    }



    Una veloce spiegazione sul listato:
    le prime due righe permettono di utilizzare una libreria (standard) per eseguire l'output su video; la libreria iostream dichiara l'oggetto cout il cui compito e` quello di visualizzare l'output che gli viene inviato tramite l'operatore di inserimento <<.
    L'operatore sizeof(<tipo>) restituisce la dimensione di Tipo, mentre endl inserisce un ritorno a capo e forza la visualizzazione dell'output. L'ultima istruzione serve a terminare il programma. Infine main e` il nome che identifica la funzione principale, ovvero il corpo del programma, parleremo in seguito e piu` in dettaglio di main().

    Tra i tipi fondamentali sono definiti gli operatori di conversione, il loro compito e` quello di trasformare un valore di un tipo in un valore di un altro tipo. Non esamineremo per adesso l'argomento, esso verra` ripreso in una apposita appendice.



    Variabili e costanti
    Siamo ora in grado di dichiarare variabili e costanti. La sintassi per la dichiarazione delle variabili e`

    < Tipo > < Lista Di Identificatori > ;

    Ad esempio:




    int a, b, B, c;
    signed char Pippo;
    unsigned short Pluto; // se omesso si intende int






    Innanzi tutto viene dichiarato il tipo Persona e quindi si dichiara la variabile Pippo di tale tipo; in particolare viene mostrato come inizializzare la variabile con una inizializzazione aggregata del tutto simile a quanto si fa per gli array, eccetto che i valori forniti devono essere compatibili con il tipo dei campi e dati nell'ordine definito nella dichiarazione. Viene mostrata anche la dichiarazione di un array i cui elementi sono di tipo struttura, e il modo in cui eseguire una inizializzazione fornendo i valori necessari all'inizializzazione dei singoli campi di ciascun elemento dell'array. Le righe successive mostrano come accedere ai campi di una variabile di tipo struttura, in particolare l'ultima riga assegna un nuovo valore al campo Nome del primo elemento dell'array tramite una funzione di libreria. Si noti che prima viene selezionato l'elemento dell'array e poi il campo Nome di tale elemento; analogamente se e` la struttura a contenere un campo di tipo non primitivo, prima si seleziona il campo e poi si seleziona l'elemento del campo che ci interessa:


    struct Data {
    unsigned short Giorno, Mese;
    unsigned Anno;
    };

    struct Persona {
    char Nome[20];
    Data DataNascita;
    };

    Persona Pippo = { "pippo", {10, 9, 1950} };

    Pippo.Nome[0] = 'P';
    Pippo.DataNascita.Giorno = 15;
    unsigned short UnGiorno = Pippo.DataNascita.Giorno;



    Per le strutture, a differenza degli array, e` definito l'operatore di assegnamento:




    struct Data {
    unsigned short Giorno, Mese;
    unsigned Anno;
    };

    Data Oggi = { 10, 11, 1996 };
    Data UnaData = { 1, 1, 1995};

    UnaData = Oggi;



    Cio` e` possibile per le strutture solo perche`, come vedremo, il compilatore le tratta come classi i cui membri sono tutti pubblici.
    L'assegnamento e` ovviamente possibile solo tra variabili dello stesso tipo struttura, ma quello che di solito sfugge e` che due tipi struttura che differiscono solo per il nome sono considerati diversi:




    // con riferimento al tipo Data visto sopra:

    struct DT {
    unsigned short Giorno, Mese;
    unsigned Anno;
    };

    Data Oggi = { 10, 11, 1996 };
    DT Ieri;

    Ieri = Oggi; // Errore di tipo!




    Unioni
    Un costrutto sotto certi aspetti simile alle strutture e quello delle unioni. Sintatticamente l'unica differenza e` che nella dichiarazione di una unione viene utilizzata la keyword union anzicche` struct:


    union TipoUnione {
    unsigned Intero;
    char Lettera;
    char Stringa[500];
    };


    Come per i tipi struttura, la selezione di un dato campo di una variabile di tipo unione viene eseguita tramite l'operatore di selezione . (punto).
    Vi e` tuttavia una profonda differenza tra il comportamento di una struttura e quello di una unione: in una struttura i vari campi vengono memorizzati in indirizzi diversi e non si sovrappongono mai, in una unione invece tutti i campi vengono memorizzati a partire dallo stesso indirizzo. Cio` vuol dire che, mentre la quantita` di memoria occupata da una struttura e` data dalla somma delle quantita` di memoria utilizzata dalle singole componenti, la quantita` di memoria utilizzata da una unione e` data da quella della componente piu` grande (Stringa nell'esempio precedente).
    Dato che le componenti si sovrappongono, assegnare un valore ad una di esse vuol dire distruggere i valori memorizzati accedendo all'unione tramite una qualsiasi altra componente.
    Le unioni vengono principalmente utilizzate per limitare l'uso di memoria memorizzando negli stessi indirizzi oggetti diversi in tempi diversi. C'e` tuttavia un altro possibile utilizzo delle unioni, eseguire "manualmente" alcune conversioni di tipo. Tuttavia tale pratica e` assolutamente da evitare (almeno quando esiste una alternativa) poiche` tali conversioni sono dipendenti dall'architettura su cui si opera e pertanto non portabili, ma anche potenzialmete scorrette.



    Enumerazioni
    A volte puo` essere utile poter definire un nuovo tipo estensionalmente, cioe` elencando esplicitamente i valori che una variabile (o una costante) di quel tipo puo` assumere. Tali tipi vengono detti enumerati e sono definiti tramite la keyword enum con la seguente sintassi:

    enum < NomeTipo > {
    < Identificatore >,
    /* ... */
    < Identificatore >
    };

    Esempio:

    enum Elemento {
    Idrogeno,
    Elio,
    Carbonio,
    Ossigeno
    };

    Elemento Atomo = Idrogeno;



    Gli identificatori Idrogeno, Elio, Carbonio e Ossigeno costituiscono l'intervallo dei valori del tipo Elemento. Si osservi che come da sintassi, i valori di una enumerazione devono essere espressi tramite identificatori, non sono ammessi valori espressi in altri modi (interi, numeri in virgola mobile, costanti carattere...), inoltre gli identificatori utilizzati per esprimere tali valori devono essere distinti da qualsiasi altro identificatore visibile nello scope dell'enumerazione onde evitare ambiguita`.
    Il compilatore rappresenta internamente i tipi enumerazione associando a ciascun identificatore di valore una costante intera, cosi` che un valore enumerazione puo` essere utilizzato in luogo di un valore intero, ma non viceversa:




    enum Elemento {
    Idrogeno,
    Elio,
    Carbonio,
    Ossigeno
    };

    Elemento Atomo = Idrogeno;
    int Numero;

    Numero = Carbonio; // Ok!
    Atomo = 3; // Errore!



    Nell'ultima riga dell'esempio si verifica un errore perche` non esiste un operatore di conversione da int a Elemento, mentre essendo i valori enumerazione in pratica delle costanti intere, il compilatore e` in grado di eseguire la conversione a int. E` possibile forzare il valore intero da associare ai valori di una enumerazione:




    enum Elemento {
    Idrogeno = 2,
    Elio,
    Carbonio = Idrogeno - 10,
    Ferro = Elio + 7,
    Ossigeno = 2
    };



    Non e` necessario specificare un valore per ogni identificatore dell'enumerazione, non ci sono limitazioni di segno e non e` necessario usare valori distinti (anche se cio` probabilmente comporterebbe qualche problema). Si puo` utilizzare anche un identificatore dell'enumerazione precedentemente definito.
    La possibilita` di scegliere i valori da associare alle etichette (identificatori) dell'enumerazione fornisce un modo alternativo di definire costanti di tipo intero.



    La keyword typedef
    Esiste anche la possibilita` di dichiarare un alias per un altro tipo (non un nuovo tipo) utilizzando la parola chiave typedef:

    typedef < Tipo > < Alias > ;

    Il listato seguente mostra alcune possibili applicazioni:

    typedef unsigned short int PiccoloIntero;
    typedef long double ArrayDiReali[20];

    typedef struct {
    long double ParteReale;
    long double ParteImmaginaria;
    } Complesso;


    Il primo esempio mostra un caso molto semplice: creare un alias per un nome di tipo. Nel secondo caso invece viene mostrato come dichiarare un alias per un tipo "array di 20 long double". Infine il terzo esempio e` il piu` interessante perche` mostra un modo alternativo di dichiarare un nuovo tipo; in realta` ad essere pignoli non viene introdotto un nuovo tipo: la definizione di tipo che precede l'identificatore Complesso dichiara una struttura anonima e poi l'uso di typedef crea un alias per quel tipo struttura.
    E` possibile dichiarare tipi anonimi solo per i costrutti struct, union e enum e sono utilizzabili quasi esclusivamente nelle dichiarazioni (come nel caso di typedef oppure nelle dichiarazioni di variabili e costanti).
    La keyword typedef e` utile per creare abbreviazioni per espressioni di tipo complesse, soprattutto quando l'espressione di tipo coinvolge puntatori e funzioni.


    Come ogni moderno linguaggio, sia il C che il C++ consentono di dichiarare sottoprogrammi che possono essere invocati nel corso dell'esecuzione di una sequenza di istruzioni a partire da una sequenza principale (il corpo del programma). Nel caso del C e del C++ questi sottoprogrammi sono chiamati funzioni e sono simili alle funzioni del Pascal. Anche il corpo del programma e` modellato tramite una funzione il cui nome deve essere sempre main.



    Funzioni
    Una funzione C/C++, analogamente ad una funzione Pascal, e` caratterizzata da un nome che la distingue univocamente nel suo scope (le regole di visibilita` di una funzione sono analoghe a quelle viste per le variabili), da un insieme (eventualmente vuoto) di argomenti (parametri della funzione) separati da virgole, e eventualmente il tipo del valore ritornato:


    // ecco una funzione che riceve due interi
    // e restituisce un altro intero
    int Sum(int a, int b);



    Gli argomenti presi da una funzione sono quelli racchiusi tra le parentesi tonde, si noti che il tipo dell'argomento deve essere specificato singolarmente per ogni parametro anche quando piu` argomenti hanno lo stesso tipo; la seguente dichiarazione e` pertanto errata:

    int Sum2(int a, b); // Errore!


    Il tipo del valore restituito dalla funzione deve essere specificato prima del nome della funzione e se omesso si sottointende int; se una funzione non ritorna alcun valore va dichiarata void, come mostra quest'altro esempio:


    // ecco una funzione che non ritorna alcun valore
    void Foo(char a, float b);


    Non e` necessario che una funzione abbia dei parametri, in questo caso basta non specificarne oppure indicarlo esplicitamente:


    // funzione che non riceve parametri
    // e restituisce un int (default)
    Funny();

    // oppure
    Funny2(void);


    Il primo esempio vale solo per il C++, in C non specificare alcun argomento equivale a dire "Qualsiasi numero e tipo di argomenti"; il secondo metodo invece e` valido in entrambi i linguaggi, in questo caso void assume il significato "Nessun argomento".
    Anche in C++ e` possibile avere funzioni con numero e tipo di argomenti non specificato:

    void Esempio1(...);
    void Esempio2(int Args, ...);


    Il primo esempio mostra come dichiarare una funzione che prende un numero imprecisato (eventualmente 0) di parametri; il secondo esempio invece mostra come dichiarare funzioni che prendono almeno qualche parametro, in questo caso bisogna prima specificare tutti i parametri necessari e poi mettere ... per indicare eventuali altri parametri.

    Quelle che abbiamo visto finora comunque non sono definizioni di funzioni, ma solo dichiarazioni, o per utilizzare un termine proprio del C++, prototipi di funzioni.
    I prototipi di funzione sono stati introdotti nel C++ per informare il compilatore dell'esistenza di una certa funzione e consentire un maggior controllo al fine di identificare errori di tipo (e non solo) e sono utilizzati soprattutto all'interno dei file header per la suddivisione di grossi programmi in piu` file e la realizzazione di librerie di funzioni; infine nei prototipi non e` necessario indicare il nome degli argomenti della funzione:

    // la funzione Sum vista sopra poteva
    // essere dichiarata anche cosi`:
    int Sum(int, int);


    Per implementare (definire) una funzione occorre ripetere il prototipo, specificando il nome degli argomenti (necessario per poter riferire ad essi, ma non obbligatorio se l'argomento non viene utilizzato), seguito da una sequenza di istruzioni racchiusa tra parentesi graffe:


    int Sum(int x, int y) {
    return x+y;
    }


    La funzione Sum e` costituita da una sola istruzione che calcola la somma degli argomenti e restituisce tramite la keyword return il risultato di tale operazione. Inoltre, benche` non evidente dall'esempio, la keyword return provoca l'immediata terminazione della funzione; ecco un esempio non del tutto corretto, che pero` mostra il comportamento di return:


    // calcola il quoziente di due numeri
    int Div(int a, int b) {
    if (b==0) return "errore";
    return a/b;
    }



    Se il divisore e` 0, la prima istruzione return restituisce (erroneamente) una stringa (anzicche` un intero) e provoca la terminazione della funzione, le successive istruzioni della funzione quindi non verrebbero eseguite.
    Concludiamo questo paragrafo con alcune considerazioni:

    La definizione di una funzione non deve essere seguita da ; (punto e virgola), cio` tra l'altro consente di distinguere facilmente tra prototipo (dichiarazione) e definizione di funzione poiche` un prototipo e` terminato da ; (punto e virgola), mentre in una definizione la lista di argomenti e` seguita da { (parentesi graffa aperta);
    Ogni funzione dichiarata non void deve restituire un valore, ne segue che da qualche parte nel corpo della funzione deve esserci una istruzione return con un qualche argomento (il valore restituito), in caso contrario viene segnalato un errore; analogamente l'uso di return in una funzione void costituisce un errore, salvo il caso in cui la keyword sia utilizzata senza argomenti (provocando cosi` solo la terminazione della funzione);
    La definizione di una funzione e` anche una dichiarazione per quella funzione e all'interno del file che definisce la funzione non e` obbligatorio indicarne il prototipo, vedremo meglio l'importanza dei prototipi piu` avanti;
    Non e` possibile dichiarare una funzione all'interno del corpo di un'altra funzione.
    Ecco ancora qualche esempio relativo alla seconda nota:


    int Sum(int a, int b) {
    a + b;
    } // ERRORE! Nessun valore restituito.

    int Sum(int a, int b) {
    return;
    } // ERRORE! Nessun valore restituito.

    int Sum(int a, int b) {
    return a + b;
    } // OK!

    void Sleep(int a) {
    for(int i=0; i < a; ++i) {};
    } // OK!

    void Sleep(int a) {
    for(int i=0; i < a; ++i) {};
    return;
    } // OK!


    La chiamata di una funzione puo` essere eseguita solo nell'ambito dello scope in cui appare la sua dichiarazione (come gia` detto le regole di scoping per le dichiarazioni di funzioni sono identiche a quelle per le variabili) specificando il valore assunto da ciascun parametro formale:


    void Sleep(int Delay); // definita da qualche parte
    int Sum(int a, int b); // definita da qualche parte

    void main(void) {
    int X = 5;
    int Y = 7;
    int Result = 0;

    /* ... */
    Sleep(X);
    Result = Sum(X, Y);
    Sum(X, 8); // Ok!
    Result = Sleep(1000); // Errore!
    return 0;
    }



    La prima e l'ultima chiamata di funzione mostrano come le funzioni void (nel nostro caso Sleep) siano identiche alle procedure Pascal, in particolare l'ultima chiamata a Sleep e` un errore poiche` Sleep non restituisce alcun valore.
    La seconda chiamata di funzione (la prima di Sum) mostra come recuperare il valore restituito dalla funzione (esattamente come in Pascal). La chiamata successiva invece potrebbe sembrare un errore, in realta` si tratta di una chiamata lecita, semplicemente il valore tornato da Sum viene scartato; l'unico motivo per scartare il risultato dell'invocazione di una funzione e` quello di sfruttare eventuali effetti laterali di tale chiamata.



    Passaggio di parametri e argomenti di default

    I parametri di una funzione si comportano all'interno del corpo della funzione come delle variabili locali e possono quindi essere usati anche a sinistra di un assegnamento (per quanto riguarda le variabili locali ad una funzione, si rimanda al capitolo III, paragrafo 3):


    void Assign(int a, int b) {
    a = b; // Tutto OK, operazione lecita!
    }


    tuttavia qualsiasi modifica ai parametri formali (quelli cioe` che compaiono nella definizione, nel nostro caso a e b) non si riflette (per quanto visto fin'ora) automaticamente sui parametri attuali (quelli effettivamente usati in una chiamata della funzione):


    #include < iostream >
    using namespace std;

    void Assign(int a, int b) {
    cout << "Inizio Assign, parametro a = " << a << endl;
    a = b;
    cout << "Fine Assign, parametro a = " << a << endl;
    }

    int main(int, char* []) {
    int X = 5;
    int Y = 10;

    cout << "X = " << X << endl;
    cout << "Y = " << Y << endl;

    // Chiamata della funzione Assign
    // con parametri attuali X e Y
    Assign(X, Y);

    cout << "X = " << X << endl;
    cout << "Y = " << Y << endl;
    return 0;
    }



    L'esempio appena visto e` perfettamente funzionante e se eseguito mostrerebbe come la funzione Assign, pur eseguendo una modifica ai suoi parametri formali, non modifichi i parametri attuali. Questo comportamento e` perfettamente corretto in quanto i parametri attuali vengono passati per valore: ad ogni chiamata della funzione viene cioe` creata una copia di ogni parametro localmente alla funzione stessa; tali copie vengono distrutte quando la chiamata della funzione termina ed il loro contenuto non viene copiato nelle eventuali variabili usate come parametri attuali.
    In alcuni casi tuttavia puo` essere necessario fare in modo che la funzione possa modificare i suoi parametri attuali, in questo caso e` necessario passare non una copia, ma un riferimento o un puntatore e agire su questo per modificare una variabile non locale alla funzione. Per adesso non considereremo queste due possibilita`, ma rimanderemo la cosa al capitolo successivo non appena avremo parlato di puntatori e reference.

    A volte siamo interessati a funzioni il cui comportamento e` pienamente definito anche quando in una chiamata non tutti i parametri sono specificati, vogliamo cioe` essere in grado di avere degli argomenti che assumano un valore di default se per essi non viene specificato alcun valore all'atto della chiamata. Ecco come fare:


    int Sum (int a = 0, int b = 0) {
    return a+b;
    }


    Quella che abbiamo appena visto e` la definizione della funzione Sum ai cui argomenti sono stati associati dei valori di default (in questo caso 0 per entrambi gli argomenti), ora se la funzione Sum viene chiamata senza specificare il valore di a e/o b il compilatore genera una chiamata a Sum sostituendo il valore di default (0) al parametro non specificato. Una funzione puo` avere piu` argomenti di default, ma le regole del C++ impongono che tali argomenti siano specificati alla fine della lista dei parametri formali nella dichiarazione della funzione:


    void Foo(int a, char b = 'a') {
    /* ... */
    } // Ok!

    void Foo2(int a, int c = 4, float f) {
    /* ... */
    } // Errore!

    void Foo3(int a, float f, int c = 4) {
    /* ... */
    } // Ok!



    La dichiarazione di Foo2 e` errata poiche` quando viene specificato un argomento con valore di default, tutti gli argomenti seguenti (in questo caso f) devono possedere un valore di default; l'ultima definizione mostra come si sarebbe dovuto definire Foo2 per non ottenere errori.

    La risoluzione di una chiamata di una funzione con argomenti di default naturalmente differisce da quella di una funzione senza argomenti di default in quanto sono necessari un numero di controlli maggiori; sostanzialmente se nella chiamata per ogni parametro formale viene specificato un parametro attuale, allora il valore di ogni parametro attuale viene copiato nel corrispondente parametro formale sovrascrivendo eventuali valori di default; se invece qualche parametro non viene specificato, quelli forniti specificano il valore dei parametri formali secondo la loro posizione e per i rimanenti parametri formali viene utilizzato il valore di default specificato (se nessun valore di default e` stato specificato, viene generato un errore):


    // riferendo alle precedenti definizioni:

    Foo(1, 'b'); // chiama Foo con argomenti 1 e 'b'
    Foo(0); // chiama Foo con argomenti 0 e 'a'
    Foo('c'); // ?????
    Foo3(0); // Errore, mancano parametri!
    Foo3(1, 0.0); // chiama Foo3(1, 0.0, 4)
    Foo3(1, 1.4, 5); // chiama Foo3(1, 1.4, 5)



    Degli esempi appena fatti, il quarto, Foo3(0), e` un errore poiche` non viene specificato il valore per il secondo argomento della funzione (che non possiede un valore di default); e` invece interessante il terzo (Foo('c');): apparentemente potrebbe sembrare un errore, in realta` quello che il compilatore fa e` convertire il parametro attuale 'c' di tipo char in uno di tipo int e chiamare la funzione sostituendo al primo parametro il risultato della conversione di 'c' al tipo int. La conversione di tipo sara` oggetto di una apposita appendice.
    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.


    Oltre ai tipi primitivi visti precedentemente, esistono altri due tipi fondamentali usati solitamente in combinazione con altri tipi (sia primitivi che non): puntatori e reference.
    L'argomento di cui ora parleremo potra` risultare particolarmente complesso, soprattuto per coloro che non hanno mai avuto a che fare con i puntatori: alcuni linguaggi non forniscono affatto i puntatori (come il Basic, almeno in alcune vecchie versioni), altri (Pascal) invece forniscono un buon supporto; tuttavia il C++ fa dei puntatori un punto di forza (se non il punto di forza) e fornisce un supporto ad essi persino superiore a quello fornito dal Pascal. E` quindi caldamente consigliata una lettura attenta di quanto segue e sarebbe bene fare pratica con i puntatori non appena possibile.



    Puntatori
    I puntatori possono essere pensati come maniglie da applicare alle porte delle celle di memoria per poter accedere al loro contenuto sia in lettura che in scrittura, nella pratica una variabile di tipo puntatore contiene l'indirizzo di una locazione di memoria.
    Vediamo alcune esempi di dichiarazione di puntatori:


    short* Puntatore1;
    Persona* Puntatore3;
    double** Puntatore2;
    int UnIntero = 5;
    int* PuntatoreAInt = &UnIntero;



    Il carattere * (asterisco) indica un puntatore, per cui le prime tre righe dichiarano rispettivamente un puntatore a short int, un puntatore a Persona e un puntatore a puntatore a double. La quinta riga dichiara un puntatore a int e ne esegue l'inizializzazione mediante l'operatore & (indirizzo di) che serve ad ottere l'indirizzo della variabile (o di una costante o ancora di una funzione) il cui nome segue l'operatore. Si osservi che un puntatore a un certo tipo puo` puntare solo a oggetti di quel tipo, (non e` possibile ad esempio assegnare l'indirizzo di una variabile di tipo float a un puntatore a char, come mostra il codice seguente), o meglio in molti casi e` possibile farlo, ma viene eseguita una coercizione (vedi appendice A):


    float Reale = 1.1;
    char * Puntatore = &Reale; // Errore!


    E` anche possibile assegnare ad un puntatore un valore particolare a indicare che il puntatore non punta a nulla:


    Puntatore = 0;


    In luogo di 0 i programmatori C usano la costante NULL, tuttavia l'uso di NULL comporta alcuni problemi di conversione di tipo; in C++ il valore 0 viene automaticamente convertito in un puntatore NULL di dimensione appropriata.

    Nelle dichiarazioni di puntatori bisogna prestare attenzione a diversi dettagli che possono essere meglio apprezzati tramite esempi:


    float* Reale, UnAltroReale;
    int Intero = 10;
    const int* Puntatore = &Intero;
    int* const CostantePuntatore = &Intero;
    const int* const CostantePuntatoreACostante = &Intero;



    La prima dichiarazione contrariamente a quanto si potrebbe pensare non dichiara due puntatori a float, ma un puntatore a float (Reale) e una variabile di tipo float (UnAltroReale): * si applica solo al primo nome che lo segue e quindi il modo corretto di eseguire quelle dichiarazioni era



    float * Reale, * UnAltroReale;



    A contribuire all'errore avra` sicuramente influito il fatto che l'asterisco stava attacato al nome del tipo, tuttavia cambiando stile il problema non si risolve piu` di tanto. La soluzione migliore solitamente consigliata e` quella di porre dichiarazioni diverse in righe diverse.

    Ritorniamo all'esempio da cui siamo partiti.
    La terza riga mostra come dichiarare un puntatore a un intero costante, attenzione non un puntatore costante; la dichiarazione di un puntatore costante e` mostrata nella penultima riga. Un puntatore a una costante consente l'accesso all'oggetto da esso puntato solo in lettura (ma cio` non implica che l'oggetto puntato sia effettivamente costante), mentre un puntatore costante e` una costante di tipo puntatore (a ...), non e` quindi possibile modificare l'indirizzo in esso contenuto e va inizializzato nella dichiarazione. L'ultima riga mostra invece come combinare puntatori costanti e puntatori a costanti per ottenere costanti di tipo puntatore a costante (intera, nell'esempio).
    Attenzione: anche const, se utilizzato per dichiarare una costante puntatore, si applica ad un solo nome (come *) e valgono quindi le stesse raccomandazioni fatte sopra.

    In alcuni casi e` necessario avere puntatori generici, in questi casi il puntatore va dichiarato void:


    void* PuntatoreGenerico;


    I puntatori void possono essere inizializzati come un qualsiasi altro puntatore tipizzato, e a differenza di questi ultimi possono puntare a qualsiasi oggetto senza riguardo al tipo o al fatto che siano costanti, variabili o funzioni; tuttavia non e` possibile eseguire sui puntatori void alcune operazioni definite sui puntatori tipizzati.



    Operazioni sui puntatori

    Dal punto di vista dell'assegnamento, una variabile di tipo puntatore si comporta esattamente come una variabile di un qualsiasi altro tipo primitivo, basta tener presente che il loro contenuto e` un indirizzo di memoria:


    int Pippo = 5, Topolino = 10;
    char Pluto = 'P';
    int* Minnie = &Pippo;
    int* Basettoni;
    void* Manetta;

    // Esempi di assegnamento a puntatori:
    Minnie = &Topolino;
    Manetta = &Minnie; // "Manetta" punta a "Minnie"
    Basettoni = Minnie; // "Basettoni" e "Minnie" ora
    // puntano allo stesso oggetto



    I primi due assegnamenti mostrano come assegnare esplicitamente l'indirizzo di un oggetto ad un puntatore: nel primo caso la variabile Minnie viene fatta puntare alla variabile Topolino, nel secondo caso al puntatore void Manetta si assegna l'indirizzo della variabile Minnie (e non quello della variabile Topolino); per assegnare il contenuto di un puntatore ad un altro puntatore non bisogna utilizzare l'operatore &, basta considerare la variabile puntatore come una variabile di un qualsiasi altro tipo, come mostrato nell'ultimo assegnamento.

    L'operazione piu` importante che viene eseguita sui puntatori e quella di dereferenziazione o indirezione al fine di ottenere accesso all'oggetto puntato; l'operazione viene eseguita tramite l'operatore di dereferenzazione * posto prefisso al puntatore, come mostra il seguente esempio:


    short* P;
    short int Val = 5;

    P = &Val; // P punta a Val (cioe` Val e *P
    // sono lo stesso oggetto);
    cout << "Ora P punta a Val:" << endl;
    cout << "*P = " << *P << endl;
    cout << "Val = " << Val << endl << endl;

    *P = -10; // Modifica l'oggetto puntato da P
    cout << "Val e` stata modificata tramite P:" << endl;
    cout << "*P = " << *P << endl;
    cout << "Val = " << Val << endl << endl;

    Val = 30;
    cout << "La modifica su Val si riflette su *P:" << endl;
    cout << "*P = " << *P << endl;
    cout << "Val = " << Val << endl << endl;



    Il codice appena mostrato fa si` che il puntatore P riferisca alla variabile Val, ed esegue una serie di assegnamenti sia alla variabile che all'oggetto puntato da P mostrandone gli effetti.
    L'operatore * prefisso ad un puntatore seleziona l'oggetto puntato dal puntatore cosi` che *P utilizzato come operando in una espressione produce l'oggetto puntato da P.
    Ecco quale sarebbe l'output del precedente frammento di codice se eseguito:


    Ora P punta a Val:
    *P = 5
    Val = 5

    Val e` stata modificata tramite P:
    *P = -10
    Val = -10

    La modifica su Val si riflette su *P:
    *P = 30
    Val = 30



    L'operazione di dereferenzazione puo` essere eseguita su un qualsiasi puntatore a condizione che questo non sia stato dichiarato void. In generale infatti non e` possibile stabilite il tipo dell'oggetto puntato da un puntatore void e il compilatore non sarebbe in grado di trattare tale oggetto.
    Quando si dereferenzia un puntatore bisogna prestare attenzione che esso sia stato inizializzato correttamente; la dereferenzazione di un puntatore inizializzato a 0 e` sempre un errore, la dereferenzazione di un puntatore non inizializzato causa errori non definiti (e potenzialmente difficili da scovare). Quando possibile comunque il compilatore segnala eventuali tentativi di dereferenziare puntatori che potrebbero non essere stati inizializzati tramite una warning.
    Per i puntatori a strutture (o unioni) e` possibile utilizzare un altro operatore di dereferenzazione che consente in un colpo solo di dereferenziare il puntatore e selezionare il campo desiderato:



    Persona Pippo;
    Persona* Puntatore = &Pippo;

    Puntatore -> Eta = 40;
    cout << "Pippo.Eta = " << Puntatore -> Eta << endl;



    La terza riga dell'esempio dereferenzia Puntatore e contemporaneamente seleziona il campo Eta (il tutto tramite l'operatore ->) per eseguire un assegnamento a quest'ultimo. Nell'ultima riga viene mostrato come utilizzare -> per ottenere il valore di un campo dell'oggetto puntato.
    Sui puntatori e` definita una speciale aritmetica composta da somma e sottrazione. Se P e` un puntatore di tipo T, sommare 1 a P significa puntare all'elemento successivo di un ipotetico array di tipo T cui P e` immaginato puntare; analogamente sottrarre 1 significa puntare all'elemento precedente. E` possibile anche sottrarre da un puntatore un altro puntatore (dello stesso tipo), in questo caso il risultato e` il numero di elementi che separano i due puntatori:



    int Array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int* P1 = &Array[5];
    int* P2 = &Array[9];

    cout << P1 - P2 << endl; // visualizza 4
    cout << *P1 << endl; // visualizza 5
    P1+=3; // equivale a P1 = P1 + 3;
    cout << *P1 << endl; // visualizza 8
    cout << *P2 << endl; // visualizza 9
    P2-=5; // equivale a P2 = P2 - 5;
    cout << *P2 << endl; // visualizza 4



    Sui puntatori sono anche definiti gli usuali operatori relazionali:



    < minore di
    > maggiore di
    <= minore o uguale
    >= maggiore o uguale
    == uguale a
    != diverso da




    Puntatori vs array

    Esiste una stretta somiglianza tra puntatori e array dovuta alla possibilita` di dereferenziare un puntatore nello stesso modo in cui si seleziona l'elemento di un array e al fatto che lo stesso nome di un array e` di fatto un puntatore al primo elemento dell'array:



    int Array[] = { 1, 2, 3, 4, 5 };
    int* Ptr = Array; // equivale a Ptr = &Array[0];

    cout << Ptr[3] << endl; // Ptr[3] equivale a *(Ptr+3);
    Ptr[4] = 7; // equivalente a *(Ptr+4) = 7;



    La somiglianza diviene maggiore quando si confrontano array e puntatori a caratteri:


    char Array[] = "Una stringa";
    char* Ptr = "Una stringa";

    // la seguente riga stampa tutte e due le stringhe
    // si osservi che non e` necessario dereferenziare
    // un char* (a differenza degli altri tipi di
    // puntatori)

    cout << Array << " == " << Ptr << endl;

    // in questo modo, invece, si stampa solo un carattere:
    // la dereferenzazione di un char* o l'indicizzazione
    // di un array causano la visualizzazione di un solo
    // carattere perche` in effetti si passa all'oggetto
    // cout non un puntatore a char, ma un oggetto di tipo
    // char (che cout tratta giustamente in modi diversi)

    cout << Array[5] << " == " << Ptr[5] << endl;
    cout << *Ptr << endl;



    In C++ le dichiarazioni char Array[] = "Una stringa" e char* Ptr = "Una stringa" hanno lo stesso effetto, entrambe creano una stringa (terminata dal carattere nullo) il cui indirizzo e` posto rispettivamente in Array e in Ptr, e come mostra l'esempio un char* puo` essere utilizzato esattamente come un array di caratteri.
    Esistono tuttavia profonde differenze tra puntatori e array: un puntatore e` una variabile a cui si possono applicare le operazioni viste sopra e che puo` essere usato come un array, ma non e` vero il viceversa, in particolare il nome di un array non e` un puntatore a cui e` possibile assegnare un nuovo valore (non e` cioe` modificabile). Ecco un esempio:



    char Array[] = "Una stringa";
    char* Ptr = "Una stringa";

    Array[3] = 'a'; // Ok!
    Ptr[7] = 'b'; // Ok!
    Ptr = Array; // Ok!
    Ptr++; // Ok!
    Array++; // Errore, tentativo di assegnamento!



    In definitiva un puntatore e` piu` flessibile di quanto non lo sia un array, anche se a costo di un maggiore overhead.
    Uso dei puntatori

    I puntatori sono utilizzati sostanzialmente per quattro scopi:

    Realizzazione di strutture dati dinamiche (es. liste linkate);
    Realizzazione di funzioni con effetti laterali sui parametri attuali;
    Ottimizzare il passaggio di parametri di grosse dimensioni;
    Rendere possibile il passaggio di parametri di tipo funzione.
    Il primo caso e` tipico di applicazioni per le quali non e` noto a priori la quantita` di dati che si andranno a manipolare. Senza i puntatori non sarebbe possibile manipolare contemporaneamente un numero non predefinito di dati, anche utilizzando un array porremmo un limite massimo al numero di oggetti di un certo tipo immediatamente disponibili.
    Utilizzando i puntatori invece e` possibile realizzare ad esempio una lista il cui numero massimo di elementi non e` definito a priori:


    #include < iostream >
    using namespace std;

    // Una lista e` composta da tante celle linkate
    // tra di loro; ogni cella contiene un valore
    // e un puntatore alla cella successiva.

    struct TCell {
    float AFloat; // per memorizzare un valore
    TCell* Next; // puntatore alla cella successiva
    };

    // La lista viene realizzata tramite questa
    // struttura contenente il numero corrente di celle
    // della lista e il puntatore alla prima cella

    struct TList {
    unsigned Size; // Dimensione lista
    TCell* First; // Puntatore al primo elemento
    };

    int main(int, char* []) {
    TList List; // Dichiara una lista
    List.Size = 0; // inizialmente vuota
    int FloatToRead;
    cout << "Quanti valori vuoi immettere? " ;
    cin >> FloatToRead;
    cout << endl;

    // questo ciclo richiede valori reali
    // e li memorizza nella lista

    for(int i=0; i < FloatToRead; ++i) {
    TCell* Temp = List.First;
    cout << "Creazione di una nuova cella..." << endl;
    List.First = new TCell; // new vuole il tipo di
    // variabile da creare
    cout << "Immettere un valore reale " ;

    // cin legge l'input da tastiera e l'operatore di
    // estrazione >> lo memorizza nella variabile.
    cin >> List.First -> AFloat;
    cout << endl;
    List.First -> Next = Temp; // aggiunge la cella in
    // testa alla lista
    ++List.Size; // incrementa la
    // dimensione della lista
    }

    // il seguente ciclo calcola la somma
    // dei valori contenuti nella lista;
    // via via che recupera i valori,
    // distrugge le relative celle
    float Total = 0.0;
    for(int j=0; j < List.Size; ++j) {
    Total += List.First -> AFloat;

    // estrae la cella in testa alla lista...
    TCell* Temp = List.First;
    List.First = List.First -> Next;

    // e quindi la distrugge
    cout << "Distruzione della cella estratta..."
    << endl;
    delete Temp;
    }
    cout << "Totale = " << Total << endl;
    return 0;
    }



    Il programma sopra riportato programma memorizza in una lista un certo numero di valori reali, aggiungendo per ogni valore una nuova cella; in seguito li estrae uno ad uno e li somma restituendo il totale; via via che un valore viene estratto dalla lista, la cella corrispondente viene distrutta. Il codice e` ampiamente commentato e non dovrebbe essere difficile capire come funziona. La creazione di un nuovo oggetto avviene allocando un nuovo blocco di memoria (sufficientemente grande) dalla heap-memory (una porzione di memoria riservata all'avvio di un programma per operazioni di questo tipo), mentre la distruzione avviene deallocando tale blocco (che ritorna a far parte della heap-memory); l'allocazione viene eseguita tramite l'operatore new cui va specificato il tipo di oggetto da creare (per sapere quanta ram allocare), la deallocazione avviene invece tramite l'operatore delete, che richiede come argomento un puntatore all'aggetto da deallocare (la quantita` di ram da deallocare viene calcolata automaticamente).
    In alcuni casi e` necessario allocare e deallocare interi array, in questi casi si ricorre agli operatori new[] e delete[]:


    // alloca un array di 10 interi
    int* ArrayOfInt = new int[10];

    // ora eseguiamo la deallocazione
    delete[] ArrayOfInt;



    La dimensione massima di strutture dinamiche e` unicamente determinata dalla dimensione della heap memory che a sua volta e` generalmente limitata dalla quantita` di memoria del sistema.
    Un altro importante aspetto degli oggetti allocati dinamicamente e` che essi non ubbidiscono alle normali regole di scoping statico, solo i puntatori in quanto tali sono soggetti a tali regole, un oggetto allocato dinamicamente puo` quindi essere creato in un certo scope ed essere acceduto in un altro semplicemente trasmettendone l'indirizzo (il valore del puntatore).
    Consideriamo ora il secondo uso che si fa dei puntatori.
    Esso corrisponde a quello che in Pascal si chiama "passaggio di parametri per variabile" e consente la realizzazione di funzioni con effetti laterali sui parametri attuali:


    void Change(int* IntPtr) {
    *IntPtr = 5;
    }


    La funzione Change riceve come unico parametro un puntatore a int, ovvero un indirizzo di una cella di memoria; anche se l'indirizzo viene copiato in una locazione di memoria visibile solo alla funzione, la dereferenzazione di tale copia consente comunque la modifica dell'oggetto puntato:



    int A = 10;

    cout << " A = " << A << endl;
    cout << " Chiamata a Change(int*)... " << endl;
    Change(&A);
    cout << " Ora A = " << A << endl;


    l'output che il precedente codice produce e`:



    A = 10
    Chiamata a Change(int*)...
    Ora A = 5



    Quello che nell'esempio accade e` che la funzione Change riceve l'indirizzo della variabile A e tramite esso e` in grado di agire sulla variabile stessa.
    L'uso dei puntatori come parametri di funzione non e` comunque utilizzato solo per consentire effetti laterali, spesso un funzione riceve parametri di dimensioni notevoli e l'operazione di copia del parametro attuale in un'area privata della funzione ha effetti deleterei sui tempi di esecuzione della funzione stessa; in questi casi e` molto piu` conveniente passare un puntatore che generalmente occupa pochi byte:


    void Func(BigParam parametro);

    // funziona, ma e` meglio quest'altra dichiarazione

    void Func(const BigParam* parametro);



    Il secondo prototipo e` piu` efficiente perche` evita l'overhead imposto dal passaggio per valore, inoltre l'uso di const previene ogni tentativo di modificare l'oggetto puntato e allo stesso tempo comunica al programmatore che usa la funzione che non esiste tale rischio.
    Infine quando l'argomento di una funzione e` un array, il compilatore passa sempre un puntatore, mai una copia dell'argomento; in questo caso inoltre l'unico modo che la funzione ha per conoscere la dimensione dell'array e` quello di ricorrere ad un parametro aggiuntivo, esattamente come accade con la funzione main() (vedi capitolo precedente).
    Ovviamente una funzione puo` restituire un tipo puntatore, in questo caso bisogna pero` prestare attenzione a cio` che si restituisce, non e` raro infatti che un principiante scriva qualcosa del tipo:



    int* Sum(int a, int b) {
    int Result = a + b;
    return &Result;
    }




    Apparentemente e` tutto corretto e un compilatore potrebbe anche non segnalare niente, tuttavia esiste un grave errore: si ritorna l'indirizzo di una variabile locale. L'errore e` dovuto al fatto che la variabile locale viene distrutta quando la funzione termina e riferire ad essa diviene quindi illecito. Una soluzione corretta sarebbe stata quella di allocare Result nello heap e restituire l'indirizzo di tale oggetto (in questo caso e` cura di chi usa la funzione occuparsi della eventuale deallocazione dell'oggetto).
    Infine un uso importante dei puntatori e` per passare come parametro un'altra funzione. Si tratta di un meccanismo che sta alla base dei linguaggi funzionali e che permette di realizzare algoritmi generici (anche se in C++ molte di queste cose sono spesso piu` semplici da ottenere con i template, in alcuni casi pero` il vecchio approccio risulta migliore):



    #include < iostream >
    using namespace std;

    // Definiamo un tipo funzione:
    typedef bool Eval(int, int);

    bool Max(int a, int b) {
    return (a>=b)? true: false;
    }

    bool Min(int a, int b) {
    return (a<=b)? true: false;
    }

    // Notare il tipo del primo parametro
    void Check(Eval* Func, char* FuncName,
    int Param1, int Param2) {
    cout << "E` vero che " << Param1 << " = " << FuncName
    << '(' << Param1 << ',' << Param2 << ") ? ";

    // Utilizzo del puntatore per eseguire la chiamata
    // alla funzione puntata (nella condizione dell'if)
    if (Func(Param1, Param2)) cout << "Si" << endl;
    else cout << "No" << endl;
    }

    int main(int, char* []) {
    for(int i=0; i<10; ++i) {
    cout << "Immetti un intero: ";
    int A;
    cin >> A;
    cout << endl << "Immetti un altro intero: ";
    int B;
    cin >> B;
    cout << endl;

    // Si osservi il modo in cui viene
    // ricavato l'indirizzo di una funzione
    // (primo parametro della Check)
    Check(Max, "Max", A, B);
    Check(Min, "Min", A, B);
    cout << endl << endl;
    }
    return 0;
    }



    La typedef dice che Eval e` un tipo "funzione che prende due interi e restituisce un bool", quindi conformemente al tipo Eval definiamo due funzioni Max e Min dall'evidente significato. Si definisce quindi una funzione Check che riceve quattro parametri: un puntatore a Eval, una stringa e due interi. La funzione Check usa Func per eseguire la chiamata alla funzione puntata e ricavarne il valore restituito. Si noti che la chiamata alla funzione puntata viene eseguita come se Func fosse esso stesso la funzione (ovvero utilizzando l'operatore () e passando normalmente i parametri).
    Si noti infine che la funzione main ricava l'indirizzo di Max e Min senza ricorrere all'operatore &, analogamente a quanto si fa con gli array.



    Reference

    I reference (riferimenti) sono sotto certi aspetti un costrutto a meta` tra puntatori e le usuali variabili: come i puntatori essi sono contenitori di indirizzi, ma non e` necessario dereferenziarli per accedere all'oggetto puntato (si usano come se fossero normali variabili). In pratica possiamo vedere i reference come un meccanismo per creare alias di variabili, anche se in effetti questa e` una definizione non del tutto esatta.
    Cosi` come un puntatore viene indicato nelle dichiarazioni dal simbolo *, un reference viene indicato dal simbolo &:


    int Var = 5;
    float f = 0.5;

    int* IntPtr = &Var;
    int& IntRef = Var; // nei reference non serve
    float& FloatRef = f; // utilizzare & a destra di =



    Le ultime due righe dichiarano rispettivamente un riferimento a int e uno a float che vengono subito inizializzati usando le due variabili dichiarate prima. Un riferimento va inizializzato immediatamente, e dopo l'inizializzazione non puo` essere piu` cambiato; si noti che non e` necessario utilizzare l'operatore & (indirizzo di) per eseguire l'inizializzazione. Dopo l'inizializzazione il riferimento potra` essere utilizzato in luogo della variabile cui e` legato, utilizzare l'uno o l'altro sara` indifferente:


    cout << "Var = " << Var << endl;
    cout << "IntRef = " << IntRef << endl;
    cout << "Assegnamento a IntRef..." << endl;
    IntRef = 8;
    cout << "Var = " << Var << endl;
    cout << "IntRef = " << IntRef << endl;
    cout << "Assegnamento a Var..." << endl;
    Var = 15;
    cout << "Var = " << Var << endl;
    cout << "IntRef = " << IntRef << endl;


    Ecco l'output del precedente codice:


    Var = 5
    IntRef = 5
    Assegnamento a IntRef...
    Var = 8
    IntRef = 8;
    Assegnamento a Var...
    Var = 15
    IntRef = 15



    Dall'esempio si capisce perche`, dopo l'inizializzazione, un riferimento non possa essere piu` associato ad un nuovo oggetto: ogni assegnamento al riferimento si traduce in un assegnamento all'oggetto riferito.
    Un riferimento puo` essere inizializzato anche tramite un puntatore:


    int* IntPtr = new int(5);
    // il valore tra parentesi specifica il valore cui
    // inizializzare l'oggetto allocato. Per adesso il
    // metodo funziona solo con i tipi primitivi.

    int& IntRef = *IntPtr;



    Si noti che il puntatore va dereferenziato, altrimenti si legherebbe il riferimento al puntatore (in questo caso l'uso del riferimento comporta implicitamente una conversione da int* a int).
    Ovviamente il metodo puo` essere utilizzato anche con l'operatore new:


    double& DoubleRef = *new double;

    // Ora si puo` accedere all'oggetto allocato
    // tramite il riferimento.

    DoubleRef = 7.3;
    // Di nuovo, e` compito del programmatore
    // distruggere l'oggetto crato con new

    delete &DoubleRef;

    // Si noti che va usato l'operatore &, per
    // indicare l'intenzione di deallocare
    // l'oggetto riferito, non il riferimento!



    L'uso dei riferimenti per accedere a oggetti dinamici e` sicuramente molto comodo perche` e` possibile uniformare tali oggetti alle comuni variabili, tuttavia e` una pratica che bisognerebbe evitare perche` puo` generare confusione e di conseguenza errori assai insidiosi.



    Uso dei reference

    I riferimenti sono stati introdotti nel C++ come ulteriore meccanismo di passaggio di parametri (per riferimento).
    Una funzione che debba modificare i parametri attuali puo` ora essere dichiarata in due modi diversi:


    void Esempio(Tipo* Parametro);


    oppure in modo del tutto equivalente


    void Esempio(Tipo& Parametro);


    Naturalmente cambierebbe il modo in cui chiamare la funzione:


    long double Var = 0.0;
    long double* Ptr = &Var;

    // nel primo caso avremmo
    Esempio(&Var);

    // oppure
    Esempio(Ptr);

    // nel caso di passaggio per riferimento
    Esempio(Var);

    // oppure
    Esempio(*Ptr);



    In modo del tutto analogo a quanto visto con i puntatori e` anche possibile ritornare un riferimento:




    double& Esempio(float Param1, float Param2) {
    /* ... */
    double* X = new double;
    /* ... */
    return *X;
    }



    Puntatori e reference possono essere liberamente scambiati, non esiste differenza eccetto che non e` necessario dereferenziare un riferimento e che i riferimenti non possono associati ad un'altra variabile dopo l'inizializzazione.
    Probabilmente vi starete chiedendo che motivo c'era dunque di introdurre questa caratteristica dato che i puntatori erano gia` sufficienti. Il problema in effetti non nasce con le funzioni, ma con gli operatori; il C++ consente anche l'overloading degli operatori e sarebbe spiacevole dover scrivere qualcosa del tipo:


    &A + &B


    non si riuscirebbe a capire se si desidera sommare due indirizzi oppure i due oggetti (che potrebbero essere troppo grossi per passarli per valore). I riferimenti invece risolvono il problema eliminando ogni possibile ambiguita` e consentendo una sintassi piu` chiara.



    Puntatori vs reference

    Visto che per le funzioni e` possibile scegliere tra puntatori e riferimenti, come decidere quale metodo scegliere? I riferimenti hanno un vantaggio sui puntatori, dato che nella chiamata di una funzione non c'e` differenza tra passaggio per valore o per riferimento, e` possibile cambiare meccanismo senza dover modificare ne` il codice che chiama la funzione ne` il corpo della funzione stessa. Tuttavia il meccanismo dei reference nasconde all'utente il fatto che si passa un indirizzo e non una copia, e cio` puo` creare grossi problemi in fase di debugging.
    Quando e` necessario passare un indirizzo e` quindi meglio usare i puntatori, che consentono un maggior controllo sugli accessi (tramite la keyword const) e rendono esplicito il modo in cui il parametro viene passato. Esiste comunque una eccezione nel caso dei tipi definiti dall'utente tramite il meccanismo delle classi. In questo caso vedremo che l'incapsulamento garantisce che l'oggetto passato possa essere modificato solo da particolari funzioni (funzioni membro e funzioni amiche), e quindi usare i riferimenti e` piu`conveniente perche` non e` necessario dereferenziarli, migliorando cosi` la chiarezza del codice; le funzioni membro e le funzioni amiche, in quanto tali, sono invece autorizzate a modificare l'oggetto e quindi quando vengono usate l'utente sa gia` che potrebbero esserci effetti laterali.
    Non si tratta comunque di una regola generale, come per tante altre cose, i progettisti del linguaggio hanno pensato di non limitare l'uso dei costrutti con rigide regole e schemi predefiniti, ma di lasciare al buon senso del programmatore il compito di decidere quale fosse di volta in volta la soluzione migliore.



    Quello che e`stato visto fin'ora costituisce sostanzialmente il sottoinsieme C del C++ (salvo l'overloading, i reference e altre piccole aggiunte), e` tuttavia sufficiente per poter realizzare un qualsiasi programma.
    A questo punto, prima di proseguire, e` doveroso soffermarci per esaminare il funzionamento del linker C++ e vedere come organizzare un grosso progetto in piu` file separati.



    Linkage





    Abbiamo gia` visto che ad ogni identificatore e` associato uno scope e una lifetime, ma gli identificatori di variabili, costanti e funzioni possiedono anche un linkage.
    Per comprendere meglio il concetto e` necessario sapere che in C e in C++ l'unita` di compilazione e` il file, un programma puo` consistere di piu` file che vengono compilati separatamente e poi linkati (collegati) per ottenere un file eseguibile. Quest'ultima operazione e` 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() {
    return a;
    }



    Il primo file dichiara una variabile intera e la inizializza, il secondo (trascuriamone per ora la prima riga di codice) dichiara una funzione che ne restituisce il valore. La compilazione del primo file non e` un problema, ma nel secondo file GetVar() deve utilizzare un nome dichiarato in un altro file; perche` la cosa sia possibile bisogna informare il compilatore che tale nome e` dichiarato da qualche altra parte e che il riferimento a tale nome non puo` essere risolto se non quando tutti i file sono stati compilati, solo il linker quindi puo` risolvere il problema collegando insieme i due file. Il compilatore deve dunque essere informato dell'esistenza della variabile al fine di non generare un messaggio di errore; tale operazione viene effettuata tramite la keyword extern.
    In effetti la riga extern int a; non dichiara un nuovo identificatore, ma dice "La variabile intera a e` 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 perche` a sarebbe stata definita due volte (una per file), il perche` di tale errore sara` chiaro piu` avanti.
    Naturalmente extern si puo` usare anche con le funzioni (anche se come vedremo e` ridondante):




    // File a.cpp
    int a = 5;

    int f(int c) {
    return a+c;
    }


    // File b.cpp
    extern int f(int);

    int GetVar() {
    return f(5);
    }



    Si noti che e` 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 gia` detto, il C++ ha un'alta compatibilita` col C, tant'e` che e` 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++ e` necessario indicare al compilatore le nostre intenzioni:




    // Contenuto file C++
    extern "C" int CFunc(char*);
    extern "C" char* CFunc2(int);

    // oppure per risparmiare tempo
    extern "C" {
    void CFunc1(void);
    int* CFunc2(int, char);
    char* strcpy(char*, const char*);
    }



    La presenza di "C" serve a indicare che bisogna adottare le convenzioni del C sulla codifica dei nomi (in quanto il compilatore C++ codifica internamente i nomi degli identificatori in modo assai diverso).
    Un altro uso di extern e` 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 int Func2(int);

    int Func1(int c) {
    if (c==0) return 1;
    return Func2(c-1);
    }

    int Func2(int c) {
    if (c==0) return 2;
    return Func1(c-1);
    }



    Tuttavia nel caso delle funzioni non e` necessario l'uso di extern, il solo prototipo e` sufficiente, e` invece necessario ad esempio per le variabili:




    int Func2(int); // extern non necessaria
    extern int a; // extern necessaria


    int Func1(int c) {
    if (c==0) return 1;
    return Func2(c-1);
    }

    int Func2(int c) {
    if (c==0) return a;
    return Func1(c-1);
    }

    int a = 10; // definisce la variabile
    // precedentemente dichiarata



    I nomi che sono visibili all'esterno di un file sono detti avere linkage esterno; tutte le variabili globali hanno linkage esterno, cosi` come le funzioni globali non inline; le funzioni inline, tutte le costanti e le dichiarazioni fatte in un blocco hanno invece linkage interno (cioe` 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 { Red, Green, Blue };

    extern void f(Color);


    // File2.cpp
    enum Color { Red, Green, Blue };

    void f(Color c) { /* ... */ }



    Una situazione di questo tipo e` 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.
    E` possibile forzare un identificatore globale ad avere linkage interno utilizzando la keyword static:




    // File a.cpp
    static int a = 5; // linkage interno

    int f(int c) { // linkage esterno
    return a+c;
    }


    // File b.cpp
    extern int f(int);

    static int GetVar() { // linkage interno
    return f(5);
    }



    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 e` ovvia; supponete di voler realizzare una libreria di funzioni, alcune serviranno solo a scopi interni alla libreria e non serve (anzi e` pericoloso) esportarle, per fare cio` basta dichiarare static i nomi globali che volete incapsulare.



    File header

    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 per ragioni pratiche (la compilazione e` legata al singolo file sorgente). 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 e` creare un file contenente tutte queste definizioni:




    // Esempio.h
    enum Color { Red, Green, Blue };
    struct Point {
    float X;
    float Y;
    };

    const int Max = 1000;

    inline int Sum(int x, int y) {
    return x + 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 e` gestita dal precompilatore che e` 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 gia` utilizzato la direttiva per poter eseguire input/output, in quei casi abbiamo utilizzato le parentesi angolari (< >) al posto dei doppi apici (" "); la differenza e` 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.
    Si noti inoltre che questa volta e` stato specificato l'estensione del file (.h), questo non dipende dall'uso degli apici, ma dal fatto che ad essere incluso e` l'header di un file di libreria (ad esempio quando si usa la libreria iostream), infatti in teoria tali header potrebbero non essere memorizzati in un normale file.

    Un file header puo` contenere in generale qualsiasi istruzione C/C++ (in particolare anche dichiarazioni extern) da condividere tra piu` moduli:




    // Esempio2.h

    // Un header puo` includere un altro header
    #include "Header1.h"

    // o dichiarazioni extern comuni ai moduli
    extern "C" { // Inclusione di un
    #include "HeaderC.h" // file header C
    }
    extern "C" {
    int CFunc1(int, float);
    void CFunc2(char*);
    }
    extern int a;
    extern double* Ptr;
    extern void Func();





    Librerie di funzioni

    I file header sono molto utili quando si vuole partizionare un programma in piu` moduli, tuttavia la potenza dei file header si esprime meglio quando si vuole realizzare una libreria di funzioni.
    L'idea e` 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 cio` che e` 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 {
    unsigned short dd; // giorno
    unsigned short mm; // mese
    unsigned yy; // anno
    unsigned short h; // ora
    unsigned short m; // minuti
    unsigned short s; // secondi
    };

    void PrintDate(Date);


    ed ecco come sarebbe il file che la implementa:


    // Date.cpp
    #include "Date.h"
    #include < iostream >
    using namespace std;

    void PrintDate(Date dt) {
    cout << dt.dd << '/' << dt.mm << '/' << dt.yy;
    cout << " " << dt.h << ':' << dt.m;
    cout << ':' << dt.s;
    }



    A questo punto la libreria e` pronta, per distribuirla basta compilare il file Date.cpp e fornire il file oggetto ottenuto ed il file header Date.h. Chi deve utilizzare la libreria non dovra` far altro che includere nel proprio programma il file header e linkarlo al file oggetto contenente le funzioni di libreria. Semplicissimo!
    Esistono tuttavia due problemi, il primo e` illustrato nel seguente esempio:




    // Modulo1.h
    #include < iostream >
    using namespace std;

    /* altre dichiarazioni */


    // Modulo2.h
    #include < iostream >
    using namespace std;

    /* altre dichiarazioni */

    // Main.cpp
    #include < iostream >
    using namespace std;

    #include < Modulo1.h >
    #include < Modulo2.h >

    int main(int, char* []) {
    /* codice funzione */
    }



    Si tratta cioe` di un programma costituito da piu` moduli, quello principale che contiene la funzione main() e altri che implementano le varie routine necessarie. Piu` moduli hanno bisogno di una stessa libreria, in particolare hanno bisogno di includere lo stesso file header (nell'esempio iostream) nei rispettivi file header.
    Per come funziona il preprocessore, poiche` il file principale include (direttamente e/o indirettamente) piu` volte lo stesso file header, il file che verra` effettivamente compilato conterra` piu` 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 e` fornita dal precompilatore stesso ed e` nota come compilazione condizionale; consiste cioe` nello specificare quando includere o meno determinate porzioni di codice. Per far cio` ci si avvale delle direttive #define SIMBOLO, #ifndef SIMBOLO e #endif: la prima ci permette di definire un simbolo, la seconda e` come l'istruzione condizionale e serve a testare un simbolo (la risposta e` positiva se SIMBOLO non e` definito, negativa 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 e verificata 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):




    // Contenuto del file iostream.h
    #ifndef __IOSTREAM_H
    #define __IOSTREAM_H

    /* contenuto file header */

    #endif



    si verifica cioe` se un certo simbolo e` stato definito, se non lo e` (cioe` #ifndef e` verificata) si definisce il simbolo e poi si inserisce il codice C/C++, alla fine si inserisce l'#endif. Ritornando all'esempio, ecco cio` 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 > e il file header specificato viene elaborato per produrre codice;
    A seguito delle direttive contenute inizialmente in iostream, 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 > contenuta in Modulo1.h porta il precompilatore ad elaborare di nuovo iostream, ma questa volta il simbolo __IOSTREAM_H e` definito e quindi #ifndef __IOSTREAM_H fa si` 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 e` 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 e` passato al compilatore per la produzione di codice oggetto;
    Utilizzando il metodo appena previsto in tutti i file header (in particolare quelli di libreria) si puo` star sicuri che non ci saranno problemi di inclusione multipla. Tutto il meccanismo richiede pero` che i simboli definiti con la direttiva #define siano unici.



    I namespace

    Il secondo problema che si verifica con la ripartizione di un progetto in piu` file e` legato alla necessita` di utilizzare identificatori globali unici. Quello che spesso accade e` che al progetto lavorino piu` persone ognuna delle quali si occupa di parti diverse che devono poi essere assemblate. Per quanto possa sembrare difficile, spesso accade che persone che lavorano a file diversi utilizzino gli stessi identificatori per indicare funzioni, variabili, costanti...
    Pensate a due persone che devono realizzare due moduli ciascuno dei quali prima di essere utilizzato vada inizializzato, sicuramente entrambi inseriranno nei rispettivi moduli una funzione per l'inizializzazione e molto probabilmente la chiameranno InitModule() (o qualcosa di simile). Nel momento in cui i due moduli saranno linkati insieme (e sempre che non siano sorti problemi prima ancora), inevitabilmente il linker segnalera` errore.
    Naturalmente basterebbe che una delle due funzioni avesse un nome diverso, ma modificare tale nome richiederebbe la modifica anche dei sorgenti in cui il modulo e` utilizzato. Molto meglio prevenire tale situazione suddividendo lo spazio globale dei nomi in parti piu` piccole (i namespace) e rendere unicamente distinguibili tali parti, a questo punto poco importa se in due namespace distinti un identificatore appare due volte... Ma vediamo un esempio:




    // File MikeLib.h
    namespace MikeLib {
    typedef float PiType;
    PiType Pi = 3.14;
    void Init();
    }

    // File SamLib.h
    namespace SamLib {
    typedef double PiType;
    PiType Pi = 3.141592;
    int Sum(int, int);
    void Init();
    void Close();
    }



    In una situazione di questo tipo non ci sarebbe piu` conflitto tra le definizioni dei due file, perche` per accedere ad esse e` necessario specificare anche l'identificatore del namespace:




    #include "MikeLib.h"
    #include "SamLib.h"

    int main(int, char* []) {
    MikeLib::Init();
    SamLib::Init();
    MikeLib::PiType AReal = MikeLib::Pi * 3.7;

    Areal *= Pi; // Errore!

    SamLib::Close();
    }



    L'operatore :: e` detto risolutore di scope e indica al compilatore dove cercare l'identificatore seguente. In particolare l'istruzione MikeLib::Init(); dice al compilatore che la Init() cui vogliamo riferirci e` quella del namespace MikeLib. Ovviamente perche` non ci siano conflitti e` necessario che i due namespace abbiano nomi diversi, ma e` piu` facile stabilire pochi nomi diversi tra loro, che molti.
    Si noti che il tentativo di riferire ad un nome senza specificarne il namespace viene interpretato come un riferimento ad un nome globale esterno ad ogni namespace e nell'esempio precedente genera un errore perche` nello spazio globale non c'e` alcun Pi.
    I namespace sono dunque dei contenitori di nomi su cui sono defite delle regole ben precise:

    Un namespace puo` essere creato solo nello scope globale;


    Se nello scope globale di un file esistono due namespace con lo stesso nome (ad esempio i due namespace sono definiti in file header diversi, ma inclusi da uno stesso file), essi vengono fusi in uno solo;


    E` possibile creare un alias di un namespace con la sintassi: namespace < ID1 > = < ID2 >;


    E` possibile avere namespace anonimi, in questo caso gli identificatori contenuti nel namespace sono visibili al file che contiene il namespace anonimo, ma essi hanno tutti automaticamente linkage interno. I namespace anonimi di file diversi non sono mai fusi insieme.



    La direttiva using

    Qualificare totalmente gli identificatori appartenenti ad un namespace puo` essere molto noioso, soprattutto se siamo sicuri che non ci sono conflitti con altri namespace. In questi casi ci viene in aiuto la direttiva using, che abbiamo gia` visto in numerosi esempi:




    #include "MikeLib.h"
    using namespace MikeLib;
    using namespace SamLib;

    /* ... */



    La direttiva using utilizzata in congiunzione con la keyword importa in un colpo solo tutti gli identificatori del namespace specificato nello scope in cui appare la direttiva (che puo` anche trovarsi nel corpo di una funzione):




    #include "MikeLib.h"
    #include "SamLib.h"

    using namespace MikeLib;
    // Da questo momento in poi non e` necessario
    // qualificare i nomi del namespace MikeLib

    void MyFunc() {
    using namespace SamLib;
    // Adesso in non bisogna qualificare
    // neanche i nomi di SamLib
    /* ... */
    }
    // Ora i nomi di SamLib devono
    // essere nuovamente qualificati con ::

    /* ... */



    Naturalmente se dopo la using ci fosse una nuova definizione di identificatore del namespace importato, quest'ultima nasconderebbe quella del namespace. L'identificatore del namespace sarebbe comunque ancora raggiungibile qualificandolo totalmente:




    #include "SamLib.h"
    using namespace SamLib;

    int Pi = 5; // Nasconde la definizione
    // presente in SamLib

    int a = Pi; // Riferisce al precedente Pi

    double b = SamLib::Pi; // Pi di samLib



    Se piu` direttive using namespace fanno si` che uno stesso nome venga importato da namespace diversi, si viene a creare una potenziale situazione di ambiguita` che diviene visibile (genera cioe` un errore) solo nel momento in cui ci si riferisce a quel nome. In questi casi per risolvere l'ambiguita` basta ricorrere ricorrere al risolutore di scope (::) qualificando totalmente il nome.

    E` anche possibile usare la using per importare singoli nomi:




    #include "SamLib.h"
    #include "MikeLib"
    using namespace MikeLib;
    using SamLib::Sum(int, int);

    void F() {
    PiType a = Pi; // Riferisce a MikeLib
    int r = Sum(5, 4); // SamLib::Sum(int, int)



    I costrutti analizzati fin'ora costituiscono gia` un linguaggio che ci consente di realizzare anche programmi complessi e di fatto, salvo alcune cose, quanto visto costituisce il linguaggio C; tuttavia il C++ e` molto di piu` e offre caratteristiche nuove che estendono e migliorano il C: programmazione a oggetti, RTTI (Run Time Type Information), template (modelli) e programmazione generica, gestione delle eccezioni. Si potrebbe apparentemente dire che si tratta solo di qualche aggiunta, in realta` nessun'altra affermazione potrebbe essere piu` errata: le eccezioni semplificano la gestione di situazioni anomale a run time, mentre il supporto alla programmazione ad oggetti e alla programmazione generica (e cio` che ruota attorno ad esse) rivoluzionano addirittura il modo di concepire e realizzare codice e caratterizzano il linguaggio fino a influenzare il codice prodotto in fase di compilazione (notevolmente diverso da quello prodotto dal compilatore C).
    Inizieremo ora a discutere dei meccanismi offerti dal C++ per la programmazione orientata agli oggetti. Per coloro che non avessero conoscenze in merito ai principi che stanno alla base di tale filosofia e` presente una breve appendice a scopo puramente introduttivo e assolutamente non completa (una trattazione approfondita richiederebbe ben altro spazio).



    Strutture e campi funzione

    La programmazione orientata agli oggetti (OOP) impone una nuova visione di concetti quali "Tipo di dato" e "Istanze di tipo". Sostanzialmente mentre altri paradigmi di programmazione vedono le istanze di un tipo di dato come una entita` passiva, nella programmazione a oggetti invece tali istanze diventano a tutti gli effetti entita` (oggetti) attive.
    L'idea e` che non bisogna piu` manipolare direttamente i valori di una struttura (intesa come generico contenitore di valori), meglio lasciare che sia la struttura stessa a manipolarsi e a compiere le operazioni per noi. Tutto cio` che bisogna fare e` inviare all'oggetto un messaggio che specifichi l'operazione da compiere e attendere poi che l'oggetto stesso ci comunichi il risultato. Il meccanismo dei messaggi viene sostanzialmente implementato tramite quello della chiamata di funzione e l'insieme dei messaggi cui un oggetto risponde viene definito associando al tipo dell'oggetto un insieme di funzioni.
    In C++ cio` puo` essere realizzato tramite le strutture:


    struct Complex {
    float Re;
    float Im;

    // Ora nelle strutture possiamo avere
    // dei campi di tipo funzione;
    void Print();
    float Abs();
    void Set(float PR, float PI);
    };


    Cio` che sostanzialmente cambia, rispetto a quanto visto, e` che una struttura puo` possedere campi di tipo funzione (detti funzioni membro oppure metodi) che costituiscono insieme ai campi ordinari (membri dato o attributi) l'insieme dei messaggi (interfaccia) a cui quel tipo e` in grado di rispondere. L'esempio non mostra come implementare le funzioni membro, per adesso ci basta sapere che esse vengono definite da qualche parte fuori dalla dichiarazione di struttura in modo pressocche` identico alle ordinarie funzioni.
    Una funzione dichiarata come campo di una struttura puo` essere invocata ovviamente solo se associata ad una istanza della struttura stessa, dato che quello che si fa e` inviare un messaggio ad un oggetto. Cio` nella pratica si fa tramite la stessa sintassi utilizzata per selezionare un qualsiasi altro campo (solo che ora ci sono anche campi funzione):


    Complex A;
    Complex* C;

    A.Set(0.2, 10.3);
    A.Print();
    C = new Complex;
    C -> Set(1.5, 3.0);
    float FloatVar = C -> Abs();



    Nell'esempio viene mostrato come inviare un messaggio: la quarta riga invia il messaggio Print() all'oggetto A, l'ultima invece invia il messaggio Abs() all'oggetto puntato da C e assegna il valore ottenuto alla variabile FloatVar. Anche la terza riga invia un messaggio ad A, in questo caso il messaggio richiede dei parametri che vengono forniti nello stesso modo in cui vengono forniti alle funzioni.
    Il vantaggio principale di questo modo di procedere e` il non doversi piu` preoccupare di come e` fatto quel tipo, se si vuole eseguire una operazione su una sua istanza (ad esempio visualizzarne il valore) basta inviare il messaggio corretto, sara` l'oggetto in questione ad eseguirla per noi. Ovviamente perche` tutto funzioni e` necessario evitare di accedere direttamente agli attributi di un oggetto, altrimenti crolla uno dei capisaldi della OOP, e sfortunatamente per noi il meccanismo delle strutture consente l'accesso diretto a tutto cio` che fa parte della dichiarazione di struttura, annullando di fatto ogni vantaggio:


    // Con riferimento agli esempi riportati sopra:

    A.Set(6.1, 4.3); // Setta il valore di A
    A.Re = 10; // Ok!
    A.Im = .5; // ancora Ok!
    A.Print();





    Sintassi della classe

    Il problema viene risolto introducendo una nuova sintassi per la dichiarazione di un tipo oggetto.
    Un tipo oggetto viene dichiarato tramite una dichiarazione di classe, che differisce dalla dichiarazione di struttura sostanzialmente per i meccanismi di protezione offerti; per il resto tutto cio` che si applica alle classi si applica allo stesso modo alla dichiarazione di struttura senza alcuna differenza.
    Vediamo dunque come sarebbe stato dichiarato il tipo Complex tramite la sintassi della classe:


    class Complex {
    public:
    void Print(); // definizione eseguita altrove!

    /* altre funzioni membro */

    private:
    float Re; // Parte reale
    float Im; // Parte immaginaria
    };



    La differenza e` data dalle keyword public e private che consentono di specificare i diritti di accesso alle dichiarazioni che le seguono:

    public: le dichiarazioni che seguono questa keyword sono visibili sia alla classe che a cio` che sta fuori della classe e l'invocazione (selezione) di uno di questi campi e` sempre possibile;


    private: tutto cio` che segue e` visibile solo alla classe stessa, l'accesso ad uno di questi campi e` possibile solo dai metodi della classe stessa;
    come mostra il seguente esempio:


    Complex A;
    Complex * C;

    A.Re = 10.2; // Errore!
    C -> Im = 0.5; // Ancora errore!
    A.Print(); // Ok!
    C -> Print() // Ok!



    Ovviamente le due keyword sono mutuamente esclusive, nel senso che alla dichiarazione di un metodo o di un attributo si applica la prima keyword che si incontra risalendo in su; se la dichiarazione non e` preceduta da nessuna di queste keyword, il default e` private:




    class Complex {
    float Re; // private per
    float Im; // default
    public:
    void Print();

    /* altre funzioni membro*/
    };



    In realta` esiste una terza categoria di visibilita` definibile tramite la keyword protected (che pero` analizzeremo quando parleremo di ereditarieta`); la sintassi per la dichiarazione di classe e` dunque:

    class <nomeclasse> {
    public:
    <membri pubblici>
    protected:
    <membri protetti>
    private:
    <membri privati>
    }; // notare il punto e virgola finale!

    Non ci sono limitazioni al tipo di dichiarazioni possibili dentro una delle tre sezioni di visibilita`: definizioni di variabili o costanti (attributi), funzioni (metodi) oppure dichiarazioni di tipi (enumerazioni, unioni, strutture e anche classi), l'importante e` prestare attenzione a evitare di dichiarare private o protected cio` che deve essere visibile anche all'esterno della classe, in particolare le definizioni dei tipi di parametri e valori di ritorno dei metodi public.



    Definizione delle funzioni membro

    La definizione dei metodi di una classe puo` essere eseguita o dentro la dichiarazione di classe, facendo seguire alla lista di argomenti una coppia di parentesi graffe racchiudente la sequenza di istruzioni:


    class Complex {
    public:
    /* ... */

    void Print() {
    if (Im >= 0)
    cout << Re << " + i" << Im;
    else
    cout << Re << " - i" << fabs(Im);
    // fabs restituisce il valore assoluto!
    }

    private:
    /* ... */
    };



    oppure riportando nella dichiarazione di classe solo il prototipo e definendo il metodo fuori dalla dichiarazione di classe, nel seguente modo (anch'esso applicabile alle strutture):




    /* Questo modo di procedere richiede l'uso
    dell'operatore di risoluzione di scope e l'uso del
    nome della classe per indicare esattamente quale
    metodo si sta definendo (classi diverse possono
    avere metodi con lo stesso nome). */

    void Complex::Print() {
    if (Im >= 0)
    cout << Re << " + i" << Im;
    else
    cout << Re << " - i" << fabs(Im);
    }



    I due metodi non sono comunque del tutto identici: nel primo caso implicitamente si richiede una espansione inline del codice della funzione, nel secondo caso se si desidera tale accorgimento bisogna utilizzare esplicitamente la keyword inline nella definizione del metodo:


    inline void Complex::Print() {
    if (Im >= 0)
    cout << Re << " + i" << Im;
    else
    cout << Re << " - i" << fabs(Im);
    }


    Se la definizione del metodo Print() e` stata studiata con attenzione, il lettore avra` notato che la funzione accede ai membri dato senza ricorrere alla notazione del punto, ma semplicemente nominandoli: quando ci si vuole riferire ai campi dell'oggetto cui e` stato inviato il messaggio non bisogna adottare alcuna particolare notazione, lo si fa e basta (i nomi di tutti i membri della classe sono nello scope di tutti i metodi della stessa classe)!
    La domanda corretta da porsi e` come si fa a stabilire dall'interno di un metodo qual'e` l'effettiva istanza cui ci si riferisce. Il compito di risolvere correttamente ogni riferimento viene svolto automaticamente dal compilatore: all'atto della chiamata, ciascun metodo riceve un parametro aggiuntivo, un puntatore all'oggetto a cui e` stato inviato il messaggio e tramite questo e` possibile risalire all'indirizzo corretto; cio` inoltre consente la chiamata di un metodo da parte di un altro metodo:


    class MyClass {
    public:
    void BigOp();
    void SmallOp();

    private:
    void PrivateOp();
    /* altre dichiarazioni */
    };

    /* definizione di SmallOp() e PrivateOp() */

    void MyClass::BigOp() {
    /* ... */
    SmallOp(); // questo messaggio arriva all'oggetto
    // a cui e` stato inviato BigOp()
    /* ... */
    PrivateOp(); // anche questo!
    /* ... */
    }



    Ovviamente un metodo puo` avere parametri e/o variabili locali che sono istanze della stessa classe cui appartiene (il nome della classe e` gia` visibile all'interno della stessa classe), in questo caso per riferirsi ai campi del parametro o della variabile locale si deve utilizzare la notazione del punto:


    class MyClass {
    /* ... */
    void Func(MyClass A);
    };

    void MyClass::Func(MyClass A, /* ... */ ) {
    /* ... */
    BigOp(); // questo messaggio arriva all'oggetto
    // cui e` stato inviato Func(MyClass)
    A.BigOp(); // questo invece arriva al parametro.
    /* ... */
    }



    In alcuni rari casi puo` essere utile avere accesso al puntatore che il compilatore aggiunge tra i parametri di un metodo, l'operazione e` fattibile tramite la keyword this (che in pratica e` il nome del parametro aggiuntivo), tale pratica quando possibile e` comunque da evitare.
    Costruttori

    L'uso di un metodo Set() per eseguire l'inizializzazione di un oggetto (come mostrato per la struct Complex) e` poco elegante e alquanto insicuro: il programmatore che usa la classe potrebbe dimenticare di chiamare tale metodo prima di cominciare ad utilizzare l'oggetto appena dichiarato. Si potrebbe pensare di scrivere qualcosa del tipo:


    class Complex {
    public:
    /* ... */

    private:
    float Re = 6; // Errore!
    float Im = 7; // Errore!
    };



    ma il compilatore rifiutera` di accettare tale codice. Il motivo e` semplice, stiamo definendo un tipo e non una variabile (o una costante) e non e` possibile inizializzare i membri di una classe (o di una struttura) in quel modo... E poi in questo modo ogni istanza della classe sarebbe sempre inizializzata con valori prefissati, e la situazione sarebbe sostanzialmente quella di prima.
    Il metodo corretto e` quello di fornire un costruttore che il compilatore possa utilizzare quando una istanza della classe viene creata, in modo che tale istanza sia sin dall'inizio in uno stato consistente. Un costruttore altro non e` che un metodo il cui nome e` lo stesso di quello della classe, che puo` avere dei parametri, ma che non restituisce alcun tipo (neanche void); il suo scopo e` quello di inizializzare le istanze della classe:


    Class Complex {
    public:
    Complex(float a, float b) { // costruttore!
    Re = a;
    Im = b;
    }

    /* altre funzioni membro */

    private:
    float Re; // Parte reale
    float Im; // Parte immaginaria
    };



    In questo modo possiamo eseguire dichiarazione e inizializzazione di un oggetto Complex in un colpo solo:


    Complex C(3.5, 4.2);



    La definizione appena vista introduce un oggetto C di tipo Complex che viene inizializzato chiamando il costruttore con gli argomenti specificati tra le parentesi. Si noti che il costruttore non viene invocato come un qualsiasi metodo (il nome del costruttore non e` cioe` esplicitamente mensionato, esso e` implicito nel tipo dell'istanza); un sistema alternativo di eseguire l'inizializzazione sarebbe:


    Complex C = Complex(3.5, 4.2);


    ma e` poco efficiente perche` quello che si fa e` creare un oggetto Complex temporaneo e poi copiarlo in C, il primo metodo invece fa tutto in un colpo solo.
    Un costruttore puo` eseguire compiti semplici come quelli dell'esempio, tuttavia non e` raro che una classe necessiti di costruttori molto complessi, specie se alcuni membri sono dei puntatori; in questi casi un costruttore puo` eseguire operazioni quali allocazione di memoria o accessi a unita` a disco se si lavora con oggetti persistenti.
    In alcuni casi, alcune operazioni possono richiedere la certezza assoluta che tutti o parte dei campi dell'oggetto che si vuole creare siano subito inizializzati prima ancora che incominci l'esecuzione del corpo del costruttore; la soluzione in questi casi prende il nome di lista di inizializzazione.
    La lista di inizializzazione e` una caratteristica propria dei costruttori e appare sempre tra la lista di argomenti del costruttore e il suo corpo:


    class Complex {
    public:
    Complex(float, float);
    /* ... */

    private:
    float Re;
    float Im;
    };

    Complex::Complex(float a, float b) : Re(a), Im(b) { }


    L'ultima riga dell'esempio implementa il costruttore della classe Complex; si tratta esattamente dello stesso costruttore visto prima, la differenza sta tutta nel modo in cui sono inizializzati i membri dato: la notazione Attributo(< Espressione >) indica al compilatore che Attributo deve memorizzare il valore fornito da Espressione; Espressione puo` essere anche qualcosa di complesso come la chiamata ad una funzione.
    Nel caso appena visto l'importanza della lista di inizializzazione puo` non essere evidente, lo sara` di piu` quando parleremo di oggetti composti e di ereditarieta`.
    Una classe puo` possedere piu` costruttori, cioe` i costruttori possono essere overloaded, in modo da offrire diversi modi per inizializzare una istanza; in particolare alcuni costruttori assumono un significato speciale:

    il costruttore di default ClassName::ClassName();
    il costruttore di copia ClassName::ClassName(ClassName& X);
    altri costruttori con un solo argomento;
    Il costruttore di default e` particolare, in quanto e` quello che il compilatore chiama quando il programmatore non utilizza esplicitamente un costruttore nella dichiarazione di un oggetto:


    #include < iostream >
    using namespace std;

    class Trace {
    public:
    Trace() {
    cout << "costruttore di default" << endl;
    }

    Trace(int a, int b) : M1(a), M2(b) {
    cout << "costruttore Trace(int, int)" << endl;
    }

    private:
    int M1, M2;
    };

    int main(int, char* []) {
    cout << "definizione di B... ";
    MyClass B(1, 5); // MyClass(int, int) chiamato!
    cout << "definizione di C... ";
    MyClass C; // costruttore di default chiamato!
    return 0;
    }


    Eseguendo tale codice si ottiene l'output:



    definizione di B... costruttore Trace(int, int)
    definizione di C... costruttore di default


    Ma l'importanza del costruttore di default e` dovuta soprattutto al fatto che se il programmatore della classe non definisce alcun costruttore, automaticamente il compilatore ne fornisce uno (che pero` non da` garanzie sul contenuto dei membri dato dell'oggetto). Se non si desidera il costruttore di default fornito dal compilatore, occorre definirne esplicitamente uno (anche se non di default).

    Il costruttore di copia invece viene invocato quando un nuovo oggetto va inizializzato in base al contenuto di un altro; modifichamo la classe Trace in modo da aggiungere i seguente costruttore di copia:


    Trace::Trace(Trace& x) : M1(x.M1), M2(x.M2) {
    cout << "costruttore di copia" << endl;
    }


    e aggiungiamo il seguente codice a main():


    cout << "definizione di D... ";
    Trace D = B;



    Cio` che viene visualizzato ora, e` che per D viene chiamato il costruttore di copia.
    Se il programmatore non definisce un costruttore di copia, ci pensa il compilatore. In questo caso il costruttore fornito dal compilatore esegue una copia bit a bit (non e` proprio cosi`, ma avremo modo di vederlo in seguito) degli attributi; in generale questo e` sufficiente, ma quando una classe contiene puntatori e` necessario definirlo esplicitamente onde evitare problemi di condivisione di aree di memoria.
    I principianti tendono spesso a confondere l'inizializzazione con l'assegnamento; benche` sintatticamente le due operazioni sono simili, in realta` esiste una profonda differenza semantica: l'inizializzazione viene compiuta una volta sola, quando l'oggetto viene creato; un assegnamento invece si esegue su un oggetto precedentemente creato. Per comprendere la differenza facciamo un breve salto in avanti.
    Il C++ consente di eseguire l'overloading degli operatori, tra cui quello per l'assegnamento; come nel caso caso del costruttore di copia, anche per l'operatore di assegnamento vale il discorso fatto nel caso che tale operatore non venga definito esplicitamente. Il costruttore di copia viene utilizzato quando si dichiara un nuovo oggetto e si inizializza il suo valore con quello di un altro; l'operatore di assegnamento invece viene invocato successivamente in tutte le operazioni che assegnamo all'oggetto dichiarato un altro oggetto. Vediamo un esempio:


    #include < iostream >
    using namespace std;

    class Trace {
    public:
    Trace(Trace& x) : M1(x.M1), M2(x.M2) {
    cout << "costruttore di copia" << endl;
    }

    Trace(int a, int b) : M1{a), M2(b) {
    cout << "costruttore Trace(int, int)" << endl;
    }

    Trace & operator=(const Trace& x) {
    cout << "operatore =" << endl;
    M1 = x.M1;
    M2 = x.M2;
    return *this;
    }

    private:
    int M1, M2;
    };

    int main(int, chra* []) {
    cout << "definizione di A... " << endl;
    Trace A(1,2);
    cout << "definizione di B... " << endl;
    Trace B(2,4);
    cout << "definizione di C... " << endl;
    Trace C = A;
    cout << "assegnamento a C... " << endl;
    C = B;
    return 0;
    }


    Eseguendo questo codice si ottiene il seguente output:


    definizione di A... costruttore Trace(int, int)
    definizione di B... costruttore Trace(int, int)
    definizione di C... costruttore di copia
    assegnamento a C... operatore =



    Restano da esaminare i costruttori che prendono un solo argomento.
    Essi sono a tutti gli effetti dei veri e propri operatori di conversione di tipo(vedi appendice A) che convertono il loro argomento in una istanza della classe. Ecco una classe che fornisce diversi operatori di conversione:


    class MyClass {
    public:
    MyClass(int);
    MyClass(long double);
    MyClass(Complex);
    /* ... */

    private:
    /* ... */
    };

    int main(int, char* []) {
    MyClass A(1);
    MyClass B = 5.5;
    MyClass D = (MyClass) 7;
    MyClass C = Complex(2.4, 1.0);
    return 0;
    }



    Le prime tre dichiarazioni sono concettualmente identiche, in tutti e tre i casi convertiamo un valore di un tipo in quello di un altro; il fatto che l'operazione sia eseguita per inizializzare degli oggetti non modifica in alcun modo il significato dell'operazione stessa.
    Solo l'untima dichiarazione puo` apparentemente sembrare diversa, in pratica e` comunque la stessa cosa: si crea un oggetto di tipo Complex e poi lo si converte (implicitamente) al tipo MyClass, infine viene chiamato il costruttore di copia per inizializzare C. Per finire, ecco un confronto tra costruttori e metodi (o normali funzioni) che riassume quanto detto:



    Costruttori Metodi
    Tipo restituito nessuno qualsiasi
    Nome quello della classe qualsiasi
    Parametri nessuna limitazione nessuna limitazione
    Lista di inizializzazione si no
    Overloading si si


    Altre differenze e similitudini verranno esaminate nel seguito.



    Distruttori

    Poiche` ogni oggetto ha una propria durata (lifetime) e` necessario disporre anche di un metodo che permetta una corretta distruzione dell'oggetto stesso, un distruttore.
    Un distruttore e` un metodo che non riceve parametri, non ritorna alcun tipo (neanche void) ed ha lo stesso nome della classe preceduto da una ~ (tilde):


    class Trace {
    public:
    /* ... */
    ~Trace() {
    cout << "distruttore ~Trace()" << endl;
    }

    private:
    /* ... */
    };


    Il compito del distruttore e` quello di assicurarsi della corretta deallocazione delle risorse e se non ne viene esplicitamente definito uno, il compilatore genera per ogni classe un distruttore di default che chiama alla fine della lifetime di una variabile:


    void MyFunc() {
    TVar A;
    /* ... */
    } // qui viene invocato automaticamente
    // il distruttore per A



    Si noti che nell'esempio non c'e` alcuna chiamata esplicita al distruttore, e` il compilatore che lo chiama alla fine del blocco applicativo (le istruzioni racchiuse tra { } ) in cui la variabile e` stata dichiarata (alla fine del programma per variabili globali e statiche). Poiche` il distruttore fornito dal compilatore non tiene conto di aree di memoria allocate tramite membri puntatore, e` sempre necessario definirlo esplicitamente ogni qual volta esistono membri puntatori; come mostra il seguente esempio:


    #include < iostream >
    using namespace std;

    class Trace {
    public:
    /* ... */
    Trace(long double);
    ~Trace();

    private:
    long double * ldPtr;
    };

    Trace::Trace(long double a) {
    cout << "costruttore chiamato... " << endl;
    ldPtr = new long double(a);
    }

    Trace::~Trace() {
    cout << "distruttore chiamato... " << endl;
    delete ldPtr;
    }



    In tutti gli altri casi, spesso il distruttore di default e` piu` che sufficiente e non occorre scriverlo.
    Solitamente il distruttore e` chiamato implicitamente dal compilatore quando un oggetto termina il suo ciclo di vita, oppure quando un oggetto allocato con new viene deallocato con delete:


    void func() {
    Trace A(5.5); // chiamata costruttore
    Trace* Ptr=new Trace(4.2); // chiamata costruttore
    /* ... */
    delete Ptr; // chiamata al
    // distruttore
    } // chiamata al
    // distruttore per A


    In alcuni rari casi puo` tuttavia essere necessario una chiamata esplicita, in questi casi pero` il compilatore puo` non tenerne traccia (in generale un compilatore non e` in grado di ricordare se il distruttore per una certa variabile e` stato chiamato) e quindi bisogna prendere precauzioni onde evitare che il compilatore, richiamando il costruttore alla fine della lifetime dell'oggetto, generi codice errato.
    Facciamo un esempio:


    void Example() {
    TVar B(10);
    /* ... */
    if (Cond) B.~TVar();
    } // Possibile errore!


    Si genera un errore poiche`, se Cond e` vera, e` il programma a distruggere esplicitamente B, e la chiamata al distruttore fatta dal compilatore e` illecita. Una soluzione al problema consiste nell'uso di un ulteriore blocco applicativo e di un puntatore per allocare nello heap la variabile:


    void Example() {
    TVar* TVarPtr = new TVar(10);
    {
    /* ... */
    if (Cond) {
    delete TVarPtr;
    TVarPtr = 0;
    }
    /* ... */
    }
    if (TVarPtr) delete TVarPtr;
    }


    Durante l'esecuzione di un applicativo possono verificarsi delle situazioni di errore non verificabili a compile-time, che in qualche modo vanno gestiti.
    Le possibili tipologie di errori sono diverse ed in generale non tutte trattabili allo stesso modo. In particolare possiamo distinguere tra errori che non compromettono il funzionamento del programma ed errori che invece costituiscono una grave impedimento al normale svolgimento delle operazioni.
    Tipico della prima categoria sono ad esempio gli errori dovuti a errato input dell'utente, facili da gestire senza grossi problemi. Meno facili da catturare e gestire e` invece la seconda categoria cui possiamo inserire ad esempio i fallimenti relativi all'acquisizione di risorse come la memoria dinamica; questo genere di errori viene solitamente indicato con il termine di eccezioni per sottolineare la loro caratteristica di essere eventi particolarmente rari e di comportare il fallimento di tutta una sequenza di operazioni.
    La principale difficolta` connessa al trattamento delle eccezioni e` quella di riportare lo stato dell'applicazione ad un valore consistente. Il verificarsi di un tale vento comporta infatti (in linea di principio) l'interruzione di tutta una sequenza di operazioni rivolta ad assolvere ad una certa funzionalita`, allo svuotamento dello stack ed alla deallocazione di eventuali risorse allocate fino a quel punto relativamente alla richiesta in esecuzione. Le informazioni necessarie allo svolgimento di queste operazioni sono in generale dipendenti anche dal momento e dal punto in cui si verifica l'eccezione e non e` quindi immaginabile (o comunque facile) che la gestione dell'errore possa essere incapsulata in un unico blocco di codice richiamabile indipendentemente dal contesto in cui si verifica il problema.
    In linguaggi che non offrono alcun supporto, catturare e gestire questi errori puo` essere particolarmente costoso e difficile, al punto che spesso si rinuncia lasciando sostanzialmente al caso le conseguenze. Il C++ comunque non rientra tra questi linguaggi e offre alcuni strumenti che saranno oggetto dei successivi paragrafi di questo capitolo.



    Segnalare le eccezioni

    Il primo problema che bisogna affrontare quando si verifica un errore e` capire dove e quando bisogna gestire l'anomalia.
    Poniamo il caso che si stia sviluppando una serie di funzioni matematiche, in particolare una che esegue la radice quadrata. Come comportarsi se l'argomento della funzione e` un numero negativo? Le possibilita` sono due:

    Terminare il processo;
    Segnalare l'errore al chiamante.
    Probabilmente la prima possibilita` e` eccessivamente drastica, tanto piu` che non sappiamo a priori se e` il caso di terminare oppure se il chiamante possa prevedere azioni alternative da compiere in caso di errore (ad esempio ignorare l'operazione e passare alla successiva). D'altronde neanche la seconda possibilita` sarebbe di per se` una buona soluzione, cosa succede se il chiamante ignora l'errore proseguendo come se nulla fosse?
    E` chiaramente necessario un meccanismo che garantisca che nel caso il chiamante non catturi l'anomalia qualcuno intervenga in qualche modo.
    Ma andiamo con ordine, e supponiamo che il chiamante preveda del codice per gestire l'anomalia.
    Se al verificarsi di un errore grave non si dispone delle informazioni necessarie per decidere cosa fare, la cosa migliore da farsi e` segnalare la condizione di errore a colui che ha invocato l'operazione. Questo obiettivo viene raggiunto con la keyword throw:


    int Divide(int a, int b) {
    if (b) return a/b;
    throw "Divisione per zero";
    }


    L'esecuzione di throw provoca l'uscita dal blocco in cui essa si trova (si noti che in questo caso la funzione non e` obbligata a restituire alcun valore tramite return) e in queste situazioni si dice che la funzione ha sollevato (o lanciato) una eccezione.
    La throw accetta un argomento come parametro che viene utilizzato per creare un oggetto che non ubbidisce alle normali regole di scope e che viene restituito a chi ha tentato l'esecuzione dell'operazione (nel nostro caso al blocco in cui Divide e` stata chiamata). Il compito di questo oggetto e` trasportare tutte le informazioni utili sull'evento.
    L'argomento di throw puo` essere sia un tipo predefinito che un tipo definito dal programmatore.
    Per compatibilita` con il vecchio codice, una funzione non e` tenuta a segnalare la possibilita` che possa lanciare una eccezione, ma e` buona norma avvisare dell'eventualita` segnalando quali tipologie di eccezioni sono possibili. Allo scopo si usa ancora throw seguita da una coppia di parentesi tonde contenente la lista dei tipi di eccezione che possono essere sollevate:


    int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore";
    }

    void MoreExceptionTypes() throw(int, float, MyClass&) {
    /* ... */
    }



    Nel caso della Divide si segnala la possibilita` che venga sollevata una eccezione di tipo char*; nel caso della seconda funzione invece a seconda dei casi puo` essere lanciata una eccezione di tipo int, oppure di tipo float, oppure ancora una di tipo MyClass& (supponendo che MyClass sia un tipo precedentemente definito).



    Gestire le eccezioni

    Quanto abbiamo visto chiaramente non e` sufficiente, non basta poter sollevare (segnalare) una eccezione ma e` necessario poterla anche catturare e gestire.
    L'intenzione di catturare e gestire l'eventuale eccezione deve essere segnalata al compilatore utilizzando un blocco try:


    #include < iostream >
    using namespace std;

    int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore";
    }

    int main() {
    cout << "Immettere il dividendo: ";
    int a;
    cin >> a;
    cout << endl << "Immettere il divisore: ";
    int b;
    cin >> b;
    try {
    cout << Divide(a, b);
    }
    /* ... */
    }



    Utilizzando try e racchidendo tra parentesi graffe (le parentesi si devono utilizzate sempre) il codice che puo` generare una eccezione si segnala al compilatore che siamo pronti a gestire l'eventuale eccezione.
    Ci si potra` chiedere per quale motivo sia necessario informare il compilatore dell'intenzione di catturare e gestire l'eccezione, il motivo sara` chiaro in seguito, al momento e` sufficiente sapere che cio` ha il compito di indicare quando certi automatismi dovranno arrestarsi e lasciare il controllo a codice ad hoc preposto alle azioni del caso.
    Il codice in questione dovra` essere racchiuso all'interno di un blocco catch che deve seguire il blocco try:


    #include < iostream >
    using namespace std;

    int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore, divisione per 0";
    }

    int main() {
    cout << "Immettere il dividendo: ";
    int a;
    cin >> a;
    cout << endl << "Immettere il divisore: ";
    int b;
    cin >> b;
    cout << endl;
    try {
    cout << "Il risultato e` " << Divide(a, b);
    }
    catch(char* String) {
    cout << String << endl;
    return -1;
    }
    return 0;
    }



    Il generico blocco catch potra` gestire in generale solo una categoria di eccezioni o una eccezione generica. Per fornire codice diverso per diverse tipologie di errori bisognera` utilizzare piu` blocchi catch:


    try {
    /* ... */
    }
    catch(Type1 Id1) {
    /* ... */
    }
    catch(Type2 Id2) {
    /* ... */
    }

    /* Altre catch */

    catch(TypeN IdN) {
    /* ... */
    }

    /* Altro */



    Ciascuna catch e` detta exception handler e riceve un parametro che e` il tipo di eccezione che viene gestito in quel blocco. Nel caso generale un blocco try sara` seguito da piu` blocchi catch, uno per ogni tipo di eccezione possibile all'interno di quel try. Si noti che le catch devono seguire immediatamente il blocco try.

    Quando viene generata una eccezione (throw) il controllo risale indietro fino al primo blocco try. Gli oggetti staticamente allocati (che cioe` sono memorizzati sullo stack) fino a quel momento nei blocchi da cui si esce vengono distrutti invocando il loro distruttore (se esiste). Nel momento in cui si giunge ad un blocco try anche gli oggetti staticamente allocati fino a quel momento dentro il blocco try vengono distrutti ed il controllo passa immediatamente dopo la fine del blocco.
    Il tipo dell'oggetto creato con throw viene quindi confrontato con i parametri delle catch che seguono la try. Se viene trovata una catch del tipo corretto, si passa ad eseguire le istruzioni contenute in quel blocco, dopo aver inizializzato il parametro della catch con l'oggetto restituito con throw. Nel momento in cui si entra in un blocco catch, l'eccezione viene considerata gestita ed alla fine del blocco catch il controllo passa alla prima istruzione che segue la lista di catch (sopra indicato con "/* Altro */").
    Vediamo un esempio:


    #include < iostream >
    #include < string.h >
    using namespace std;

    class Test {
    char Name[20];
    public:
    Test(char* name){
    Name[0] = '\0';
    strcpy(Name, name);
    cout << "Test constructor inside "
    << Name << endl;
    }
    ~Test() {
    cout << "Test distructor inside "
    << Name << endl;
    }
    };

    int Sub(int b) throw(int) {
    cout << "Sub called" << endl;
    Test k("Sub");
    Test* K2 = new Test("Sub2");
    if (b > 2) return b-2;
    cout << "exception inside Sub..." << endl;
    throw 1;
    }

    int Div(int a, int b) throw(int) {
    cout << "Div called" << endl;
    Test h("Div");
    b = Sub(b);
    Test h2("Div 2");
    if (b) return a/b;
    cout << "exception inside Div..." << endl;
    throw 0;
    }

    int main() {
    try {
    Test g("try");
    int c = Div(10, 2);
    cout << "c = " << c << endl;
    } // Il controllo ritorna qua
    catch(int exc) {
    cout << "exception catched" << endl;
    cout << "exception value is " << exc << endl;
    }
    return 0;
    }



    La chiamata a Div all'interno della main provoca una eccezione nella Sub, viene quindi distrutto l'oggetto k ed il puntatore k2, ma non l'oggetto puntato (allocato dinamicamente). La deallocazione di oggetti allocati nello heap e` a carico del programmatore.
    In seguito alla eccezione, il controllo risale a Div, ma la chiamata a Sub non era racchiusa dentro un blocco try e quindi anche Div viene terminata distruggendo l'oggetto h. L'oggetto h2 non e` stato ancora creato e quindi nessun distruttore per esso viene invocato.
    Il controllo e` ora giunto al blocco che ha chiamato la Div, essendo questo un blocco try, vengono distrutti gli oggetti g e c ed il controllo passa nel punto in cui si trova il commento.
    A questo punto viene eseguita la catch poiche` il tipo dell'eccezione e` lo stesso del suo argomento e quindi il controllo passa alla return della main.
    Ecco l'output del programma:


    Test constructor inside try
    Div called
    Test constructor inside Div
    Sub called
    Test constructor inside Sub
    Test constructor inside Sub 2
    exception inside Sub...
    Test distructor inside Sub
    Test distructor inside Div
    Test distructor inside try
    exception catched
    exception value is 0


    Si provi a tracciare l'esecuzione del programma e a ricostruirne la storia, il meccanismo diverra` abbastanza chiaro.

    Il compito delle istruzioni contenute nel blocco catch costituiscono quella parte di azioni di recupero che il programma deve svolgere in caso di errore, cosa esattamente mettere in questo blocco e` ovviamente legato alla natura del programma e a cio` che si desidera fare; ad esempio ci potrebbero essere le operazioni per eseguire dell'output su un file di log. E` buona norma studiare gli exception handler in modo che al loro interno non possano verificarsi eccezioni.

    Nei casi in cui non interessa distinguere tra piu` tipologie di eccezioni, e` possibile utilizzare un unico blocco catch utilizzando le ellissi:


    try {
    /* ... */
    }
    catch(...) {
    /* ... */
    }


    In altri casi invece potrebbe essere necessari passare l'eccezione ad un blocco try ancora piu` esterno, ad esempio perche` a quel livello e` sufficiente (o possibile) fare solo certe operazioni, in questo caso basta utilizzare throw all'interno del blocco catch per reinnescare il meccanismo delle eccezioni a partire da quel punto:


    try {
    /* ... */
    }
    catch(Type Id) {
    /* ... */
    throw; // Bisogna scrivere solo throw
    }


    In questo modo si puo` portare a conoscenza dei blocchi piu` esterni della condizione di errore.



    Casi particolari

    Esistono ancora due problemi da affrontare

    Cosa fare se una funzione solleva una eccezione non specificata tra quelle possibili;
    Cosa fare se non si trova un blocco try seguito da una catch compatibile con quel tipo di eccezione;
    Esaminiamo il primo punto.
    Per default una funzione che non specifica una lista di possibili tipi di eccezione puo` sollevare una eccezione di qualsiasi tipo. Una funzione che specifica una lista dei possibili tipi di eccezione e` teoricamente tenuta a rispettare tale lista, ma nel caso non lo facesse, in seguito ad una throw di tipo non previsto, verrebbe eseguita immediatamente la funzione predefinita unexpected(). Per default unexpected() chiama terminate() provocando la terminazione del programma. Tuttavia e` possibile alterare tale comportamento definendo una funzione che non riceve alcun parametro e restituisce void ed utilizzando set_unexpected() come mostrato nel seguente esempio:


    #include < exception >
    using namespace std;

    void MyUnexpected() {
    /* ... */
    }

    typedef void (* OldUnexpectedPtr) ();

    int main() {
    OldUnexpectedPtr = set_unexpected(MyUnexpected);
    /* ... */
    return 0;
    }


    unexpected() e set_unexpected() sono dichiarate nell'header < exception >. E` importante ricordare che la vostra unexpected non deve ritornare, in altre parole deve terminare l'esecuzione del programma:


    #include < exception >
    #include < stdlib.h >
    using namespace std;

    void MyUnexpected() {
    /* ... */
    abort(); // Termina il programma
    }

    typedef void (* OldHandlerPtr) ();

    int main() {
    OldhandlerPtr = set_unexpected(MyUnexpected);
    /* ... */
    return 0;
    }



    Il modo in cui terminate l'esecuzione non e` importante, quello che conta e` che la funzione non ritorni.
    set_unexpected() infine restituisce l'indirizzo della unexpected precedentemente installata e che in talune occasioni potrebbe servire.

    Rimane da trattare il caso in cui in seguito ad una eccezione, risalendo i blocchi applicativi, non si riesca a trovare un blocco try oppure una catch compatibile con il tipo di eccezione sollevata.
    Nel caso si trovi un blocco try ma nessuna catch idonea, il processo viene iterato fino a quando una catch adatta viene trovata, oppure non si riesce a trovare alcun altro blocco try. Se nessun blocco try viene trovato, viene chiamata la funzione terminate().
    Anche in questo caso, come per unexpected(), terminate() e` implementata tramite puntatore ed e` possibile alterarne il funzionamento utilizzando set_terminate() in modo analogo a quanto visto per unexpected() e set_unexpected() (ovviamente la nuova terminate non deve ritornare).
    set_terminate() restituisce l'indirizzo della terminate() precedentemente installata.



    Eccezioni e costruttori

    Il meccanismo di stack unwinding (srotolamento dello stack) che si innesca quando viene sollevata una eccezione garantisce che gli oggetti allocati sullo stack vengano distrutti via via che il controllo esce dai vari blocchi applicativi.
    Ma cosa succede se l'eccezione viene sollevata nel corso dell'esecuzione di un costruttore? In tal caso l'oggetto non puo` essere considerato completamente costruito ed il compilatore non esegue la chiamata al suo distruttore, viene comunque eseguita la chiamata dei distruttori per le componenti dell'oggetto che sono state create:


    #include < iostream >
    using namespace std;

    class Component {
    public:
    Component() {
    cout << "Component constructor called..." << endl;
    }
    ~Component() {
    cout << "Component distructor called..." << endl;
    }
    };

    class Composed {
    private:
    Component A;

    public:
    Composed() {
    cout << "Composed constructor called..." << endl;
    cout << "Throwing an exception..." << endl;
    throw 10;
    }
    ~Composed() {
    cout << "Composed distructor called..." << endl;
    }
    };

    int main() {
    try {
    Composed B;
    }
    catch (int) {
    cout << "Exception handled!" << endl;
    };
    return 0;
    }


    Dall'output di questo programma:


    Component constructor called...
    Composed constructor called...
    Throwing an exception...
    Component distructor called...
    Exception handled!


    e` possibile osservare che il distruttore per l'oggetto B istanza di Composed non viene eseguito perche` solo al termine del costruttore tale oggetto puo` essere considerato totalmente realizzato.

    Le conseguenze di questo comportamento possono passare inosservate, ma e` importante tenere presente che eventuali risorse allocate nel corpo del costruttore non possono essere deallocate dal distruttore. Bisogna realizzare con cura il costruttore assicurandosi che risorse allocate prima dell'eccezione vengano opportunamente deallocate:


    #include < iostream >
    using namespace std;

    int Divide(int a, int b) throw(int) {
    if (b) return a/b;
    cout << endl;
    cout << "Divide: throwing an exception..." << endl;
    cout << endl;
    throw 10;
    }

    class Component {
    public:
    Component() {
    cout << "Component constructor called..." << endl;
    }
    ~Component() {
    cout << "Component distructor called..." << endl;
    }
    };

    class Composed {
    private:
    Component A;
    float* FloatArray;
    int AnInt;
    public:
    Composed() {
    cout << "Composed constructor called..." << endl;
    FloatArray = new float[10];
    try {
    AnInt = Divide(10,0);
    }
    catch(int) {
    cout << "Exception in Composed constructor...";
    cout << endl << "Cleaning up..." << endl;
    delete[] FloatArray;
    cout << "Rethrowing exception..." << endl;
    cout << endl;
    throw;
    }
    }
    ~Composed() {
    cout << "Composed distructor called..." << endl;
    delete[] FloatArray;
    }
    };

    int main() {
    try {
    Composed B;
    }
    catch (int) {
    cout << "main: exception handled!" << endl;
    };
    return 0;
    }



    All'interno del costruttore di Composed viene sollevata una eccezione. Quando questo evento si verifica, il costruttore ha gia` allocato delle risorse (nel nostro caso della memoria); poiche` il distruttore non verrebbe eseguito e` necessario provvedere alla deallocazione di tale risorsa. Per raggiungere tale scopo, le operazioni soggette a potenziale fallimento vengono racchiuse in una try seguita dall'opportuna catch. Nel exception handler tale risorsa viene deallocata e l'eccezione viene nuovamente propagata per consentire alla main di intraprendere ulteriori azioni.
    Ecco l'output del programma:


    Component constructor called...
    Composed constructor called...

    Divide: throwing an exception...

    Exception in Composed constructor...
    Cleaning up...
    Rethrowing exception...

    Component distructor called...
    main: exception handled!


    Si noti che se la catch del costruttore della classe Composed non avesse rilanciato l'eccezione, il compilatore considerando gestita l'eccezione, avrebbe terminato l'esecuzione del costruttore considerando B completamente costruito. Cio` avrebbe comportato la chiamata del distruttore al termine dell'esecuzione della main con il conseguente errore dovuto al tentativo di rilasciare nuovamente la memoria allocata per FloatArray.
    Per verificare cio` si modifichi il programma nel seguente modo:


    #include < iostream >
    using namespace std;

    int Divide(int a, int b) throw(int) {
    if (b) return a/b;
    cout << endl;
    cout << "Divide: throwing an exception..." << endl;
    cout << endl;
    throw 10;
    }

    class Component {
    public:
    Component() {
    cout << "Component constructor called..." << endl;
    }
    ~Component() {
    cout << "Component distructor called..." << endl;
    }
    };

    class Composed {
    private:
    Component A;
    float* FloatArray;
    int AnInt;
    public:
    Composed() {
    cout << "Composed constructor called..." << endl;
    FloatArray = new float[10];
    try {
    AnInt = Divide(10,0);
    }
    catch(int) {
    cout << "Exception in Composed constructor...";
    cout << endl << "Cleaning up..." << endl;
    delete[] FloatArray;
    }
    }
    ~Composed() {
    cout << "Composed distructor called..." << endl;
    }
    };

    int main() {
    try {
    Composed B;
    cout << endl << "main: no exception here!" << endl;
    }
    catch (int) {
    cout << endl << "main: Exception handled!" << endl;
    };
    }


    eseguendolo otterrete il seguente output:


    Component constructor called...
    Composed constructor called...

    Divide: throwing an exception...

    Exception in Composed constructor...
    Cleaning up...

    main: no exception here!
    Composed distructor called...
    Component distructor called...



    Come si potra` osservare, il blocco try della main viene eseguito normalmente e l'oggetto B viene distrutto non in seguito all'eccezione, ma solo perche` si esce dallo scope del blocco try cui appartiene.

    La realizzazione di un costruttore nella cui esecuzione puo` verificarsi una eccezione, e` dunque un compito non banale e in generale sono richieste due operazioni:

    Eseguire eventuali pulizie all'interno del costruttore se non si e` in grado di terminare correttamente la costruzione dell'oggetto;
    Se il distruttore non termina correttamente (ovvero l'oggetto non viene totalmente costruito), propagare una eccezione anche al codice che ha invocato il costruttore e che altrimenti rischierebbe di utilizzare un oggetto non correttamente creato.


    La gerarchia exception

    Lo standard prevede tutta una serie di eccezioni, ad esempio l'operatore ::new puo` sollevare una eccezione di tipo bad_alloc, alcune classi standard (ad esempio la gerarchia degli iostream) ne prevedono altre. E` stata prevista anche una serie di classi da utilizzare all'interno dei propri programmi, in particolare in fase di debugging.
    Alla base della gerarchia si trova la classe exception da cui derivano logic_error e runtime_error. La classe base attraverso il metodo virtuale what() (che restituisce un char*) e` in grado di fornire una descrizione dell'evento (cosa esattamente c'e` scritto nell'area puntata dal puntatore restituito dipende dall'implementazione).
    Le differenze tra logic_error e runtime_error sono sostanzialmente astratte, la prima classe e` stata pensata per segnalare errori logici rilevabili attraverso le precondizioni o le invarianti, la classe runtime_error ha invece lo scopo di riportare errori rilevabili solo a tempo di esecuzione.
    Da logic_error e runtime_error derivano poi altre classi secondo il seguente schema: clicca qui



    Le varie classi sostanzialmente differiscono solo concettualmente, non ci sono differenze nel loro codice, in questo caso la derivazione e` stata utilizzata al solo scopo di sottolineare delle differenze di ruolo forse non immediate da capire.
    Dallo standard:

    logic_error ha lo scopo di segnalare un errore che presumibilmente potrebbe essere rilevato prima di eseguire il programma stesso, come la violazione di una precondizione;
    domain_error va lanciata per segnalare errori relativi alla violazione del dominio;
    invalid_argument va utilizzato per segnalare il passaggio di un argomento non valido;
    length_error segnala il tentativo di costruire un oggetto di dimensioni superiori a quelle permesse;
    out_of_range riporta un errore di argomento con valore non appartenente all'intervallo di definizione;
    runtime_error rappresenta un errore che puo` essere rilevato solo a runtime;
    range_error segnala un errore di intervallo durante una computazione interna;
    overflow_error riporta un errore di overflow;
    underflow_error segnala una condizione di underflow;
    Eccetto che per la classe base che non e` stata pensata per essere impiegata direttamente, il costruttore di tutte le altre classi riceve come unico argomento un const string&; il tipo string e` definito nella libreria standard del linguaggio.



    Conclusioni

    I meccanismi che sono stati descritti nei paragrafi precedenti costituiscono un potente mezzo per affrontare e risolvere situazioni altrimenti difficilmente trattabili. Non si tratta comunque di uno strumento facile da capire e utilizzare ed e` raccomandabile fare diverse prove ed esercizi per comprendere cio` che sta dietro le quinte. La principale difficolta` e` quella di riconoscere i contesti in cui bisogna utilizzare le eccezioni ed ovviamente la strategia da seguire per gestire tali eventi. Cio` che bisogna tener presente e` che il meccanismo delle eccezioni e` sostanzialmente non locale poiche` il controllo ritorna indietro risalendo i vari blocchi applicativi. Cio` significa che bisogna pensare ad una strategia globale, ma che non tenti di raggruppare tutte le situazioni in un unico errore generico altrimenti si verrebbe schiacciati dalla complessita` del compito.
    In generale non e` concepibile occuparsi di una possibile eccezione al livello di ogni singola funzione, a questo livello cio` che e` pensabile fare e` solo lanciare una eccezione; e` invece bene cercare di rendere i propri applicativi molto modulari e isolare e risolvere all'interno di ciascun blocco quante piu` situazioni di errore possibile, lasciando filtrare una eccezione ai livelli superiori solo se le conseguenze possono avere ripercussioni a quei livelli.
    Ricordate infine di catturare e trattare le eccezioni standard che si celano dietro ai costrutti predefiniti quali l'operatore globale ::new.
     
    .
  2. Davidan90
     
    .

    User deleted


    Io non avrò mai la forza di leggere tutto questo
     
    .
  3.  
    .
    Avatar

    Prince Of Game Legend Member lv15

    Group
    Member
    Posts
    6,746
    Location
    dalla casa dell'Uomo del Monte

    Status
    Offline
    ecco in pdf:
    c++

    Edited by ripper_92 - 22/5/2007, 22:07
     
    .
  4. Molko92
     
    .

    User deleted


    CITAZIONE (Davidan90 @ 22/5/2007, 10:42)
    Io non avrò mai la forza di leggere tutto questo

    in effetti XD

    io lo studio un pò alla volta
     
    .
  5.  
    .
    Avatar

    Prince Of Game Legend Member lv15

    Group
    Member
    Posts
    6,746
    Location
    dalla casa dell'Uomo del Monte

    Status
    Offline
    ank'io me lo studio 1 po' alla volta
     
    .
  6. Molko92
     
    .

    User deleted


    così in terza facciamo il baffo anche al prof XD
     
    .
  7.  
    .
    Avatar

    Prince Of Game Legend Member lv15

    Group
    Member
    Posts
    6,746
    Location
    dalla casa dell'Uomo del Monte

    Status
    Offline
    un casino di 10 in arrivo...
     
    .
6 replies since 22/5/2007, 09:32   266 views
  Share  
.