Istruzioni Condizionali in C++

Il linguaggio C++ fornisce due istruzioni che permettono l'esecuzione condizionale.

L'istruzione if determina il flusso di controllo in base a una condizione. L'istruzione switch valuta un'espressione intero e sceglie uno tra diversi percorsi di esecuzione in base al valore di un'espressione.

Concetti Chiave
  • Il linguaggio C++ fornisce due istruzioni che permettono l'esecuzione condizionale: if e switch.
  • L'istruzione if determina il flusso di controllo in base a una condizione.
  • L'istruzione else fornisce un percorso alternativo quando la condizione dell'if è falsa.
  • L'istruzione switch valuta un'espressione intero e sceglie uno tra diversi percorsi di esecuzione in base al valore di un'espressione.

Istruzione if

Un'istruzione if esegue condizionalmente un'altra istruzione in base al fatto che una condizione specificata sia vera. Esistono due forme di if: una con un ramo else e una senza. La forma sintattica del semplice if è

if (condizione) istruzione;

Un'istruzione if else ha, invece, la forma

if (condizione) istruzione else istruzione2;

In entrambe le versioni, condizione deve essere racchiusa tra parentesi. condizione può essere un'espressione o una dichiarazione di variabile inizializzata. L'espressione o la variabile deve avere un tipo convertibile in bool. Come al solito, sia istruzione che istruzione2 possono essere un blocco.

Se condizione è vera, allora istruzione viene eseguita. Dopo il completamento di istruzione, l'esecuzione continua con l'istruzione successiva all'if.

Se condizione è falsa, istruzione viene saltata. In un semplice if, l'esecuzione continua con l'istruzione successiva all'if. In un if else, viene eseguita istruzione2.

Utilizzo di un'Istruzione if else

Per illustrare un'istruzione if, calcoleremo un voto in lettere da un voto numerico. Supponiamo che i voti numerici vadano da zero a 100 inclusi. Un voto di 100 ottiene un "A++", i voti sotto 60 ottengono una "F", e gli altri sono raggruppati in fasce di dieci: i voti da 60 a 69 inclusi ottengono una "D", da 70 a 79 una "C", e così via. Useremo un vector<string> per contenere i possibili voti in lettere:

const vector<string> voti = {"F", "D", "C", "B", "A", "A++"};

Per risolvere questo problema, possiamo usare un'istruzione if else per eseguire azioni diverse per voti insufficienti e sufficienti:

// se il voto è inferiore a 60 è una F, altrimenti calcola un indice
string voto_lettera;
if (voto < 60)
    voto_lettera = voti[0];
else
    voto_lettera = voti[(voto - 50)/10];

A seconda del valore di voto, eseguiamo l'istruzione dopo l'if o quella dopo l'else. Nell'else, calcoliamo un indice dal voto riducendo il voto per tenere conto della gamma più ampia di voti insufficienti. Quindi usiamo la divisione intera, che tronca il resto, per calcolare l'indice appropriato di voti.

Istruzioni if Annidate

Per rendere il nostro programma più interessante, aggiungeremo un più o un meno ai voti sufficienti. Daremo un più ai voti che terminano con 8 o 9, e un meno a quelli che terminano con 0, 1 o 2:

if (voto % 10 > 7)
    voto_lettera += '+'; // i voti che terminano con 8 o 9 ottengono un +
else if (voto % 10 < 3)
    voto_lettera += '-'; // quelli che terminano con 0, 1 o 2 ottengono un -

Qui usiamo l'operatore modulo per ottenere il resto e decidere in base al resto se aggiungere più o meno.

Successivamente incorporeremo il codice che aggiunge un più o un meno al codice che recupera il voto in lettere da voti:

// se il voto è insufficiente,
// non c'è bisogno di controllare più o meno
if (voto < 60)
    voto_lettera = voti[0];
else {
    voto_lettera = voti[(voto - 50)/10]; // recupera il voto in lettere

    // aggiungi più o meno solo se non è già un A++
    if (voto != 100)
        if (voto % 10 > 7)
            // i voti che terminano con 8 o 9 ottengono un +
            voto_lettera += '+';
        else if (voto % 10 < 3)
            // quelli che terminano con 0, 1 o 2 ottengono un -
            voto_lettera += '-';
}

Nota che usiamo un blocco per racchiudere le due istruzioni che seguono il primo else. Se il voto è 60 o superiore, abbiamo due azioni da eseguire: recuperare il voto in lettere da voti e impostare condizionalmente il più o il meno.

Attenzione alle Parentesi Graffe

È un errore comune dimenticare le parentesi graffe quando più istruzioni devono essere eseguite come un blocco. Nell'esempio seguente, contrariamente all'indentazione, il codice per aggiungere un più o un meno viene eseguito incondizionatamente:

if (voto < 60)
    voto_lettera = voti[0];
else // ERRORE: manca la parentesi graffa
    voto_lettera = voti[(voto - 50)/10];
    // nonostante le apparenze, senza la parentesi graffa, questo codice viene sempre eseguito
    // i voti insufficienti otterranno erroneamente un - o un +
    if (voto != 100)
        if (voto % 10 > 7)
            // i voti che terminano con 8 o 9 ottengono un +
            voto_lettera += '+';
        else if (voto % 10 < 3)
            // quelli che terminano con 0, 1 o 2 ottengono un -
            voto_lettera += '-';

Scovare questo errore può essere molto difficile perché il programma sembra corretto.

Per evitare tali problemi, alcuni stili di codifica raccomandano di usare sempre le parentesi graffe dopo un if o un else (e anche intorno ai corpi delle istruzioni while e for).

Facendo così si evita qualsiasi possibile confusione. Significa anche che le parentesi graffe sono già presenti se modifiche successive del codice richiedono l'aggiunta di istruzioni.

Molti editor e ambienti di sviluppo hanno strumenti per indentare automaticamente il codice sorgente in modo che corrisponda alla sua struttura. È una buona idea usare tali strumenti se sono disponibili.

else Pendente

Quando annidiamo un if dentro un altro if, è possibile che ci siano più rami if che rami else. Infatti, il nostro programma di valutazione ha quattro if e due else. Si pone la questione: Come facciamo a sapere a quale if appartiene un dato else?

Questo problema, solitamente chiamato else pendente (dangling else in inglese), è comune a molti linguaggi di programmazione che hanno sia istruzioni if che if else. Linguaggi diversi risolvono questo problema in modi diversi. In C++ l'ambiguità viene risolta specificando che ogni else viene abbinato all'if precedente non abbinato più vicino.

I programmatori a volte si mettono nei guai quando scrivono codice che contiene più if che rami else. Per illustrare il problema, riscriveremo l'if else più interno che aggiunge un più o un meno usando un insieme diverso di condizioni:

// SBAGLIATO: l'esecuzione NON corrisponde all'indentazione;
// l'else va con l'if interno
if (voto % 10 >= 3)
    if (voto % 10 > 7)
        voto_lettera += '+'; // i voti che terminano con 8 o 9 ottengono un +
else
    voto_lettera += '-'; // i voti che terminano con 3, 4, 5, 6 o 7 ottengono un meno!

L'indentazione nel nostro codice indica che intendiamo che l'else vada con l'if esterno, ossia intendiamo che il ramo else venga eseguito quando il voto termina con una cifra inferiore a 3. Tuttavia, nonostante le nostre intenzioni, e contrariamente all'indentazione, il ramo else fa parte dell'if interno. Questo codice aggiunge un '-' ai voti che terminano da 3 a 7 inclusi! Indentato correttamente per corrispondere al percorso di esecuzione effettivo, quello che abbiamo scritto è:

// l'indentazione corrisponde al percorso di esecuzione, non all'intento del programmatore
if (voto % 10 >= 3)
    if (voto % 10 > 7)
        // i voti che terminano con 8 o 9 ottengono un +
        voto_lettera += '+';
    else
        // i voti che terminano con 3, 4, 5, 6 o 7 ottengono un meno!
        voto_lettera += '-';

Controllo del Percorso di Esecuzione con le Parentesi Graffe

Possiamo far sì che l'else faccia parte dell'if esterno racchiudendo l'if interno in un blocco:

// aggiungi un più per i voti che terminano con 8 o 9 e un meno per quelli che terminano con 0, 1 o 2
if (voto % 10 >= 3) {
    if (voto % 10 > 7)
        // i voti che terminano con 8 o 9 ottengono un +
        voto_lettera += '+';
} else // le parentesi graffe forzano l'else ad andare con l'if esterno
    // i voti che terminano con 0, 1 o 2 otterranno un meno
    voto_lettera += '-';

Le istruzioni non attraversano i confini dei blocchi, quindi l'if interno termina alla parentesi graffa di chiusura prima dell'else. L'else non può far parte dell'if interno. Ora, l'if non abbinato più vicino è l'if esterno, che è ciò che intendevamo fin dall'inizio.

Esercizi

  • Usando un'istruzione if–else, scrivi la tua versione del programma per generare il voto in lettere da un voto numerico.

    Soluzione:

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
        const string voti[] = {"F", "D", "C", "B", "A", "A++"};
        int voto;
        cout << "Inserisci un voto numerico (0-100): ";
        cin >> voto;
    
        string voto_lettera;
        if (voto < 60)
            voto_lettera = voti[0];
        else {
            voto_lettera = voti[(voto - 50) / 10];
            if (voto != 100) {
                if (voto % 10 > 7)
                    voto_lettera += '+';
                else if (voto % 10 < 3)
                    voto_lettera += '-';
            }
        }
    
        cout << "Il voto in lettere è: " << voto_lettera << endl;
        return 0;
    }
    
  • Riscrivi il tuo programma di valutazione per usare l'operatore condizionale al posto dell'istruzione if–else.

    Soluzione:

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
        const string voti[] = {"F", "D", "C", "B", "A", "A++"};
        int voto;
        cout << "Inserisci un voto numerico (0-100): ";
        cin >> voto;
    
        string voto_lettera;
        voto_lettera = (voto < 60) ? voti[0] : voti[(voto - 50) / 10];
        if (voto != 100) {
            voto_lettera += (voto % 10 > 7) ? '+' : '-';
        }
    
        cout << "Il voto in lettere è: " << voto_lettera << endl;
        return 0;
    }
    
  • Correggi gli errori in ciascuno dei seguenti frammenti di codice:

    • Frammento (a):

      if (val_int1 != val_int2)
          val_int1 = val_int2
      else val_int1 = val_int2 = 0;
      

      Soluzione:

      Manca un punto e virgola alla fine della prima istruzione. Inoltre, l'uso di else senza parentesi graffe può portare a confusione. Ecco la versione corretta:

      if (val_int1 != val_int2)
          val_int1 = val_int2;
      else
          val_int1 = val_int2 = 0;
      
    • Frammento (b):

      if (val_int < val_minimo)
          val_minimo = val_int;
          occorrenze = 1;
      

      Soluzione:

      L'istruzione occorrenze = 1; viene eseguita sempre, indipendentemente dal risultato dell'if. Per far sì che venga eseguita solo quando la condizione è vera, racchiudiamo le istruzioni in un blocco:

      if (val_int < val_minimo) {
          val_minimo = val_int;
          occorrenze = 1;
      }
      
    • Frammento (c):

      if (int val_int = ottieni_valore())
          cout << "val_int = " << val_int << endl;
      if (!val_int)
          cout << "val_int = 0\n";
      

      Soluzione:

      La variabile val_int è definita all'interno del primo if, quindi non è accessibile nel secondo if. Per risolvere questo problema, dobbiamo definire val_int al di fuori dell'if:

      int val_int = ottieni_valore();
      if (val_int)
          cout << "val_int = " << val_int << endl;
      if (!val_int)
          cout << "val_int = 0\n";
      
    • Frammento (d):

      if (val_int = 0)
          val_int = ottieni_valore();
      

      Soluzione:

      L'operatore di assegnazione = viene usato invece dell'operatore di confronto ==. La condizione dovrebbe essere val_int == 0:

      if (val_int == 0)
          val_int = ottieni_valore();
      

L'Istruzione switch

Un'istruzione switch fornisce un modo conveniente per selezionare tra un numero (possibilmente grande) di alternative fisse. Come esempio, supponiamo di voler contare quante volte ciascuna delle cinque vocali appare in un segmento di testo. La logica del nostro programma è la seguente:

  • Leggi ogni carattere nell'input.
  • Confronta ogni carattere con l'insieme delle vocali.
  • Se il carattere corrisponde a una delle vocali, aggiungi 1 al conteggio di quella vocale.
  • Visualizza i risultati.

Ad esempio, quando eseguiamo il programma su di un testo di esempio, il possibile output è:

Numero di vocali a: 123
Numero di vocali e: 274
Numero di vocali i: 157
Numero di vocali o: 546
Numero di vocali u: 98

Possiamo risolvere il nostro problema nel modo più diretto usando un'istruzione switch per confrontare ogni carattere con ciascuna delle cinque vocali:

// inizializza i contatori per ogni vocale
unsigned cont_a = 0, cont_e = 0, cont_i = 0, cont_o = 0, cont_u = 0;

char car;

while (cin >> car) {
    // se car è una vocale, incrementa il contatore appropriato
    switch (car) {
        case 'a':
            ++cont_a;
            break;
        case 'e':
            ++cont_e;
            break;
        case 'i':
            ++cont_i;
            break;
        case 'o':
            ++cont_o;
            break;
        case 'u':
            ++cont_u;
            break;
    }
}

// stampa i risultati
cout << "Numero di vocali a: \t" << cont_a << '\n'
     << "Numero di vocali e: \t" << cont_e << '\n'
     << "Numero di vocali i: \t" << cont_i << '\n'
     << "Numero di vocali o: \t" << cont_o << '\n'
     << "Numero di vocali u: \t" << cont_u << endl;

Un'istruzione switch viene eseguita valutando l'espressione tra parentesi che segue la parola chiave switch. Quell'espressione può essere una dichiarazione di variabile inizializzata. L'espressione viene convertita in tipo intero. Il risultato dell'espressione viene confrontato con il valore associato a ogni case.

Se l'espressione corrisponde al valore di un'etichetta case, l'esecuzione inizia con la prima istruzione che segue quell'etichetta. L'esecuzione continua normalmente da quell'istruzione fino alla fine dello switch o fino a un'istruzione break.

Poiché l'esecuzione continua fino a un break o alla fine dello switch, di solito vogliamo includere un break dopo l'ultima istruzione di ogni case. Tuttavia, ci sono situazioni in cui il comportamento predefinito dello switch è esattamente quello di cui abbiamo bisogno. Ogni etichetta case può avere un solo valore, ma a volte abbiamo due o più valori che condividono un insieme comune di azioni. In tali casi, omettiamo un'istruzione break, permettendo al programma di attraversare più etichette case.

Ad esempio, potremmo voler contare solo il numero totale di vocali:

unsigned cont_vocali = 0;
// ...
switch (car)
{
    // qualsiasi occorrenza di a, e, i, o, o u incrementa cont_vocali
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        ++cont_vocali;
        break;
}

Qui abbiamo impilato diverse etichette case insieme senza alcun break intermedio. Lo stesso codice verrà eseguito ogni volta che car è una vocale.

Poiché i programmi C++ sono a forma libera, le etichette case non devono necessariamente apparire su una nuova riga. Possiamo enfatizzare che i case rappresentano un intervallo di valori elencandoli tutti su una singola riga:

switch (car)
{
    // sintassi alternativa legale
    case 'a': case 'e': case 'i': case 'o': case 'u':
        ++cont_vocali;
        break;
}

Omettere un break alla fine di un case accade raramente. Se lo ometti, includi un commento che spieghi la logica.

Dimenticare un break è una fonte comune di bug

È un malinteso comune pensare che vengano eseguite solo le istruzioni associate all'etichetta case corrispondente. Ad esempio, ecco un'implementazione errata della nostra istruzione switch per contare le vocali:

// attenzione: deliberatamente errato!
switch (car) {
    case 'a':
        ++cont_a; // oops: dovrebbe avere un'istruzione break
    case 'e':
        ++cont_e; // oops: dovrebbe avere un'istruzione break
    case 'i':
        ++cont_i; // oops: dovrebbe avere un'istruzione break
    case 'o':
        ++cont_o; // oops: dovrebbe avere un'istruzione break
    case 'u':
        ++cont_u;
}

Per capire cosa succede, supponiamo che il valore di car sia 'e'. L'esecuzione salta al codice che segue l'etichetta case 'e', che incrementa cont_e. L'esecuzione continua attraverso le etichette case, incrementando anche cont_i, cont_o e cont_u.

Sebbene non sia necessario includere un break dopo l'ultima etichetta di uno switch, la soluzione più sicura è fornirne uno. In questo modo, se successivamente viene aggiunto un case addizionale, il break è già presente.

L'Etichetta default

Le istruzioni che seguono l'etichetta default vengono eseguite quando nessuna etichetta case corrisponde al valore dell'espressione dello switch. Ad esempio, potremmo aggiungere un contatore per tenere traccia di quante non-vocali leggiamo. Incrementeremo questo contatore, che chiameremo cont_altro, nel case default:

// se car è una vocale, incrementa il contatore appropriato
switch (car) {
    case 'a': case 'e': case 'i': case 'o': case 'u':
        ++cont_vocali;
        break;
    default:
        ++cont_altro;
        break;
}

In questa versione, se car non è una vocale, l'esecuzione inizierà dall'etichetta default e incrementeremo cont_altro.

Può essere utile definire un'etichetta default anche se non c'è lavoro da fare per il case default. Definire una sezione default vuota indica ai lettori successivi che il caso è stato considerato.

Un'etichetta non può stare da sola; deve precedere un'istruzione o un'altra etichetta case. Se uno switch termina con un case default che non ha lavoro da fare, allora l'etichetta default deve essere seguita da un'istruzione nulla o da un blocco vuoto.

Definizioni di Variabili all'Interno del Corpo di uno switch

Come abbiamo visto, l'esecuzione in uno switch può saltare attraverso le etichette case. Quando l'esecuzione salta a un particolare case, qualsiasi codice che si trovava dentro lo switch prima di quell'etichetta viene ignorato. Il fatto che il codice venga saltato solleva una domanda interessante: Cosa succede se il codice che viene saltato include una definizione di variabile?

La risposta è che è illegale saltare da un punto in cui una variabile con un inizializzatore è fuori scope a un punto in cui quella variabile è in scope:

case true:
    // questa istruzione switch è illegale perché
    // queste inizializzazioni potrebbero essere saltate

    // ERRORE: il controllo salta una variabile inizializzata implicitamente
    string nome_file;

    // ERRORE: il controllo salta una variabile inizializzata esplicitamente
    int val_int = 0;

    // OK: perché val_j non è inizializzata
    int val_j;
    break;
case false:
    // OK: val_j è in scope ma non è inizializzata
    val_j = prossimo_num(); // OK: assegna un valore a val_j
    if (nome_file.empty()) // nome_file è in scope ma non è stata inizializzata
        // ...

Se questo codice fosse legale, allora ogni volta che il controllo saltasse al case false, salterebbe l'inizializzazione di nome_file e val_int. Quelle variabili sarebbero in scope. Il codice che segue false potrebbe usare quelle variabili. Tuttavia, queste variabili non sarebbero state inizializzate. Di conseguenza, il linguaggio non ci permette di saltare oltre un'inizializzazione se la variabile inizializzata è in scope nel punto in cui il controllo viene trasferito.

Se abbiamo bisogno di definire e inizializzare una variabile per un particolare case, possiamo farlo definendo la variabile dentro un blocco, assicurando così che la variabile sia fuori scope nel punto di qualsiasi etichetta successiva.

case true:
    {
        // OK: dichiarazione di istruzione all'interno di un blocco di istruzioni
        string nome_file = ottieni_nome_file();
        // ...
    }
    break;
case false:
    if (nome_file.empty()) // ERRORE: nome_file non è in scope
        // ...

Esercizi

  • Scrivi un programma usando una serie di istruzioni if per contare il numero di vocali nel testo letto da cin.

    Soluzione:

    #include <iostream>
    
    using namespace std;
    
    int main() {
        unsigned cont_a = 0, cont_e = 0, cont_i = 0, cont_o = 0, cont_u = 0;
        char car;
    
        while (cin >> car) {
            if (car == 'a')
                ++cont_a;
            else if (car == 'e')
                ++cont_e;
            else if (car == 'i')
                ++cont_i;
            else if (car == 'o')
                ++cont_o;
            else if (car == 'u')
                ++cont_u;
        }
    
        cout << "Numero di vocali a: \t" << cont_a << endl
             << "Numero di vocali e: \t" << cont_e << endl
             << "Numero di vocali i: \t" << cont_i << endl
             << "Numero di vocali o: \t" << cont_o << endl
             << "Numero di vocali u: \t" << cont_u << endl;
    
        return 0;
    }
    
  • C'è un problema con il nostro programma di conteggio delle vocali come l'abbiamo implementato: Non conta le lettere maiuscole come vocali. Scrivi un programma che conti sia le lettere minuscole che maiuscole come la vocale appropriata, cioè, il tuo programma dovrebbe contare sia 'a' che 'A' come parte di cont_a, e così via.

    Soluzione:

    #include <iostream>
    #include <cctype>
    
    using namespace std;
    
    int main() {
        unsigned cont_a = 0, cont_e = 0, cont_i = 0, cont_o = 0, cont_u = 0;
        char car;
    
        while (cin >> car) {
            car = tolower(car); // Converti il carattere in minuscolo
            if (car == 'a')
                ++cont_a;
            else if (car == 'e')
                ++cont_e;
            else if (car == 'i')
                ++cont_i;
            else if (car == 'o')
                ++cont_o;
            else if (car == 'u')
                ++cont_u;
        }
    
        cout << "Numero di vocali a: \t" << cont_a << endl
             << "Numero di vocali e: \t" << cont_e << endl
             << "Numero di vocali i: \t" << cont_i << endl
             << "Numero di vocali o: \t" << cont_o << endl
             << "Numero di vocali u: \t" << cont_u << endl;
    
        return 0;
    }
    

    In questa versione, usiamo la funzione tolower dalla libreria <cctype> per convertire ogni carattere in minuscolo prima di confrontarlo.

  • Modifica il nostro programma di conteggio delle vocali in modo che conti anche il numero di spazi vuoti, tabulazioni e caratteri di nuova riga letti.

    Soluzione:

    #include <iostream>
    #include <cctype>
    
    using namespace std;
    
    int main() {
        unsigned cont_a = 0, cont_e = 0, cont_i = 0, cont_o = 0, cont_u = 0;
        unsigned cont_spazi = 0;
        char car;
    
        // Usa cin.get per leggere tutti i caratteri, inclusi spazi e nuove righe
        while (cin.get(car)) {
            if (isspace(car)) {
                ++cont_spazi; // Conta spazi, tabulazioni e nuove righe
            } else {
                car = tolower(car); // Converti il carattere in minuscolo
                if (car == 'a')
                    ++cont_a;
                else if (car == 'e')
                    ++cont_e;
                else if (car == 'i')
                    ++cont_i;
                else if (car == 'o')
                    ++cont_o;
                else if (car == 'u')
                    ++cont_u;
            }
        }
    
        cout << "Numero di vocali a: \t" << cont_a << endl
             << "Numero di vocali e: \t" << cont_e << endl
             << "Numero di vocali i: \t" << cont_i << endl
             << "Numero di vocali o: \t" << cont_o << endl
             << "Numero di vocali u: \t" << cont_u << endl
             << "Numero di spazi vuoti, tabulazioni e nuove righe: \t"
             << cont_spazi << endl;
    
        return 0;
    }
    

    In questa versione, usiamo cin.get(car) per leggere ogni carattere, inclusi spazi e nuove righe. Usiamo la funzione isspace per verificare se un carattere è uno spazio vuoto, una tabulazione o una nuova riga.