Salta ai contenuti

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

Copertina Lezione 5

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.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;
}
#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.

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).

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)”

È possibile inserire un ciclo all’interno del corpo di un altro ciclo. Questa struttura è fondamentale per elaborare griglie bidimensionali, matrici o combinazioni.

#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.


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).

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.
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;
}
#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;
}

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.

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.


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 3276732767). 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 [a,b][a, b] (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 [a,b][a, b], usiamo la formula:

int casuale = a + rand() % (b - a + 1);
#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.

Compiliamo il codice aggiungendo il flag -g per inserire i simboli di debug:

Terminal window
clang -g -Wall programma.c -o programma

Quindi avviamo il debugger:

Terminal window
lldb ./programma

Una volta aperto il prompt (lldb), utilizziamo i comandi principali:

  • b main: imposta un punto di interruzione (breakpoint) all’inizio del main.
  • 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.

Utilizzando l’estensione CodeLLDB ed i file .vscode/tasks.json e launch.json configurati:

  1. Posiziona il mouse a sinistra del numero di riga nel file .c e clicca per inserire un breakpoint (cerchio rosso).
  2. Premi F5 per avviare la sessione di debug.
  3. 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.

Questo programma simula un algoritmo reale: calcolare la somma di tutti i numeri primi compresi tra 22 a 1515 (il cui risultato corretto atteso è 2+3+5+7+11+13=412 + 3 + 5 + 7 + 11 + 13 = 41).

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;
}
  1. Esecuzione iniziale: Compilando ed eseguendo il codice così com’è, si noterà che il programma termina stampando un valore incoerente o enorme (es. 1042779).
  2. Indagine su BUG 1: Imposta un breakpoint all’inizio del main ed avvia il debug. Ispezionando il valore di somma prima 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;.
  3. 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 per j = 2 la condizione j <= i è vera. Di conseguenza, si esegue i % j == 0 (ovvero 2 % 2 == 0, che è vero) impostando immediatamente is_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.
  4. Indagine su BUG 2: Riavvia il debug dopo aver applicato la seconda correzione. Avanzando passo-passo, vedrai che i = 2 e i = 3 vengono ora identificati come primi e sommati. Arrivando a i = 4 (non primo), il ciclo imposta giustamente is_prime = 0. All’iterazione successiva per i = 5 (primo), ispezionando il valore della variabile is_prime all’ingresso del ciclo vedrai che essa è rimasta a 0 (il valore dell’iterazione precedente!).
    • Correzione: Il flag is_prime deve essere resettato a 1 all’inizio di ogni iterazione del ciclo esterno (dichiarandola o inizializzandola all’interno del corpo del primo for, non prima di esso).

1. Qual è la differenza fondamentale tra un ciclo while e un ciclo do-while?

  • A) Il do-while può contenere solo variabili intere.
  • B) Il do-while esegue 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-while non permette l’uso di break.
▶ 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.


Scrivete un programma che acquisisca un numero intero positivo NN da tastiera e stampi un conto alla rovescia a partire da NN fino a 00 (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 NN. A ogni iterazione, decrementiamo i di una unità (i--) e continuiamo finché i è maggiore o uguale a zero.


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.


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 NN ad ogni passo, stampando il risultato formattato.


Scrivete un programma che legga da tastiera un intero positivo LL e stampi una griglia quadrata di asterischi di lato LL (righe e colonne pari a LL), 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 LL righe del quadrato. Per ogni singola riga creata dall’esterno, il ciclo interno provvede a stampare esattamente LL asterischi affiancati da uno spazio.


Scrivete un programma che legga da tastiera un intero positivo LL e stampi un quadrato “vuoto” di lato LL (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.


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.


Scrivete un programma che acquisisca un numero intero positivo NN e visualizzi una tabella formattata a tre colonne contenente: il numero ii, il suo quadrato i2i^2, e il suo cubo i3i^3, per tutti i valori di ii da 11 a NN.

💻 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 NN. 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 BB e l’esponente EE. Calcolate la potenza BEB^E 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.


Scrivete un programma che acquisisca un numero intero positivo HH (altezza) e stampi una piramide di numeri di altezza HH. Ad esempio, per H=4H=4, il programma dovrà stampare:

1
2 2
3 3 3
4 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 ii 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 11 a 1010. Ad ogni turno, il programma genera un tiro casuale (un valore da 11 a 1010). Se il tiro è inferiore o uguale a 33, viene considerato un “mancato” e non assegna punti. Il gioco termina non appena la somma totale dei punti validi raggiunge o supera 3030, oppure se il giocatore compie un totale di 88 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 >3>3). Terminati i cicli, valutiamo se la fine è avvenuta per superamento dell’obiettivo (Vittoria) o per esaurimento tiri (Sconfitta).