Operatori di Incremento e Decremento in C++
- Gli operatori di incremento (
++
) e decremento (--
) forniscono una comoda abbreviazione notazionale per aggiungere o sottrarre 1 da un oggetto. - Ci sono due forme di questi operatori: prefissa e postfissa.
- Gli operatori prefissi incrementano (o decrementano) il loro operando e producono l'oggetto modificato come risultato.
- Gli operatori postfissi incrementano (o decrementano) l'operando ma producono una copia del valore originale, non modificato come risultato.
- Questi operatori richiedono operandi l-value.
- Gli operatori prefissi restituiscono l'oggetto stesso come l-value.
- Gli operatori postfissi restituiscono una copia del valore originale dell'oggetto come r-value.
- Le versioni postfisse di
++
e--
vengono usate quando vogliamo usare il valore corrente di una variabile e incrementarla in una singola espressione composta.
Operatori di Incremento e Decremento
Gli operatori di incremento (++
) e decremento (--
) forniscono una comoda abbreviazione notazionale per aggiungere o sottrarre 1 da un oggetto. Questa notazione va oltre la mera convenienza quando usiamo questi operatori con gli iteratori, perché molti iteratori non supportano l'aritmetica.
Ci sono due forme di questi operatori: prefissa e postfissa. Finora, abbiamo usato solo la forma prefissa. Questa forma incrementa (o decrementa) il suo operando e produce l'oggetto modificato come risultato. Gli operatori postfissi incrementano (o decrementano) l'operando ma producono una copia del valore originale, non modificato come risultato:
int i = 0, j;
// j = 1, i = 1: il prefisso produce il valore incrementato
j = ++i;
// j = 1, i = 2: il postfisso produce il valore non incrementato
j = i++;
Questi operatori richiedono operandi l-value. Gli operatori prefissi restituiscono l'oggetto stesso come l-value. Gli operatori postfissi restituiscono una copia del valore originale dell'oggetto come r-value.
Usare gli operatori postfissi solo quando necessario
I lettori con un background in linguaggio C potrebbero essere sorpresi che usiamo l'incremento prefisso nei programmi che abbiamo scritto. La ragione è semplice: La versione prefissa evita lavoro non necessario. Incrementa il valore e restituisce la versione incrementata. L'operatore postfisso deve memorizzare il valore originale in modo da poter restituire il valore non incrementato come risultato. Se non abbiamo bisogno del valore non incrementato, non c'è bisogno del lavoro extra fatto dall'operatore postfisso.
Per int
e puntatori, il compilatore può ottimizzare via questo lavoro extra. Per tipi iteratori più complicati, questo lavoro extra potrebbe potenzialmente essere più costoso. Usando abitualmente le versioni prefisse, non dobbiamo preoccuparci se la differenza di prestazioni conta. Inoltre possiamo esprimere l'intento dei nostri programmi in maniera più diretta.
Combinare Dereferenziazione e Incremento in una Singola Espressione
Le versioni postfisse di ++
e --
vengono usate quando vogliamo usare il valore corrente di una variabile e incrementarla in una singola espressione composta.
Come esempio, possiamo usare l'incremento postfisso per scrivere un ciclo per stampare i valori in un vector
fino a, ma non includendo, il primo valore negativo:
auto pbeg = v.begin();
// stampa gli elementi fino al primo valore negativo
while (pbeg != v.end() && *pbeg >= 0)
// stampa il valore corrente e avanza pbeg
cout << *pbeg++ << endl;
L'espressione *pbeg++
solitamente crea confusione per i programmatori nuovi sia a C++ che a C. Tuttavia, poiché questo pattern di utilizzo è così comune, i programmatori C++ devono comprendere tali espressioni.
La precedenza dell'incremento postfisso è più alta di quella dell'operatore di dereferenziazione, quindi *pbeg++
è equivalente a *(pbeg++)
. La sottoespressione pbeg++
incrementa pbeg
e produce una copia del valore precedente di pbeg
come risultato. Di conseguenza, l'operando di *
è il valore non incrementato di pbeg
. Quindi, l'istruzione stampa l'elemento a cui pbeg
puntava originariamente e incrementa pbeg
.
Questo utilizzo si basa sul fatto che l'incremento postfisso restituisce una copia del suo operando originale, non incrementato. Se restituisse il valore incrementato, dereferenzieremmo il valore incrementato, con risultati disastrosi. Salteremmo il primo elemento. Peggio, se la sequenza non avesse valori negativi, tenteremmo di dereferenziare un elemento di troppo.
C++ idiomatico e brevità
Espressioni come *pbeg++
possono essere sconcertanti all'inizio. Tuttavia, è un idioma utile e ampiamente usato. Una volta che la notazione è familiare, scrivere
cout << *iter++ << endl;
è più facile e meno soggetto a errori dell'equivalente più verboso
cout << *iter << endl;
++iter;
Vale la pena studiare esempi di tale codice finché i loro significati non sono immediatamente chiari. La maggior parte dei programmi C++ usa espressioni succinte piuttosto che equivalenti più verbosi. Pertanto, i programmatori C++ devono essere a proprio agio con tali utilizzi. Inoltre, una volta che queste espressioni sono familiari, le troverai meno soggette a errori.
Gli Operandi Possono Essere Valutati in Qualsiasi Ordine
La maggior parte degli operatori non dà alcuna garanzia sull'ordine in cui gli operandi saranno valutati. Questa mancanza di ordine garantito spesso non conta. I casi in cui conta sono quando una sotto-espressione cambia il valore di un operando che viene usato in un'altra sotto-espressione. Poiché gli operatori di incremento e decremento cambiano i loro operandi, è facile usare male questi operatori in espressioni composte.
Per illustrare il problema, riprendiamo l'esempio usato nella lezione sugli iteratori in cui utilizzavamo un iteratore per capitalizzare la prima parola in una stringa. Quell'esempio usava un ciclo for
:
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it); // capitalizza il carattere corrente
che ci permetteva di separare l'istruzione che dereferenziava beg
da quella che la incrementava. Sostituire il for
con un apparentemente equivalente while
// il comportamento del seguente ciclo è indefinito!
while (beg != s.end() && !isspace(*beg))
// ERRORE: questo assegnamento è indefinito
*beg = toupper(*beg++);
risulta in comportamento indefinito. Il problema è che nella versione rivista, sia gli operandi sinistro che destro di =
usano beg
e l'operando destro cambia beg
. L'assegnamento è quindi indefinito. Il compilatore potrebbe valutare questa espressione come
*beg = toupper(*beg); // esecuzione se il lato sinistro è valutato per primo
*(beg + 1) = toupper(*beg); // esecuzione se il lato destro è valutato per primo
oppure potrebbe valutarla in qualche altro modo ancora.
Esercizi
-
Cosa succederebbe se il ciclo
while
dell'esempio di sopra che stampa gli elementi da unvector
usasse l'operatore di incremento prefisso?auto pbeg = v.begin(); // stampa gli elementi fino al primo valore negativo while (pbeg != v.end() && *pbeg >= 0) // stampa il valore corrente e avanza pbeg cout << *pbeg++ << endl;
Soluzione: Usare l'operatore di incremento prefisso in questo esempio produrrebbe un comportamento errato. L'espressione
*pbeg++
verrebbe interpretata come*(++pbeg)
, il che significa chepbeg
verrebbe incrementato prima della dereferenziazione. Di conseguenza, il primo elemento delvector
verrebbe saltato e il ciclo inizierebbe a stampare dal secondo elemento. Se ilvector
non contenesse valori negativi, si tenterebbe di dereferenziare un elemento oltre la fine delvector
, portando a un comportamento indefinito. -
Dato che
ptr
punta a unint
, chevec
è unvector<int>
, e cheival
è unint
, spiega il comportamento di ciascuna di queste espressioni. Quali, se ce ne sono, è probabile che siano incorrette? Perché? Come potrebbe essere corretto ciascuno?ptr != 0 && *ptr++ ival++ && ival vec[ival++] <= vec[ival]
Soluzione:
ptr != 0 && *ptr++
: Questa espressione è corretta. Valuta septr
non è nullo e, se vero, dereferenziaptr
prima di incrementarlo. Non c'è conflitto tra l'uso e la modifica diptr
perché l'incremento avviene dopo la dereferenziazione.ival++ && ival
: In questo caso, dato che l'operatore&&
valuta il suo operando sinistro prima del destro, l'operando destro utilizza il valore diival
dopo che è stato incrementato. Quindi, questa espressione è corretta e non causa conflitti. Gli operatori logici&&
e||
sono gli unici operatori in C++ che garantiscono un ordine di valutazione specifico.vec[ival++] <= vec[ival]
: Questa espressione è problematica. Non c'è garanzia sull'ordine di valutazione degli operandi di<=
, quindiival
potrebbe essere incrementato prima o dopo che viene usato nel secondo operando. Questo porta a un comportamento indefinito perchéival
viene sia letto che modificato senza un ordine definito. Per correggere questa espressione, si potrebbe separare l'incremento in una riga distinta:
int currentIndex = ival++; bool result = vec[currentIndex] <= vec[ival];