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` 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.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|