Vai al contenuto
Topos Uranos

Riepilogo

La lezione Cicli ha insegnato a scrivere una ripetizione; questa insegna a scriverla bene e a sapere perché è corretta. Comincia dagli schemi con cui si costruisce la maggior parte dei cicli (il contatore, l’accumulatore, il massimo e il minimo, la sentinella e il flag), ciascuno definito da ciò che la sua variabile significa dopo ogni iterazione. Questa osservazione conduce al concetto centrale: l’invariante, un’affermazione sulle variabili che vale prima della prima iterazione e dopo ciascuna, e che, insieme alla condizione di uscita, implica il risultato. È un’induzione sul numero di iterazioni, e con essa un ciclo si progetta a ritroso: prima ciò che deve valere alla fine, poi l’invariante, infine il corpo. La terminazione si giustifica con la variante, già presentata nella lezione Dal problema all’algoritmo, e la lezione mostra tre cicli che non terminano: uno con una condizione su numeri reali, uno con un contatore senza segno e uno il cui contatore con segno va in overflow, il che è comportamento indefinito. Si chiude con il primo contatto con il debugger di Visual Studio Code, che ferma il programma su una riga e ne mostra le variabili iterazione per iterazione.

Obiettivi di apprendimento

  1. Riconoscere in un ciclo gli schemi di contatore, accumulatore, massimo e sentinella, e usarli per progettare cicli nuovi.
  2. Enunciare l’invariante di un ciclo semplice e verificare che si conserva in un’iterazione qualsiasi.
  3. Giustificare la terminazione di un ciclo con una quantità intera che decresce ed è limitata.
  4. Seguire l’esecuzione di un ciclo con il debugger di Visual Studio Code, con un punto di interruzione e la vista delle variabili.

Schemi

Ciò che una variabile significa

La maggior parte dei cicli non si inventa: si monta con pochi pezzi che ricompaiono in tutti i programmi. Ogni pezzo è una variabile con un’inizializzazione prima del ciclo, un aggiornamento al suo interno e, soprattutto, un significato che vale dopo ogni iterazione. Conoscere i pezzi per il loro significato, e non per la loro forma, è ciò che permette di combinarli senza errori.

  • Un contatore vale quanti dati sono stati elaborati (o quanti soddisfano una condizione). Si inizializza a 0, perché prima della prima iterazione non ne è stato elaborato nessuno, e si aggiorna con ++count, solo quando c’è qualcosa da contare.
  • Un accumulatore vale la combinazione dei dati elaborati: la loro somma, il loro prodotto. Si inizializza all’elemento neutro dell’operazione (0 per la somma, 1 per il prodotto), perché la somma di nessun dato è 0 e il prodotto di nessun dato è 1.
  • Un massimo vale il maggiore dei dati elaborati, e un minimo, il minore. Il massimo di nessun dato non esiste: per questo si inizializza con il primo dato, e non con un numero scelto a caso.
  • Una sentinella è un valore dell’input che non è un dato, ma il segnale che i dati sono finiti. Lo schema esige di leggere una volta prima del ciclo e un’altra alla fine del corpo, come ha fatto la lezione Cicli sommando fino allo zero, in modo che la condizione esamini ogni valore prima che venga elaborato, e la sentinella non sia mai contata come dato.
  • Un flag è un bool che dice se qualcosa è accaduto almeno una volta tra i dati elaborati. Si inizializza a false e passa a true la prima volta che accade; nessuna iterazione successiva lo riporta a false.

Un esempio con tutti i pezzi

Un registro pluviometrico annota i millimetri caduti ogni giorno. Il programma legge prima una soglia, e poi le misure di vari giorni, terminate da un numero negativo, che non è una misura possibile e fa da sentinella; scrive quante misure ci sono, la loro media, il massimo, quanti giorni hanno superato la soglia e se c’è stato qualche giorno senza pioggia.

Programma in C++
#include <iostream>#include <print>​int main(){    double threshold{};    std::cin >> threshold;    int count{0};          // contatore: misure elaborate    int aboveCount{0};     // contatore: misure sopra la soglia    double total{0.0};     // accumulatore: somma delle misure    double largest{0.0};   // massimo: vale solo se count > 0    bool dryDay{false};    // flag: almeno un giorno senza pioggia    double value{};    std::cin >> value;    while (std::cin && value >= 0.0) {        if (count == 0 || value > largest) {            largest = value;        }        ++count;        total += value;        if (value > threshold) {            ++aboveCount;        }        if (value == 0.0) {            dryDay = true;        }        std::cin >> value;    }    if (count == 0) {        std::println("Nessuna misura.");    } else {        std::println("Misure: {}; media: {:.2f} mm", count, total / count);        std::println("Massimo: {:.1f} mm; giorni sopra {:.1f} mm: {}", largest, threshold, aboveCount);        std::println("Giorni senza pioggia: {}", dryDay ? "sì" : "no");    }}
Input del programma
104.5 0 12.5 21.5 7.5 -1
Output
Misure: 5; media: 9.20 mmMassimo: 21.5 mm; giorni sopra 10.0 mm: 2Giorni senza pioggia: sì

Quattro decisioni meritano un commento. La condizione count == 0 || value > largest inizializza il massimo con il primo dato senza una lettura a parte: nella prima iterazione il contatore vale 0 e l’assegnamento avviene sempre; nelle successive, solo se il dato supera il massimo. Il confronto value == 0.0 è esatto, perché il valore è stato letto e non calcolato: la cifra 0 si converte nello zero esatto, e l’avvertenza della lezione I numeri in virgola mobile contro == riguarda i risultati di operazioni. La condizione del ciclo include std::cin, in modo che un input che finisce senza sentinella termini anch’esso il ciclo. E la media si calcola solo se ci sono misure: con count uguale a 0, total / count sarebbe 0/00/0, che in virgola mobile non è un errore, ma il valore «non è un numero», che il programma scriverebbe senza avviso.

EsempioLe variabili iterazione per iterazione

Dimostrazione

  1. Prima del ciclo: la soglia è 10, i contatori e l’accumulatore valgono 0, il flag è falso, e la prima misura, 4.5, è già stata letta. Il massimo vale 0, ma questo valore non significa nulla finché count è 0.

  2. Prima iterazione: count è 0, quindi 4.5 diventa il massimo; il contatore sale a 1 e il totale a 4.5. Si legge 0.

  3. Seconda iterazione: 0 non supera il massimo, ma alza il flag. Si legge 12.5.

  4. Terza iterazione: 12.5 è il nuovo massimo e supera la soglia: aboveCount sale a 1. Si legge 21.5.

  5. Quarta iterazione: 21.5 è il nuovo massimo e supera anch’esso la soglia. Si legge 7.5.

  6. Quinta iterazione: 7.5 non cambia né il massimo né aboveCount; il totale arriva a 46. Si legge −1-1.

  7. La condizione esamina −1-1, che è negativo: il ciclo termina senza elaborarlo. La media è 46/5=9.246 / 5 = 9.2.

Si osservi che in ogni riga della figura ogni variabile rispetta il suo significato rispetto alle misure elaborate fin lì: total è la loro somma, largest il loro massimo, dryDay dice se tra esse c’è stato uno zero. Questa proprietà, che vale dopo ogni iterazione, ha un nome.