12 Livello 10
12.1 La gestione dei file
L’introduzione dei file rappresenta uno snodo cruciale nella programmazione in C perché offre finalmente la possibilità di rendere persistenti i dati elaborati da un programma, superando il limite della volatilità della memoria RAM (Stack e Heap).
A differenza di linguaggi di più alto livello, il C non fornisce astrazioni sofisticate per la gestione dei file, ma si affida a un approccio a basso livello basato sui puntatori.
Un aspetto da valutare in fase di progettazione (che dipende dalla destinazione d’uso dei dati) è la scelta tra file di testo e file binari:
- File di testo: i dati vengono convertiti in sequenze di caratteri. Sono leggibili dall’utente tramite un normale editor di testo e risultano ideali per garantire la portabilità esterna (es. file di configurazione, file di log). L’acquisizione di questi dati richiede però un’operazione di parsing, ovvero la trasformazione della stringa di caratteri letta in dati strutturati (interi, float) utilizzabili dal programma.
- File binari: contengono l’esatta rappresentazione in byte presente nella memoria del calcolatore. Non sono leggibili da un essere umano, ma non richiedono alcuna conversione o parsing. Sono la scelta perfetta per salvare o caricare rapidamente intere strutture dati.
12.1.1 Apertura e chiusura
Per operare sui file è necessario includere la libreria standard <stdio.h>, la quale definisce il tipo derivato FILE. Ogni operazione di lettura o scrittura avviene tramite un puntatore a questa struttura.
FILE *fileptr; // Dichiarazione del puntatore al fileLa struttura interna del tipo FILE non è strettamente standardizzata, ma dipende dall’implementazione specifica della libreria sul Sistema Operativo in uso (Windows, Linux, macOS). Il programmatore interagisce con essa solo tramite il puntatore.
12.1.1.1 L’apertura: fopen()
Prima di qualsiasi manipolazione, il file deve essere aperto tramite la funzione fopen(), che associa il puntatore al file fisico sul disco, specificando la modalità di accesso.
| Modalità | Significato | Comportamento se il file non esiste | Comportamento se esiste |
|---|---|---|---|
r |
Solo lettura | Restituisce NULL |
Apre il file dall’inizio |
w |
Solo scrittura | Crea un nuovo file | Sovrascrive il contenuto |
a |
Append (scrittura in coda) | Crea un nuovo file | Scrive alla fine del file |
r+ |
Lettura e Scrittura | Restituisce NULL |
Apre il file dall’inizio |
w+ |
Scrittura e Lettura | Crea un nuovo file | Sovrascrive il contenuto |
a+ |
Lettura e Scrittura in coda | Crea un nuovo file | Mantiene il contenuto, scrive in coda |
Buona pratica: per i file binari, lo standard raccomanda di aggiungere il suffisso b alla modalità (es. rb, wb, ab). Sebbene su sistemi come Linux e macOS non vi sia differenza pratica, su sistemi Windows questa distinzione è indispensabile per evitare la corruzione dei dati dovuta alla traslazione automatica dei caratteri di fine riga.
12.1.1.2 La gestione degli errori e la chiusura: fclose()
È obbligatorio verificare sempre che l’apertura sia andata a buon fine controllando che il puntatore non sia NULL. In caso di errore, l’uso di perror() fornisce il dettaglio esatto dell’errore restituito dal Sistema Operativo. Infine, ogni file aperto deve essere chiuso con fclose(), operazione che garantisce il salvataggio fisico dei dati sul disco liberando il buffer.
fileptr = fopen("dati.txt", "r");
if (fileptr == NULL) {
perror("Errore durante l'apertura del file");
return 1; // Termina il programma con codice di errore
}
// ... operazioni sul file ...
fclose(fileptr); // Chiusura fondamentale12.1.2 File di testo
La libreria standard offre tre famiglie di funzioni per gestire l’I/O sui file di testo, classificate in base alla granularità del dato trattato.
12.1.2.1 fprintf() e fscanf()
Agiscono in modo identico a printf e scanf, ma operano sul flusso di dati (stream) del file anziché sullo standard input/output.
La funzione fscanf restituisce il numero di elementi letti e convertiti con successo. Questo meccanismo è cruciale per leggere un file di testo fino alla sua conclusione logica (EOF - End Of File), anche quando non si conosce a priori il numero di elementi.
// Esempio: Caricamento dinamico da file a un array
#include <stdio.h>
#define DIM 100
int main(void) {
FILE *fileptr;
int arr[DIM];
int num, i = 0, n;
fileptr = fopen("file_01.txt", "r");
if(fileptr != NULL) {
// Continua a leggere finché trova interi validi e non supera la DIM
while(fscanf(fileptr, "%d", &num) == 1 && i < DIM) {
arr[i] = num;
i++;
}
fclose(fileptr);
n = i; // Numero effettivo di elementi letti
}
return 0;
}12.1.2.2 fgets() e fputs()
fgets(buffer, dimensione, fileptr): legge un’intera riga dal file fino al raggiungimento del carattere\n(incluso) o fino al limite della dimensione del buffer, aggiungendo automaticamente il terminatore di stringa\0.fputs(stringa, fileptr): scrive una stringa sul file.
fgets() e la tastiera
Poiché la tastiera è considerata a tutti gli effetti un flusso di file di nome stdin (Standard Input), la funzione fgets() è raccomandata per l’acquisizione sicura di stringhe testuali dall’utente, superando i ben noti limiti della scanf (che si interrompe al primo spazio).
12.1.2.3 fgetc() e fputc()
Permettono di leggere e scrivere un carattere alla volta, utili per la manipolazione a bassissimo livello dei file di testo. Il ciclo di lettura tipico continua finché fgetc non restituisce la macro EOF.
12.1.3 File binari
I file binari bypassano le problematiche di formattazione. Le funzioni fread() e fwrite() consentono di spostare direttamente blocchi di memoria (low-level reading/writing) dalla RAM al disco e viceversa.
fread(destinazione, dimensione_elemento, numero_elementi, fileptr)fwrite(sorgente, dimensione_elemento, numero_elementi, fileptr)
Questo approccio si rivela estremamente potente quando associato alle funzioni di riposizionamento spaziale del cursore.
12.1.3.1 fseek() e ftell()
L’accesso ai file non deve essere necessariamente sequenziale. Il C permette l’accesso “random” (casuale o diretto) spostando il cursore interno del file.
fseek(fp, offset, origine): sposta il cursore di un numero di byte pari aoffseta partire da una specificaorigine. Per indicare l’origine, lo standard C prescrive l’utilizzo delle seguenti macro (definite all’interno di<stdio.h>):SEEK_SET: inizio del file (sostituisce il valore numerico0)SEEK_CUR: posizione attuale del cursore (sostituisce il valore numerico1)SEEK_END: fine del file (sostituisce il valore numerico2)ftell(fp): restituisce l’esatta posizione attuale del cursore espressa in byte.
Sebbene molti compilatori tollerino ancora l’uso dei numeri diretti (0, 1, 2) per il parametro di origine, questa è considerata una cattiva pratica. L’uso rigoroso delle macro SEEK_SET, SEEK_CUR e SEEK_END garantisce la portabilità del codice su architetture diverse.