Lezione 7: L'Astrazione del Codice e l'Anatomia dello Stack

Nelle lezioni precedenti abbiamo imparato a strutturare programmi in grado di compiere scelte e ripetere istruzioni. Tuttavia, man mano che la complessità dei problemi aumenta, scrivere tutto il codice all’interno di un unico blocco monolitico (il main) porta inevitabilmente a codici confusi, difficili da leggere e pieni di duplicazioni (il pericoloso pattern del copia-incolla). Questa lezione introduce lo strumento fondamentale per superare questo limite: le funzioni. Impareremo a scomporre un problema complesso in sotto-problemi indipendenti, a definire l’interfaccia di una funzione e a comprendere cosa accade “sotto il cofano” della memoria RAM, analizzando la struttura dello Stack e l’evoluzione dei Record di Attivazione.
1. Il Limite del Codice Monolitico e il Principio DRY
Sezione intitolata “1. Il Limite del Codice Monolitico e il Principio DRY”1.1 Perché il copia-incolla è il male
Sezione intitolata “1.1 Perché il copia-incolla è il male”Immaginiamo di dover trovare il valore massimo all’interno di tre array distinti nel nostro programma. Senza uno strumento per riutilizzare la logica di calcolo, l’unica soluzione possibile sarebbe duplicare lo stesso codice per tre volte, adattando manualmente i nomi delle variabili:
// Ricerca massimo nel primo arrayint max1 = array1[0];for (int i = 1; i < N; i++) { if (array1[i] > max1) { max1 = array1[i]; }}
// Ricerca massimo nel secondo array (COPIA-INCOLLA)int max2 = array2[0];for (int i = 1; i < N; i++) { if (array2[i] > max2) { max2 = array2[i]; }}
// E così via per il terzo array...Quando ci troviamo a dover eseguire lo stesso calcolo su insiemi di dati diversi, la tentazione immediata è quella di copiare e incollare il blocco di codice interessato, modificando solo i nomi delle variabili. Questo approccio viola il principio cardine dell’Ingegneria del Software noto come DRY (Don’t Repeat Yourself).
Se in un blocco di codice duplicato è presente un bug (ad esempio un errore off-by-one nel limite di un ciclo), saremo costretti a rintracciare e correggere l’errore in ogni singola copia. Qualora dimenticassimo anche solo una di queste copie, il programma presenterebbe comportamenti inconsistenti.
1.2 L’approccio procedurale
Sezione intitolata “1.2 L’approccio procedurale”Per risolvere questo problema, il linguaggio C adotta il paradigma procedurale. Esso permette di isolare una porzione di codice, assegnarle un nome auto-esplicativo e richiamarla ogni volta che ne sorge la necessità.
Questa operazione prende il nome di astrazione: l’utente della funzione ha bisogno di conoscere solo l’interfaccia (cosa entra e cosa esce) senza doversi preoccupare dei dettagli implementativi interni.
In realtà, abbiamo utilizzato le funzioni fin dal nostro primissimo programma: in C non è possibile scrivere codice senza ricorrere ad esse, dato che il main stesso è una funzione obbligatoria che funge da punto d’ingresso del programma.
Inoltre, abbiamo fatto largo uso delle funzioni della libreria standard (quali printf, scanf, pow, sqrt, rand, srand). Questo ci ha permesso di risparmiare tempo prezioso, di modularizzare il nostro codice e di appoggiarci a implementazioni robuste, sicure e ampiamente testate. Questo rappresenta già un enorme guadagno in termini di produttività.
Ma cosa succede se il nostro programma necessita di un calcolo specifico che non è presente in nessuna libreria di sistema? In questo caso, il C ci permette di costruire le nostre funzioni personalizzate (user-defined functions).
Per comprendere l’utilità pratica, consideriamo cosa dovremmo fare se volessimo calcolare la radice quadrata di un numero reale senza poter richiamare la funzione sqrt di <math.h>:
// Come calcoliamo la radice oggi grazie alle librerie (1 sola riga, zero pensieri):double radice = sqrt(25.0);
// Come dovremmo scriverlo nel main se NON potessimo usare le funzioni:double n = 25.0;double radice;if (n < 0) { printf("Errore: radice di numero negativo!\n"); // gestione dell'errore...} else { // Algoritmo di approssimazione di Newton-Raphson iterativo double x = n; double y = 1.0; double e = 0.000001; while (x - y > e) { x = (x + y) / 2; y = n / x; } radice = x;}
// E dovremmo fare questo COPIA-INCOLLA ogni volta che ci serve una radice nel programma!2. Anatomia di una Funzione in C
Sezione intitolata “2. Anatomia di una Funzione in C”Il concetto di funzione in informatica deriva direttamente dalla matematica. In matematica, una funzione associa ad ogni elemento di un insieme di partenza (detto dominio ) uno e un solo elemento di un insieme di arrivo (detto codominio ), formalizzato come .
In programmazione facciamo esattamente lo stesso:
- Il dominio è rappresentato dai tipi dei parametri formali accettati in ingresso.
- Il codominio è rappresentato dal tipo di ritorno della funzione.
- La legge di associazione è descritta dal corpo della funzione che elabora gli input per produrre l’output.
Vediamo il parallelo sintattico:
Una funzione in C è definita da tre elementi essenziali che costituiscono la sua firma (o prototipo), seguiti dal corpo racchiuso tra parentesi graffe:
tipo_ritorno nome_funzione(tipo_parametro1 parametro1, tipo_parametro2 parametro2, ...) { // Corpo della funzione return valore_ritorno; //deve essere di tipo "tipo_ritorno"}- Tipo di Ritorno: Specifica il tipo di dato che la funzione restituisce al termine del suo calcolo (es.
int,double,char). Se la funzione esegue solo delle azioni (come stampare a schermo) e non deve restituire alcun valore, si utilizza la parola chiavevoid. - Nome Identificativo: Segue le stesse regole dei nomi di variabili (deve iniziare con una lettera o underscore e non contenere spazi).
- Parametri Formali: Le variabili dichiarate tra parentesi tonde che accolgono i dati in ingresso. Fungono da variabili locali temporanee che verranno popolate all’atto della chiamata.
- Istruzione return: Termina immediatamente l’esecuzione della funzione e restituisce il valore specificato al chiamante. Se la funzione è di tipo
void(non restituisce nulla), l’uso direturn;è facoltativo e serve solo ad interrompere anticipatamente l’esecuzione del corpo. Se invece la funzione ha un tipo di ritorno nonvoid, l’istruzionereturncon un valore coerente è obbligatoria per trasmettere il risultato al chiamante.
Facciamo un esempio pratico:
// Prototipo o Firma (Il "Cosa")int calcola_somma(int a, int b);
// Definizione (Il "Come")int calcola_somma(int a, int b) { int risultato = a + b; return risultato;}Prototipi e Definizioni
Sezione intitolata “Prototipi e Definizioni”Poiché il compilatore C analizza il codice dall’alto verso il basso, se una funzione viene chiamata nel main prima di essere definita, il compilatore genererà un errore. Per evitare questo, si separano i prototipi (dichiarazioni) posti in cima al file, dalle definizioni effettive collocate in fondo:
#include <stdio.h>
// Dichiarazione (Prototipo)double half(int x);
int main(void) { int val = 5; double ris = half(val); // Chiamata printf("La meta' e': %f\n", ris); return 0;}
// Definizionedouble half(int x) { return (double)x / 2;}3. Il Flusso di Esecuzione e lo Scope delle Variabili
Sezione intitolata “3. Il Flusso di Esecuzione e lo Scope delle Variabili”3.1 Alterazione del flusso
Sezione intitolata “3.1 Alterazione del flusso”Quando viene effettuata una chiamata a funzione, il flusso principale (es. main) viene temporaneamente “congelato”. La CPU esegue un salto in memoria passando al corpo della funzione chiamata. Solo quando la funzione incontra l’istruzione return (o termina il suo blocco), il controllo ritorna al chiamante che si “scongela” e riprende l’esecuzione dall’istruzione immediatamente successiva.
Consideriamo il seguente esempio di codice numerato:
1. int calcola_somma(int a, int b) {2. int risultato = a + b;3. return risultato;4. }5.6. int main(void) {7. int x = 5, y = 3;8. int totale = calcola_somma(x, y);9. printf("Totale: %d\n", totale);10. return 0;11. }L’ordine di esecuzione delle righe è il seguente:
- Riga 6-7: Il programma inizia dal
main. Vengono allocate e inizializzate le variabili localix(5) ey(3). - Riga 8: Viene invocata la funzione
calcola_somma. Il flusso principale si interrompe: i valori dixeyvengono copiati rispettivamente inaeb. La CPU esegue un salto in memoria. - Riga 1: Inizia l’esecuzione di
calcola_somma. I parametri formalia(5) eb(3) sono ora pronti in memoria. - Riga 2: Viene dichiarata la variabile locale
risultatoed eseguito il calcolo (5 + 3 = 8). - Riga 3: L’istruzione
return risultatorestituisce il valore8. Lo Stack Frame dicalcola_sommaviene distrutto. - Riga 8 (ripresa): Il controllo ritorna al
main. Il valore restituito (8) viene assegnato alla variabiletotale. - Riga 9: Viene stampato il testo
"Totale: 8". - Riga 10: Il
maintermina restituendo0al Sistema Operativo.
3.2 Passaggio per Valore (Copia)
Sezione intitolata “3.2 Passaggio per Valore (Copia)”In C, il passaggio dei parametri avviene sempre per valore. Ciò significa che quando passiamo una variabile come argomento (parametro attuale) a una funzione, il sistema non passa la variabile stessa, ma una copia del suo valore che viene memorizzata nel parametro formale.
Di conseguenza, qualsiasi modifica apportata al parametro formale all’interno della funzione non ha alcun effetto sulla variabile originale del chiamante.
Parametri Formali vs Parametri Attuali
- Parametri Formali: Le variabili dichiarate nella firma della funzione che definiscono l’interfaccia (es.
int a, int bincalcola_somma). Esistono solo all’interno del corpo della funzione. - Parametri Attuali (o Argomenti): I valori concreti o le variabili passati al momento della chiamata (es.
x, yo5, 3nelmain).
Vediamo un esempio di codice che dimostra che le modifiche ai parametri formali non influenzano i parametri attuali:
#include <stdio.h>
void incrementa(int n) { n = n + 1; // Modifica solo la copia locale 'n' printf("Valore dentro la funzione: %d\n", n);}
int main(void) { int x = 10; incrementa(x); // Passiamo x (parametro attuale) alla funzione printf("Valore nel main dopo la chiamata: %d\n", x); // x rimarrà 10! return 0;}3.3 Scope Locale (Block Scope)
Sezione intitolata “3.3 Scope Locale (Block Scope)”Le variabili dichiarate all’interno di una funzione (compresi i parametri formali) hanno uno scope locale: nascono al momento della chiamata e vengono distrutte non appena la funzione termina. Variabili dichiarate in funzioni diverse possono avere lo stesso nome senza causare conflitti, poiché risiedono in aree di memoria differenti.
#include <stdio.h>
void funzioneA(void) { int valore = 42; // Locale a funzioneA printf("Dentro funzioneA: %d\n", valore);}
void funzioneB(void) { int valore = 99; // Locale a funzioneB. Non collide con quella di funzioneA! printf("Dentro funzioneB: %d\n", valore);}
int main(void) { funzioneA(); funzioneB(); // printf("%d", valore); // ERRORE DI COMPILAZIONE! 'valore' non esiste nel main return 0;}Le variabili globali (dichiarate fuori da qualsiasi funzione) sono invece accessibili da chiunque. Il loro uso è vivamente sconsigliato poiché rende il comportamento del programma non deterministico e difficile da debuggare.
#include <stdio.h>
int contatore = 0; // VARIABILE GLOBALE - Sconsigliata!
void incrementa_contatore(void) { contatore++; // Modifica lo stato globale}
void resetta_contatore(void) { contatore = 0; // Modifica lo stato globale inaspettatamente}
int main(void) { incrementa_contatore(); incrementa_contatore(); printf("Contatore: %d\n", contatore); // Stampa 2
resetta_contatore(); // Se resetta_contatore viene chiamata altrove, lo stato si corrompe
printf("Contatore dopo reset: %d\n", contatore); // Stampa 0! return 0;}4. Sotto il Cofano: Stack e Record di Attivazione
Sezione intitolata “4. Sotto il Cofano: Stack e Record di Attivazione”4.1 Organizzazione della RAM
Sezione intitolata “4.1 Organizzazione della RAM”La memoria RAM allocata per un programma in esecuzione è suddivisa in quattro aree principali:
- Text: Contiene il codice macchina (le istruzioni CPU).
- Data / BSS: Ospita le variabili globali e statiche. Il segmento Data memorizza le variabili inizializzate esplicitamente (es.
int x = 42;), mentre il segmento BSS (Block Started by Symbol) memorizza quelle non inizializzate (es.int y;), che il Sistema Operativo azzera automaticamente all’avvio. - Heap: Destinata all’allocazione dinamica manuale.
- Stack: Struttura LIFO (Last In, First Out) gestita automaticamente per le funzioni.

4.2 Lo Stack Frame (Record di Attivazione)
Sezione intitolata “4.2 Lo Stack Frame (Record di Attivazione)”Ogni volta che una funzione viene invocata, viene riservato nello Stack un blocco contiguo di memoria chiamato Stack Frame (o Record di Attivazione). Questo frame contiene:
- I parametri passati alla funzione.
- L’indirizzo di ritorno (l’istruzione del chiamante a cui saltare alla fine).
- Il puntatore al vecchio frame (per ripristinare lo stato precedente).
- Le variabili locali della funzione.
Quando la funzione termina (arriva al return), il suo Stack Frame viene rimosso dallo Stack (operazione di Pop) e lo spazio di memoria viene immediatamente invalidato, rendendolo disponibile per le chiamate successive. Questo spiega perché le variabili locali non sopravvivono al termine della funzione.
4.3 Allineamento e Padding a 16 Byte
Sezione intitolata “4.3 Allineamento e Padding a 16 Byte”Nelle moderne architetture hardware a 64-bit, il trasferimento dei dati tra RAM e CPU avviene tramite bus ottimizzati per blocchi di grandi dimensioni. Per questioni di efficienza prestazionale, il compilatore richiede che ogni Stack Frame sia allineato a multipli di 16 byte.
Se la somma dei byte occupati dalle variabili locali e dai parametri non corrisponde esattamente a un multiplo di 16, il compilatore inserisce dei byte vuoti invisibili chiamati padding per completare l’allineamento. Ad esempio:
- Se dichiariamo una sola variabile locale
int(4 byte), lo Stack Frame verrà comunque allineato a 16 byte inserendo 12 byte di padding.
Consideriamo il codice d’esempio corrispondente all’evoluzione delle immagini sottostanti:
double half(int x) { return (double)x / 2;}
int half_sum(int a, int b) { int somma = a + b; double res = half(somma); return res;}
int main(void) { int param = 5; double totale = half_sum(param, 3); return 0;}

Come fa la CPU a sapere quanta memoria serve?
Come fa il sistema a stabilire in anticipo la dimensione corretta di ogni Stack Frame?
La risposta sta nel lavoro svolto dal compilatore a tempo di compilazione. Grazie al sistema dei tipi del C, il compilatore conosce a priori la dimensione in byte di ogni variabile locale e parametro dichiarato (es. int occupa 4 byte, double occupa 8 byte). Analizzando il corpo di ciascuna funzione, il compilatore somma lo spazio occupato dalle variabili, calcola l’allineamento a 16 byte (aggiungendo il padding necessario) e genera le istruzioni macchina appropriate per spostare lo Stack Pointer (es. sub rsp, 24).
4.4 I Registri RSP e RBP
Sezione intitolata “4.4 I Registri RSP e RBP”La CPU tiene traccia del frame attivo attraverso due registri fondamentali:
RSP(Stack Pointer): Punta costantemente alla cima dello Stack (l’indirizzo più basso correntemente occupato). Si sposta continuamente ad ogni push o pop.RBP(Base Pointer): Punta a un indirizzo fisso che fa da base per lo Stack Frame corrente. Tutte le variabili locali e i parametri all’interno della funzione vengono referenziati calcolando un offset rispetto adRBP(es.RBP - 4).
Vediamo un semplicissimo esempio di codice C e il corrispondente codice assembly (in sintassi Intel x86-64) commentato per capire il ruolo dei registri e dello Stack:
// Codice Cint somma(int a, int b) { return a + b;}
int main(void) { int ris = somma(2, 3); return 0;}; Assembly Intel x86-64 corrispondentemain: ; 1. Preparazione dei parametri negli appositi registri mov edi, 2 ; Metti il primo argomento (2) nel registro EDI mov esi, 3 ; Metti il secondo argomento (3) nel registro ESI
; 2. Invocazione della funzione call somma ; Salva l'indirizzo della prossima istruzione nello Stack ; e salta all'etichetta "somma"
; ... La CPU ritorna esattamente qui al termine della funzione ... mov DWORD PTR [rbp-4], eax ; Salva il valore restituito in EAX dentro 'ris' ret
somma: ; 3. Setup (Prologo) del nuovo Stack Frame push rbp ; Salva il Base Pointer del chiamante nello Stack mov rbp, rsp ; Imposta il nuovo Base Pointer pari allo Stack Pointer corrente
; 4. Esecuzione del calcolo add edi, esi ; Somma a + b (EDI + ESI) mov eax, edi ; Memorizza il risultato nel registro EAX (registro di ritorno)
; 5. Distruzione (Epilogo) dello Stack Frame pop rbp ; Ripristina il vecchio Base Pointer del chiamante dallo Stack ret ; Preleva l'indirizzo di ritorno dallo Stack e vi salta indietro5. Quiz di Auto-valutazione
Sezione intitolata “5. Quiz di Auto-valutazione”1. Quale tipo di ritorno si utilizza per dichiarare una funzione che non restituisce alcun valore?
- A)
int - B)
void - C)
NULL - D)
double
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: La parola chiave void indica esplicitamente al compilatore che la funzione esegue solo istruzioni (effetti collaterali come stampe) e non produce un rvalue da restituire al chiamante.
2. Cosa accade a una variabile passata come argomento a una funzione se viene modificata all’interno della funzione stessa?
- A) Viene modificata anche nel chiamante
- B) Il programma va in errore di segmentazione
- C) Rimane invariata nel chiamante poiché viene passata solo una copia
- D) Diventa una variabile globale
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Il C adotta rigorosamente il passaggio dei parametri per valore. La funzione riceve una copia fisica della variabile originale posizionata in un indirizzo di memoria differente (il proprio Stack Frame).
3. Qual è lo scopo del prototipo di una funzione (Forward Declaration)?
- A) Allocare la memoria nello Stack
- B) Informare il compilatore dell’esistenza della funzione prima del suo utilizzo effettivo nel codice
- C) Ottimizzare l’esecuzione del codice assembly
- D) Dichiarare variabili globali
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Il prototipo dice al compilatore il nome, il tipo di ritorno e i parametri della funzione in modo che possa validare sintatticamente le chiamate prima di averne letto l’effettiva definizione del corpo.
4. In quale segmento di memoria RAM vengono allocati gli Stack Frame?
- A) Text Segment
- B) Heap
- C) Stack
- D) Data/BSS
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Lo Stack è l’area di memoria automatica e organizzata a pila adibita proprio alla gestione delle chiamate a funzione e delle loro variabili locali.
5. Cosa si intende per “Padding” all’interno di uno Stack Frame?
- A) L’eliminazione delle variabili locali inutilizzate
- B) L’inserimento di byte vuoti per allineare il frame a blocchi di 16 byte
- C) Il passaggio dei parametri tramite registri CPU
- D) Un errore di overflow dello Stack
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Nelle architetture a 64-bit il compilatore esegue il padding per allineare lo Stack Frame ai confini di 16 byte per massimizzare la velocità di trasferimento dei bus hardware.
6. Quale registro punta costantemente alla base stabile del record di attivazione corrente?
- A)
RSP - B)
RAX - C)
RBP - D)
RIP
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Il registry RBP (Base Pointer) rimane fisso per tutta la durata della funzione ed è usato come riferimento costante per calcolare gli indirizzi delle variabili locali.
7. Cosa succede alla memoria allocata per le variabili locali di una funzione quando questa termina con un return?
- A) Viene spostata nell’Heap
- B) Viene deallocata automaticamente rimuovendo lo Stack Frame (Pop)
- C) Rimane memorizzata per sempre
- D) Viene salvata su file
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Al termine della funzione, lo Stack Pointer RSP viene ripristinato, invalidando lo Stack Frame associato. La memoria occupata diventa immediatamente riutilizzabile per altre chiamate.
8. Qual è il comportamento se una funzione dichiara una variabile locale con lo stesso nome di una variabile del main?
- A) Il compilatore genera un errore di duplicazione
- B) Le due variabili condividono lo stesso spazio di memoria
- C) La variabile locale nasconde quella del main senza conflitti (sono in due Stack Frame separati)
- D) Il programma va in crash all’avvio
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Ciascuna funzione lavora esclusivamente all’interno del proprio scope locale. Le variabili risiedono in indirizzi di memoria fisicamente distinti all’interno dei rispettivi record di attivazione.
9. Perché l’uso delle variabili globali è considerato una pessima pratica di programmazione?
- A) Rende l’esecuzione del programma più lenta
- B) Impedisce l’uso dei cicli
for - C) Rompe l’incapsulamento rendendo lo stato del programma imprevedibile e difficile da debuggare
- D) Funzionano solo con il tipo di dato
char
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Qualsiasi funzione può modificare una variabile globale inaspettatamente, creando effetti collaterali difficili da tracciare e compromettendo l’indipendenza delle singole procedure.
10. In che direzione cresce lo Stack nella memoria RAM nella convenzione comune?
- A) Verso indirizzi di memoria più alti
- B) Rimane di dimensione fissa senza crescere
- C) Verso indirizzi di memoria più bassi
- D) Cresce in modo casuale ad ogni esecuzione
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: Nella convenzione architetturale standard, lo Stack cresce dall’alto verso il basso (ovvero verso indirizzi numericamente decrescenti) per evitare di scontrarsi con l’Heap che cresce dal basso verso l’alto.
6. Esercizi Pratici
Sezione intitolata “6. Esercizi Pratici”Esercizio 1: Il Cubo di un intero
Sezione intitolata “Esercizio 1: Il Cubo di un intero”Scrivete una funzione int cubo(int x) che accetti un numero intero e ne restituisca il cubo. Scrivete un main che legga un numero da tastiera, chiami la funzione e stampi il risultato.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int cubo(int x) { return x * x * x;}
int main(void) { int n = 0; printf("Inserisci un intero: "); if (scanf("%d", &n) == 1) { printf("Il cubo di %d e' %d\n", n, cubo(n)); } return 0;}Esercizio 2: Media di tre numeri reali
Sezione intitolata “Esercizio 2: Media di tre numeri reali”Scrivete una funzione double media3(double a, double b, double c) che restituisca la media aritmetica di tre numeri decimali.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
double media3(double a, double b, double c) { return (a + b + c) / 3.0;}
int main(void) { double x = 0.0, y = 0.0, z = 0.0; printf("Inserisci tre numeri reali: "); if (scanf("%lf %lf %lf", &x, &y, &z) == 3) { printf("La media e': %.2f\n", media3(x, y, z)); } return 0;}Esercizio 3: Area del Cerchio
Sezione intitolata “Esercizio 3: Area del Cerchio”Scrivete una funzione double area_cerchio(double raggio) che calcoli l’area di un cerchio. Si utilizzi una costante #define PI 3.14159. Se il raggio è negativo, la funzione deve restituire -1.0.
💻 Mostra Soluzione e Codice C
#include <stdio.h>#define PI 3.1415926535
double area_cerchio(double raggio) { if (raggio < 0.0) { return -1.0; } return PI * raggio * raggio;}
int main(void) { double r = 0.0; printf("Inserisci il raggio: "); if (scanf("%lf", &r) == 1) { double area = area_cerchio(r); if (area < 0.0) { printf("Errore: raggio negativo.\n"); } else { printf("L'area del cerchio e': %.4f\n", area); } } return 0;}Esercizio 4: Fattoriale Iterativo
Sezione intitolata “Esercizio 4: Fattoriale Iterativo”Scrivete una funzione long long fattoriale(int n) che calcoli il fattoriale di un numero intero positivo in modo iterativo (tramite cicli). Se l’input è negativo, la funzione deve restituire -1.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
long long fattoriale(int n) { if (n < 0) { return -1; } long long ris = 1; for (int i = 2; i <= n; i++) { ris *= i; } return ris;}
int main(void) { int num = 0; printf("Inserisci un numero intero: "); if (scanf("%d", &num) == 1) { long long f = fattoriale(num); if (f < 0) { printf("Errore: inserito un numero negativo.\n"); } else { printf("Il fattoriale di %d e' %lld\n", num, f); } } return 0;}Esercizio 5: Ricerca Lineare in Array
Sezione intitolata “Esercizio 5: Ricerca Lineare in Array”Scrivete una funzione int cerca(const int arr[], int dim, int target) che effettui una ricerca sequenziale dell’elemento target all’interno di un array di interi. La funzione deve restituire l’indice della prima occorrenza se presente, altrimenti -1.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int cerca(const int arr[], int dim, int target) { for (int i = 0; i < dim; i++) { if (arr[i] == target) { return i; // Ritorna l'indice immediatamente } } return -1; // Sentinella non trovato}
int main(void) { int numeri[5] = {10, 20, 30, 40, 50}; int t = 30; int pos = cerca(numeri, 5, t); if (pos != -1) { printf("Trovato all'indice %d\n", pos); } else { printf("Non presente.\n"); } return 0;}Esercizio 6: Massimo in un Array
Sezione intitolata “Esercizio 6: Massimo in un Array”Scrivete una funzione int trova_massimo(const int arr[], int dim) che restituisca il valore massimo presente in un array di interi. Si assuma che la dimensione sia sempre maggiore o uguale a 1.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int trova_massimo(const int arr[], int dim) { int max = arr[0]; for (int i = 1; i < dim; i++) { if (arr[i] > max) { max = arr[i]; } } return max;}
int main(void) { int dati[6] = {12, 45, 2, 67, 34, 19}; printf("Il massimo e': %d\n", trova_massimo(dati, 6)); return 0;}Esercizio 7: Somma degli elementi di un Array
Sezione intitolata “Esercizio 7: Somma degli elementi di un Array”Scrivete una funzione int somma_array(const int arr[], int dim) che calcoli e restituisca la somma di tutti gli elementi di un array di interi.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int somma_array(const int arr[], int dim) { int somma = 0; for (int i = 0; i < dim; i++) { somma += arr[i]; } return somma;}
int main(void) { int valori[5] = {1, 2, 3, 4, 5}; printf("Somma: %d\n", somma_array(valori, 5)); return 0;}Esercizio 8: Conteggio numeri pari
Sezione intitolata “Esercizio 8: Conteggio numeri pari”Scrivete una funzione int conta_pari(const int arr[], int dim) che restituisca il numero di elementi pari presenti in un array di interi.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int conta_pari(const int arr[], int dim) { int contatore = 0; for (int i = 0; i < dim; i++) { if (arr[i] % 2 == 0) { contatore++; } } return contatore;}
int main(void) { int numeri[6] = {11, 22, 33, 44, 55, 66}; printf("Pari presenti: %d\n", conta_pari(numeri, 6)); return 0;}Esercizio 9: Verifica ordinamento
Sezione intitolata “Esercizio 9: Verifica ordinamento”Scrivete una funzione int is_ordinato(const int arr[], int dim) che restituisca 1 se l’array è ordinato in ordine crescente (non decrescente), e 0 altrimenti.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int is_ordinato(const int arr[], int dim) { for (int i = 1; i < dim; i++) { if (arr[i] < arr[i - 1]) { return 0; // Trovata anomalia, l'array non è ordinato } } return 1; // Tutto in ordine}
int main(void) { int a1[5] = {1, 3, 5, 5, 8}; int a2[5] = {1, 3, 2, 5, 8};
printf("a1 ordinato? %d\n", is_ordinato(a1, 5)); printf("a2 ordinato? %d\n", is_ordinato(a2, 5)); return 0;}Esercizio 10: Smoothing Lineare 1D
Sezione intitolata “Esercizio 10: Smoothing Lineare 1D”Scrivete una funzione void smooth1d(const double sorgente[], double destinazione[], int dim) che esegua lo smoothing (media mobile a 3 elementi) dell’array sorgente scrivendo il risultato nell’array destinazione. Per i bordi (indice 0 e dim - 1), si copi semplicemente il valore originale senza calcolare la media.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void smooth1d(const double sorgente[], double destinazione[], int dim) { if (dim < 1) return;
// Bordi destinazione[0] = sorgente[0]; if (dim > 1) { destinazione[dim - 1] = sorgente[dim - 1]; }
// Elementi intermedi for (int i = 1; i < dim - 1; i++) { destinazione[i] = (sorgente[i - 1] + sorgente[i] + sorgente[i + 1]) / 3.0; }}
int main(void) { double segnale[5] = {10.0, 20.0, 15.0, 30.0, 25.0}; double segnale_filtrato[5];
smooth1d(segnale, segnale_filtrato, 5);
printf("Segnale Filtrato: "); for (int i = 0; i < 5; i++) { printf("%.2f ", segnale_filtrato[i]); } printf("\n"); return 0;}