Operatori Logici e Relazionali in C++

Concetti Chiave
  • Gli operatori logici e relazionali in C++ includono AND, OR, NOT e vari operatori di confronto.
  • La precedenza degli operatori determina l'ordine in cui le operazioni vengono eseguite in un'espressione.
  • Gli operatori logici includono && (AND), || (OR) e ! (NOT).
  • Gli operatori relazionali includono <, <=, >, >=, == e !=.
  • Gli operatori logici e relazionali restituiscono valori di tipo bool.
  • La valutazione di cortocircuito si applica agli operatori logici && e ||.

Operatori Logici e Relazionali

Gli operatori relazionali prendono operandi di tipo aritmetico o puntatore; gli operatori logici prendono operandi di qualsiasi tipo che possa essere convertito in bool. Questi operatori restituiscono tutti valori di tipo bool. Gli operandi aritmetici e puntatore con un valore di zero sono false; tutti gli altri valori sono true. Gli operandi di questi operatori sono r-value e il risultato è un r-value.

Associatività Operatore Funzione Uso Precedenza
Destra ! NOT logico !expr 5
Sinistra < Minore di expr < expr 4
Sinistra <= Minore o uguale di expr <= expr 4
Sinistra > Maggiore di expr > expr 4
Sinistra >= Maggiore o uguale di expr >= expr 4
Sinistra == Uguale a expr == expr 3
Sinistra != Non uguale a expr != expr 3
Sinistra && AND logico expr && expr 2
Sinistra || OR logico expr || expr 1
Tabella 1: Operatori Logici e Relazionali in C++

Operatori AND e OR Logici

Il risultato complessivo dell'operatore AND logico è true se e solo se entrambi i suoi operandi valutano true. L'operatore OR logico (||) valuta true se uno qualsiasi dei suoi operandi valuta true.

Gli operatori AND e OR logici valutano sempre il loro operando sinistro prima del destro. Inoltre, l'operando destro viene valutato se e solo se l'operando sinistro non determina il risultato. Questa strategia è nota come valutazione di cortocircuito:

  • Il lato destro di un && viene valutato se e solo se il lato sinistro è true.
  • Il lato destro di un || viene valutato se e solo se il lato sinistro è false.

Diversi programmi che abbiamo scritto in precedenza hanno usato l'operatore AND logico. Quei programmi hanno usato l'operando sinistro per verificare se fosse sicuro valutare l'operando destro. Ad esempio, supponiamo di voler verificare se la divisione tra due numeri interi i e j sia maggiore di 10. Per evitare di dividere per zero, potremmo scrivere:

if (j != 0 && i / j > 10)
    cout << "i/j è maggiore di 10\n";

Il codice controlla prima che j non sia zero. Così abbiamo la garanzia che l'operando destro, ossia la divisione, non verrà valutato a meno che j non sia diverso da zero.

Come esempio che usa l'OR logico, immaginiamo di avere del testo in un vector di stringhe. Vogliamo stampare le stringhe, aggiungendo una nuova riga dopo ogni stringa vuota o dopo una stringa che termina con un punto. Useremo un ciclo for basato su range per elaborare ogni elemento:

// nota s come riferimento a const;
// gli elementi non vengono copiati e non possono essere modificati
for (const auto &s : text) { // per ogni elemento in text
    cout << s; // stampa l'elemento corrente
    // le righe vuote e quelle che terminano
    // con un punto ottengono una nuova riga
    if (s.empty() || s[s.size() - 1] == '.')
         cout << endl;
    else
         cout << " "; // altrimenti separa solo con uno spazio
}

Dopo aver stampato l'elemento corrente, controlliamo se dobbiamo stampare una nuova riga. La condizione nell'if controlla prima se s è una stringa vuota. Se sì, dobbiamo stampare una nuova riga indipendentemente dal valore dell'operando destro. Solo se la stringa non è vuota valutiamo la seconda espressione, che controlla se la stringa termina con un punto. In questa espressione, ci affidiamo alla valutazione di cortocircuito di || per assicurarci di indicizzare s solo se s non è vuota.

Vale la pena notare che abbiamo dichiarato s come riferimento a const. Gli elementi in text sono stringhe e potrebbero essere grandi. Facendo di s un riferimento, evitiamo di copiare gli elementi. Poiché non abbiamo bisogno di scrivere negli elementi, abbiamo fatto di s un riferimento a const.

Operatore NOT Logico

L'operatore NOT logico (!) restituisce l'inverso del valore di verità del suo operando. Come esempio, assumendo che vec sia un vector di int, potremmo usare l'operatore NOT logico per vedere se vec ha elementi negando il valore restituito da empty:

// stampa il primo elemento in vec se ce n'è uno
if (!vec.empty())
     cout << vec[0];

La sottoespressione

!vec.empty()

valuta a true se la chiamata a empty restituisce false.

Gli Operatori Relazionali

Gli operatori relazionali (<, <=, >, >=) hanno i loro significati ordinari e restituiscono valori bool. Questi operatori sono associativi a sinistra.

Poiché gli operatori relazionali restituiscono bool, il risultato del concatenamento di questi operatori è probabilmente sorprendente:

// ERRORE!
// questa condizione confronta k con il risultato bool di (i<j)
if (i < j < k) // true se k è maggiore di 1!

Questa condizione raggruppa i e j al primo operatore <. Il risultato bool di quell'espressione è l'operando sinistro del secondo operatore minore di. Cioè, k viene confrontato con il risultato true/false del primo confronto! Per ottenere il test che intendevamo, possiamo riscrivere l'espressione come segue:

// OK: la condizione è true se i è minore di j e j è minore di k
if (i < j && j < k) { /* ... */ }

Test di Uguaglianza e i Letterali bool

Se vogliamo testare il valore di verità di un oggetto aritmetico o puntatore, il modo più diretto è usare il valore come condizione:

// true se val è qualsiasi valore non zero
if (val) { /* ... */ }

// true se val è zero
if (!val) { /* ... */ }

In entrambe le condizioni, il compilatore converte val in bool. La prima condizione ha successo finché val è diverso da zero; la seconda ha successo se val è zero.

Potremmo pensare di poter riscrivere un test di questo tipo come

// true solo se val è uguale a 1!
if (val == true) { /* ... */ }

Ci sono due problemi con questo approccio. Primo, è più lungo e meno diretto del codice precedente (anche se ammettendo che quando si impara C++ per la prima volta questo tipo di abbreviazione può essere sconcertante). Molto più importante, quando val non è un bool, questo confronto non funziona come previsto.

Se val non è un bool, allora true viene convertito nel tipo di val prima che l'operatore == venga applicato. Cioè, quando val non è un bool, è come se avessimo scritto

if (val == 1) { /* ... */ }

Come abbiamo visto, quando un bool viene convertito in un altro tipo aritmetico, false si converte in 0 e true si converte in 1. Se ci importasse davvero se val fosse lo specifico valore 1, dovremmo scrivere la condizione per testare quel caso direttamente.

Di solito è una cattiva idea usare i letterali booleani true e false come operandi in un confronto. Questi letterali dovrebbero essere usati solo per confrontare con un oggetto di tipo bool.

Esercizi

  • Spiega il comportamento della condizione nel seguente if:

    const char *cp = "Hello World";
    if (cp && *cp)
    

    Soluzione: La condizione verifica che cp non sia un puntatore nullo e che il primo carattere a cui cp punta non sia il carattere nullo ('\0'). In altre parole, controlla che cp punti a una stringa valida e non vuota.

  • Scrivi la condizione per un ciclo while che leggerebbe int dallo standard input e si fermerebbe quando il valore letto è uguale a 42.

    Soluzione:

    int val;
    while (cin >> val && val != 42) {
        // ...
    }
    
  • Scrivi un'espressione che testa quattro valori, a, b, c e d, e assicura che a sia maggiore di b, che è maggiore di c, che è maggiore di d.

    Soluzione:

    if (a > b && b > c && c > d) {
        // ...
    }
    
  • Assumendo che i, j e k siano tutti int, spiega cosa significa i != j < k.

    Soluzione: L'espressione i != j < k viene valutata come i != (j < k). Prima viene valutato j < k, che restituisce un valore booleano (true o false). Poi, questo valore booleano viene confrontato con i usando l'operatore !=. Quindi, l'espressione verifica se i è diverso dal risultato del confronto j < k.