Espressioni e istruzioni


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` vuol dire che ad esempio 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 derivati 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: 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++;



Espressioni

Le espressioni, per quanto visto sopra, rappresentano un elemento basilare del C++, tant'e` che il linguaggio fornisce un ampio insieme di operatori. Eccone l'elenco completo:

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
delete
delete[ ]
( )
dimensione di
pre incremento
pre decremento
complemento
negazione
meno unario
piu` unario
indirizzo di
dereferenzazione
allocatore di oggetti
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.
Gli operatori di assegnamento e quelli di (auto)incremento e (auto)decremento sono gia` stati descritti, esaminiamo ora l'operatore per le espressioni condizionali. L'operatore ? : e` l'unico operatore ternario:
    <Cond> ? <Expr2> : <Expr3>
Per definire la semantica di questo operatore e` necessario prima parlare di vero e falso in C++. A differenza di linguaggi quali il Pascal, il C++ non fornisce un tipo primitivo (vedi tipi primitivi) per codificare i valori booleani; essi sono rappresentati tramite valori interi: 0 (zero) indica falso e un valore diverso da 0 indica vero. Cio` implica che ovunque sia richiesta una condizione e` possibile mettere una qualsiasi espressione che possa produrre un valore intero (quindi anche una somma, ad esempio). Non solo, dato che l'applicazione di un operatore booleano o relazionale a due sottoespressioni produce 0 o 1 (a seconda del valore di verita` della formula), e` possibile mescolare operatori booleani, relazionali e aritmetici.
Premesso cio`, la semantica associata all'operatore ? : e` la seguente: si valuta Cond, se essa e` vera (diversa da zero) il risultato di tale operatore e` la valutazione di Expr2, altrimenti il risultato e` Expr3.
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.



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.


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 eseguita alcuna istruzione, nel secondo 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 l'esempio (si considerino X, Y e Z variabili intere):
    if ( X==10 ) X--;
    else {
      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 (diversa da 0) si esegue Istruzione e poi si ripete il tutto; l'istruzione termina quando Condizione valuta a 0 (falsa).
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; si noti inoltre che Istruzione non e` seguita da punto e virgola.
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 a valori interi; 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 paragrafo successivo) in Istruzione causa un salto a Iterazione nel caso del ciclo for, nel while invece causa una uscita dal 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 tra le parentesi tonde, ommesso 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 body del ciclo (pertanto il ciclo potrebbe ancora essere eseguito):
    Fattoriale = 1;
    while (1) {            // all'infinito...
       if (InteroPositivo > 1) {
         Fattoriale *=Interopositivo--;
         continue;
       }
       break;                 // se eseguita allora 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 diverse tra loro; 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, se 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;
        break;
    }


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 obbiettivamente 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 evitare l'uso di goto.
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 OK;
      Topolino = 5;
      /* ... */
 OK : 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.


Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - © Copyright 1997, Paolo Marotta