Salta ai contenuti

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

Copertina Lezione 7

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”

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 array
int 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.

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!

Il concetto di funzione in informatica deriva direttamente dalla matematica. In matematica, una funzione ff associa ad ogni elemento di un insieme di partenza (detto dominio DD) uno e un solo elemento di un insieme di arrivo (detto codominio CC), formalizzato come f:D→Cf: D \to C.

In programmazione facciamo esattamente lo stesso:

  • Il dominio DD è rappresentato dai tipi dei parametri formali accettati in ingresso.
  • Il codominio CC è 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"
}
  1. 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 chiave void.
  2. Nome Identificativo: Segue le stesse regole dei nomi di variabili (deve iniziare con una lettera o underscore e non contenere spazi).
  3. 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.
  4. 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 di return; è facoltativo e serve solo ad interrompere anticipatamente l’esecuzione del corpo. Se invece la funzione ha un tipo di ritorno non void, l’istruzione return con 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;
}

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;
}
// Definizione
double 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”

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:

  1. Riga 6-7: Il programma inizia dal main. Vengono allocate e inizializzate le variabili locali x (5) e y (3).
  2. Riga 8: Viene invocata la funzione calcola_somma. Il flusso principale si interrompe: i valori di x e y vengono copiati rispettivamente in a e b. La CPU esegue un salto in memoria.
  3. Riga 1: Inizia l’esecuzione di calcola_somma. I parametri formali a (5) e b (3) sono ora pronti in memoria.
  4. Riga 2: Viene dichiarata la variabile locale risultato ed eseguito il calcolo (5 + 3 = 8).
  5. Riga 3: L’istruzione return risultato restituisce il valore 8. Lo Stack Frame di calcola_somma viene distrutto.
  6. Riga 8 (ripresa): Il controllo ritorna al main. Il valore restituito (8) viene assegnato alla variabile totale.
  7. Riga 9: Viene stampato il testo "Totale: 8".
  8. Riga 10: Il main termina restituendo 0 al Sistema Operativo.

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 b in calcola_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, y o 5, 3 nel main).

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

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

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.

Layout Memoria RAM

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:

  1. I parametri passati alla funzione.
  2. L’indirizzo di ritorno (l’istruzione del chiamante a cui saltare alla fine).
  3. Il puntatore al vecchio frame (per ripristinare lo stato precedente).
  4. 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.

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

Espansione dello Stack Contrazione dello Stack

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

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 ad RBP (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 C
int somma(int a, int b) {
return a + b;
}
int main(void) {
int ris = somma(2, 3);
return 0;
}
; Assembly Intel x86-64 corrispondente
main:
; 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 indietro

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.


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

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

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

Scrivete una funzione long long fattoriale(int n) che calcoli il fattoriale di un numero intero positivo nn 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;
}

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

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

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

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

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

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