6 Livello 4
Il presente capitolo analizza i costrutti iterativi del linguaggio C, esaminando la logica di esecuzione e l’equivalenza computazionale dei cicli for, while e do-while, l’impiego degli operatori di assegnamento composto e l’applicazione del cast di tipo nella gestione dei cicli.
6.1 Iterazione
Per sviluppare programmi di maggiore complessità, strutturando il codice sorgente in modo chiaro e conciso, è necessario introdurre le istruzioni di iterazione (o cicli). Quando l’algoritmo richiede l’esecuzione ripetuta di un’istruzione o di un blocco di istruzioni, si definisce un ciclo. La regola fondamentale, in questo contesto, impone di non duplicare mai la medesima istruzione nel codice.
6.1.1 Istruzione for
Il costrutto di iterazione utilizzato con maggiore frequenza nel linguaggio C è il ciclo for. Nella sua configurazione standard, un ciclo for presenta la seguente struttura sintattica:
for (i = 0; i < n; i++) {
// Blocco di istruzioni da eseguire ripetutamente
}Come si può notare, il costrutto racchiude tre campi distinti, separati dal carattere punto e virgola ;:
Primo Campo, inizializzazione (
i = 0): la variabilei(preventivamente dichiarata nel programma) funge da variabile di controllo dell’iterazione. Questa istruzione viene eseguita una sola volta, esclusivamente all’avvio del ciclo, e determina lo stato iniziale del contatore. Una corretta inizializzazione è cruciale, infatti un errore in questa fase compromette l’integrità logica del programma senza che il compilatore segnali alcuna anomalia.Secondo Campo, condizione di terminazione (
i < n): ospita un’espressione condizionale che viene valutata prima di intraprendere ciascuna iterazione del blocco di codice. Questo significa che il ciclofor(analogamente al ciclowhile) è classificato come un costrutto a condizione preventiva, se l’espressione risulta falsa sin dal primo controllo, il blocco di codice interno non verrà eseguito mai.Terzo Campo, aggiornamento (
i++): specifica l’incremento (o decremento) della variabile di controllo. Questa operazione viene eseguita dopo che tutte le istruzioni racchiuse nel blocco{}sono state portate a termine. L’espressionei++rappresenta la forma contratta della scrittura di assegnamentoi = i + 1.
Un fattore critico nella progettazione dei costrutti iterativi consiste nell’evitare la generazione di un ciclo infinito. Questa condizione anomala si verifica quando la logica delle istruzioni interne o del terzo campo non modifica la variabile di controllo in modo coerente, rendendo l’espressione condizionale del secondo campo costantemente vera.
Consideriamo il seguente esempio volto a illustrare l’applicazione pratica del ciclo for:
#include <stdio.h>
int main(void) {
int i, n, numero, somma = 0;
float media;
printf("Quanti numeri interi si desidera sommare? ");
scanf("%d", &n);
for (i = 1; i <= n; i++) {
printf("\nNumero intero #%d: ", i);
scanf("%d", &numero);
somma += numero;
}
media = (float)somma / n;
printf("\nLa media è pari a: %f\n", media);
return 0;
}In questo frammento di codice vengono introdotti alcuni elementi nuovi. L’accumulazione dei valori è espressa tramite la forma contratta (+=), derivata dalla combinazione di due operatori distinti. L’istruzione somma += numero è equivalente a somma = somma + numero;.
Sebbene la prima formulazione sia più compatta e ampiamente adottata nella pratica, richiede attenzione in quanto rende infatti meno esplicito il fatto che la variabile somma figuri anche sul lato destro dell’espressione. Per tale ragione, risulta fondamentale provvedere alla sua inizializzazione (impostandola a zero) prima del suo utilizzo, al fine di evitare l’elaborazione di valori imprevedibili residui della memoria RAM.
La seconda novità risiede nell’introduzione del costrutto di conversione di tipo o cast ((float)somma). Attraverso questo operatore, si modifica temporaneamente il tipo di dato della variabile somma all’interno dell’espressione di calcolo. Questo passaggio è di fondamentale importanza nel caso in esame perché in assenza di cast, la divisione tra somma e n verrebbe valutata dal compilatore come una divisione intera, troncando la parte decimale del risultato.
6.1.2 Istruzione while
Come accennato in precedenza, esiste almeno un altro costrutto di iterazione estremamente utile: il ciclo while. Nella maggior parte dei casi, questi due diversi cicli possono essere utilizzati in modo intercambiabile. La buona pratica prevede l’impiego del ciclo for quando il numero di iterazioni è noto a priori, riservando il while agli scenari in cui la terminazione dipende dal verificarsi di una determinata condizione dinamica.
Una delle differenze più evidenti risiede nella sintassi. Dopo la parola chiave while, all’interno delle parentesi (), deve essere specificata esclusivamente l’espressione condizionale. Nell’intestazione del costrutto non sono previsti spazi formali per l’inizializzazione della variabile di controllo o per il suo incremento/decremento. Questo non significa che tali fasi possano essere omesse, i medesimi passaggi logici devono semplicemente essere distribuiti in punti diversi del codice sorgente. Si consideri il seguente esempio, funzionalmente equivalente a quello analizzato in precedenza con il ciclo for:
#include <stdio.h>
int main(void) {
int i = 1, n, numero, somma = 0;
float media;
printf("Quanti numeri interi si desidera sommare? ");
scanf("%d", &n);
while (i <= n) {
printf("\nNumero intero #%d: ", i);
scanf("%d", &numero);
somma += numero;
i++;
}
media = (float)somma / n;
printf("\nLa media è pari a: %f\n", media);
return 0;
}I due programmi sono equivalenti dal punto di vista computazionale ed è teoricamente possibile utilizzare indistintamente l’uno o l’altro costrutto.
6.1.3 Istruzione do-while
Sebbene sia di utilizzo meno frequente, il ciclo do-while costituisce una variante che non deve essere trascurata. La differenza sostanziale rispetto al ciclo while risiede nel momento in cui viene effettuata la verifica della condizione: nel do-while la condizione viene valutata dopo l’esecuzione del blocco di codice. Questo aspetto garantisce che le istruzioni racchiuse nel ciclo vengano eseguite almeno una volta, indipendentemente dal valore iniziale dell’espressione di controllo. Tale costrutto trova una collocazione ideale nelle procedure di validazione dell’input, dove è necessario acquisire un dato almeno una volta prima di poterne verificare la validità.
Per completezza, esistono due istruzioni in grado di alterare il normale flusso di un ciclo: break interrompe immediatamente l’iterazione e trasferisce il controllo alla prima istruzione successiva al ciclo, mentre continue salta direttamente all’iterazione successiva, omettendo le istruzioni rimanenti del blocco corrente. Si tratta di strumenti da utilizzare con cautela, poiché un impiego eccessivo rende più difficile seguire la logica del programma.
Scrivere un programma che permetta di inserire N voti, calcoli la media e conti il numero di voti sopra una soglia definita dall’utente.
#include <stdio.h>
int main(void) {
int n,i,voto,somma=0,soglia,sopra=0;
printf("Quanti voti?");
scanf("%d",&n);
printf("Che soglia?");
scanf("%d",&soglia);
for(i=1;i<=n;i++) {
printf("Inserisci voto %d: ",i);
scanf("%d",&voto);
somma+=voto;
if(voto>soglia) ++sopra;
}
printf("\nLa media e': %.1f\n",(float)somma/n);
printf("\n%d voti sopra %d", sopra,soglia);
return 0;
}i++ oppure ++i?
Si incontrano entrambe le forme i++ (post-incremento) e ++i (pre-incremento). Quando l’incremento costituisce un’istruzione a sé stante, come nel terzo campo di un ciclo for, le due forme sono perfettamente equivalenti. La differenza emerge solo quando l’espressione viene usata all’interno di un’espressione più ampia: ++i incrementa la variabile e restituisce il valore già aggiornato, mentre i++ restituisce il valore originale ed esegue l’incremento subito dopo.