Lezione 13: Modellare la Realtà (Struct, Padding e Persistenza a Blocchi)

Nelle lezioni precedenti abbiamo visto come gestire insiemi di dati omogenei tramite gli array e come persistere le informazioni su file di testo e binari. Tuttavia, la realtà che ci circonda è complessa ed eterogenea: un’entità del mondo reale come uno “Studente” non è definibile tramite un singolo numero o una singola stringa, ma possiede un insieme di caratteristiche di tipo diverso (un nome, un numero di matricola intero, una media pesata float).
In questa lezione supereremo la fragilità degli array paralleli introducendo il concetto di Struttura (struct), che permette di aggregare tipi di dato eterogenei in un unico record logico. Esploreremo la fisica della memoria analizzando come vengono passate le struct alle funzioni (per valore vs riferimento con l’operatore freccia ->) e svelando le bolle di memoria invisibili create dal compilatore per l’efficienza dei bus hardware (Padding e Allineamento). Infine, vedremo come le struct rivoluzionano la persistenza binaria consentendo la scrittura e la lettura massiva a blocchi e l’accesso diretto istantaneo tramite la navigazione con fseek.
1. La Fine degli Array Paralleli e l’Incapsulamento
Sezione intitolata “1. La Fine degli Array Paralleli e l’Incapsulamento”1.1 Il Problema: I limiti dei tipi predefiniti
Sezione intitolata “1.1 Il Problema: I limiti dei tipi predefiniti”Fino ad ora, per modellare una tabella di elementi eterogenei, siamo stati costretti ad adottare la tecnica degli array paralleli. Se ad esempio desideriamo gestire un elenco di esami universitari composto da materia, CFU e voto, dichiariamo tre array distinti che condividono implicitamente lo stesso indice logico:
char materie[100][50]; // Nomi degli esamiint cfu[100]; // Crediti di ciascun esameint voti[100]; // Voto conseguitoSebbene questa soluzione funzioni, essa presenta gravissimi problemi di manutenibilità e sicurezza dei dati. Consideriamo il caso in cui si voglia ordinare gli esami in base al voto usando l’algoritmo Bubble Sort. Per non mescolare i dati, siamo costretti a effettuare lo scambio (swap) per ciascuno dei tre array paralleli coordinando manualmente gli indici:
// Scambio del votoint temp_voto = voti[i];voti[i] = voti[j];voti[j] = temp_voto;
// Scambio dei CFUint temp_cfu = cfu[i];cfu[i] = cfu[j];cfu[j] = temp_cfu;
// Scambio della stringa materiachar temp_mat[50];strcpy(temp_mat, materie[i]);strcpy(materie[i], materie[j]);strcpy(materie[j], temp_mat);Se il programmatore dimentica di scambiare anche solo una delle variabili (ad esempio omettendo lo swap del nome della materia), l’associazione logica viene irrimediabilmente distrutta, attribuendo ad esempio i CFU di Matematica all’esame di Storia. Il C mette a disposizione tipi primitivi (int, float, char), ma non possiede un tipo nativo denominato Esame. Diventa quindi necessario creare un nostro tipo di dato personalizzato.
1.2 La Creazione di un Nuovo Tipo: La struct
Sezione intitolata “1.2 La Creazione di un Nuovo Tipo: La struct”In C, la parola chiave struct (Struttura) permette di raggruppare variabili di tipo diverso all’interno di un unico contenitore logico e fisico. Ciascuna variabile interna alla struttura viene chiamata campo o membro.
Definizione di una Struct
Sezione intitolata “Definizione di una Struct”La definizione descrive il modello (“progetto”) della struttura senza allocare spazio in memoria RAM:
struct materia_s { char nome[50]; char codice[10]; int cfu; int voto;}; // ATTENZIONE: Il punto e virgola finale è obbligatorio!Per istanziare una variabile di questo tipo e accedere ai suoi membri, utilizziamo l’operatore punto (.):
#include <stdio.h>#include <string.h>
int main(void) { // Dobbiamo ripetere la parola chiave 'struct' struct materia_s analisi;
// Accesso e scrittura nei campi (i campi array si riempiono con strcpy) strcpy(analisi.nome, "Analisi"); strcpy(analisi.codice, "AN29345"); analisi.cfu = 9; analisi.voto = 28;
printf("Voto in analisi: %d\n", analisi.voto); return 0;}Definizione Elegante con typedef
Sezione intitolata “Definizione Elegante con typedef”Dover anteporre costantemente struct davanti al nome del tipo risulta ridondante. Possiamo utilizzare la keyword typedef per creare un alias (sinonimo) semplificato:
typedef struct { char nome[50]; char codice[10]; int cfu; int voto;} Esame; // "Esame" diventa a tutti gli effetti un nuovo tipo di dato!È anche possibile scorporare la definizione della struct e il typedef:
struct esame { char nome[50]; char codice[10]; int cfu; int voto;};
typedef struct esame Esame; // "Esame" diventa a tutti gli effetti un nuovo tipo di dato!1.3 Inizializzazione e Assegnazione Magica
Sezione intitolata “1.3 Inizializzazione e Assegnazione Magica”Inizializzazione a Graffe
Sezione intitolata “Inizializzazione a Graffe”Una struct può essere inizializzata sinteticamente all’atto della dichiarazione usando una lista di valori racchiusi tra parentesi graffe {}, rispettando rigorosamente l’ordine di dichiarazione dei campi:
// Corretto: {nome, codice, cfu, voto}Esame prog = {"Programmazione 1", "INF101", 12, 30};
// Errato o disallineato:Esame errore = {"Fisica", "FIS01", 18, 6}; // Assegna 18 a cfu e 6 a voto!Copia per Assegnazione
Sezione intitolata “Copia per Assegnazione”A differenza degli array tradizionali (che non possono essere duplicati tramite l’operatore =), le struct godono del privilegio della copia diretta per assegnamento. Il compilatore effettua autonomamente un dump perfetto bit-a-bit dell’intera area di memoria occupata dalla struct:
Esame backup = prog; // Copia immediata di tutti i campi in RAM2. Puntatori a Struct e Fisica della Memoria
Sezione intitolata “2. Puntatori a Struct e Fisica della Memoria”2.1 Passaggio per Valore vs Riferimento: L’Operatore Freccia (->)
Sezione intitolata “2.1 Passaggio per Valore vs Riferimento: L’Operatore Freccia (->)”Sommando lo spazio occupato dai campi di Esame ricaviamo la sua dimensione: .
Passaggio per Valore (Inefficiente)
Sezione intitolata “Passaggio per Valore (Inefficiente)”Se passiamo una struct a una funzione per valore, l’intera struttura da 68 byte viene interamente duplicata sullo Stack all’interno del record di attivazione della funzione chiamata:
void stampa_copia(Esame e) { printf("%s: %d\n", e.nome, e.voto);}Qualora la struct contenesse array di dimensioni maggiori, questa operazione comporterebbe un pesante spreco di memoria Stack e cicli di clock per effettuare la fotocopia dei byte.
Passaggio per Riferimento (Efficiente)
Sezione intitolata “Passaggio per Riferimento (Efficiente)”Per ottimizzare le prestazioni, passiamo unicamente l’indirizzo di memoria della struct (8 byte su sistemi a 64-bit). Per accedere ai campi della struct puntata abbiamo due opzioni sintattiche:
- Sintassi Esplicita: Dereferenziare il puntatore e usare l’operatore punto. Richiede le parentesi a causa della precedenza degli operatori:
(*e).nome. - Sintassi Semplificata (Operatore Freccia
->): Zucchero sintattico che sostituisce la combinazione di dereferenziazione e punto:e->nome.
#include <stdio.h>
void stampa_rif(const Esame *e) { // Accesso tramite operatore freccia -> printf("%s (%s) - CFU: %d, Voto: %d\n", e->nome, e->codice, e->cfu, e->voto);}
int main(void) { Esame info = {"Informatica", "INF01", 6, 27}; stampa_rif(&info); // Passiamo l'indirizzo return 0;}
2.2 Sotto il Cofano: Padding e Allineamento (Data Alignment)
Sezione intitolata “2.2 Sotto il Cofano: Padding e Allineamento (Data Alignment)”Consideriamo l’esperimento di verificare lo spazio occupato in RAM da due struct apparentemente identiche nei campi, ma disposte in ordine differente:
typedef struct { char c; // 1 byte double d; // 8 byte int i; // 4 byte} ProvaA;
typedef struct { double d; // 8 byte int i; // 4 byte char c; // 1 byte} ProvaB;A livello algebrico, lo spazio occupato dai campi dovrebbe essere pari a byte. Tuttavia, compilando ed eseguendo sizeof su una macchina a 64-bit ricaviamo risultati sorprendenti:
sizeof(ProvaA)restituisce 24 byte.sizeof(ProvaB)restituisce 16 byte.
Il Padding
Sezione intitolata “Il Padding”Questo fenomeno è causato dal Padding (imbottitura) introdotto dal compilatore. I moderni processori leggono la memoria RAM a blocchi allineati (solitamente di 8 byte su sistemi a 64-bit). Se una variabile multi-byte (come un double da 8 byte) venisse memorizzata a cavallo tra due blocchi fisici di memoria, la CPU sarebbe costretta a eseguire due letture RAM distinte per ricomporre il dato, rallentando l’esecuzione.
Per ottimizzare gli accessi hardware, il compilatore applica le seguenti regole di allineamento:
- Ogni tipo di dato nativo deve iniziare a un indirizzo di memoria che sia multiplo della sua stessa dimensione (es. un
doubleallineato a indirizzi multipli di 8, uninta multipli di 4). - Il compilatore inietta byte vuoti (padding) invisibili per ripristinare l’allineamento ottimale.
Analizziamo il posizionamento dei byte per le due strutture:
ProvaA(char c, double d, int i):- Il carattere
coccupa la posizione 0. - Il campo successivo
d(double) richiede allineamento a 8 byte. Il compilatore inserisce 7 byte di padding vuoti. - Il
doubleviene allocato dall’offset 8 all’offset 15. - L’intero
iviene allocato dall’offset 16 al 19 (allineamento a 4 rispettato). - La dimensione totale della struct deve essere multipla del campo più grande (
double= 8). Vengono iniettati altri 4 byte di padding finali, portando il totale a 24 byte (offset 0-23).
- Il carattere
ProvaB(double d, int i, char c):- Il
doubleviene allocato al blocco iniziale dall’offset 0 al 7. - L’intero
ioccupa la posizione dall’offset 8 all’11 (allineamento a 4 rispettato). - Il carattere
coccupa l’offset 12. - Vengono iniettati 3 byte di padding finali per allineare l’intera struttura al multiplo di 8, portando il totale a 16 byte (offset 0-15).
- Il

Nei normali esercizi di laboratorio non è richiesto riordinare ossessivamente le struct per risparmiare memoria. È tuttavia fondamentale, in contesti a basso livello (sistemi operativi, sistemi embedded e programmazione di rete), conoscere l’esistenza del padding per prevenire anomalie e sprechi di RAM.
2.3 Enumerazioni (enum): Un tocco di leggibilità
Sezione intitolata “2.3 Enumerazioni (enum): Un tocco di leggibilità”Spesso all’interno delle struct è necessario salvare lo stato di un oggetto (es. un esame può essere “In Corso”, “Superato” o “Bocciato”). Utilizzare numeri interi generici (0, 1, 2) rende il codice cryptico ed esposto a errori.
Il C permette di creare delle Enumerazioni (enum), che associano identificatori mnemonici a costanti intere incrementali (di default partendo da 0):
#include <stdio.h>
// Definizione dell'enumerazionetypedef enum { IN_CORSO, // 0 SUPERATO, // 1 BOCCIATO // 2} StatoEsame;
// Utilizzo all'interno di una Structtypedef struct { char materia[50]; StatoEsame stato;} EsameCarriera;
int main(void) { EsameCarriera prog = {"Programmazione 1", IN_CORSO};
if (prog.stato == IN_CORSO) { printf("Stai ancora studiando %s. Coraggio!\n", prog.materia); }
prog.stato = SUPERATO; // Aggiornamento dello stato return 0;}3. Il Trionfo di fread, fwrite ed fseek
Sezione intitolata “3. Il Trionfo di fread, fwrite ed fseek”3.1 Unire la Teoria: Struct e File Binari
Sezione intitolata “3.1 Unire la Teoria: Struct e File Binari”Nella Lezione 12 abbiamo notato la difficoltà di gestire file binari composti da stringhe e dati a lunghezza variabile: senza una dimensione nota e costante, risulta impossibile calcolare l’offset fisico per saltare a un record specifico.
La struct rappresenta il mattoncino fondamentale per risolvere questo problema: poiché la dimensione di una struct (ad esempio sizeof(Esame)) è fissa e nota a tempo di compilazione, un file binario composto da una successione di struct si comporterà fisicamente come un array sequenziale su disco.
3.2 Il Salvataggio Istantaneo dell’intero Database
Sezione intitolata “3.2 Il Salvataggio Istantaneo dell’intero Database”Grazie alla contiguità fisica della memoria RAM, possiamo salvare un intero array di struct su disco con una singola invocazione di fwrite, riversando i blocchi in un unico colpo:
#include <stdio.h>
typedef struct { char nome[50]; int cfu; int voto;} Esame;
int main(void) { // Array di struct in RAM Esame libretto[3] = { {"Analisi 1", 9, 28}, {"Programmazione 1", 12, 30}, {"Fisica 1", 6, 25} };
FILE *fp = fopen("database.bin", "wb"); if (fp != NULL) { // Scrittura in blocco: (sizeof(Esame) * 3) byte trasferiti sul disco fwrite(libretto, sizeof(Esame), 3, fp); fclose(fp); printf("Database salvato con successo!\n"); } return 0;}3.3 Accesso Diretto
Sezione intitolata “3.3 Accesso Diretto”Grazie alla struttura a blocchi costanti, la testina di lettura può saltare istantaneamente all’inizio del record -esimo senza dover scansionare i record precedenti. L’offset esatto in byte a partire dall’inizio del file (SEEK_SET) è calcolato con la formula:
#include <stdio.h>
typedef struct { char nome[50]; int cfu; int voto;} Esame;
int main(void) { FILE *fp = fopen("database.bin", "rb"); if (fp == NULL) { return 1; }
Esame cercato; int indice_desiderato = 1; // Vogliamo leggere il secondo esame (indice 1)
// SALTO MATEMATICO fseek(fp, indice_desiderato * sizeof(Esame), SEEK_SET);
// Lettura isolata dell'unico record posizionato sotto la testina fread(&cercato, sizeof(Esame), 1, fp); fclose(fp);
printf("Esame estratto al volo: %s, Voto: %d\n", cercato.nome, cercato.voto); return 0;}
Nota: il diagramma riporta blocchi da 68 byte (dimensione della Esame con il campo codice); con la Esame di questa sezione, priva di codice, sizeof(Esame) vale 60 byte su x86-64 (50 + 2 di padding + 4 + 4), quindi il record di indice 1 inizia all’offset 60.
Quiz di Auto-valutazione
Sezione intitolata “Quiz di Auto-valutazione”1. Quale delle seguenti affermazioni sulla definizione di una struct in C è corretta?
- A) La definizione alloca immediatamente spazio nella memoria RAM.
- B) È obbligatorio includere il punto e virgola
;dopo la chiusura della parentesi graffa. - C) Permette di raggruppare unicamente elementi dello stesso tipo.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: La definizione di una struct funge solo da modello (“stampo”) per il compilatore e non alloca memoria (che viene allocata solo all’istanza delle variabili). La sintassi del linguaggio C impone tassativamente il punto e virgola ; al termine della definizione della struttura.
2. Cosa accade quando si esegue l’operazione di assegnamento structA = structB in C?
- A) Viene copiato solo l’indirizzo di memoria del primo elemento (come per gli array).
- B) Il compilatore restituisce un errore, poiché le struct non si possono copiare con
=. - C) Viene effettuata una copia fisica bit-a-bit di tutti i campi in una nuova porzione di RAM.
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: A differenza degli array, le struct in C sono trattate come valori atomici per l’assegnamento. Il compilatore genera codice per copiare l’intera porzione di memoria occupata dalla struct sorgente in quella di destinazione.
3. Data la struct typedef struct { int x; char y; } Punto; Punto p; Punto *ptr = &p;, qual è la sintassi corretta per accedere al campo y tramite il puntatore?
- A)
ptr->y - B)
*ptr.y - C)
ptr.y
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: L’operatore freccia -> è lo zucchero sintattico utilizzato per accedere ai membri di una struttura puntata da un puntatore. Scrivere ptr->y equivale a scrivere (*ptr).y. La scrittura *ptr.y senza parentesi è errata poiché l’operatore punto . ha una precedenza maggiore rispetto all’operatore di dereferenziazione *.
4. Perché sizeof(struct { char c; double d; }) restituisce solitamente 16 anziché 9?
- A) A causa dell’allineamento dei dati (padding) effettuato dal compilatore per l’efficienza della CPU.
- B) Perché il compilatore C riserva byte aggiuntivi per i nomi delle variabili.
- C) Perché il tipo
charviene convertito automaticamente indouble.
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: Le CPU a 64-bit accedono in modo efficiente a dati situati ad indirizzi multipli di 8. Per evitare accessi disallineati, il compilatore inserisce 7 byte di padding vuoti subito dopo il char da 1 byte, allineando il double (8 byte) ai blocchi hardware della CPU.
5. Cos’è una enum in C?
- A) Un tipo di dato che alloca dinamicamente spazio nello Stack.
- B) Un insieme di costanti intere con nomi mnemonici per migliorare la leggibilità del codice.
- C) Un file binario in cui memorizzare le struct.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Le enumerazioni consentono di mappare nomi mnemonici a valori interi sequenziali (es. 0, 1, 2). Sotto il cofano, le variabili di tipo enum sono gestite dal compilatore come normali numeri interi, ma prevengono l’uso di “numeri magici” sparsi nel codice.
6. Se definiamo typedef struct { char nome[20]; } Persona;, quanti byte occuperà un puntatore Persona *p in un sistema a 64-bit?
- A) 20 byte.
- B) 8 byte.
- C) 4 byte.
▶ Show Correct Answer
Risposta corretta: B
Spiegazione: La dimensione di un puntatore dipende esclusivamente dall’architettura del processore (indirizzamento a 64-bit = 8 byte, 32-bit = 4 byte), indipendentemente dal tipo di dato o dalla dimensione della struttura a cui punta.
7. Qual è la formula matematica per calcolare l’offset in byte per raggiungere il record -esimo (con indice -based) all’interno di un file binario strutturato con struct di tipo Record?
- A)
N * sizeof(Record) - B)
(N - 1) * sizeof(Record) - C)
N * sizeof(Record) + 1
▶ Mostra Risposta Corretta
Risposta corretta: A
Spiegazione: Avendo i record una dimensione fissa costante, il record all’indice N si trova esattamente dopo che si sono saltati N record interi di dimensione sizeof(Record). Per l’indice 0, lo spostamento sarà di 0 byte (l’inizio del file).
8. Per quale motivo gli array paralleli sono considerati una pratica fragile in programmazione C?
- A) Perché il compilatore impedisce la compilazione di più di due array contemporaneamente.
- B) Perché richiedono la gestione sincrona manuale degli indici; se si effettua uno swap su un solo array, i dati degli altri si disallineano corrompendo l’informazione.
- C) Perché consumano il doppio dello spazio in memoria rispetto a una struct equivalente.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: Gli array paralleli non garantiscono l’integrità del dato a livello di compilatore. Qualsiasi operazione di ordinamento o inserimento costringe a manipolare simultaneamente tutti gli array; un singolo errore disallinea irrimediabilmente i record.
9. Con quale costante in fseek si indica che lo spostamento deve avvenire a partire dalla posizione corrente del cursore?
- A)
SEEK_SET - B)
SEEK_END - C)
SEEK_CUR
▶ Mostra Risposta Corretta
Risposta corretta: C
Spiegazione: In fseek, SEEK_SET indica l’inizio del file, SEEK_END indica la fine del file, e SEEK_CUR indica la posizione corrente in cui si trova la testina di lettura/scrittura.
10. Qual è l’effetto di omettere il punto e virgola ; alla fine della definizione di una struct?
- A) Nessuno, il compilatore lo aggiunge automaticamente.
- B) Un errore di compilazione (“syntax error”).
- C) La struct viene allocata nello Heap invece che nello Stack.
▶ Mostra Risposta Corretta
Risposta corretta: B
Spiegazione: La sintassi formale del linguaggio C richiede obbligatoriamente il punto e virgola al termine della definizione di una struttura. In caso contrario, il compilatore genera un errore bloccante.
Esercizi Pratici
Sezione intitolata “Esercizi Pratici”Esercizio 1: Definizione e Stampa (stampa_punto.c)
Sezione intitolata “Esercizio 1: Definizione e Stampa (stampa_punto.c)”Si definisca una struct Punto3D avente tre coordinate reali (x, y, z). Si scriva una funzione stampa_punto che accetti la struct per valore e ne stampi le coordinate nel formato (x, y, z).
💻 Mostra Soluzione e Codice C
#include <stdio.h>
// Definizione della structtypedef struct { double x; double y; double z;} Punto3D;
// Funzione che accetta la struct per valorevoid stampa_punto(Punto3D p) { printf("(%.2f, %.2f, %.2f)\n", p.x, p.y, p.z);}
int main(void) { Punto3D origine = {0.0, 0.0, 0.0}; Punto3D p1 = {3.5, -2.0, 7.8};
printf("Origine: "); stampa_punto(origine);
printf("Punto 1: "); stampa_punto(p1);
return 0;}Spiegazione dell’algoritmo:
Definiamo il tipo Punto3D contenente tre variabili double. La funzione stampa_punto riceve una copia locale della struttura e stampa i singoli campi accedendovi con l’operatore punto (.).
Esercizio 2: Modifica per Riferimento (trasla_punto.c)
Sezione intitolata “Esercizio 2: Modifica per Riferimento (trasla_punto.c)”Si scriva una funzione trasla_punto che accetti un puntatore a Punto3D e tre valori reali dx, dy, dz per traslare il punto sommandoli alle coordinate originali.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { double x; double y; double z;} Punto3D;
// Modifica tramite puntatore e operatore freccia ->void trasla_punto(Punto3D *p, double dx, double dy, double dz) { p->x += dx; p->y += dy; p->z += dz;}
void stampa_punto(Punto3D p) { printf("(%.2f, %.2f, %.2f)\n", p.x, p.y, p.z);}
int main(void) { Punto3D p = {1.0, 2.0, 3.0};
printf("Punto originale: "); stampa_punto(p);
// Passiamo l'indirizzo di memoria di p trasla_punto(&p, 1.5, -0.5, 2.0);
printf("Punto traslato: "); stampa_punto(p);
return 0;}Spiegazione dell’algoritmo:
Passando il puntatore Punto3D *p, la funzione può accedere e modificare le coordinate reali del punto originale allocato nel frame del main tramite l’operatore freccia ->, evitando di creare copie ridondanti in memoria Stack.
Esercizio 3: Calcolo Statistiche su Array di Struct (statistiche_studenti.c)
Sezione intitolata “Esercizio 3: Calcolo Statistiche su Array di Struct (statistiche_studenti.c)”Si definisca una struct Studente contenente il nome (stringa max 50 caratteri) e la media dei voti (valore float). Si scriva un programma che istanzi un array di 3 studenti, legga i dati da tastiera e visualizzi la media complessiva della classe.
💻 Mostra Soluzione e Codice C
#include <stdio.h>#include <string.h>
typedef struct { char nome[50]; float media;} Studente;
int main(void) { Studente classe[3]; float somma = 0.0;
for (int i = 0; i < 3; i++) { printf("Inserisci il nome dello studente %d: ", i + 1); // Lettura stringa con gestione del fine riga scanf("%49s", classe[i].nome);
printf("Inserisci la media dello studente %d: ", i + 1); scanf("%f", &classe[i].media);
somma += classe[i].media; }
float media_generale = somma / 3.0; printf("\nMedia generale della classe: %.2f\n", media_generale);
return 0;}Spiegazione dell’algoritmo:
Creiamo un array in cui ogni cella contiene una struct Studente. Nel ciclo for accediamo ai campi di ciascun elemento combinando l’indice dell’array e l’operatore punto: classe[i].nome e &classe[i].media (notare l’operatore indirizzo & posizionato davanti al membro per leggerlo con scanf).
Esercizio 4: Ricerca per Chiave in Archivio (cerca_studente.c)
Sezione intitolata “Esercizio 4: Ricerca per Chiave in Archivio (cerca_studente.c)”Aggiungere all’esercizio precedente un campo intero matricola. Si realizzi una funzione che, dato un array di studenti e una matricola da cercare, restituisca l’indice dello studente trovato, oppure -1 se non presente.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
// Funzione di ricerca lineareint cerca_studente(const Studente elenco[], int n, int matricola_cercata) { for (int i = 0; i < n; i++) { if (elenco[i].matricola == matricola_cercata) { return i; // Restituisce l'indice della corrispondenza } } return -1; // Non trovato}
int main(void) { Studente archivio[3] = { {"Alice", 104523, 28.5}, {"Bob", 104589, 24.2}, {"Charlie", 104612, 27.8} };
int matr = 104589; int idx = cerca_studente(archivio, 3, matr);
if (idx != -1) { printf("Studente trovato: %s, Media: %.2f\n", archivio[idx].nome, archivio[idx].media); } else { printf("Nessuno studente con matricola %d in archivio.\n", matr); }
return 0;}Spiegazione dell’algoritmo:
Scansiamo sequenzialmente l’array di struct. Ad ogni iterazione, confrontiamo il valore della variabile cercata con il campo elenco[i].matricola. Non appena riscontriamo uguaglianza, interrompiamo restituendo la posizione corrente dell’array.
Esercizio 5: Scambio e Ordinamento di Struct (ordina_studenti.c)
Sezione intitolata “Esercizio 5: Scambio e Ordinamento di Struct (ordina_studenti.c)”Si scriva una funzione per scambiare due struct Studente e la si utilizzi per ordinare in senso decrescente (in base alla media) l’array di studenti precedentemente creato.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
// Funzione di swap per intere structvoid scambia_studenti(Studente *a, Studente *b) { // Sfruttiamo la copia diretta del C Studente temp = *a; *a = *b; *b = temp;}
int main(void) { Studente archivio[3] = { {"Bob", 104589, 24.2}, {"Alice", 104523, 28.5}, {"Charlie", 104612, 27.8} };
// Ordinamento Bubble Sort for (int i = 0; i < 3 - 1; i++) { for (int j = 0; j < 3 - i - 1; j++) { if (archivio[j].media < archivio[j+1].media) { // Passiamo gli indirizzi per lo swap scambia_studenti(&archivio[j], &archivio[j+1]); } } }
printf("Archivio ordinato per media decrescente:\n"); for (int i = 0; i < 3; i++) { printf("%s (Matr: %d) - Media: %.2f\n", archivio[i].nome, archivio[i].matricola, archivio[i].media); }
return 0;}Spiegazione dell’algoritmo:
La funzione scambia_studenti sfrutta la caratteristica nativa del C di copiare per intero le struct tramite assegnamento (temp = *a;). Nel ciclo del Bubble Sort, verifichiamo la condizione sulla media e scambiamo le intere strutture dati con un’unica chiamata, mantenendo l’allineamento dei record.
Esercizio 6: Gestione di Stati tramite enum (carriera_esami.c)
Sezione intitolata “Esercizio 6: Gestione di Stati tramite enum (carriera_esami.c)”Si definisca una struct Corso contenente il nome della materia e un campo stato di tipo enum (DA_SOSTENERE, SUPERATO, FALLITO). Si scriva una funzione che stampi a schermo l’elenco dei corsi colorando simbolicamente l’output a seconda dello stato.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef enum { DA_SOSTENERE, SUPERATO, FALLITO} StatoCorso;
typedef struct { char materia[50]; StatoCorso stato;} Corso;
void stampa_libretto(const Corso libretto[], int n) { for (int i = 0; i < n; i++) { printf("Materia: %-20s -> Stato: ", libretto[i].materia); switch (libretto[i].stato) { case DA_SOSTENERE: printf("[ ] Da Sostenere\n"); break; case SUPERATO: printf("[X] Superato\n"); break; case FALLITO: printf("[!] Fallito\n"); break; } }}
int main(void) { Corso esami[3] = { {"Programmazione 1", SUPERATO}, {"Architettura", DA_SOSTENERE}, {"Fisica 1", FALLITO} };
stampa_libretto(esami, 3); return 0;}Spiegazione dell’algoritmo:
Il tipo StatoCorso mappa i tre possibili valori simbolici. Nella funzione stampa_libretto utilizziamo un costrutto switch sul campo enum libretto[i].stato per determinare in modo leggibile quale descrizione testuale stampare.
Esercizio 7: Persistenza a Blocchi su Disco (salva_archivio.c)
Sezione intitolata “Esercizio 7: Persistenza a Blocchi su Disco (salva_archivio.c)”Si scriva un programma C che legga un array statico di 3 struct Studente e lo scriva integralmente in formato binario all’interno del file studenti.bin in un solo colpo tramite la funzione fwrite.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
int main(void) { Studente elenco[3] = { {"Alice", 104523, 28.5}, {"Bob", 104589, 24.2}, {"Charlie", 104612, 27.8} };
FILE *fp = fopen("studenti.bin", "wb"); if (fp == NULL) { fprintf(stderr, "Errore di apertura del file in scrittura.\n"); return 1; }
// Salvataggio massivo dell'intero array in RAM sul file stream size_t scritti = fwrite(elenco, sizeof(Studente), 3, fp); fclose(fp);
printf("Salvati correttamente %zu record di dimensione %zu byte.\n", scritti, sizeof(Studente)); return 0;}Spiegazione dell’algoritmo:
Apriamo il file con modalità "wb" (write binary). La funzione fwrite riceve come primo parametro il puntatore alla base dell’array elenco, e copia sul file byte consecutivi, effettuando il dump della RAM su disco.
Esercizio 8: Recupero Blocco Dati da File Binario (carica_archivio.c)
Sezione intitolata “Esercizio 8: Recupero Blocco Dati da File Binario (carica_archivio.c)”Si realizzi il programma speculare al precedente: il programma apre studenti.bin in lettura binaria, carica l’intero blocco di dati all’interno di un array locale in RAM tramite fread e ne stampa a schermo le informazioni.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
int main(void) { Studente archivio_letti[3];
FILE *fp = fopen("studenti.bin", "rb"); if (fp == NULL) { fprintf(stderr, "Errore di apertura del file in lettura. Eseguire prima il salvataggio.\n"); return 1; }
// Caricamento massivo dei record dal file binario size_t letti = fread(archivio_letti, sizeof(Studente), 3, fp); fclose(fp);
printf("Caricati con successo %zu record.\n", letti); for (int i = 0; i < 3; i++) { printf("Studente %d: %s (Matr: %d) -> Media: %.2f\n", i + 1, archivio_letti[i].nome, archivio_letti[i].matricola, archivio_letti[i].media); }
return 0;}Spiegazione dell’algoritmo:
Utilizziamo fread fornendo l’indirizzo dell’array archivio_letti. La funzione carica istantaneamente i byte dal file ripristinando in RAM la struttura originale delle struct.
Esercizio 9: Accesso Casuale a Record di Struct (leggi_record.c)
Sezione intitolata “Esercizio 9: Accesso Casuale a Record di Struct (leggi_record.c)”Si scriva un programma C che consenta all’utente di inserire da tastiera un indice () e, tramite fseek, legga direttamente ed esclusivamente il record corrispondente del file studenti.bin, senza caricare in memoria gli altri record.
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
int main(void) { FILE *fp = fopen("studenti.bin", "rb"); if (fp == NULL) { fprintf(stderr, "Errore: impossibile leggere 'studenti.bin'.\n"); return 1; }
int indice = 0; printf("Inserisci l'indice del record da caricare (0, 1, o 2): "); if (scanf("%d", &indice) != 1 || indice < 0 || indice > 2) { printf("Indice non valido!\n"); fclose(fp); return 1; }
// Salto mirato: calcoliamo l'offset matematico long offset = (long)(indice * sizeof(Studente)); fseek(fp, offset, SEEK_SET);
// Lettura del singolo record isolato Studente cercato; size_t letti = fread(&cercato, sizeof(Studente), 1, fp); fclose(fp);
if (letti == 1) { printf("\n[RECORD %d ESTRATTO]\n", indice); printf("Nome: %s\n", cercato.nome); printf("Matricola: %d\n", cercato.matricola); printf("Media: %.2f\n", cercato.media); } else { printf("Errore durante la lettura del record.\n"); }
return 0;}Spiegazione dell’algoritmo:
Sfruttando la dimensione fissa di Studente nota a tempo di compilazione, posizioniamo la testina di lettura all’offset in byte pari a indice * sizeof(Studente). In questo modo la chiamata a fread estrae esclusivamente i byte associati allo studente richiesto.
Esercizio 10: Aggiornamento Diretto su File Binario (aggiorna_record.c)
Sezione intitolata “Esercizio 10: Aggiornamento Diretto su File Binario (aggiorna_record.c)”Si realizzi una semplice utility C che consenta di modificare in loco la media di un determinato studente salvato su studenti.bin. Il programma deve:
- Chiedere l’indice da modificare e il nuovo valore della media.
- Posizionarsi sul record desiderato.
- Sovrascrivere esclusivamente quel record sul file aperto in modalità di aggiornamento
"r+b".
💻 Mostra Soluzione e Codice C
#include <stdio.h>
typedef struct { char nome[50]; int matricola; float media;} Studente;
int main(void) { // Apriamo il file in lettura/scrittura senza distruggerlo (r+b) FILE *fp = fopen("studenti.bin", "r+b"); if (fp == NULL) { fprintf(stderr, "Errore: impossibile aprire il file. Esegui prima il salvataggio.\n"); return 1; }
int indice = 0; float nuova_media = 0.0;
printf("Inserisci l'indice dello studente da aggiornare (0, 1 o 2): "); scanf("%d", &indice); printf("Inserisci la nuova media: "); scanf("%f", &nuova_media);
// 1. Spostiamo la testina sul record da aggiornare per leggerlo prima fseek(fp, indice * sizeof(Studente), SEEK_SET);
Studente s; fread(&s, sizeof(Studente), 1, fp);
// 2. Modifichiamo il dato in memoria RAM s.media = nuova_media;
// 3. Riposizioniamo la testina sullo stesso record prima di sovrascriverlo // (Nota: la fread ha fatto avanzare il cursore interno del file) fseek(fp, indice * sizeof(Studente), SEEK_SET);
// 4. Sovrascrittura mirata del blocco modificato fwrite(&s, sizeof(Studente), 1, fp); fclose(fp);
printf("Media aggiornata con successo su disco!\n"); return 0;}Spiegazione dell’algoritmo:
L’apertura "r+b" consente di modificare porzioni del file preservando il resto del contenuto. Effettuiamo una prima fseek per leggere lo studente, aggiorniamo il campo media nella struct in RAM, dopodiché eseguiamo una seconda fseek per riposizionare indietro il cursore del file (poiché la precedente fread lo aveva fatto avanzare di sizeof(Studente) byte) ed effettuiamo la sovrascrittura tramite fwrite.