Introduzione al Flusso di Controllo in C++

Concetti Chiave
  • Il flusso di controllo determina l'ordine in cui le istruzioni di un programma vengono eseguite.
  • Le istruzioni condizionali, come if, permettono di eseguire diverse sezioni di codice in base a condizioni specifiche.
  • I cicli, come while e for, permettono di eseguire ripetutamente una sezione di codice finché una condizione è vera.
  • La corretta indentazione e formattazione del codice migliorano la leggibilità e la manutenzione del programma.

In C++, le istruzioni normalmente si eseguono sequenzialmente: La prima istruzione in un blocco viene eseguita per prima, seguita dalla seconda, e così via. Naturalmente, pochi programmi, possono essere scritti usando solo l'esecuzione sequenziale. Invece, i linguaggi di programmazione forniscono varie istruzioni di flusso di controllo che permettono percorsi di esecuzione più complicati.

In questa lezione, vedremo una panoramica dei modi con cui il C++ permette di controllare il flusso di esecuzione, attraverso le istruzioni condizionali e i cicli. Vedremo anche come il compilatore può aiutarci a trovare errori nei nostri programmi.

Ritorneremo su questi concetti in lezioni successive. Per ora, ci concentreremo sui concetti di base.

L'istruzione while

Un'istruzione while esegue ripetutamente una sezione di codice finché una data condizione è vera. Possiamo usare un while per scrivere un programma che somma i numeri da 1 a 10, estremi inclusi, come segue:

#include <iostream>

int main()
{
    int somma = 0, valore = 1;

    // continua ad eseguire il while
    // finché valore è minore o uguale a 10
    while (valore <= 10) {
         somma += valore; // assegna somma + valore a somma
         ++valore; // aggiungi 1 a valore
    }

    std::cout << "La somma da 1 a 10 inclusi è "
               << somma << std::endl;
    return 0;
}

Quando compiliamo ed eseguiamo questo programma, vedremo a schermo:

La somma da 1 a 10 inclusi è 55

Come prima cosa, iniziamo includendo l'intestazione iostream e definendo main. All'interno di main definiamo due variabili int: somma, che conterrà la nostra somma, e valore, che rappresenterà ciascuno dei valori da 1 a 10. Diamo a somma un valore iniziale di 0 e iniziamo valore con il valore 1.

La nuova parte di questo programma è l'istruzione while. Un while ha la forma che segue:

while (condizione)
    istruzione;

Un while si esegue testando alternativamente la condizione ed eseguendo l'istruzione associata fino a quando la condizione è falsa. Una condizione è un'espressione che produce un risultato che è vero o falso. Finché la condizione è vera, l'istruzione viene eseguita. Dopo aver eseguito l'istruzione, la condizione viene testata di nuovo. Se la condizione è di nuovo vera, allora l'istruzione viene di nuovo eseguita. Il while continua, testando alternativamente la condizione ed eseguendo l'istruzione fino a quando la condizione è falsa.

In questo programma, l'istruzione while è

// continua ad eseguire il while finché valore è minore o uguale a 10
while (valore <= 10) {
    somma += valore; // assegna somma + valore a somma
    ++valore; // aggiungi 1 a valore
}

La condizione utilizza l'operatore minore-o-uguale (l'operatore <=) per confrontare il valore attuale di valore e 10. Finché valore è minore o uguale a 10, la condizione è vera. Se la condizione è vera, eseguiamo il corpo del while. In questo caso, quel corpo è un blocco con due istruzioni:

{
    somma += valore; // assegna somma + valore a somma
    ++valore; // aggiungi 1 a valore
}

Un blocco è una sequenza di zero o più istruzioni racchiuse tra parentesi graffe. Un blocco è un'istruzione e può essere utilizzato ovunque sia richiesta un'istruzione. La prima istruzione in questo blocco utilizza l'operatore di assegnazione composta (l'operatore +=). Questo operatore aggiunge il suo operando destro al suo operando sinistro e memorizza il risultato nell'operando sinistro. Ha essenzialmente lo stesso effetto di scrivere un'addizione e un'assegnazione:

somma = somma + valore; // *assegna* somma + valore *a* somma

Quindi, la prima istruzione nel blocco aggiunge il valore di valore all'attuale valore di somma e memorizza il risultato di nuovo in somma.

L'istruzione successiva

++valore; // *aggiungi* 1 *a* valore

usa l'operatore di incremento prefisso (l'operatore ++). L'operatore di incremento aggiunge 1 al suo operando. Scrivere ++valore è lo stesso che scrivere valore = valore + 1.

Dopo aver eseguito il corpo del while, il ciclo valuta di nuovo la condizione. Se il valore di valore (ora incrementato) è ancora minore o uguale a 10, allora il corpo del while viene eseguito di nuovo. Il ciclo continua, testando la condizione ed eseguendo il corpo, fino a quando valore non è più piccolo o uguale a 10.

Una volta che valore è maggiore di 10, il programma esce dal ciclo while e continua l'esecuzione con l'istruzione successiva al while. In questo caso, quell'istruzione stampa il nostro output, seguita dal return, che completa il nostro programma principale.

Ritorneremo sui concetti di questa sezione nelle prossime lezioni. Per ora, si noti che il flusso di esecuzione di un programma non è sempre sequenziale. In questo esempio, l'istruzione while ha causato l'esecuzione ripetuta di un blocco di codice.

Esercizi

  • Scrivi un programma che usa un while per sommare i numeri da 50 a 100:

    Soluzione:

    #include <iostream>
    
    int main()
    {
        int somma = 0, valore = 50;
        // somma i valori da 50 a 100 inclusi
        while (valore <= 100) {
            somma += valore; // equivalente a somma = somma + valore
            ++valore; // aggiungi 1 a valore
        }
        std::cout << "La somma da 50 a 100 inclusi è "
                  << somma << std::endl;
        return 0;
    }
    
  • Oltre all'operatore ++ che aggiunge 1 al suo operando, c'è un operatore di decremento (--) che sottrae 1. Usa l'operatore di decremento per scrivere un while che stampa i numeri da dieci a zero.

    Soluzione:

    #include <iostream>
    
    int main()
    {
        int valore = 10;
        // stampa i valori da 10 a 0 inclusi
        while (valore >= 0) {
            std::cout << valore << std::endl;
            --valore; // equivalente a valore = valore - 1
        }
        return 0;
    }
    
  • Scrivi un programma che chiede all'utente due interi. Stampa ogni numero nell'intervallo specificato da questi due interi.

    Soluzione:

    #include <iostream>
    
    int main()
    {
        int v1 = 0, v2 = 0;
        std::cout << "Enter two integers:" << std::endl;
        std::cin >> v1 >> v2; // leggere l'input
        // stampa i valori nell'intervallo da v1 a v2 inclusi
        while (v1 <= v2) {
            std::cout << v1 << std::endl;
            ++v1; // equivalente a v1 = v1 + 1
        }
        return 0;
    }
    

L'istruzione for

Nel nostro ciclo while abbiamo usato la variabile valore per controllare quante volte abbiamo eseguito il ciclo. Abbiamo testato il valore di valore nella condizione e incrementato valore nel corpo del while.

Questo schema, ossia usare una variabile in una condizione e incrementare quella variabile nel corpo, si verifica così spesso che il linguaggio definisce un secondo costrutto, l'istruzione for, che abbrevia il codice che segue questo schema. Possiamo riscrivere questo programma usando un ciclo for per sommare i numeri da 1 a 10 come segue:

#include <iostream>

int main()
{
    int somma = 0;
    // somma i valori da 1 a 10 inclusi
    for (int valore = 1; valore <= 10; ++valore)
        somma += valore; // equivalente a somma = somma + valore
    std::cout << "La somma da 1 a 10 inclusi è "
               << somma << std::endl;
    return 0;
}

Come prima, definiamo somma e la inizializziamo a zero. In questa versione, definiamo valore come parte dell'istruzione for stessa:

for (int valore = 1; valore <= 10; ++valore)
    somma += valore;

Ogni istruzione for ha due parti: un'intestazione e un corpo. L'intestazione controlla quanto spesso viene eseguito il corpo. L'intestazione stessa è composta da tre parti: un istruzione-di-inizializzazione, una condizione, e un'espressione. In questo caso, l'istruzione-di-inizializzazione

int valore = 1;

definisce un oggetto int chiamato valore e gli dà un valore iniziale di 1. La variabile valore esiste solo all'interno del for; non è possibile utilizzare valore dopo che questo ciclo termina. L'istruzione-di-inizializzazione viene eseguita solo una volta, all'ingresso nel for. La condizione

valore <= 10

confronta il valore attuale in valore con 10. La condizione viene testata ogni volta che si attraversa il ciclo. Finché valore è minore o uguale a 10, eseguiamo il corpo del for. L'espressione viene eseguita dopo il corpo del for. Qui, l'espressione

++valore

usa l'operatore di incremento prefisso, che aggiunge 1 al valore di valore. Dopo aver eseguito l'espressione, il for ritesta la condizione. Se il nuovo valore di valore è ancora minore o uguale a 10, allora il corpo del ciclo for viene eseguito di nuovo. Dopo aver eseguito il corpo, valore viene incrementato di nuovo. Il ciclo continua fino a quando la condizione fallisce.

In questo ciclo, il corpo del for esegue la somma

somma += valore; // equivalente a somma = somma + valore

Per riassumere, il flusso di esecuzione complessivo di questo for è:

  1. Crea valore e inizializzalo a 1.
  2. Controlla se valore è minore o uguale a 10. Se il test ha successo, esegui il corpo del for. Se il test fallisce, esci dal ciclo e continua l'esecuzione con la prima istruzione dopo il corpo del for.
  3. Incrementa valore.
  4. Ripeti il test nel passo 2, continuando con i passi rimanenti finché la condizione è vera.

Esercizi sul for

  • Cosa fa il seguente ciclo for? Qual è il valore finale di somma?

    int somma = 0;
    for (int i = -100; i <= 100; ++i)
        somma += i;
    

    Questo ciclo somma tutti i numeri interi da -100 a 100. Il valore finale di somma sarà 0, poiché i numeri negativi e positivi si annullano a vicenda.

  • Scrivi un programma che usa un for per sommare i numeri da 50 a 100:

    Soluzione:

    #include <iostream>
    
    int main()
    {
        int somma = 0;
        // somma i valori da 50 a 100 inclusi
        for (int valore = 50; valore <= 100; ++valore)
            somma += valore; // equivalente a somma = somma + valore
        std::cout << "La somma da 50 a 100 inclusi è "
                  << somma << std::endl;
        return 0;
    }
    

Lettura di un numero sconosciuto di valori

Nelle sezioni precedenti, abbiamo scritto programmi che sommavano i numeri da 1 a 10. Un'estensione logica di questo programma sarebbe chiedere all'utente di inserire un insieme di numeri da sommare. In questo caso, non sapremo quanti numeri aggiungere. Invece, continueremo a leggere numeri fino a quando non ce ne saranno più da leggere:

#include <iostream>

int main()
{
    int somma = 0, valore = 0;
    // leggi fino alla fine della riga
    // calcolando un totale corrente di tutti i valori letti
    while (std::cin >> valore)
         somma += valore; // equivalente a somma = somma + valore
    std::cout << "La somma è: " << somma << std::endl;
    return 0;
}

Se diamo a questo programma l'input

3 4 5 6

allora il nostro output sarà

La somma è: 18

La prima riga all'interno di main definisce due variabili int, chiamate somma e valore, che inizializziamo a 0. Useremo valore per contenere ogni numero mentre lo leggiamo dall'input. Leggiamo i dati all'interno della condizione del while:

while (std::cin >> valore)

Valutare la condizione del while esegue l'espressione

std::cin >> valore

Quell'espressione legge il numero successivo dall'input standard e memorizza quel numero in valore. L'operatore di input restituisce il suo operando sinistro, che in questo caso è std::cin. Questa condizione, quindi, testa std::cin.

Quando usiamo un istream come condizione, l'effetto è testare lo stato del flusso. Se il flusso è valido, cioè, se il flusso non ha incontrato un errore, allora il test ha successo. Un istream diventa non valido quando raggiungiamo la fine del file o incontriamo un input non valido, come leggere un valore che non è un intero. Un istream che è in uno stato non valido farà sì che la condizione restituisca falso.

Pertanto, il nostro while si esegue fino a quando non incontriamo la fine del file (o un errore di input). Il corpo del while utilizza l'operatore di assegnazione composta per aggiungere il valore attuale alla somma in evoluzione. Una volta che la condizione fallisce, il while termina. Quindi viene eseguita l'istruzione successiva, che stampa la somma seguita da endl.

Consiglio

Inserimento della fine del file da tastiera

Quando inseriamo input a un programma dalla tastiera, i diversi sistemi operativi usano convenzioni diverse per permetterci di indicare la fine del file.

Nei sistemi Windows inseriamo una fine del file digitando CTRL+Z seguito dal tasto Invio o Ritorno. Nei sistemi UNIX, compresi Mac OS X, la fine del file è di solito CTRL+D.

Compilazione Rivisitata

Parte del lavoro del compilatore è cercare errori nel testo del programma. Un compilatore non può rilevare se un programma fa ciò che il suo autore intende, ma può rilevare errori nella forma del programma. I seguenti sono i tipi più comuni di errori che un compilatore può rilevare.

  • Errori di sintassi: Il programmatore ha commesso un errore grammaticale nel linguaggio C++. Il seguente programma illustra errori di sintassi comuni; ogni commento descrive l'errore sulla riga seguente:

    // errore: manca ) nella lista dei parametri per main
    int main ( {
        // errore: usato due punti, non un punto e virgola, dopo endl
        std::cout << "Leggi ogni file." << std::endl:
        // errore: mancano virgolette attorno al letterale di stringa
        std::cout << Aggiorna master. << std::endl;
        // errore: il secondo operatore di output è mancante
        std::cout << "Scrivi nuovo master." std::endl;
        // errore: manca ; nella dichiarazione di ritorno
        return 0
    }
    
  • Errori di tipo: Ogni elemento di dati in C++ ha un tipo associato. Il valore 10, ad esempio, ha un tipo di int (o, più colloquialmente, "è un int"). La parola "ciao", comprese le virgolette doppie, è un letterale di stringa. Un esempio di errore di tipo è passare un letterale di stringa a una funzione che si aspetta un argomento int.

  • Errori di dichiarazione: Ogni nome usato in un programma C++ deve essere dichiarato prima di essere usato. La mancata dichiarazione di un nome di solito comporta un messaggio di errore. I due errori di dichiarazione più comuni sono dimenticare di usare std:: per un nome della libreria e scrivere male il nome di un identificatore:

    #include <iostream>
    int main()
    {
        int v1 = 0, v2 = 0;
        std::cin >> v >> v2; // errore: usa "v" non "v1"
        // errore: cout non definito; dovrebbe essere std::cout
        cout << v1 + v2 << std::endl;
        return 0;
    }
    

I messaggi di errore di solito contengono un numero di riga e una breve descrizione di ciò che il compilatore crede che abbiamo fatto di sbagliato. È una buona pratica correggere gli errori nella sequenza in cui vengono segnalati. Spesso un singolo errore può avere un effetto a cascata e causare al compilatore di segnalare più errori di quelli che sono effettivamente presenti. È anche una buona idea ricompilare il codice dopo ogni correzione o dopo aver apportato al massimo un piccolo numero di correzioni ovvie. Questo ciclo è noto come modifica-compila-debug.

L'istruzione if

Come la maggior parte dei linguaggi, C++ fornisce un'istruzione if che supporta l'esecuzione condizionale. Possiamo usare un if per scrivere un programma per contare quante volte consecutive appare ogni valore distinto nell'input:

#include <iostream>

int main()
{
    // valoreCorrente è il numero che stiamo contando;
    // leggeremo nuovi valori in valore
    int valoreCorrente = 0, valore = 0;

    // leggi il primo numero e assicurati di avere dati da elaborare
    if (std::cin >> valoreCorrente) {

        // memorizza il conteggio per il valore corrente che stiamo elaborando
        int cnt = 1;

        // leggi i numeri rimanenti
        while (std::cin >> valore) {

            // se i valori sono uguali
            if (valore == valoreCorrente)
                ++cnt; // aggiungi 1 a cnt
            else {
                // altrimenti, stampa il conteggio per il valore precedente
                std::cout << valoreCorrente << " si verifica "
                          << cnt << " volte" << std::endl;
                valoreCorrente = valore; // ricorda il nuovo valore
                cnt = 1; // ripristina il contatore
            }
        } // il ciclo while termina qui

        // ricorda di stampare il conteggio per l'ultimo valore nel file
        std::cout << valoreCorrente << " si verifica "
                  << cnt << " volte" << std::endl;
    } // l'istruzione if più esterna termina qui

    return 0;
}

Se diamo a questo programma il seguente input:

42 42 42 42 42 55 55 62 100 100 100

allora l'output dovrebbe essere

42 si verifica 5 volte
55 si verifica 2 volte
62 si verifica 1 volta
100 si verifica 3 volte

Gran parte del codice in questo programma dovrebbe essere familiare dai nostri programmi precedenti. Iniziamo definendo valore e valoreCorrente: valoreCorrente terrà traccia di quale numero stiamo contando; valore conterrà ogni numero mentre lo leggiamo dall'input. Ciò che è nuovo sono le due istruzioni if. La prima if

if (std::cin >> valoreCorrente) {

assicura che l'input non sia vuoto. Come un while, un if valuta una condizione. La condizione nel primo if legge un valore in valoreCorrente. Se la lettura ha successo, allora la condizione è vera ed eseguiamo il blocco che inizia con la graffa aperta dopo la condizione. Quel blocco termina con la graffa chiusa appena prima dell'istruzione di ritorno.

Una volta che sappiamo che ci sono numeri da contare, definiamo cnt, che conterà quante volte ogni numero distinto si verifica. Usiamo un ciclo while simile a quello nella sezione precedente per (ripetutamente) leggere numeri dall'input standard.

Il corpo del while è un blocco che contiene la seconda istruzione if:

// se i valori sono uguali
if (valore == valoreCorrente)
    ++cnt; // aggiungi 1 a cnt
else {
    // altrimenti, stampa il conteggio per il valore precedente
    std::cout << valoreCorrente << " si verifica "
                << cnt << " volte" << std::endl;
    valoreCorrente = valore; // ricorda il nuovo valore
    cnt = 1; // ripristina il contatore
}

La condizione in questo if usa l'operatore di uguaglianza (l'operatore ==) per testare se valore è uguale a valoreCorrente. Se sì, eseguiamo l'istruzione che segue immediatamente la condizione. Quell'istruzione incrementa cnt, indicando che abbiamo visto valoreCorrente ancora una volta.

Se la condizione è falsa, cioè, se valore non è uguale a valoreCorrente, allora eseguiamo l'istruzione dopo l'else. Questa istruzione è un blocco costituito da un'istruzione di output e due assegnazioni. L'istruzione di output stampa il conteggio per il valore che abbiamo appena finito di elaborare. Le assegnazioni ripristinano cnt a 1 e valoreCorrente a valore, che è il numero che abbiamo appena letto.

Nota

Differenza tra = e ==

C++ usa = per l'assegnazione e == per l'uguaglianza. Entrambi gli operatori possono apparire all'interno di una condizione. È un errore comune scrivere = quando si intende == all'interno di una condizione.

Esercizi

  • Cosa succede nel programma presentato in questa sezione se i valori di input sono tutti uguali? E se non ci sono valori duplicati?

    Soluzione: Se tutti i valori di input sono uguali, il programma stamperà una sola riga con il conteggio totale di quel valore. Se non ci sono valori duplicati, il programma stamperà una riga per ogni valore, ciascuna con un conteggio di 1.

Indentazione e Formattazione

I programmi C++ sono per lo più a formato libero, il che significa che i posti dove mettiamo le parentesi graffe, l'indentazione, i commenti e le nuove righe di solito non hanno effetto su ciò che i nostri programmi significano. Ad esempio, la parentesi graffa che denota l'inizio del corpo di main potrebbe essere sulla stessa riga di main; posizionata come abbiamo fatto, all'inizio della riga successiva; o collocata ovunque ci piaccia. L'unico requisito è che la graffa aperta deve essere il primo carattere non vuoto e non commentato dopo la lista dei parametri di main.

Sebbene siamo per lo più liberi di formattare i programmi come desideriamo, le scelte che facciamo influenzano la leggibilità dei nostri programmi. Potremmo, ad esempio, aver scritto main su un'unica lunga riga. Tale definizione, sebbene legale, sarebbe difficile da leggere.

Si svolgono dibattiti senza fine su quale sia il modo giusto di formattare i programmi C o C++. La nostra convinzione è che non ci sia uno stile corretto unico, ma che ci sia valore nella coerenza. La maggior parte dei programmatori indenta le parti secondarie dei loro programmi, come abbiamo fatto con le istruzioni all'interno di main e i corpi dei nostri cicli. Tendiamo a mettere le parentesi graffe che delimitano le funzioni sulle loro righe. Indentiamo anche le espressioni IO composte in modo che gli operatori si allineino. Altre convenzioni di indentazione diventeranno chiare man mano che i nostri programmi diventeranno più sofisticati.

L'importante è tenere a mente che sono possibili altri modi di formattare i programmi. Quando scegli uno stile di formattazione, pensa a come influisce sulla leggibilità e sulla comprensione. Una volta scelto uno stile, usalo in modo coerente.