Lezione 8: I Puntatori e la Gestione Esplicita della Memoria

Nelle lezioni precedenti abbiamo visto come le funzioni siano isolate e ricevano solo “fotocopie” dei dati passati dal chiamante (passaggio per valore). Sebbene questo garantisca sicurezza e pulizia del codice, introduce dei limiti evidenti quando vogliamo che una funzione modifichi direttamente una variabile del chiamante o quando dobbiamo evitare di copiare grandi quantità di dati in memoria. In questa lezione supereremo questo limite entrando nei dettagli fisici della memoria RAM. Introdurremo i puntatori, capiremo la reale natura degli array (il decadimento ad array decay), vedremo come eseguire calcoli complessi con l’aritmetica dei puntatori e scopriremo l’ottimizzazione tramite funzioni inline.
1. La Realtà della RAM e i Tipi Puntatore
Sezione intitolata “1. La Realtà della RAM e i Tipi Puntatore”1.1 Oltre l’illusione delle variabili
Sezione intitolata “1.1 Oltre l’illusione delle variabili”Quando scriviamo un programma in C, il compilatore ci offre l’astrazione comoda delle variabili nominate (es. int x). Tuttavia, a livello hardware, la CPU non ha idea di cosa sia “x”. La memoria RAM è vista dal processore come una sequenza lineare di celle microscopiche (byte), ciascuna identificata da un indirizzo fisico numerico univoco (solitamente espresso in notazione esadecimale, es. 0x7fffd9b3).
Durante l’esecuzione, lo Stack Pointer alloca i byte necessari nello stack per memorizzare le nostre variabili locali. Se conosciamo l’indirizzo esatto di una di queste celle, possiamo scavalcare l’isolamento dei record di attivazione e scrivervi o leggervi direttamente, a prescindere dal fatto che la variabile appartenga a un’altra funzione.
Il tipo di dato che ci permette di memorizzare e manipolare questi indirizzi di memoria è il puntatore.
Per comprendere meglio questo concetto, osserviamo cosa accade durante la traduzione del codice da C ad Assembly (x86-64).
Quello che scriviamo in C:
int x = 42;x = 100;Quello che vede la CPU (Assembly x86-64):
; La variabile "x" a livello macchina non esiste più. Esiste solo una; coordinata (offset) calcolata a partire dalla base dello stack frame (rbp).mov DWORD PTR [rbp-4], 42 ; Scrivi il valore 42 nella cella a indirizzo (RBP - 4)mov DWORD PTR [rbp-4], 100 ; Sovrascrivi con 100 nella stessa identica cellaCome si può notare, l’identificatore x è stato rimpiazzato dall’indirizzo relativo [rbp-4].
1.2 Dichiarazione e interpretazione dei puntatori
Sezione intitolata “1.2 Dichiarazione e interpretazione dei puntatori”Un puntatore è a tutti gli effetti una variabile che contiene un indirizzo di memoria. In un’architettura moderna a 64 bit, gli indirizzi sono lunghi 64 bit, quindi qualsiasi variabile puntatore occupa esattamente 8 byte in memoria.
La sintassi di dichiarazione prevede l’uso dell’asterisco *:
int *p; // p è un puntatore ad un interodouble *q; // q è un puntatore a un doubleSi consiglia di leggere questa dichiarazione da destra a sinistra: “p è un puntatore (*) a un intero (int)”.
Perché i puntatori sono tipizzati?
Sezione intitolata “Perché i puntatori sono tipizzati?”Se tutti gli indirizzi occupano 8 byte, perché dobbiamo dichiarare int *p invece di un ipotetico tipo generico pointer p?
La risposta risiede nell’interpretazione dei dati: il tipo indica al compilatore come deve essere letta e modificata la memoria una volta giunti a quell’indirizzo.
- Con
char *p, il compilatore leggerà o scriverà 1 byte a partire dall’indirizzo memorizzato. - Con
int *p, leggerà o scriverà 4 byte consecutivi. - Con
double *p, interpreterà 8 byte consecutivi secondo lo standard IEEE 754.
Per visualizzare questo comportamento in memoria, consideriamo il seguente codice:
// Variabili Standardint x = 5;double y = 8.2;char z = 'a';
// Variabili Puntatoreint *px = &x;double *py = &y;char *pz = &z;A livello di Stack Frame, le variabili occupano spazi differenti a seconda del tipo (1, 4 o 8 byte), mentre tutti i puntatori (px, py, pz) occupano esattamente 8 byte, indipendentemente dal tipo di dato a cui puntano:

Trappola sintattica
Sezione intitolata “Trappola sintattica”Prestare attenzione a dichiarazioni multiple sulla stessa riga:
int* p, q; // Crea p come puntatore a int, ma q come normale intero!L’asterisco si lega solo all’identificatore immediatamente successivo. La best practice raccomanda di dichiarare un solo puntatore per riga:
int *p = NULL;int q = 0;Puntatori a vuoto (NULL)
Sezione intitolata “Puntatori a vuoto (NULL)”Dichiarare un puntatore senza inizializzarlo lo rende un dangling pointer (puntatore selvaggio), contenente un indirizzo spazzatura casuale. Tentarne l’accesso provocherà un crash improvviso (Segmentation Fault).
È regola aurea inizializzare sempre i puntatori non associati a NULL (o 0):
Inizializzare un puntatore a NULL (che sotto il cofano corrisponde all’indirizzo 0) è una fondamentale pratica di programmazione difensiva. L’indirizzo 0 è riservato dal sistema operativo e non è accessibile per la lettura o la scrittura diretta da parte del programma utente.
Impostando un puntatore a NULL dichiariamo esplicitamente: “questo puntatore al momento è vuoto, non punta a nulla di valido”. Questo ci permette di implementare controlli logici nel codice per verificare se il puntatore contiene o meno un indirizzo utilizzabile prima di effettuare accessi (evitando di andare incontro a crash del programma).
int *ptr = NULL; // ptr non punta a nessun indirizzo valido1.3 Operatori & (Indirizzo) e * (Dereferenziazione)
Sezione intitolata “1.3 Operatori & (Indirizzo) e * (Dereferenziazione)”Per lavorare con i puntatori utilizziamo due operatori unari fondamentali:
- Operatore Indirizzo (
&): Restituisce la coordinata di memoria (l’indirizzo) di una variabile esistente. - Operatore di Dereferenziazione (
*): Consente di accedere al valore memorizzato all’indirizzo puntato, sia in lettura che in scrittura.
#include <stdio.h>
int main(void) { int x = 42; int *p = &x; // p memorizza l'indirizzo di x
printf("Valore di x: %d\n", x); // Stampa 42 printf("Indirizzo di x: %p\n", (void*)p); // Stampa es. 0x7ffd82
*p = 100; // Dereferenziazione: vai all'indirizzo in p e scrivi 100
printf("Nuovo valore di x: %d\n", x); // Stampa 100! return 0;}A livello di Stack Frame, l’istruzione di dereferenziazione si traduce in un accesso diretto alle celle originali:

Sicurezza nell’accesso
Sezione intitolata “Sicurezza nell’accesso”Prima di dereferenziare un puntatore, possiamo sfruttare l’inizializzazione a NULL per assicurarci che l’indirizzo sia valido:
if (ptr != NULL) { *ptr = 10; // Sicuro}Esempio di Crash: Dimenticare il controllo di sicurezza
Sezione intitolata “Esempio di Crash: Dimenticare il controllo di sicurezza”Cosa succede se omettiamo questo controllo in un programma più strutturato? Consideriamo questo esempio non banale in cui una funzione elabora i dati di un sensore passati tramite puntatore:
#include <stdio.h>
void elabora_sensore(const int *dati, int *risultato) { // BUG: Mancano i controlli di sicurezza! // Se dati o risultato sono NULL, avremo un crash. *risultato = (*dati) * 2;}
int main(void) { int sensor_val = 25; int output = 0;
// Caso d'uso corretto: elabora_sensore(&sensor_val, &output); printf("Risultato 1: %d\n", output);
// Caso d'uso errato (ad esempio, sensore scollegato o guasto): int *sensore_guasto = NULL; elabora_sensore(sensore_guasto, &output); // Qui il programma andrà in crash!
printf("Risultato 2: %d\n", output); return 0;}Compilando ed eseguendo questo programma da terminale, l’esecuzione si interromperà bruscamente restituendo un errore del tipo Segmentation fault (o Errore di segmentazione), senza dare alcuna indicazione su quale riga di codice abbia causato il problema.
Come individuare il crash con il Debugger (LLDB)
Sezione intitolata “Come individuare il crash con il Debugger (LLDB)”Per trovare la riga esatta del crash, possiamo utilizzare il debugger LLDB integrato. Ecco i passi da seguire da terminale:
- Compilare con i simboli di debug:
Dobbiamo indicare al compilatoregcc(oclang) di includere i metadati di debug con l’opzione-g:Terminal window gcc -g sensore.c -o sensore - Avviare LLDB:
Lanciamo il debugger sul file eseguibile appena generato:Terminal window lldb ./sensore - Eseguire il programma:
All’interno dell’interfaccia interattiva di LLDB, digitiamorun(o semplicementer) per avviare l’esecuzione:(lldb) run - Analizzare il punto di arresto:
Il programma verrà eseguito finché non incontrerà l’errore di memoria. LLDB intercetterà il crash e ci mostrerà l’esatta riga di codice che lo ha causato:L’output di LLDB ci dice che:Process 12345 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)frame #0: 0x0000000100000f58 sensore`elabora_sensore(dati=0x0000000000000000, risultato=0x00007ff7bfefef08) at sensore.c:6:183 void elabora_sensore(const int *dati, int *risultato) {4 // BUG: Mancano i controlli di sicurezza!5 // Se dati o risultato sono NULL, avremo un crash.-> 6 *risultato = (*dati) * 2;7 }- Lo stop è avvenuto alla riga
6del filesensore.call’interno della funzioneelabora_sensore. - Il motivo dello stop è
EXC_BAD_ACCESS(un tentativo di accedere a una zona di memoria non consentita). - Il parametro
datiè impostato a0x0000000000000000(ovveroNULL).
- Lo stop è avvenuto alla riga
- Esaminare lo Stack dei Frame:
Digitando il comandobacktrace(obt), possiamo vedere l’intera catena di chiamate che ha portato a quella riga, capendo che la funzioneelabora_sensoreè stata invocata dalmainalla riga 19:(lldb) backtrace* frame #0: 0x0000000100000f58 sensore`elabora_sensore(dati=0x0000000000000000, risultato=0x00007ff7bfefef08) at sensore.c:6:18frame #1: 0x0000000100000fc4 sensore`main at sensore.c:19:5 - Risolvere il Bug:
Una volta individuata l’origine, possiamo correggere la funzione aggiungendo la guardiaif:Infine usciamo dal debugger digitandovoid elabora_sensore(const int *dati, int *risultato) {if (dati == NULL || risultato == NULL) {printf("Errore: puntatori non validi.\n");return;}*risultato = (*dati) * 2;}quitoq.
2. Passaggio per Riferimento e la funzione Swap
Sezione intitolata “2. Passaggio per Riferimento e la funzione Swap”2.1 Il limite del passaggio per valore
Sezione intitolata “2.1 Il limite del passaggio per valore”Proviamo a scrivere un programma che inverte due variabili intere usando una funzione swap:
#include <stdio.h>
void swap_sbagliato(int a, int b) { int temp = a; a = b; b = temp;}
int main(void) { int x = 5; int y = 10; swap_sbagliato(x, y); // Passa i valori di x e y (fotocopie) printf("x = %d, y = %d\n", x, y); // Stampa x = 5, y = 10! return 0;}Se eseguiamo questo codice, il valore di x e y nel main rimarrà rispettivamente 5 e 10. Questo perché la funzione lavora su copie isolate collocate nel proprio Stack Frame, che vengono distrutte non appena la funzione termina.

2.2 Il passaggio per riferimento simulato
Sezione intitolata “2.2 Il passaggio per riferimento simulato”Per consentire a una funzione di modificare le variabili del chiamante, dobbiamo passarle gli indirizzi delle variabili (riferimento simulato tramite puntatori):
#include <stdio.h>
void swap_corretto(int *a, int *b) { int temp = *a; // Copia il valore puntato da a *a = *b; // Scrive il valore puntato da b all'indirizzo a *b = temp; // Scrive temp all'indirizzo b}
int main(void) { int x = 5, y = 10; swap_corretto(&x, &y); // Passa gli indirizzi di x e y printf("x = %d, y = %d\n", x, y); // Stampa x = 10, y = 5! return 0;}Grazie ai puntatori, swap_corretto può raggiungere direttamente la memoria riservata al main e modificare fisicamente le variabili originali.

3.1 La corrispondenza tra Array e Puntatori
Sezione intitolata “3.1 La corrispondenza tra Array e Puntatori”In C esiste un legame indissolubile tra gli array e i puntatori. Quando dichiariamo un array statico:
int arr[5] = {10, 20, 30, 40, 50};Il nome dell’array, arr, si comporta a tutti gli effetti come un puntatore costante al suo primo elemento. In altre parole, l’espressione arr è semanticamente equivalente a &arr[0].
Questa corrispondenza si riflette anche nella sintassi di accesso. Quando scriviamo arr[i], il compilatore traduce questa notazione in un’operazione di aritmetica dei puntatori seguita da dereferenziazione:
arr[i] <==> *(arr + i)Ciò significa che possiamo scorrere un array sia utilizzando l’indice [i] sia manipolando direttamente l’indirizzo tramite l’aritmetica dei puntatori.
Per chiarire questo legame intimo tra array e puntatori, osserviamo il seguente codice d’esempio:
#include <stdio.h>
int main(void) { // Allocazione contigua di 8 interi (32 Byte) int array[8] = {10, 20, 30, 40, 50, 60, 70, 80};
// x memorizza l'indirizzo di partenza dell'array int *x = array;
// Equivalenza fondamentale: // array == &array[0] printf("Indirizzo array: %p\n", (void*)array); printf("Indirizzo primo elemento: %p\n", (void*)&array[0]); printf("Valore puntato da x: %d\n", *x); // Stampa 10
return 0;}A livello dello Stack Frame, le celle dell’array sono allocate in posizioni consecutive di memoria, e il puntatore x punta all’inizio di questa sequenza:

3.2 Il decadimento degli array (Array Decay)
Sezione intitolata “3.2 Il decadimento degli array (Array Decay)”Quando passiamo un array come parametro a una funzione, il linguaggio C non copia l’intero array in memoria (operazione che richiederebbe molto tempo e memoria per array di grandi dimensioni). Invece, applica la regola del decadimento degli array (array decay): l’array decade a un semplice puntatore al suo primo elemento.
Di conseguenza, qualsiasi parametro formale dichiarato come int arr[] o int arr[100] viene interpretato dal compilatore come int *arr.
Questo ha un effetto collaterale importante: la funzione perde l’informazione sulla dimensione dell’array. Possiamo verificarlo empiricamente confrontando l’output dell’operatore sizeof all’interno del main e all’interno di una funzione:
#include <stdio.h>
void stampa_dimensione(int arr[], int dim) { // All'interno della funzione, arr è diventato un semplice puntatore a int (int *). // sizeof(arr) restituirà la dimensione di un puntatore (8 byte su sistemi a 64-bit). printf("In funzione: sizeof(arr) = %lu byte\n", sizeof(arr));}
int main(void) { int voti[5] = {18, 22, 25, 28, 30};
// Nel main, voti è un array statico di 5 interi. // sizeof(voti) restituisce la dimensione totale: 5 * sizeof(int) = 20 byte. printf("Nel main: sizeof(voti) = %lu byte\n", sizeof(voti));
stampa_dimensione(voti, 5); return 0;}Output del programma (su sistema a 64-bit):
Nel main: sizeof(voti) = 20 byteIn funzione: sizeof(arr) = 8 bytePer questo motivo, in C è sempre necessario passare la dimensione dell’array come parametro aggiuntivo (int dim) a qualsiasi funzione debba elaborarlo.
3.3 Aritmetica dei puntatori e zucchero sintattico
Sezione intitolata “3.3 Aritmetica dei puntatori e zucchero sintattico”Sui puntatori è possibile eseguire operazioni aritmetiche (come la somma e la sottrazione). L’aritmetica dei puntatori è tipizzata: aggiungere 1 a un puntatore non aumenta l’indirizzo di 1 byte, ma di una quantità pari a sizeof(tipo) associato al puntatore (ovvero la dimensione del tipo a cui punta).
Consideriamo il confronto nel codice seguente:
Aritmetica su Interi (sizeof(int) == 4):
int i[4] = {10, 20, 30, 40};int *p = i; // p punta a i[0] (es. indirizzo 1000)
// p + 1 incrementa l'indirizzo di 1 * sizeof(int) = 4 byte// p + 2 incrementa l'indirizzo di 2 * sizeof(int) = 8 byteprintf("p : %p\n", (void*)p); // 1000printf("p+1 : %p\n", (void*)(p + 1)); // 1004Aritmetica su Reali (sizeof(double) == 8):
double d[4] = {1.5, 2.5, 3.5, 4.5};double *q = d; // q punta a d[0] (es. indirizzo 1000)
// q + 1 incrementa l'indirizzo di 1 * sizeof(double) = 8 byte// q + 2 incrementa l'indirizzo di 2 * sizeof(double) = 16 byteprintf("q : %p\n", (void*)q); // 1000printf("q+1 : %p\n", (void*)(q + 1)); // 1008A livello fisico, la CPU calcola l’indirizzo sommando al valore del puntatore l’indice moltiplicato per la dimensione del tipo di dato. Il passo di incremento (passo dell’aritmetica) si adatta quindi automaticamente al tipo di puntatore:

Questo spiega perché in C l’accesso agli elementi sia estremamente veloce: si tratta di una semplice operazione aritmetica di offset seguita da dereferenziazione.
3.4 Sicurezza: Il modificatore const
Sezione intitolata “3.4 Sicurezza: Il modificatore const”Perché non possiamo cambiare l’indirizzo di un array
Sezione intitolata “Perché non possiamo cambiare l’indirizzo di un array”Come abbiamo detto, il nome dell’array arr è un puntatore al primo elemento, ma con una restrizione cruciale: è un puntatore costante.
Questo significa che il compilatore fissa l’indirizzo base dell’array al momento dell’allocazione e non permette di cambiarlo. Un’istruzione come la seguente genererà un errore di compilazione:
int x = 42;int arr[5];arr = &x; // ERRORE DI COMPILAZIONE! arr equivale a un int * const, non può essere riassegnatoProteggere i dati in lettura con const
Sezione intitolata “Proteggere i dati in lettura con const”Poiché passare un array (ovvero un puntatore al suo primo elemento) a una funzione dà a quest’ultima il potere di modificarne il contenuto originale, dobbiamo poter proteggere i dati quando vogliamo che la funzione si limiti a leggerli.
Per farlo, dichiariamo il parametro formale come puntatore a costante utilizzando la parola chiave const:
void stampa_array(const int *arr, int dim) { // arr[0] = 99; // ERRORE DI COMPILAZIONE! Il compilatore impedisce la scrittura}const int *p: Il puntatore può cambiare indirizzo, ma il dato puntato è costante (sola lettura).int * const p: Il dato puntato può essere modificato, ma il puntatore non può cambiare indirizzo (comportamento nativo degli array).const int * const p: Sia il puntatore che il dato puntato sono costanti.
3.4 Funzioni inline
Sezione intitolata “3.4 Funzioni inline”Le chiamate a funzione richiedono un costo architetturale (overhead) per la creazione e distruzione dei record di attivazione nello stack. Per funzioni piccolissime invocate frequentemente (es. all’interno di cicli intensivi), questo overhead può impattare sulle prestazioni.
La keyword inline suggerisce al compilatore di sostituire la chiamata con il codice effettivo direttamente nel punto in cui è invocata:
static inline int quadrato(int x) { return x * x;}Questo elimina totalmente l’overhead della chiamata a funzione senza perdere i vantaggi di modularità e leggibilità del codice.
Per comprendere concretamente l’effetto dell’inlining, osserviamo a confronto il codice C e l’Assembly generato da una funzione static inline (in cui il compilatore ha scelto di non espandere a causa di mancate ottimizzazioni) rispetto ad una funzione classica, notando la chiamata effettiva tramite call.
Caso 1: Utilizzo di static inline (senza ottimizzazioni attive)
Sezione intitolata “Caso 1: Utilizzo di static inline (senza ottimizzazioni attive)”Se scriviamo una funzione dichiarata come static inline, in assenza di flag di ottimizzazione del compilatore, quest’ultimo potrebbe comunque generare una chiamata a funzione standard.
Codice C:
#include <stdio.h>
static inline int add(int x, int y) { return x + y;}
int main(void) { int x = 3 + add(5, 5); return x;}Assembly x86-64 generato per il main:
main: push rbp mov rbp, rsp sub rsp, 16 mov dword ptr [rbp - 4], 0 mov esi, 5 mov edi, esi call add ; Esegue comunque la chiamata alla funzione (locale) add add eax, 3 ; Somma 3 al risultato restituito in eax mov dword ptr [rbp - 8], eax mov eax, dword ptr [rbp - 8] add rsp, 16 pop rbp retCaso 2: Funzione Classica (Senza inline)
Sezione intitolata “Caso 2: Funzione Classica (Senza inline)”Nel caso di una funzione standard, il compilatore genera separatamente il codice della funzione add e lo invoca esplicitamente nel main.
Codice C:
#include <stdio.h>
int add(int x, int y) { return x + y;}
int main(void) { int x = 3 + add(5, 5); return x;}Assembly x86-64 generato:
add: push rbp mov rbp, rsp mov dword ptr [rbp - 4], edi ; Primo parametro mov dword ptr [rbp - 8], esi ; Secondo parametro mov eax, dword ptr [rbp - 4] add eax, dword ptr [rbp - 8] ; Esegue la somma pop rbp ret
main: push rbp mov rbp, rsp sub rsp, 16 mov dword ptr [rbp - 4], 0 mov esi, 5 mov edi, esi call add ; Chiama direttamente la funzione add add eax, 3 ; Somma 3 mov dword ptr [rbp - 8], eax mov eax, dword ptr [rbp - 8] add rsp, 16 pop rbp retQuando invece attiviamo le ottimizzazioni del compilatore (es. -O2), la keyword inline fa sì che il codice di add venga iniettato direttamente all’interno di main, traducendo l’intera espressione in un unico calcolo immediato (con -O2 il main diventa semplicemente mov eax, 13 seguito da ret) ed eliminando sia l’istruzione call sia lo stack frame associato alla funzione chiamata.
4. Quiz di Auto-valutazione
Sezione intitolata “4. Quiz di Auto-valutazione”1. Qual è la dimensione in byte di una variabile puntatore su un calcolatore a 64 bit?
- A) Dipende dal tipo a cui punta (es. 4 byte per int*, 1 byte per char*)
- B) Sempre 8 byte
- C) Sempre 4 byte
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Un puntatore contiene un indirizzo di memoria. Nelle architetture a 64 bit, l’indirizzo di memoria ha sempre una lunghezza fissa di 64 bit (ovvero 8 byte), indipendentemente dal tipo di dato a cui punta.
2. Quale operatore restituisce l’indirizzo di memoria di una variabile esistente?
- A)
* - B)
& - C)
->
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: L’operatore unario di indirizzo & (address-of) estrae le coordinate di memoria fisiche in cui è allocata la variabile.
3. Se ptr è un puntatore a intero inizializzato a NULL, cosa succede se proviamo a eseguire l’istruzione *ptr = 10;?
- A) La variabile intera viene correttamente modificata
- B) Il programma viene compilato correttamente ma va in crash (Segmentation Fault) all’esecuzione
- C) Errore di compilazione immediato
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Compilare la dereferenziazione di un puntatore è sintatticamente valido. All’atto pratico, tuttavia, tentare di accedere all’indirizzo 0x0 (NULL) genera una violazione di accesso di memoria (Segmentation Fault) gestita dal sistema operativo che termina forzatamente il processo.
4. Perché i puntatori richiedono una tipizzazione (es. int* o char*)?
- A) Per definire lo spazio occupato dal puntatore stesso (8 byte vs 4 byte)
- B) Per consentire al compilatore di sapere quanti byte leggere/scrivere e come interpretarli all’indirizzo puntato
- C) È solo un vincolo estetico del linguaggio C
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Sebbene l’indirizzo occupi sempre 8 byte, il tipo associato specifica la “lente” attraverso cui guardare la cella: definisce quanti byte consecutivi considerare a partire da quell’indirizzo e con quale schema interpretarli.
5. Cosa significa il termine “Array Decay” in C?
- A) La progressiva perdita di precisione dei dati all’interno dell’array
- B) Il fatto che il nome di un array decade implicitamente a un puntatore al suo primo elemento quando passato come parametro
- C) La distruzione automatica dell’array alla fine del ciclo di vita
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Quando un array viene passato come argomento a una funzione, decade in un semplice puntatore (&arr[0]), perdendo l’informazione sulla sua dimensione totale originale (sizeof).
6. Se un puntatore a intero p memorizza l’indirizzo decimale 2000, quale indirizzo conterrà l’espressione p + 2?
- A) 2002
- B) 2008
- C) 2016
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: L’aritmetica dei puntatori scala l’addizione per la dimensione del tipo a cui punta. Poiché in genere un intero (int) occupa 4 byte, p + 2 equivale a 2000 + (2 * 4) = 2008.
7. Qual è l’equivalenza fondamentale che lega gli array e la dereferenziazione in C?
- A)
arr[i]equivale a*(arr + i) - B)
arr[i]equivale a&arr + i - C)
arr[i]equivale a*arr + i
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: La notazione con parentesi quadre è solo zucchero sintattico per l’aritmetica dei puntatori. arr[i] esegue una somma sull’indirizzo base arr con offset i e dereferenzia il risultato.
8. Cosa dichiara l’istruzione const int *ptr = &x;?
- A) Un puntatore costante: non possiamo spostare ptr su altri indirizzi
- B) Un puntatore a un intero costante: ptr può puntare altrove, ma non possiamo modificare il valore di x tramite ptr
- C) Tutto bloccato: sia ptr che x sono immutabili
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: La parola chiave const prima dell’asterisco protegge il valore puntato. Il puntatore stesso rimane modificabile (ptr = &y; è valido), ma il contenuto all’indirizzo puntato diventa in sola lettura.
9. A cosa serve la parola chiave inline davanti alla definizione di una funzione?
- A) A dire al compilatore di inserire il corpo della funzione direttamente nel punto di chiamata, riducendo l’overhead dello stack frame
- B) A consentire alla funzione di essere dichiarata su un’unica riga
- C) A forzare la funzione a essere compilata solo per sistemi multi-thread
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: La keyword inline suggerisce al compilatore di sostituire la chiamata con il codice vero e proprio, eliminando l’overhead computazionale dovuto a push e pop dei record di attivazione dello Stack.
10. Qual è il comportamento atteso dichiarando un parametro formale come int * const arr?
- A) L’array è di sola lettura
- B) L’indirizzo puntato da arr non può essere modificato (arr è costante), ma gli elementi all’interno possono essere cambiati
- C) Il compilatore genera un errore di sintassi
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Collocare const dopo l’asterisco rende il puntatore stesso costante (sola scrittura dell’indirizzo). Gli elementi a cui punta restano accessibili in scrittura.
5. Esercizi Pratici
Sezione intitolata “5. Esercizi Pratici”Esercizio 1: Incremento tramite Puntatore
Sezione intitolata “Esercizio 1: Incremento tramite Puntatore”Scrivete una funzione void incrementa(int *x) che incrementi di una unità il valore contenuto all’indirizzo puntato da x. Nel main, dichiarate un intero, chiamate la funzione passandone l’indirizzo e verificate il cambiamento.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void incrementa(int *x) { if (x != NULL) { (*x)++; // Nota: le parentesi sono necessarie a causa delle precedenze }}
int main(void) { int valore = 10; printf("Valore originale: %d\n", valore); incrementa(&valore); printf("Valore incrementato: %d\n", valore); // Stamperà 11 return 0;}Esercizio 2: Prodotto di due numeri
Sezione intitolata “Esercizio 2: Prodotto di due numeri”Scrivete una funzione double calcola_prodotto(const double *a, const double *b) che calcoli e restituisca il prodotto dei due valori reali puntati da a e b. I valori originali non devono essere modificabili all’interno della funzione.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
double calcola_prodotto(const double *a, const double *b) { if (a != NULL && b != NULL) { return (*a) * (*b); } return 0.0;}
int main(void) { double n1 = 3.5; double n2 = 2.0; double ris = calcola_prodotto(&n1, &n2); printf("Prodotto: %.2f\n", ris); // Stamperà 7.00 return 0;}Esercizio 3: Swap di due Double
Sezione intitolata “Esercizio 3: Swap di due Double”Scrivete una funzione void swap_double(double *x, double *y) che scambi i valori reali contenuti in due variabili reali del chiamante.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void swap_double(double *x, double *y) { if (x != NULL && y != NULL) { double temp = *x; *x = *y; *y = temp; }}
int main(void) { double a = 1.23, b = 9.87; printf("Prima: a=%.2f, b=%.2f\n", a, b); swap_double(&a, &b); printf("Dopo: a=%.2f, b=%.2f\n", a, b); return 0;}Esercizio 4: Azzera Array tramite Puntatore
Sezione intitolata “Esercizio 4: Azzera Array tramite Puntatore”Scrivete una funzione void azzera_array(int *arr, int dim) che azzeri tutti gli elementi di un array di interi. Utilizzate l’aritmetica dei puntatori per scorrere l’array invece degli indici quadrate (zucchero sintattico).
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void azzera_array(int *arr, int dim) { if (arr == NULL) return; for (int i = 0; i < dim; i++) { *(arr + i) = 0; // Uso dell'aritmetica dei puntatori }}
int main(void) { int numeri[5] = {1, 2, 3, 4, 5}; azzera_array(numeri, 5); for (int i = 0; i < 5; i++) { printf("%d ", numeri[i]); // Stamperà: 0 0 0 0 0 } printf("\n"); return 0;}Esercizio 5: Riferimento a minimo (Doppio Puntatore)
Sezione intitolata “Esercizio 5: Riferimento a minimo (Doppio Puntatore)”Scrivete una funzione void trova_min_ptr(const int *arr, int dim, const int **min_ptr) che trovi l’indirizzo dell’elemento minimo all’interno di un array e scriva tale indirizzo nella variabile puntata da min_ptr.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void trova_min_ptr(const int *arr, int dim, const int **min_ptr) { if (arr == NULL || min_ptr == NULL || dim <= 0) return;
const int *min_corrente = arr; // Inizializza al primo elemento for (int i = 1; i < dim; i++) { if (*(arr + i) < *min_corrente) { min_corrente = arr + i; } } *min_ptr = min_corrente; // Scrive l'indirizzo trovato}
int main(void) { int dati[5] = {34, 12, 56, 8, 45}; const int *min_addr = NULL;
trova_min_ptr(dati, 5, &min_addr);
if (min_addr != NULL) { printf("Il minimo e': %d, all'indirizzo %p\n", *min_addr, (void*)min_addr); } return 0;}Esercizio 6: Inversione di un array in-place con due puntatori
Sezione intitolata “Esercizio 6: Inversione di un array in-place con due puntatori”Scrivete una funzione void inverti_array(int *arr, int dim) che inverta gli elementi di un array sul posto usando due puntatori di supporto: uno impostato al primo elemento, l’altro all’ultimo elemento. Si effettui lo swap degli elementi e si muovano i puntatori l’uno verso l’altro finché non si incontrano.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void inverti_array(int *arr, int dim) { if (arr == NULL || dim <= 1) return;
int *sinistra = arr; int *destra = arr + dim - 1;
while (sinistra < destra) { // Swap degli elementi puntati int temp = *sinistra; *sinistra = *destra; *destra = temp;
// Spostamento dei puntatori sinistra++; destra--; }}
int main(void) { int v[6] = {10, 20, 30, 40, 50, 60}; inverti_array(v, 6); for (int i = 0; i < 6; i++) { printf("%d ", v[i]); // Stamperà: 60 50 40 30 20 10 } printf("\n"); return 0;}Esercizio 7: Confronto di Puntatori
Sezione intitolata “Esercizio 7: Confronto di Puntatori”Scrivete una funzione int confronta_puntatori(const int *p1, const int *p2) che verifichi i valori puntati. Restituisca 1 se il valore puntato da p1 è maggiore di quello di p2, -1 se è minore, e 0 se sono uguali. Se uno dei due puntatori è nullo, la funzione deve restituire -999.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int confronta_puntatori(const int *p1, const int *p2) { if (p1 == NULL || p2 == NULL) { return -999; } if (*p1 > *p2) return 1; if (*p1 < *p2) return -1; return 0;}
int main(void) { int a = 15, b = 20; printf("Risultato: %d\n", confronta_puntatori(&a, &b)); // Stamperà -1 printf("Risultato NULL: %d\n", confronta_puntatori(&a, NULL)); // Stamperà -999 return 0;}Esercizio 8: Lunghezza stringa con Aritmetica dei Puntatori
Sezione intitolata “Esercizio 8: Lunghezza stringa con Aritmetica dei Puntatori”Scrivete una funzione int calcola_lunghezza_stringa(const char *s) che calcoli la lunghezza di una stringa in C (terminata dal carattere \0) incrementando un puntatore da inizio stringa e calcolando la differenza di indirizzi alla fine. Non utilizzate indici o variabili di conteggio.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
int calcola_lunghezza_stringa(const char *s) { if (s == NULL) return 0; const char *p = s; while (*p != '\0') { p++; // Avanza il puntatore } return p - s; // Differenza tra puntatori: restituisce il numero di char}
int main(void) { char testo[] = "Puntatori"; printf("Lunghezza di '%s': %d\n", testo, calcola_lunghezza_stringa(testo)); // Stamperà 9 return 0;}Esercizio 9: Copia Sicura di un Array Constant
Sezione intitolata “Esercizio 9: Copia Sicura di un Array Constant”Scrivete una funzione void copia_array_const(const int *sorgente, int *destinazione, int dim) che copi tutti gli elementi da un array sorgente (in sola lettura) a un array di destinazione usando puntatori.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void copia_array_const(const int *sorgente, int *destinazione, int dim) { if (sorgente == NULL || destinazione == NULL) return; for (int i = 0; i < dim; i++) { *(destinazione + i) = *(sorgente + i); }}
int main(void) { int src[4] = {5, 10, 15, 20}; int dst[4]; copia_array_const(src, dst, 4); for (int i = 0; i < 4; i++) { printf("%d ", dst[i]); // Stamperà: 5 10 15 20 } printf("\n"); return 0;}Esercizio 10: Swap di Puntatori
Sezione intitolata “Esercizio 10: Swap di Puntatori”Scrivete una funzione void swap_puntatori(int **p1, int **p2) che scambi gli indirizzi di memoria memorizzati all’interno di due variabili puntatore. Nel main, verificate che dopo lo swap, i due puntatori puntino a variabili inverse.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
void swap_puntatori(int **p1, int **p2) { if (p1 != NULL && p2 != NULL) { int *temp = *p1; *p1 = *p2; *p2 = temp; }}
int main(void) { int x = 100, y = 200; int *ptr1 = &x; int *ptr2 = &y;
printf("ptr1 punta a valore: %d, ptr2 punta a valore: %d\n", *ptr1, *ptr2);
swap_puntatori(&ptr1, &ptr2); // Scambia gli indirizzi contenuti in ptr1 e ptr2
printf("Dopo swap:\n"); printf("ptr1 punta a valore: %d, ptr2 punta a valore: %d\n", *ptr1, *ptr2); // Stamperà ptr1 punta a valore 200 e ptr2 punta a valore 100
return 0;}