Lezione 5: Iterazione Avanzata, Visibilità, Casualità e Debugging

Nelle lezioni precedenti abbiamo appreso il funzionamento delle strutture condizionali e del ciclo while come pilastri del controllo del flusso. In questa lezione completeremo lo studio dell’iterazione introducendo il costrutto for (ideale per cicli controllati da contatore) e il costrutto do-while (per cicli post-condizionati che devono essere eseguiti almeno una volta). Approfondiremo come la visibilità e la durata delle variabili siano determinate dal blocco di codice in cui vengono dichiarate (scope). Vedremo poi come simulare la casualità nei nostri programmi tramite generatori pseudo-casuali (rand e srand). Infine, affronteremo uno degli strumenti più importanti per lo sviluppo software: l’uso del debugger LLDB sia da terminale sia graficamente in VS Code.
1. Iterazione Avanzata: I Cicli for e do-while
Sezione intitolata “1. Iterazione Avanzata: I Cicli for e do-while”1.1 Il Ciclo for (Iterazione controllata da contatore)
Sezione intitolata “1.1 Il Ciclo for (Iterazione controllata da contatore)”Il ciclo for è un’abbreviazione sintattica (“zucchero sintattico”) del ciclo while. Viene impiegato quando il numero di ripetizioni è noto a priori o quando il ciclo è guidato da una variabile che funge da contatore.
La sintassi generale è:
for (inizializzazione; condizione; incremento) { // Corpo del ciclo}Questo blocco equivale esattamente alla seguente struttura realizzata con un ciclo while:
inizializzazione;while (condizione) { // Corpo del ciclo incremento;}Esempio: Stampa dei numeri pari da 2 a N
Sezione intitolata “Esempio: Stampa dei numeri pari da 2 a N”#include <stdio.h>
int main(void) { int limit = 10;
// Inizializzazione di i, controllo della condizione, e incremento eseguiti in un'unica riga for (int i = 2; i <= limit; i += 2) { printf("%d ", i); } printf("\n"); return 0;}La variabile i dichiarata all’interno dell’inizializzazione del for è locale al ciclo stesso. Come vedremo più avanti, non è possibile fare riferimento ad essa al di fuori del corpo del for.
Il while equivalente
Sezione intitolata “Il while equivalente”Per comprendere meglio la mappatura del for sul while, ecco come si scrive lo stesso programma usando l’iterazione base:
#include <stdio.h>
int main(void) { int limit = 10;
int i = 2; // 1. Inizializzazione while (i <= limit) { // 2. Condizione printf("%d ", i); i += 2; // 3. Incremento } printf("\n"); return 0;}1.2 Il Ciclo do-while (Iterazione post-condizionata)
Sezione intitolata “1.2 Il Ciclo do-while (Iterazione post-condizionata)”A differenza dei cicli while e for che valutano la condizione prima di eseguire il corpo del ciclo (e quindi potrebbero non essere eseguiti affatto se la condizione iniziale è falsa), il ciclo do-while valuta la condizione dopo l’esecuzione del corpo.
Questo garantisce che le istruzioni all’interno del ciclo vengano eseguite almeno una volta.
La sintassi è:
do { // Corpo del ciclo (eseguito almeno una volta)} while (condizione);(Nota il punto e virgola ; obbligatorio alla fine della tonda).
Confronto didattico: Validazione dell’input
Sezione intitolata “Confronto didattico: Validazione dell’input”La validazione dell’input da tastiera è lo scenario ideale per il ciclo do-while. Vediamo il confronto tra le due implementazioni:
Opzione A: Con il ciclo while (ridondante o con valori fittizi)
Sezione intitolata “Opzione A: Con il ciclo while (ridondante o con valori fittizi)”#include <stdio.h>
int main(void) { int voto = 0; // Inizializzazione fittizia per forzare l'ingresso nel loop
printf("Inserisci il voto dell'esame (18-30): "); scanf("%d", &voto);
while (voto < 18 || voto > 30) { printf("Voto non valido! Inserisci il voto dell'esame (18-30): "); scanf("%d", &voto); // Duplicazione del codice di lettura }
printf("Voto registrato: %d\n", voto); return 0;}Opzione B: Con il ciclo do-while (lineare e pulito)
Sezione intitolata “Opzione B: Con il ciclo do-while (lineare e pulito)”#include <stdio.h>
int main(void) { int voto; // Nessuna necessità di inizializzazione fittizia
do { printf("Inserisci il voto dell'esame (18-30): "); scanf("%d", &voto);
// Se il voto inserito non è valido, stampiamo un avviso prima di ripetere il ciclo if (voto < 18 || voto > 30) { printf("Voto non valido! Riprova.\n\n"); } } while (voto < 18 || voto > 30);
printf("Voto registrato con successo: %d\n", voto); return 0;}In questo modo otteniamo il meglio dei due mondi: un codice elegante privo di duplicazioni I/O, che però notifica l’errore all’utente in modo chiaro.
2. Cicli Annidati e Visibilità delle Variabili (Scope & Lifetime)
Sezione intitolata “2. Cicli Annidati e Visibilità delle Variabili (Scope & Lifetime)”2.1 Cicli Annidati (Nested Loops)
Sezione intitolata “2.1 Cicli Annidati (Nested Loops)”È possibile inserire un ciclo all’interno del corpo di un altro ciclo. Questa struttura è fondamentale per elaborare griglie bidimensionali, matrici o combinazioni.
Esempio: La Griglia di Coordinate
Sezione intitolata “Esempio: La Griglia di Coordinate”#include <stdio.h>
int main(void) { int righe = 3; int colonne = 4;
for (int r = 1; r <= righe; r++) { // Ciclo esterno for (int c = 1; c <= colonne; c++) { // Ciclo interno printf("(%d,%d) ", r, c); } printf("\n"); // Andata a capo al termine di ogni riga } return 0;}Output:
(1,1) (1,2) (1,3) (1,4)(2,1) (2,2) (2,3) (2,4)(3,1) (3,2) (3,3) (3,4)Il ciclo interno viene completato interamente per ciascuna iterazione del ciclo esterno.
2.2 Scope e Lifetime delle Variabili
Sezione intitolata “2.2 Scope e Lifetime delle Variabili”Ogni variabile in C possiede uno Scope (l’ambito in cui il nome della variabile è visibile e utilizzabile) e un Lifetime (il tempo durante il quale la variabile occupa fisicamente uno spazio in memoria).
La regola del Block Scope (Scope di Blocco)
Sezione intitolata “La regola del Block Scope (Scope di Blocco)”In C, ogni coppia di parentesi graffe { ... } definisce un blocco di codice con un proprio scope locale:
- Qualsiasi variabile dichiarata all’interno di un blocco
{}esiste ed è visibile solo all’interno del blocco stesso e dei blocchi in esso nidificati. - All’uscita del blocco, la variabile cessa di esistere e la sua memoria viene rilasciata.
Esempi di codice che non compilano
Sezione intitolata “Esempi di codice che non compilano”Caso 1: Variabile locale a una selezione condizionale
Sezione intitolata “Caso 1: Variabile locale a una selezione condizionale”#include <stdio.h>
int main(void) { int x = 10; if (x > 5) { int y = 20; // y ha scope limitato al blocco dell'if printf("y: %d\n", y); // OK } // printf("y: %d\n", y); // ERRORE DI COMPILAZIONE! 'y' non esiste in questo scope. return 0;}Caso 2: Variabile dichiarata dentro un ciclo
Sezione intitolata “Caso 2: Variabile dichiarata dentro un ciclo”#include <stdio.h>
int main(void) { for (int i = 0; i < 5; i++) { int quadrato = i * i; // nasce e muore ad ogni singola iterazione } // printf("%d\n", quadrato); // ERRORE: 'quadrato' non esiste più! // printf("%d\n", i); // ERRORE: 'i' (contatore del for) non esiste più! return 0;}Cenni sullo Stack di Memoria
Sezione intitolata “Cenni sullo Stack di Memoria”Quando una funzione viene avviata (incluso il main), il sistema operativo riserva una porzione di memoria temporanea e organizzata chiamata Stack (pila). Le variabili locali dichiarate all’interno delle funzioni e dei blocchi vengono create inserendole in cima allo Stack. Non appena l’esecuzione esce dal blocco di appartenenza, queste variabili vengono eliminate liberando automaticamente la memoria dello Stack.
Anticipazione: Variabili Globali
Sezione intitolata “Anticipazione: Variabili Globali”Le variabili dichiarate all’esterno di qualsiasi funzione (fuori dal main) sono dette variabili globali. Esse vivono per tutta la durata del programma in una zona di memoria statica e possono essere lette e modificate da qualsiasi parte del codice. L’uso di variabili globali è considerato una pessima pratica perché rende il codice instabile, difficile da testare e prono a bug logici. Nei laboratori e nelle prove d’esame il loro utilizzo è vietato.
3. Numeri Pseudo-Casuali e Debugging
Sezione intitolata “3. Numeri Pseudo-Casuali e Debugging”3.1 Generazione di Numeri Pseudo-Casuali
Sezione intitolata “3.1 Generazione di Numeri Pseudo-Casuali”Essendo macchine deterministiche, i computer non possono generare veri numeri casuali, ma utilizzano formule matematiche per produrre sequenze di numeri pseudo-casuali. Le funzioni per la casualità sono incluse nella libreria <stdlib.h>.
La funzione rand() e l’inizializzazione del seme (srand)
Sezione intitolata “La funzione rand() e l’inizializzazione del seme (srand)”La funzione rand() restituisce un numero intero pseudo-casuale compreso tra 0 e la costante RAND_MAX (almeno ). Se eseguiamo un programma che usa rand() più volte, noteremo che genera sempre gli stessi identici numeri. Questo accade perché l’algoritmo parte da un valore di innesco predefinito, chiamato seme (seed), impostato a 1.
Per ottenere numeri diversi ad ogni esecuzione, dobbiamo inizializzare il seme con un valore che cambia costantemente, come l’ora di sistema fornita dalla funzione time(NULL) della libreria <time.h>.
#include <stdio.h>#include <stdlib.h>#include <time.h>
int main(void) { srand(time(NULL)); // Inizializza il seme con l'ora corrente (da invocare una sola volta)
printf("Numero casuale: %d\n", rand()); return 0;}Formula generale per l’intervallo (estremi inclusi)
Sezione intitolata “Formula generale per l’intervallo [a,b][a, b][a,b] (estremi inclusi)”Per ricondurre un numero casuale generato da rand() all’interno di un intervallo , usiamo la formula:
int casuale = a + rand() % (b - a + 1);Esempio: Sfida a dadi su 3 lanci
Sezione intitolata “Esempio: Sfida a dadi su 3 lanci”#include <stdio.h>#include <stdlib.h>#include <time.h>
int main(void) { srand(time(NULL));
int somma_giocatore = 0; int somma_computer = 0;
printf("=== SFIDA A DADI ===\n\n");
for (int i = 1; i <= 3; i++) { int tiro_giocatore = 1 + rand() % 6; // Dado da 1 a 6 int tiro_computer = 1 + rand() % 6; // Dado da 1 a 6
printf("Lancio %d:\n", i); printf(" Giocatore: %d\n", tiro_giocatore); printf(" Computer: %d\n\n", tiro_computer);
somma_giocatore += tiro_giocatore; somma_computer += tiro_computer; }
printf("Totale Giocatore: %d | Totale Computer: %d\n", somma_giocatore, somma_computer);
if (somma_giocatore > somma_computer) { printf("Hai vinto!\n"); } else if (somma_computer > somma_giocatore) { printf("Vince il computer.\n"); } else { printf("Pareggio!\n"); }
return 0;}3.2 La Cultura del Debugging: LLDB da Terminale e VS Code
Sezione intitolata “3.2 La Cultura del Debugging: LLDB da Terminale e VS Code”Il debugger è lo strumento fondamentale per ispezionare l’esecuzione passo-passo del programma e trovare gli errori (bug) senza procedere per tentativi.
Compilazione ed esecuzione in LLDB
Sezione intitolata “Compilazione ed esecuzione in LLDB”Compiliamo il codice aggiungendo il flag -g per inserire i simboli di debug:
clang -g -Wall programma.c -o programmaQuindi avviamo il debugger:
lldb ./programmaUna volta aperto il prompt (lldb), utilizziamo i comandi principali:
b main: imposta un punto di interruzione (breakpoint) all’inizio delmain.r(run): avvia l’esecuzione del programma.n(next): esegue la riga corrente e passa alla successiva (Step Over).s(step): entra all’interno della funzione chiamata nella riga (Step In).v(frame variable): stampa a schermo tutte le variabili locali attive nello Stack.p nome_var: mostra il valore corrente di una singola variabile.c(continue): riprende l’esecuzione normale fino al prossimo breakpoint.q(quit): esce da LLDB.
Debugging Grafico in VS Code
Sezione intitolata “Debugging Grafico in VS Code”Utilizzando l’estensione CodeLLDB ed i file .vscode/tasks.json e launch.json configurati:
- Posiziona il mouse a sinistra del numero di riga nel file
.ce clicca per inserire un breakpoint (cerchio rosso). - Premi
F5per avviare la sessione di debug. - Avanza nel flusso usando i pulsanti grafici in alto (Step Over, Step In) e controlla lo stato della memoria in tempo reale nel pannello Variables a sinistra.
Codice di esempio per la sessione di Debug
Sezione intitolata “Codice di esempio per la sessione di Debug”Questo programma simula un algoritmo reale: calcolare la somma di tutti i numeri primi compresi tra a (il cui risultato corretto atteso è ).
Tuttavia, contiene tre insidiosi bug logici estremamente comuni e difficili da individuare a occhio (mancata inizializzazione dell’accumulatore, un errore di resettaggio del flag ed un errore sui limiti dei cicli). Compilando ed eseguendo, il programma restituirà un valore casuale senza senso. Usando il debugger scopriremo dove si inceppa la logica.
#include <stdio.h>
int main(void) { int limit = 15; int somma; // BUG 1: variabile accumulatore non inizializzata a 0! Conterrà valore spazzatura (garbage value). int is_prime = 1; // BUG 2: dichiarato qui, ma non resettato a 1 ad ogni iterazione del ciclo!
printf("Calcolo la somma dei numeri primi fino a %d...\n", limit);
for (int i = 2; i <= limit; i++) { // Verifichiamo se i è primo dividendolo per tutti i numeri da 2 a i for (int j = 2; j <= i; j++) { // BUG 3: j <= i includerà anche i stesso, quindi i % i == 0 sarà sempre vero! if (i % j == 0) { is_prime = 0; break; } }
if (is_prime) { somma += i; printf("Trovato primo: %d\n", i); } }
printf("La somma finale dei numeri primi è: %d (Attesa: 41)\n", somma); return 0;}Come procedere all’ispezione con il debugger:
Sezione intitolata “Come procedere all’ispezione con il debugger:”- Esecuzione iniziale: Compilando ed eseguendo il codice così com’è, si noterà che il programma termina stampando un valore incoerente o enorme (es.
1042779). - Indagine su BUG 1: Imposta un breakpoint all’inizio del
mained avvia il debug. Ispezionando il valore disommaprima del ciclo, vedrai che contiene un numero casuale sporco (garbage value) derivante da rimasugli nello Stack della RAM.- Correzione: È fondamentale inizializzare l’accumulatore a zero:
int somma = 0;.
- Correzione: È fondamentale inizializzare l’accumulatore a zero:
- Indagine su BUG 3: Riavvia il debug dopo aver corretto la dichiarazione di
somma. Avanza passo-passo (next) per la prima iterazione (i = 2). Entrando nel ciclo interno, vedrai che perj = 2la condizionej <= iè vera. Di conseguenza, si eseguei % j == 0(ovvero2 % 2 == 0, che è vero) impostando immediatamenteis_prime = 0. Il numero 2 viene quindi erroneamente scartato.- Correzione: La divisibilità va testata solo per i divisori minori del numero stesso. Dobbiamo correggere la condizione del ciclo interno in
j < i.
- Correzione: La divisibilità va testata solo per i divisori minori del numero stesso. Dobbiamo correggere la condizione del ciclo interno in
- Indagine su BUG 2: Riavvia il debug dopo aver applicato la seconda correzione. Avanzando passo-passo, vedrai che
i = 2ei = 3vengono ora identificati come primi e sommati. Arrivando ai = 4(non primo), il ciclo imposta giustamenteis_prime = 0. All’iterazione successiva peri = 5(primo), ispezionando il valore della variabileis_primeall’ingresso del ciclo vedrai che essa è rimasta a 0 (il valore dell’iterazione precedente!).- Correzione: Il flag
is_primedeve essere resettato a1all’inizio di ogni iterazione del ciclo esterno (dichiarandola o inizializzandola all’interno del corpo del primofor, non prima di esso).
- Correzione: Il flag
4. Quiz di Autovalutazione
Sezione intitolata “4. Quiz di Autovalutazione”1. Qual è la differenza fondamentale tra un ciclo while e un ciclo do-while?
- A) Il
do-whilepuò contenere solo variabili intere. - B) Il
do-whileesegue il corpo delle istruzioni almeno una volta prima di valutare la condizione. - C) Il
whileè più veloce da eseguire a livello hardware. - D) Il
do-whilenon permette l’uso dibreak.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Valutando la condizione in coda (post-condizione), il corpo del do-while viene sempre eseguito una prima volta, a prescindere dal fatto che la condizione sia vera o falsa.
2. Quale delle seguenti definizioni esprime esattamente il ciclo for (int i = 0; i < 5; i++) { ... } come ciclo while equivalente?
- A)
int i = 0; while (i < 5) { ... i++; } - B)
while (int i = 0; i < 5) { i++; ... } - C)
int i = 0; while (i < 5) { i++; ... } - D)
int i = 0; do { ... } while (i < 5);
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: L’inizializzazione avviene prima del ciclo, il controllo della condizione si effettua in cima ad ogni iterazione, e l’incremento viene eseguito come ultima istruzione all’interno del corpo.
3. Se dichiariamo una variabile int temp = 5; all’interno delle parentesi graffe di un blocco if, cosa succede se proviamo a leggerla dopo la parentesi graffa di chiusura dell’if?
- A) Il compilatore legge il valore
5. - B) Il programma compila ma restituisce un valore casuale (garbage value).
- C) Si verifica un errore di compilazione poiché la variabile non è definita al di fuori del blocco.
- D) Il sistema operativo arresta il programma con un crash di runtime.
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: In C vige lo scope di blocco (Block Scope). Una variabile dichiarata all’interno di {} esiste ed è visibile solo all’interno di quelle parentesi graffe.
4. A cosa serve la porzione di memoria RAM denominata Stack durante l’esecuzione del programma?
- A) Ad archiviare in modo permanente i file su disco.
- B) A contenere le istruzioni assembly del processore.
- C) Ad allocare e rimuovere automaticamente le variabili locali delle funzioni e dei blocchi attivi.
- D) A memorizzare le variabili globali utilizzate in tutti i file.
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Lo Stack è una memoria a pila veloce utilizzata per la gestione del ciclo di vita delle variabili automatiche locali: le variabili vengono impilate all’ingresso del blocco e rimosse all’uscita.
5. Qual è il valore massimo teorico che può essere restituito dall’espressione rand() % 10?
- A) 10
- B) 9
- C) 0
- D) RAND_MAX
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: L’operatore modulo % 10 restituisce i resti della divisione intera per 10, che possono variare esclusivamente da 0 a 9 inclusi.
6. Perché è necessario invocare la funzione srand(time(NULL)) all’inizio di un programma che genera numeri casuali?
- A) Per rendere l’esecuzione più veloce.
- B) Perché altrimenti la funzione
rand()restituirebbe sempre zero. - C) Per impostare un seme iniziale dinamico legato al tempo corrente, evitando di generare la stessa sequenza di numeri a ogni avvio del programma.
- D) Per calibrare il clock hardware della CPU del computer.
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: I computer usano generatori deterministici. Senza inizializzare il seme con un valore variabile (come il tempo corrente), l’algoritmo pseudo-casuale partirebbe sempre dallo stesso stato iniziale generano sempre la stessa sequenza.
7. Nel debugger LLDB, qual è il comando utilizzato per avanzare di una singola riga di codice senza scendere all’interno delle funzioni chiamate?
- A)
step - B)
run - C)
next - D)
continue
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Il comando next (o n) esegue l’istruzione corrente interamente ed avanza alla riga successiva dello stack frame corrente (Step Over).
8. Nel debugger LLDB, a cosa serve il comando frame variable (o v)?
- A) A cambiare il file sorgente visualizzato.
- B) A mostrare i valori di tutte le variabili locali attive nello stack frame corrente.
- C) A modificare il valore di una variabile in tempo reale.
- D) A terminare l’esecuzione del programma.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Il comando frame variable (abbreviato in v) stampa a schermo il nome e il valore corrente di tutte le variabili locali allocate nello Stack per la porzione di codice in esecuzione.
9. Quale flag deve essere passato al compilatore (Clang/GCC) in fase di compilazione per abilitare i simboli di debug utili a LLDB?
- A)
-Wall - B)
-O3 - C)
-g - D)
-std=c99
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Il flag -g dice al compilatore di inserire nell’eseguibile le tabelle dei simboli che associano gli indirizzi macchina delle istruzioni alle righe del file sorgente C.
10. Quale libreria deve essere inclusa per poter usare la funzione time(NULL)?
- A)
<stdlib.h> - B)
<stdio.h> - C)
<time.h> - D)
<math.h>
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: La libreria standard <time.h> contiene le definizioni e le funzioni per la manipolazione e l’acquisizione dell’ora e della data di sistema.
5. Esercizi Pratici
Sezione intitolata “5. Esercizi Pratici”Esercizio 1: Conto alla rovescia
Sezione intitolata “Esercizio 1: Conto alla rovescia”Scrivete un programma che acquisisca un numero intero positivo da tastiera e stampi un conto alla rovescia a partire da fino a (incluso) utilizzando un ciclo for.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int n = 0;
printf("Inserisci un numero intero positivo: "); if (scanf("%d", &n) != 1 || n < 0) { printf("Inserimento non valido.\n"); return 1; }
// Conto alla rovescia decrescente for (int i = n; i >= 0; i--) { printf("%d ", i); } printf("\n");
return 0;}Spiegazione dell’algoritmo:
Inizializziamo il contatore del ciclo for al valore . A ogni iterazione, decrementiamo i di una unità (i--) e continuiamo finché i è maggiore o uguale a zero.
Esercizio 2: Somma di numeri positivi
Sezione intitolata “Esercizio 2: Somma di numeri positivi”Scrivete un programma che chieda ripetutamente all’utente di inserire numeri interi positivi, sommandoli tra loro. Il programma deve terminare non appena l’utente inserisce il numero 0. Utilizzate un ciclo do-while e gestite la validazione in modo da ignorare (e segnalare come errore) eventuali numeri negativi senza interrompere il ciclo.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int numero; int somma = 0;
printf("Inserisci una serie di numeri positivi da sommare (0 per terminare):\n");
do { printf("Inserisci numero: "); if (scanf("%d", &numero) != 1) { printf("Errore di lettura.\n"); return 1; }
if (numero < 0) { printf("Errore: inserire solo numeri positivi!\n"); } else { somma += numero; } } while (numero != 0);
printf("La somma totale dei numeri validi inseriti è: %d\n", somma); return 0;}Spiegazione dell’algoritmo:
Il ciclo do-while garantisce che venga eseguita almeno una lettura. Se il valore inserito è negativo, stampiamo un messaggio di errore e non lo aggiungiamo alla variabile somma. Se il valore è positivo (o uguale a 0), lo sommiamo. Il ciclo termina non appena viene rilevato lo 0.
Esercizio 3: Stampa della tabellina
Sezione intitolata “Esercizio 3: Stampa della tabellina”Scrivete un programma che legga da tastiera un numero intero compreso tra 1 e 10 e stampi la sua tabellina (moltiplicata da 1 a 10) usando un ciclo for.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int n;
printf("Inserisci un numero da 1 a 10: "); if (scanf("%d", &n) != 1 || n < 1 || n > 10) { printf("Valore non valido.\n"); return 1; }
printf("Tabellina del %d:\n", n); for (int i = 1; i <= 10; i++) { printf("%d x %d = %d\n", n, i, n * i); }
return 0;}Spiegazione dell’algoritmo:
Dopo aver validato l’input, utilizziamo un ciclo for che conta da 1 a 10 per moltiplicare il numero ad ogni passo, stampando il risultato formattato.
Esercizio 4: Griglia quadrata di asterischi
Sezione intitolata “Esercizio 4: Griglia quadrata di asterischi”Scrivete un programma che legga da tastiera un intero positivo e stampi una griglia quadrata di asterischi di lato (righe e colonne pari a ), utilizzando cicli for annidati.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int lato;
printf("Inserisci il lato del quadrato: "); if (scanf("%d", &lato) != 1 || lato <= 0) { printf("Lato non valido.\n"); return 1; }
// Ciclo esterno per le righe for (int i = 0; i < lato; i++) { // Ciclo interno per le colonne della riga corrente for (int j = 0; j < lato; j++) { printf("* "); } printf("\n"); // A capo al termine di ciascuna riga }
return 0;}Spiegazione dell’algoritmo:
Il ciclo esterno gestisce la creazione delle righe del quadrato. Per ogni singola riga creata dall’esterno, il ciclo interno provvede a stampare esattamente asterischi affiancati da uno spazio.
Esercizio 5: Cornice quadrata vuota di asterischi
Sezione intitolata “Esercizio 5: Cornice quadrata vuota di asterischi”Scrivete un programma che legga da tastiera un intero positivo e stampi un quadrato “vuoto” di lato (ossia stampando asterischi solo sui bordi esterni e spazi all’interno).
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int lato;
printf("Inserisci il lato (minimo 2): "); if (scanf("%d", &lato) != 1 || lato < 2) { printf("Lato non valido.\n"); return 1; }
for (int i = 0; i < lato; i++) { for (int j = 0; j < lato; j++) { // Stampiamo l'asterisco solo se siamo sulla prima/ultima riga o colonna if (i == 0 || i == lato - 1 || j == 0 || j == lato - 1) { printf("* "); } else { printf(" "); // Due spazi per compensare "* " } } printf("\n"); }
return 0;}Spiegazione dell’algoritmo:
Nidifichiamo due cicli per scansionare ogni cella del quadrato. Grazie alla struttura condizionale if, stampiamo un asterisco solo se la coordinata corrente si trova su un bordo esterno (riga 0, riga lato - 1, colonna 0 o colonna lato - 1). Altrimenti stampiamo spazi vuoti.
Esercizio 6: Caccia al numero casuale
Sezione intitolata “Esercizio 6: Caccia al numero casuale”Scrivete un programma che generi continuamente numeri casuali interi compresi tra 1 e 50 e li stampi a schermo. Il ciclo deve interrompersi non appena viene generato il numero 42, stampando a schermo il numero totale di tentativi effettuati.
💻 Mostra Soluzione e Codice C
#include <stdio.h>#include <stdlib.h>#include <time.h>
int main(void) { srand(time(NULL));
int casuale; int tentativi = 0;
printf("Generazione di numeri casuali fino al 42...\n");
do { casuale = 1 + rand() % 50; // Valore tra 1 e 50 tentativi++; printf("Tentativo %d: %d\n", tentativi, casuale); } while (casuale != 42);
printf("\nTrovato 42 in %d tentativi!\n", tentativi); return 0;}Spiegazione dell’algoritmo:
Inizializziamo il seme dei numeri casuali con time(NULL). Utilizziamo un ciclo do-while per generare un numero ad ogni iterazione, incrementando una variabile contatore. La condizione di controllo fa sì che il ciclo si ripeta finché il numero estratto è diverso da 42.
Esercizio 7: Tabella di potenze
Sezione intitolata “Esercizio 7: Tabella di potenze”Scrivete un programma che acquisisca un numero intero positivo e visualizzi una tabella formattata a tre colonne contenente: il numero , il suo quadrato , e il suo cubo , per tutti i valori di da a .
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int n;
printf("Inserisci un intero positivo N: "); if (scanf("%d", &n) != 1 || n <= 0) { printf("Inserimento errato.\n"); return 1; }
printf("%5s | %10s | %10s\n", "N", "Quadrato", "Cubo"); printf("----------------------------------\n");
for (int i = 1; i <= n; i++) { int quadrato = i * i; int cubo = i * i * i; printf("%5d | %10d | %10d\n", i, quadrato, cubo); }
return 0;}Spiegazione dell’algoritmo:
Utilizziamo un ciclo for da 1 a . Per formattare le colonne in modo che i dati siano allineati a destra, sfruttiamo le specifiche di ampiezza minima di campo di printf (es. %10d riserva almeno 10 caratteri di spazio per la stampa del numero).
Esercizio 8: Calcolo della potenza (Moltiplicazione cumulativa)
Sezione intitolata “Esercizio 8: Calcolo della potenza (Moltiplicazione cumulativa)”Scrivete un programma che legga da tastiera due interi positivi: la base e l’esponente . Calcolate la potenza utilizzando un ciclo, senza fare uso della funzione pow della libreria <math.h>.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int base, esponente; long risultato = 1;
printf("Inserisci la base (B >= 0): "); if (scanf("%d", &base) != 1 || base < 0) { return 1; }
printf("Inserisci l'esponente (E >= 0): "); if (scanf("%d", &esponente) != 1 || esponente < 0) { return 1; }
// Moltiplicazione ripetuta for (int i = 0; i < esponente; i++) { risultato = risultato * base; }
printf("%d elevato a %d è pari a: %ld\n", base, esponente, risultato); return 0;}Spiegazione dell’algoritmo:
Inizializziamo il risultato a 1. Eseguiamo un ciclo for che si ripete un numero di volte pari all’esponente, moltiplicando ad ogni iterazione il risultato accumulato per la base.
Esercizio 9: Piramide di numeri
Sezione intitolata “Esercizio 9: Piramide di numeri”Scrivete un programma che acquisisca un numero intero positivo (altezza) e stampi una piramide di numeri di altezza . Ad esempio, per , il programma dovrà stampare:
1 2 2 3 3 34 4 4 4💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) { int altezza;
printf("Inserisci l'altezza della piramide: "); if (scanf("%d", &altezza) != 1 || altezza <= 0) { printf("Altezza non valida.\n"); return 1; }
for (int i = 1; i <= altezza; i++) { // 1. Stampa degli spazi iniziali per centrare la riga for (int spazi = 0; spazi < altezza - i; spazi++) { printf(" "); }
// 2. Stampa dei numeri separati da spazio for (int j = 0; j < i; j++) { printf("%d ", i); }
printf("\n"); // A capo a fine riga }
return 0;}Spiegazione dell’algoritmo:
Utilizziamo un ciclo esterno che controlla le righe (da 1 a altezza). Per ciascuna riga i, stampiamo prima altezza - i spazi vuoti per spingere la piramide a destra, e poi eseguiamo un ciclo interno che stampa il valore di i (ossia il numero di riga corrente) per volte.
Esercizio 10: Simulatore di Tiro al Bersaglio (Gioco interattivo)
Sezione intitolata “Esercizio 10: Simulatore di Tiro al Bersaglio (Gioco interattivo)”Scrivete un programma che simuli un gioco di tiro al bersaglio. Il bersaglio ha punteggi da a . Ad ogni turno, il programma genera un tiro casuale (un valore da a ). Se il tiro è inferiore o uguale a , viene considerato un “mancato” e non assegna punti. Il gioco termina non appena la somma totale dei punti validi raggiunge o supera , oppure se il giocatore compie un totale di tiri complessivi. Stampate l’esito di ogni tiro, il punteggio totale ed un messaggio di vittoria (se si raggiungono i 30 punti) o di sconfitta.
💻 Mostra Soluzione e Codice C
#include <stdio.h>#include <stdlib.h>#include <time.h>
int main(void) { srand(time(NULL));
int punteggio_totale = 0; int tiri = 0; int bersaglio;
printf("=== BERSAGLIO CASUALE ===\n"); printf("Obiettivo: Raggiungere 30 punti. Massimo 8 tentativi.\n\n");
while (tiri < 8 && punteggio_totale < 30) { bersaglio = 1 + rand() % 10; // Genera punteggio da 1 a 10 tiri++;
printf("Tiro %d: Colpito settore %d", tiri, bersaglio);
if (bersaglio <= 3) { printf(" -> MANCATO! (0 punti)\n"); } else { punteggio_totale += bersaglio; printf(" -> Centro! (%d punti aggiunti)\n", bersaglio); }
printf("Punteggio corrente: %d\n\n", punteggio_totale); }
printf("--- Partita Terminata ---\n"); if (punteggio_totale >= 30) { printf("Complimenti, hai vinto in %d tiri!\n", tiri); } else { printf("Tentativi esauriti! Hai perso. Punteggio finale: %d\n", punteggio_totale); }
return 0;}Spiegazione dell’algoritmo:
Il ciclo continua finché si verificano contemporaneamente due condizioni: il numero di tiri è inferiore al limite di 8, e il punteggio accumulato è inferiore all’obiettivo di 30. A ogni iterazione, generiamo un punteggio casuale, incrementiamo i tiri e controlliamo se il colpo è andato a segno (valore ). Terminati i cicli, valutiamo se la fine è avvenuta per superamento dell’obiettivo (Vittoria) o per esaurimento tiri (Sconfitta).