Istruzioni Iterative in C++

Le istruzioni iterative, comunemente chiamate cicli, permettono l'esecuzione ripetuta fino a quando una condizione è vera. Le istruzioni while e for testano la condizione prima di eseguire il corpo. Il do while esegue il corpo e poi testa la sua condizione.

Concetti Chiave
  • Le istruzioni iterative (cicli) permettono l'esecuzione ripetuta di un'istruzione finché una condizione è vera.
  • Le istruzioni while e for testano la condizione prima di eseguire il corpo del ciclo.
  • L'istruzione do while esegue il corpo del ciclo almeno una volta prima di testare la condizione.
  • Il range for è un modo semplice per iterare attraverso gli elementi di una sequenza.

L'Istruzione while

Un'istruzione while esegue ripetutamente un'istruzione target finché una condizione è vera. La sua forma sintattica è

while (condizione)
    istruzione

In un while, istruzione (che spesso è un blocco) viene eseguita finché condizione viene valutata come vera. condizione non può essere vuota. Se la prima valutazione di condizione produce falso, istruzione non viene eseguita.

La condizione può essere un'espressione o una dichiarazione di variabile inizializzata. Normalmente, la condizione stessa o il corpo del ciclo devono fare qualcosa per cambiare il valore dell'espressione. Altrimenti, il ciclo potrebbe non terminare mai.

Le variabili definite in una condizione while o nel corpo while vengono create e distrutte ad ogni iterazione.

Utilizzo di un Ciclo while

Un ciclo while è generalmente usato quando vogliamo iterare indefinitamente, come ad esempio quando leggiamo l'input. Un while è anche utile quando vogliamo accedere al valore della variabile di controllo del ciclo dopo che il ciclo termina. Ad esempio:

vector<int> v;
int i;
// leggi fino a fine-file o altro errore di input
while (cin >> i)
    v.push_back(i);
// trova il primo elemento negativo
auto inizio = v.begin();
while (inizio != v.end() && *inizio >= 0)
    ++inizio;
if (inizio == v.end())
    // sappiamo che tutti gli elementi in v sono maggiori o uguali a zero

Il primo ciclo legge dati dall'input standard. Non abbiamo idea di quante volte questo ciclo verrà eseguito. La condizione fallisce quando cin legge dati non validi, incontra qualche altro errore di input, o raggiunge la fine del file. Il secondo ciclo continua finché non troviamo un valore negativo. Quando il ciclo termina, inizio è uguale a v.end(), oppure denota un elemento in v il cui valore è minore di zero. Possiamo usare lo stato di inizio al di fuori del while per determinare l'ulteriore elaborazione.

L'Istruzione for Tradizionale

La forma sintattica dell'istruzione for è:

for (istruzione_iniziale; condizione; espressione)
    istruzione

Il for e la parte dentro le parentesi è spesso chiamata intestazione del for.

istruzione_iniziale deve essere un'istruzione di dichiarazione, un'istruzione di espressione, o un'istruzione nulla. Ciascuna di queste istruzioni termina con un punto e virgola, quindi la forma sintattica può anche essere pensata come

for (inizializzatore; condizione; espressione)
    istruzione

In generale, istruzione_iniziale è usata per inizializzare o assegnare un valore iniziale che viene modificato nel corso del ciclo. condizione serve come controllo del ciclo. Finché condizione viene valutata come vera, istruzione viene eseguita. Se la prima valutazione di condizione produce falso, istruzione non viene eseguita. espressione di solito modifica la/le variabile/i inizializzata/e in istruzione_iniziale e testata in condizione. espressione viene valutata dopo ogni iterazione del ciclo. Come al solito, istruzione può essere una singola istruzione o un'istruzione composta.

Flusso di Esecuzione in un Ciclo for Tradizionale

Dato il seguente ciclo for:

// processa i caratteri in s fino a quando esauriamo i caratteri o troviamo uno spazio
for (decltype(s.size()) indice = 0;
        indice != s.size() && !isspace(s[indice]); ++indice)
    s[indice] = toupper(s[indice]); // metti in maiuscolo il carattere corrente

l'ordine di valutazione è il seguente:

  1. istruzione_iniziale viene eseguita una volta all'inizio del ciclo. In questo esempio, indice viene definito e inizializzato a zero.
  2. Successivamente, condizione viene valutata. Se indice non è uguale a s.size() e il carattere a s[indice] non è uno spazio, il corpo del for viene eseguito. Altrimenti, il ciclo termina. Se la condizione è falsa alla prima iterazione, allora il corpo del for non viene eseguito affatto.
  3. Se la condizione è vera, il corpo del for viene eseguito. In questo caso, il corpo del for mette in maiuscolo il carattere a s[indice].
  4. Infine, espressione viene valutata. In questo esempio, indice viene incrementato di 1.

Questi quattro passaggi rappresentano la prima iterazione del ciclo for. Il passaggio 1 viene eseguito solo una volta all'ingresso nel ciclo. I passaggi 2, 3 e 4 vengono ripetuti fino a quando la condizione viene valutata come falsa—cioè, quando incontriamo un carattere di spazio in s, o indice è maggiore di s.size().

Vale la pena ricordare che la visibilità di qualsiasi oggetto definito nell'intestazione del for è limitata al corpo del ciclo for. Quindi, in questo esempio, indice è inaccessibile dopo che il for si completa.

Definizioni Multiple nell'Intestazione del for

Come in qualsiasi altra dichiarazione, istruzione_iniziale può definire diversi oggetti. Tuttavia, istruzione_iniziale può essere solo una singola istruzione di dichiarazione. Pertanto, tutte le variabili devono avere lo stesso tipo base. Come esempio, potremmo scrivere un ciclo per duplicare gli elementi di un vettore alla fine come segue:

// ricorda la dimensione di v e fermati quando arriviamo all'ultimo elemento originale
for (decltype(v.size()) i = 0, dim = v.size(); i != dim; ++i)
    v.push_back(v[i]);

In questo ciclo definiamo sia l'indice, i, che il controllo del ciclo, dim, in istruzione_iniziale.

Omissione di Parti dell'Intestazione del for

Un'intestazione for può omettere qualsiasi (o tutte) parti di istruzione_iniziale, condizione, o espressione.

Possiamo usare un'istruzione nulla per istruzione_iniziale quando un'inizializzazione non è necessaria. Ad esempio, potremmo riscrivere il ciclo che cercava il primo numero negativo in un vettore in modo che usi un for:

auto inizio = v.begin();
for ( /* null */; inizio != v.end() && *inizio >= 0; ++inizio)
    ; // nessun lavoro da fare

Nota che il punto e virgola è necessario per indicare l'assenza di istruzione_iniziale. Più precisamente, il punto e virgola rappresenta un'istruzione_iniziale nulla. In questo ciclo, anche il corpo del for è vuoto perché tutto il lavoro del ciclo è fatto dentro la condizione e l'espressione del for. La condizione decide quando è il momento di smettere di cercare e l'espressione incrementa l'iteratore.

Omettere condizione è equivalente a scrivere true come condizione. Poiché la condizione viene sempre valutata come vera, il corpo del for deve contenere un'istruzione che esce dal ciclo. Altrimenti il ciclo verrà eseguito indefinitamente:

for (int i = 0; /* nessuna condizione */ ; ++i) {
    // processa i; il codice dentro il ciclo deve fermare l'iterazione!
}

Possiamo anche omettere espressione dall'intestazione del for. In tali cicli, o la condizione o il corpo deve fare qualcosa per far avanzare l'iterazione. Come esempio, riscriveremo il ciclo while che leggeva l'input in un vettore di int:

vector<int> v;
for (int i; cin >> i; /* nessuna espressione */ )
    v.push_back(i);

In questo ciclo non c'è bisogno di un'espressione perché la condizione cambia il valore di i. La condizione testa il flusso di input in modo che il ciclo termini quando abbiamo letto tutto l'input o incontriamo un errore di input.

Esercizi

  • Spiega ciascuno dei seguenti cicli. Correggi eventuali problemi che rilevi.

    • Ciclo 1:

      for (int ix = 0; ix != dim; ++ix) { /* ... */ }
      if (ix != dim)
          // ...
      

      Risposta: Il ciclo è corretto, ma la variabile ix non è accessibile dopo il ciclo perché è stata definita nell'intestazione del for. Per correggere questo problema, ix dovrebbe essere definito prima del ciclo.

    • Ciclo 2:

      int ix = 0;
      for (; ix != dim; ++ix) { /* ... */ }
      

      Risposta: Questo ciclo è corretto. La variabile ix è definita prima del ciclo, quindi è accessibile dopo il ciclo.

    • Ciclo 3:

      for (int ix = 0; ix != dim; ++ix, ++dim) { /* ... */ }
      

      Risposta: Questo ciclo è problematico perché dim viene incrementato ad ogni iterazione, il che potrebbe causare un ciclo infinito se dim è inizialmente maggiore di ix. Per correggere questo, dim non dovrebbe essere modificato all'interno dell'espressione del ciclo.

  • Dati due vettori di int, scrivi un programma per determinare se un vettore è un prefisso dell'altro. Per vettori di lunghezza disuguale, confronta il numero di elementi del vettore più piccolo. Ad esempio, dati i vettori contenenti 0, 1, 1, e 2 e 0, 1, 1, 2, 3, 5, 8, rispettivamente, il tuo programma dovrebbe restituire true.

    *Risposta:

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> v1 = {0, 1, 1, 2};
        std::vector<int> v2 = {0, 1, 1, 2, 3, 5, 8};
    
        size_t minSize = std::min(v1.size(), v2.size());
        bool isPrefix = true;
    
        for (size_t i = 0; i < minSize; ++i) {
            if (v1[i] != v2[i]) {
                isPrefix = false;
                break;
            }
        }
    
        std::cout << (isPrefix ? "true" : "false") << std::endl;
        return 0;
    }
    

    In questo programma abbiamo usato std::min per determinare la lunghezza del vettore più piccolo e poi confrontato gli elementi fino a quella lunghezza.

L'Istruzione for a Intervallo (Range for)

Lo standard C++11 ha introdotto un'istruzione for più semplice che può essere usata per iterare attraverso gli elementi di un contenitore o altra sequenza. La forma sintattica dell'istruzione for a intervallo è:

for (dichiarazione : espressione)
    istruzione

espressione deve rappresentare una sequenza, come una lista di inizializzatori tra parentesi graffe, un array, o un oggetto di un tipo come vector o string che ha membri begin ed end che restituiscono iteratori.

dichiarazione definisce una variabile. Deve essere possibile convertire ogni elemento della sequenza al tipo della variabile. Il modo più semplice per assicurarsi che i tipi corrispondano è usare lo specificatore di tipo auto. In questo modo il compilatore dedurrà il tipo per noi. Se vogliamo scrivere sugli elementi della sequenza, la variabile di ciclo deve essere un tipo riferimento.

Ad ogni iterazione, la variabile di controllo viene definita e inizializzata dal valore successivo nella sequenza, dopodiché istruzione viene eseguita. Come al solito, istruzione può essere una singola istruzione o un blocco. L'esecuzione termina una volta che tutti gli elementi sono stati processati.

Abbiamo già visto diversi di questi cicli, ma per completezza, eccone uno che raddoppia il valore di ogni elemento in un vettore:

vector<int> v = {0,1,2,3,4,5,6,7,8,9};
// la variabile di intervallo deve essere un riferimento
// così possiamo scrivere sugli elementi
for (auto &r : v) // per ogni elemento in v
    r *= 2; // raddoppia il valore di ogni elemento in v

L'intestazione del for dichiara la variabile di controllo del ciclo, r, e la associa a v. Usiamo auto per lasciare che il compilatore deduca il tipo corretto per r. Poiché vogliamo cambiare il valore degli elementi in v, dichiariamo r come riferimento. Quando assegniamo a r dentro il ciclo, quell'assegnazione cambia l'elemento a cui r è legato.

Un for a intervallo è definito in termini del for tradizionale equivalente:

for (auto inizio = v.begin(), fine = v.end(); inizio != fine; ++inizio) {
    // r deve essere un riferimento così possiamo cambiare l'elemento
    auto &r = *inizio;
    // raddoppia il valore di ogni elemento in v
    r *= 2;
}

Ora che sappiamo come funziona un for a intervallo, possiamo capire perché abbiamo detto che non possiamo usare un for a intervallo per aggiungere elementi a un vettore (o altro contenitore). In un for a intervallo, il valore di end() viene memorizzato in cache. Se aggiungiamo elementi a (o li rimuoviamo da) la sequenza, il valore di end potrebbe essere invalidato. Avremo più da dire su queste questioni in seguito.

L'Istruzione do while

Un'istruzione do while è come un while ma la condizione viene testata dopo che il corpo dell'istruzione si completa. Indipendentemente dal valore della condizione, eseguiamo il ciclo almeno una volta. La forma sintattica è la seguente:

do
    istruzione
while (condizione);

Un do while termina con un punto e virgola dopo la condizione tra parentesi.

In un do, istruzione viene eseguita prima che condizione venga valutata. condizione non può essere vuota. Se condizione viene valutata come falsa, allora il ciclo termina; altrimenti, il ciclo viene ripetuto. Le variabili usate in condizione devono essere definite al di fuori del corpo dell'istruzione do while.

Possiamo scrivere un programma che (indefinitamente) fa somme usando un do while:

// chiedi ripetutamente all'utente una coppia di numeri da sommare
string risp; // usata nella condizione; non può essere definita dentro il do
do {
    cout << "per favore inserisci due valori: ";
    int val1 = 0, val2 = 0;
    cin >> val1 >> val2;
    cout << "La somma di " << val1 << " e " << val2
         << " = " << val1 + val2 << "\n\n"
         << "Ancora? Inserisci si o no: ";
    cin >> risp;
} while (!risp.empty() && risp[0] != 'n');

Il ciclo inizia chiedendo all'utente due numeri. Poi stampa la loro somma e chiede se l'utente desidera fare un'altra somma. La condizione controlla che l'utente abbia dato una risposta. Se no, o se l'input inizia con una 'n', il ciclo viene terminato. Altrimenti il ciclo viene ripetuto.

Poiché la condizione non viene valutata fino a dopo che l'istruzione o il blocco viene eseguito, il ciclo do while non permette definizioni di variabili dentro la condizione:

do {
    // ...
    borbotta(pippo);
} while (int pippo = ottieni_pippo()); // errore: dichiarazione in una condizione do

Se potessimo definire variabili nella condizione, allora qualsiasi uso della variabile avverrebbe prima che la variabile venisse definita!

Esercizi

  • Spiega ciascuno dei seguenti cicli. Correggi eventuali problemi che rilevi.

    • Ciclo 1:

      do
          int v1, v2;
          cout << "Per favore inserisci due numeri da sommare:" ;
          if (cin >> v1 >> v2)
              cout << "La somma è: " << v1 + v2 << endl;
      while (cin);
      

      Risposta: Il ciclo è problematico perché la variabile v1 e v2 sono definite all'interno del ciclo ma non sono racchiuse in un blocco {}. Questo significa che solo la prima istruzione dopo do è considerata parte del ciclo. Per correggere questo, dobbiamo racchiudere il corpo del ciclo in un blocco:

      do {
          int v1, v2;
          cout << "Per favore inserisci due numeri da sommare:" ;
          if (cin >> v1 >> v2)
              cout << "La somma è: " << v1 + v2 << endl;
      } while (cin);
      
    • Ciclo 2:

      do {
              // ...
      } while (int val_int = ottieni_risposta());
      

      Risposta: Questo ciclo è problematico perché la variabile val_int è definita nella condizione del while, il che non è permesso. Per correggere questo, dobbiamo definire val_int prima del ciclo:

      int val_int;
      do {
          // ...
      } while (val_int = ottieni_risposta());
      
    • Ciclo 3:

      do {
          int val_int = ottieni_risposta();
      } while (val_int);
      

      Risposta: Questo ciclo è corretto. La variabile val_int è definita all'interno del corpo del ciclo e viene usata nella condizione del while. Tuttavia, poiché val_int viene ridefinita ad ogni iterazione, il ciclo funzionerà come previsto.

  • Scrivi un programma che usa un ciclo do while per richiedere ripetutamente due stringhe all'utente e riporta quale stringa è minore dell'altra.

    Risposta:

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
        string str1, str2;
        do {
            cout << "Inserisci la prima stringa: ";
            cin >> str1;
            cout << "Inserisci la seconda stringa: ";
            cin >> str2;
    
            if (str1 < str2) {
                cout << "La stringa minore è: " << str1 << endl;
            } else {
                cout << "La stringa minore è: " << str2 << endl;
            }
        } while (!str1.empty() && !str2.empty());
    
        return 0;
    }