Operatori Aritmetici in C++

Concetti Chiave
  • Gli operatori aritmetici in C++ includono addizione, sottrazione, moltiplicazione, divisione e modulo.
  • La precedenza degli operatori determina l'ordine in cui le operazioni vengono eseguite in un'espressione.
  • Gli operatori aritmetici possono essere unari (es. -x) o binari (es. x + y).
  • La divisione tra interi tronca il risultato verso zero.
  • L'operatore modulo (%) calcola il resto della divisione tra due interi.
  • Le operazioni aritmetiche possono causare overflow, che porta a comportamenti indefiniti.

Operatori Aritmetici

Operatore Funzione Uso Precedenza
+ più unario + expr 3
- meno unario - expr 3
* moltiplicazione expr * expr 2
/ divisione expr / expr 2
% resto expr % expr 2
+ addizione expr + expr 1
- sottrazione expr - expr 1
Tabella 1: Operatori aritmetici in C++ (Associativi a Sinistra)

La tabella di sopra (e le tabelle degli operatori nelle lezioni successive) raggruppa gli operatori in base alla loro precedenza. Gli operatori aritmetici unari hanno precedenza più alta degli operatori di moltiplicazione e divisione, che a loro volta hanno precedenza più alta degli operatori binari di addizione e sottrazione. Gli operatori con precedenza più alta si raggruppano più strettamente degli operatori con precedenza più bassa. Questi operatori sono tutti associativi a sinistra, il che significa che si raggruppano da sinistra a destra quando i livelli di precedenza sono uguali.

Se non diversamente indicato, gli operatori aritmetici possono essere applicati a uno qualsiasi dei tipi aritmetici o a qualsiasi tipo che può essere convertito in un tipo aritmetico. Gli operandi e i risultati di questi operatori sono r-value. Come vedremo nelle prossime lezioni, gli operandi di tipi interi di piccola dimensione (bool, short e char) vengono promossi a un tipo intero più grande, e tutti gli operandi possono essere convertiti in un tipo comune come parte della valutazione di questi operatori.

L'operatore più unario e gli operatori di addizione e sottrazione possono anche essere applicati ai puntatori. Nella lezione sui puntatori abbiamo trattato l'uso dei + e - binari con operandi puntatore. Quando applicato a un puntatore o valore aritmetico, il più unario restituisce una copia (possibilmente promossa) del valore del suo operando.

L'operatore meno unario restituisce il risultato della negazione di una copia (possibilmente promossa) del valore del suo operando:

int i = 1024;
int k = -i; // k è -1024
bool b = true;
bool b2 = -b; // b2 è true!

Nelle lezioni precedenti abbiamo notato che i valori bool non dovrebbero essere usati per i calcoli. Il risultato di -b è un buon esempio di ciò che avevamo in mente.

Per la maggior parte degli operatori, gli operandi di tipo bool vengono promossi a int. In questo caso, il valore di b è true, che viene promosso al valore int 1. Quel valore (promosso) viene negato, producendo -1. Il valore -1 viene riconvertito in bool e usato per inizializzare b2. Questo inizializzatore è un valore diverso da zero, che quando convertito in bool diventa true. Quindi, il valore di b2 è true!

Quando applicati a oggetti di tipi aritmetici, gli operatori aritmetici, +, -, * e /, hanno i loro significati ovvi: addizione, sottrazione, moltiplicazione e divisione. La divisione tra interi restituisce un intero. Se il quoziente contiene una parte frazionaria, viene troncato verso zero:

// ival1 è 3; il risultato è troncato; il resto è scartato
int ival1 = 21 / 6;

// ival2 è 3; nessun resto; il risultato è un valore intero
int ival2 = 21 / 7;

L'operatore %, noto come operatore "resto" o "modulo", calcola il resto che risulta dalla divisione dell'operando sinistro per l'operando destro. Gli operandi di % devono avere tipo intero:

int ival = 42;
double dval = 3.14;
ival % 12; // ok: il risultato è 6
ival % dval; // ERRORE: operando in virgola mobile

In una divisione, un quoziente non zero è positivo se gli operandi hanno lo stesso segno e negativo altrimenti. Le versioni precedenti del linguaggio permettevano che un quoziente negativo fosse arrotondato verso l'alto o verso il basso; lo standard C++11 richiede che il quoziente sia arrotondato verso zero (cioè, troncato).

L'operatore modulo è definito in modo tale che se m e n sono interi e n è diverso da zero, allora (m / n) * n + m % n è uguale a m. Per implicazione, se m % n è diverso da zero, ha lo stesso segno di m. Le versioni precedenti del linguaggio permettevano che m % n avesse lo stesso segno di n nelle implementazioni in cui m / n negativo veniva arrotondato lontano da zero, ma tali implementazioni sono ora vietate. Inoltre, eccetto per il caso oscuro in cui -m va in overflow, (-m) / n e m / (-n) sono sempre uguali a -(m / n), m % (-n) è uguale a m % n, e (-m) % n è uguale a -(m % n). In modo più concreto:

21 % 6;   /* il risultato è 3 */
21 / 6;   /* il risultato è 3 */
21 % 7;   /* il risultato è 0 */
21 / 7;   /* il risultato è 3 */
-21 % -8; /* il risultato è -5 */
-21 / -8; /* il risultato è 2 */
21 % -5;  /* il risultato è 1 */
21 / -5;  /* il risultato è -4 */
Nota

Overflow e altre eccezioni aritmetiche

Alcune espressioni aritmetiche producono risultati indefiniti. Alcune di queste espressioni indefinite sono dovute alla natura della matematica dell'espressione. Ad esempio, la divisione per zero. Altre sono indefinite a causa della natura stessa dei computer, ad esempio, a causa dell'overflow. L'overflow si verifica quando viene calcolato un valore che è al di fuori dell'intervallo di valori che il tipo può rappresentare.

Considera una macchina in cui gli short sono di 16 bit. In quel caso, il valore massimo di short è 32767. Su una tale macchina, la seguente assegnazione composta va in overflow:

// valore massimo se gli short sono a 16 bit
short short_value = 32767;

// questo calcolo va in overflow
short_value += 1;

cout << "short_value: " << short_value << endl;

L'assegnazione a short_value è indefinita. Rappresentare un valore con segno di 32768 richiede 17 bit, ma solo 16 sono disponibili. Su molti sistemi, non c'è nessun avviso a tempo di compilazione o a runtime quando si verifica un overflow. Come per qualsiasi comportamento indefinito, quello che accade è imprevedibile. Sul nostro sistema il programma si completa e scrive

short_value: -32768

Il valore "si è avvolto" (wrap-around): Il bit del segno, che era 0, è stato impostato a 1, risultando in un valore negativo. Su un altro sistema, il risultato potrebbe essere diverso, o il programma potrebbe comportarsi diversamente, incluso il crash completo.

Esercizi

  • Metti tra parentesi la seguente espressione per mostrare come viene valutata. Verifica la tua risposta compilando l'espressione (senza parentesi) e stampando il suo risultato.

    12 / 3 * 4 + 5 * 15 + 24 % 4 / 2
    

    Soluzione:

    (((12 / 3) * 4) + (5 * 15)) + ((24 % 4) / 2)
    
  • Determina il risultato delle seguenti espressioni.

    -30 * 3 + 21 / 5
    

    Soluzione: -87

    -30 + 3 * 21 / 5
    

    Soluzione: -18

    30 / 3 * 21 % 5
    

    Soluzione: 0

    -30 / 3 * 21 % 4
    

    Soluzione: 2

  • Scrivi un'espressione per determinare se un valore int è pari o dispari.

    Soluzione:

    int value = /* qualche valore */;
    if (value % 2 == 0) {
        cout << value << " è pari" << endl;
    } else {
        cout << value << " è dispari" << endl;
    }
    
  • Mostra tre espressioni che andranno in overflow.

    Soluzione:

    short short_value = 32767;
    short_value += 1;
    
    int int_value = 2147483647;
    int_value += 1;
    
    char char_value = 127;
    char_value += 1;