Operatori di Assegnamento in C++
- Gli operatori di assegnamento in C++ includono l'assegnamento semplice e gli operatori di assegnamento composto.
- La precedenza degli operatori determina l'ordine in cui le operazioni vengono eseguite in un'espressione.
- L'operatore di assegnamento (
=
) assegna il valore del suo operando destro all'oggetto rappresentato dal suo operando sinistro. - L'operando sinistro di un operatore di assegnamento deve essere un l-value modificabile.
- L'assegnamento è associativo a destra, il che significa che si raggruppa da destra a sinistra.
- L'assegnamento ha bassa precedenza, quindi spesso sono necessarie le parentesi intorno agli assegnamenti nelle condizioni.
Operatori di Assegnamento
In C++, il più semplice operatore di assegnamento è il segno di uguale (=
). L'operatore di assegnamento assegna il valore del suo operando destro all'oggetto rappresentato dal suo operando sinistro:
i = 42; // assegna 42 a i
j = i; // assegna il valore di i a j
k = j + 2; // assegna a k il valore di j più 2
Bisogna fare attenzione, però, a non confondere l'operazione di assegnamento con l'operazione di inizializzazione. Entrambe le due operazioni usano il segno di uguale (=
), ma sono concettualmente diverse. L'inizializzazione si verifica quando un oggetto viene creato e gli viene assegnato un valore iniziale. L'assegnamento, d'altra parte, si verifica quando un oggetto esistente viene aggiornato con un nuovo valore.
Ad esempio, nella seguente dichiarazione:
int i = 0; // inizializzazione, non assegnamento
i
viene creato e inizializzato a 0. Non c'è alcun oggetto esistente a cui assegnare un nuovo valore; i
sta semplicemente ottenendo il suo valore iniziale.
Viceversa, in questa istruzione:
i = 42; // assegnamento: assegna 42 a i
i
è già stato creato (forse inizializzato a 0 in precedenza) e ora gli viene assegnato un nuovo valore, 42.
Sebbene molto simili, in C++ queste due operazioni sono distinte e hanno regole diverse. Ad esempio, un oggetto const
può essere inizializzato ma non può essere assegnato un nuovo valore dopo la sua creazione.
L'operando sinistro di un operatore di assegnamento deve essere un l-value modificabile. Ad esempio, dato:
// Inizializzazioni, non assegnamento
int i = 0, j = 0, k = 0;
// inizializzazione, non assegnamento
const int ci = i;
Ognuno di questi assegnamenti è illegale:
// ERRORE: I letterali non sono l-value ma r-value
1024 = k;
// ERRORE: Le espressioni aritmetiche sono r-value
i + j = k;
// ERRORE: ci è una costante non modificabile
ci = k;
Il risultato di un assegnamento è il suo operando sinistro, che è un l-value. Il tipo del risultato è il tipo dell'operando sinistro. Se i tipi degli operandi sinistro e destro differiscono, l'operando destro viene convertito nel tipo del sinistro:
k = 0; // risultato: tipo int, valore 0
k = 3.14159; // risultato: tipo int, valore 3
Pertanto, possiamo scrivere un codice simile al seguente:
i = (k = 3)
Poiché il risultato di un'assegnamento è il suo operando sinistro, l'espressione di sopra è equivalente a queste due espressioni in sequenza:
k = 3;
i = k;
A partire dallo standard C++11, possiamo usare una lista di inizializzatori tra parentesi graffe sul lato destro:
// ERRORE: Conversione con perdita da double a int
k = {3.14};
// Inizialmente vuoto
vector<int> vi;
// vi adesso avrà 10 elementi
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Se l'operando sinistro è di un tipo built-in, la lista di inizializzatori può contenere al massimo un valore, e quel valore non deve richiedere una conversione restrittiva, ad esempio una conversione da double
a int
.
Per i tipi classe, ciò che accade dipende dai dettagli della classe. Nel caso di vector
, il template vector
definisce la propria versione di un operatore di assegnamento che può prendere una lista di inizializzatori. Questo operatore sostituisce gli elementi del lato sinistro con gli elementi nella lista sul lato destro.
Indipendentemente dal tipo dell'operando sinistro, la lista di inizializzatori può essere vuota. In questo caso, il compilatore genera un temporaneo inizializzato per valore e assegna quel valore all'operando sinistro.
L'Assegnazione È Associativa a Destra
A differenza degli altri operatori binari, l'assegnamento è associativo a destra:
int ival, jval;
// OK: Ciascuna variabile è inizializzata a 0
ival = jval = 0;
Poiché l'assegnamento è associativo a destra, l'assegnamento più a destra, jval = 0
, è l'operando destro dell'operatore di assegnamento più a sinistra. Poiché l'assegnamento restituisce il suo operando sinistro, il risultato dell'assegnamento più a destra (cioè, jval
) viene assegnato a ival
.
Ogni oggetto in un assegnamento multiplo deve avere lo stesso tipo del suo vicino di destra o un tipo al quale quel vicino può essere convertito:
// ival è un int; pval è un puntatore a int
int ival, *pval;
// ERRORE: non si può assegnare il valore di un puntatore a un int
ival = pval = 0;
string s1, s2;
// il letterale stringa "OK" convertito in string
s1 = s2 = "OK";
Il primo assegnamento è illegale perché ival
e pval
hanno tipi diversi e non c'è conversione dal tipo di pval
(int *
) al tipo di ival
(int
). È illegale anche se zero è un valore che può essere assegnato a entrambi gli oggetti.
D'altra parte, il secondo assegnamento è corretto. Il letterale stringa viene convertito in string
, e quella string
viene assegnata a s2
. Il risultato di quell'assegnamento è s2
, che ha lo stesso tipo di s1
.
L'Assegnamento Ha Bassa Precedenza
Gli assegnamenti spesso compaiono nelle condizioni. Poiché l'assegnamento ha precedenza relativamente bassa, di solito dobbiamo mettere tra parentesi l'assegnamento affinché la condizione funzioni correttamente. Per capire perché l'assegnamento in una condizione è utile, considera il seguente ciclo. Vogliamo chiamare una funzione finché non restituisce un valore desiderato, ad esempio 42:
// un modo verboso e quindi più soggetto a errori di scrivere questo ciclo
// ottieni il primo valore
int i = get_value();
while (i != 42) {
// fai qualcosa ...
// ottieni i valori rimanenti
i = get_value();
}
Qui iniziamo chiamando get_value
seguito da un ciclo la cui condizione usa il valore restituito da quella chiamata. L'ultima istruzione in questo ciclo fa un'altra chiamata a get_value
, e il ciclo si ripete. Possiamo scrivere questo codice più direttamente come
int i;
// un modo migliore di scrivere il nostro ciclo
// ciò che fa la condizione è ora più chiaro
while ((i = get_value()) != 42) {
// fai qualcosa ...
}
La condizione ora esprime più chiaramente la nostra intenzione: Vogliamo continuare finché get_value
non restituisce 42. La condizione viene eseguita assegnando il risultato restituito da get_value
a i
e poi confrontando il risultato di quell'assegnamento con 42.
Senza le parentesi, gli operandi di !=
sarebbero il valore restituito da get_value
e 42. Il risultato true
o false
di quel test verrebbe assegnato a i
e chiaramente non è quello che intendevamo!
Poiché l'assegnamento ha precedenza inferiore rispetto agli operatori relazionali, di solito sono necessarie le parentesi intorno agli assegnamenti nelle condizioni.
Attenzione a Non Confondere gli Operatori di Uguaglianza e Assegnamento
Il fatto che possiamo usare l'assegnamento in una condizione può avere effetti sorprendenti:
if (i = j)
La condizione in questo if
assegna il valore di j
a i
e poi testa il risultato dell'assegnamento. Se j
è diverso da zero, la condizione sarà true
. L'autore di questo codice quasi certamente intendeva testare se i
e j
hanno lo stesso valore:
if (i == j)
I bug di questo tipo sono notoriamente difficili da trovare. Alcuni, ma non tutti, i compilatori sono abbastanza gentili da avvisare su codice come questo esempio.
Operatori di Assegnamento Composto
Spesso applichiamo un operatore a un oggetto e poi assegniamo il risultato a quello stesso oggetto. Come esempio, consideriamo un semplice programma che somma i numeri da 1 a 10:
#include <iostream>
using namespace std;
int main() {
int sum = 0;
// somma i valori da 1 a 10 inclusi
for (int val = 1; val <= 10; ++val)
sum += val; // equivalente a sum = sum + val
cout << "La somma è: " << sum << endl;
return 0;
}
Questo tipo di operazione è comune non solo per l'addizione ma anche per gli altri operatori aritmetici e gli operatori bit a bit, che vedremo nelle prossime lezioni. Ci sono assegnamenti composti per ognuno di questi operatori:
Operatore | Significato |
---|---|
+= |
somma e assegna |
-= |
sottrai e assegna |
*= |
moltiplica e assegna |
/= |
dividi e assegna |
%= |
modulo e assegna |
Operatore | Significato |
---|---|
&= |
AND bit a bit e assegna |
|= |
OR bit a bit e assegna |
^= |
XOR bit a bit e assegna |
<<= |
shift a sinistra e assegna |
>>= |
shift a destra e assegna |
Ogni operatore composto è essenzialmente equivalente a
a = a operatore b;
con l'eccezione che, quando usiamo l'assegnamento composto, l'operando sinistro viene valutato solo una volta. Se usiamo un assegnamento ordinario, quell'operando viene valutato due volte: una volta nell'espressione sul lato destro e di nuovo come operando sul lato sinistro. In molti, forse la maggior parte, contesti questa differenza è irrilevante a parte le possibili conseguenze sulle prestazioni.
Esercizi
-
Quali sono i valori di
i
ed
dopo ogni assegnamento?int i; double d;
d = i = 3.5;
Soluzione: In questo caso, l'assegnamento è associativo a destra, quindi l'assegnamento più a destra viene eseguito per primo. Il valore
3.5
(undouble
) viene assegnato ai
, che è unint
. Poiché3.5
non è un valore intero, viene troncato a3
durante la conversione dadouble
aint
. Quindi, dopo il primo assegnamento,i
vale3
. Di conseguenza, il valore dii
(cioè3
) viene assegnato ad
. Poichéd
è undouble
, il valore3
viene convertito in3.0
durante l'assegnamento. Alla fine,i
vale3
ed
vale3.0
.i = d = 3.5;
Soluzione: In questo caso, l'assegnamento è ancora associativo a destra, quindi l'assegnamento più a destra viene eseguito per primo. Il valore
3.5
(undouble
) viene assegnato ad
, che è undouble
. Quindi, dopo il primo assegnamento,d
vale3.5
. Di conseguenza, il valore did
(cioè3.5
) viene assegnato ai
. Poichéi
è unint
, il valore3.5
viene troncato a3
durante la conversione dadouble
aint
. Alla fine,i
vale3
ed
vale3.5
. -
Spiega cosa succede in ognuno dei test
if
:if (42 = i) // ... if (i = 42) // ...
Soluzione: Nel primo test
if (42 = i)
, si tenta di assegnare il valore dii
al letterale42
. Questo è un errore perché i letterali non sono l-value modificabili; non possono essere assegnati. Pertanto, questo codice non compila e genera un errore. Nel secondo testif (i = 42)
, si assegna il valore42
alla variabilei
. L'operazione di assegnamento restituisce il valore assegnato, che è42
. Poiché42
è un valore diverso da zero, la condizione risulta esseretrue
, e il blocco di codice all'interno dell'if
verrà eseguito. Tuttavia, questo comportamento potrebbe non essere quello che il programmatore intendeva, poiché probabilmente voleva confrontarei
con42
usando l'operatore di uguaglianza (==
) invece di assegnare42
ai
. -
Il seguente assegnamento è illegale. Perché? Come lo correggeresti?
double dval; int ival; int *pi; dval = ival = pi = 0;
Soluzione: L'assegnamento
dval = ival = pi = 0;
è illegale perchéival
epi
hanno tipi diversi e non c'è una conversione valida tra questi tipi. In particolare,ival
è di tipoint
, mentrepi
è di tipoint*
(puntatore a int). Non è possibile assegnare un puntatore a un intero direttamente. Per correggere questo problema, possiamo separare gli assegnamenti in modo che ogni variabile venga assegnata correttamente con il tipo appropriato. Ad esempio:int *pi = nullptr; // Assegna un puntatore nullo a pi int ival = 0; // Assegna 0 a ival double dval = 0.0; // Assegna 0.0 a dval
-
Sebbene le seguenti righe siano legali, probabilmente non si comportano come il programmatore si aspetta. Perché? Riscrivi le espressioni come pensi dovrebbero essere.
if (p = getPtr() != 0) if (i = 1024)
Soluzione: Nella prima riga
if (p = getPtr() != 0)
, l'operatore di uguaglianza (!=
) ha una precedenza più alta rispetto all'operatore di assegnamento (=
). Quindi, l'espressione viene valutata comeif (p = (getPtr() != 0))
. Questo significa che il risultato della condizionegetPtr() != 0
(che è un valore booleano) viene assegnato ap
. SegetPtr()
restituisce un puntatore non nullo, la condizione saràtrue
(1), e se restituisce un puntatore nullo, la condizione saràfalse
(0). Tuttavia, questo non è probabilmente ciò che il programmatore intendeva. Per correggere questo, possiamo riscrivere l'espressione come:if ((p = getPtr()) != 0)
Nella seconda riga
if (i = 1024)
, l'operatore di assegnamento (=
) viene usato invece dell'operatore di uguaglianza (==
). Quindi, l'espressione assegna il valore1024
ai
e poi valuta la condizione. Poiché1024
è un valore diverso da zero, la condizione sarà sempretrue
, il che potrebbe non essere ciò che il programmatore intendeva. Per correggere questo, possiamo riscrivere l'espressione come:if (i == 1024)