Vai al contenuto
Topos Uranos

Riepilogo

Un programma che si limita a concatenare istruzioni fa sempre la stessa cosa, quali che siano i suoi dati; perché risponda ad essi, deve decidere. Questa lezione studia le decisioni di C++: if ed else, che scelgono tra due cammini secondo una condizione; le catene di else if, che scelgono tra più cammini, e nelle quali l’ordine delle condizioni decide quale caso tratta ciascun ramo; le decisioni annidate e la trappola dell’else pendente; e switch con enum class, che scelgono tra valori discreti con nome, con un compilatore che avvisa se manca un caso o se uno cade nel successivo. Il concetto che unifica tutto è l’analisi per casi: una decisione corretta partiziona l’insieme degli input in casi disgiunti che lo coprono completamente, e ogni caso si verifica con un test al suo interno e un altro su ciascuno dei suoi bordi.

Obiettivi di apprendimento

  1. Scrivere decisioni semplici, concatenate e annidate che trattino tutti i casi di una specifica.
  2. Verificare che i casi di una catena di if ed else if siano disgiunti ed esaustivi, e progettare un input di test per ogni caso.
  3. Usare switch con enum class per valori discreti, e spiegare di che cosa avvisa il compilatore se manca un caso.
  4. Individuare l’else pendente e la caduta tra i casi di uno switch, e correggerli.

if ed else

La forma di una decisione

La lezione Dal problema all’algoritmo ha presentato tre forme con cui si costruisce ogni algoritmo: la sequenza, la decisione e la ripetizione. Finora i programmi del corso sono stati sequenze, salvo qualche if anticipato nell’unità 1; questa lezione scrive in C++ la seconda forma. Una decisione valuta una condizione, cioè un’espressione di tipo bool come quelle della lezione Relazioni e logica, ed esegue un blocco se la condizione è vera e un altro se è falsa:

Codice C++
if (condizione) {    // si esegue se la condizione è vera} else {    // si esegue se la condizione è falsa}

La condizione va sempre tra parentesi. La parte else è facoltativa: senza di essa, se la condizione è falsa non si fa nulla e il programma prosegue con l’istruzione successiva alla decisione. In entrambi i casi i due cammini si riuniscono alla fine, così che la decisione, vista dall’esterno, è una sola istruzione con un ingresso e un’uscita, come esigeva la lezione 1.1.

EsempioIl resto euclideo

La lezione Aritmetica ha mostrato che a % b ha il segno del dividendo, così che con a=−7a = -7 e b=2b = 2 vale −1-1, mentre il resto euclideo, che non è mai negativo, vale 1; e ha annunciato che il modo sicuro di correggerlo è sommare bb solo quando il resto è negativo. Il programma suppone b>0b > 0.

Programma in C++
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    int r{a % b};    if (r < 0) {        r = r + b;    }    std::println("Resto euclideo di {} diviso {}: {}", a, b, r);}
Input del programma
-7 2
Output
Resto euclideo di -7 diviso 2: 1

La variabile r non è più costante, perché la decisione può modificarla. La figura segue i due cammini possibili.

Dimostrazione

  1. L’esecuzione arriva alla decisione con r già calcolato, e la prima cosa che fa è valutare la condizione r < 0: il rombo è l’unico punto del programma in cui i cammini si separano.

  2. Se la condizione è vera, come con −7-7 e 2, si esegue il blocco dell’if: r passa da −1-1 a 1.

  3. Se è falsa, come con 7 e 2, il blocco viene saltato per intero, e r conserva il suo valore.

  4. Entrambi i cammini si riuniscono nello stesso punto, e da lì il programma prosegue allo stesso modo nei due casi: l’istruzione che scrive il risultato si esegue sempre.

Con else, la decisione sceglie tra due azioni che si escludono. La condizione di anno bisestile della lezione Relazioni e logica produce un bool; una decisione lo trasforma in un comportamento del programma.

Programma in C++
#include <iostream>#include <print>​int main(){    int year{};    std::cin >> year;    const bool isLeap{(year % 4 == 0 && year % 100 != 0) || year % 400 == 0};    if (isLeap) {        std::println("{} è bisestile: febbraio ha 29 giorni.", year);    } else {        std::println("{} non è bisestile: febbraio ha 28 giorni.", year);    }}
Input del programma
1900
Output
1900 non è bisestile: febbraio ha 28 giorni.

Dare un nome alla condizione, come isLeap, separa ciò che si decide da ciò che si fa con la decisione.

Sempre le graffe

La grammatica di C++ permette di omettere le parentesi graffe quando il blocco ha una sola istruzione: if (r < 0) r = r + b; è valido. Ora, senza graffe la decisione governa solo l’istruzione successiva, e ciò che il lettore crede governato dipende dall’indentazione, che il compilatore ignora. Se è così, ogni modifica successiva (aggiungere una seconda istruzione, commentare la prima) può cambiare il significato senza che il testo lo mostri. Ne segue la regola del corso: il blocco di un if o di un else porta sempre le graffe, anche se contiene una sola istruzione.

L’errore più breve di tutti conferma la regola. Un punto e virgola dopo le parentesi è un’istruzione vuota, e la decisione governa quell’istruzione vuota; il blocco che segue non appartiene più all’if e si esegue sempre.

Programma in C++
#include <print>​int main(){    const int stock{12};    if (stock == 0);    {        std::println("Prodotto esaurito");    }    std::println("Unità disponibili: {}", stock);}
Avvisi del compilatore
main.cpp: In function ‘int main()’:main.cpp:6:20: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]    6 |     if (stock == 0);      |                    ^main.cpp:6:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]    6 |     if (stock == 0);      |     ^~main.cpp:7:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’    7 |     {      |     ^
Output
Prodotto esauritoUnità disponibili: 12

Il programma annuncia che il prodotto è esaurito benché restino 12 unità. GCC dà due avvisi: -Wempty-body suggerisce le graffe attorno al corpo vuoto, e -Wmisleading-indentation segnala che il blocco è indentato come se l’if lo governasse. L’altro errore classico, if (x = 0) invece di if (x == 0), è stato visto nella lezione Compilare, eseguire e configurare: l’assegnamento vale zero, che si converte in false, e -Wparentheses avvisa. In entrambi i casi il programma compila; senza gli avvisi, l’errore passerebbe inosservato.

Le graffe hanno una conseguenza in più: una variabile dichiarata dentro il blocco di un if esiste solo lì. Se un valore calcolato in un ramo si usa dopo la decisione, la variabile si dichiara prima, come r nell’esempio del resto; l’ambito di visibilità sarà studiato nella lezione Parametri e ambito di visibilità.

L’operatore condizionale

Quando i due rami di una decisione si limitano a scegliere un valore, C++ offre una forma più breve: condizione ? a : b vale a se la condizione è vera e b se è falsa, e valuta solo l’elemento scelto. A differenza di if, che è un’istruzione, l’operatore condizionale è un’espressione: ha un valore e un tipo, e può stare dove sta qualsiasi espressione, per esempio nell’inizializzazione di una costante, che un if non permetterebbe senza rinunciare a const.

Programma in C++
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    const int larger{a > b ? a : b};    std::println("Maggiore: {}", larger);    std::println("{} {}", b, b == 1 ? "pezzo" : "pezzi");    const auto mixed{a > b ? a : 0.5};    std::println("Tipo comune: {:.1f}", mixed);    std::println("Senza parentesi: {}", 10 + a > b ? 1 : 0);    std::println("Con parentesi: {}", 10 + (a > b ? 1 : 0));}
Input del programma
7 3
Output
Maggiore: 73 pezziTipo comune: 7.0Senza parentesi: 1Con parentesi: 11

Le prime due righe mostrano l’uso che chiarisce: scegliere il maggiore e accordare una parola con il numero, senza ripetere l’istruzione intera in due rami. La terza mostra che l’espressione ha un solo tipo, qualunque sia il ramo scelto: se i due risultati sono di tipi diversi, si convertono in un tipo comune con le conversioni aritmetiche usuali della lezione Aritmetica; qui, un int e un double danno double, e il 7 scelto si conserva come 7.0. Le ultime due mostrano la precedenza: l’operatore condizionale è tra quelli di priorità più bassa, così che senza parentesi la condizione è 10 + a > b e il valore, 1, invece di 11. Quando il condizionale fa parte di un’espressione più grande, va tra parentesi.

L’operatore oscura non appena gli si chiede più che scegliere un valore. Annidato, come in a > b ? (a > c ? a : c) : (b > c ? b : c), costringe a ricostruire mentalmente una decisione annidata senza le graffe che la rendono leggibile; e usato per i suoi effetti, come in ok ? std::println("sì") : std::println("no"), è un if travestito. La regola del corso è usarlo solo per scegliere tra due valori brevi, senza annidarlo; tutto il resto si scrive con if.