Perché i file?
Tutto ciò che si memorizza in variabili, array o struct esiste solo durante l'esecuzione del programma. Appena il programma termina, quei dati spariscono. I file risolvono il problema della persistenza: scrivere dati su disco significa conservarli tra un'esecuzione e la successiva, condividerli con altri programmi o stamparli.
File binari — contenuto scritto byte per byte, identico alla rappresentazione in memoria. Più compatti e veloci da leggere/scrivere, non leggibili direttamente. Adatti per struct, array numerici, dati strutturati.
Le tre classi stream
| Classe | Uso | Include |
|---|---|---|
| ofstream | Scrittura su file (output) | <fstream> |
| ifstream | Lettura da file (input) | <fstream> |
| fstream | Lettura e scrittura (entrambe) | <fstream> |
Modalità di apertura
| Flag | Significato | Default per |
|---|---|---|
| ios::in | Apre in lettura | ifstream |
| ios::out | Apre in scrittura (sovrascrive) | ofstream |
| ios::app | Aggiunge in fondo senza sovrascrivere | — |
| ios::trunc | Svuota il file all'apertura | ofstream |
| ios::binary | Modalità binaria (nessuna conversione) | — |
File di testo
In un file di testo ogni valore viene convertito nella sua rappresentazione a caratteri prima di essere scritto. L'operatore << funziona esattamente come con cout, e >> come con cin.
Scrittura e lettura — esempio base
#include <iostream> #include <fstream> using namespace std; int main() { // scrittura: ofstream crea il file se non esiste ofstream out("dati.txt"); out << "Mario Rossi" << endl; out << 7.5 << endl; out.close(); // lettura: ifstream apre il file esistente ifstream in("dati.txt"); string nome; float media; getline(in, nome); // legge una riga intera in >> media; // legge il valore numerico in.close(); cout << nome << " | " << media << endl; return 0; }
Gestione degli errori e pattern completo
Aprire un file può fallire: file inesistente, permessi insufficienti, disco pieno. Il metodo is_open() verifica se l'apertura è riuscita — va sempre controllato prima di usare lo stream.
#include <iostream> #include <fstream> #include <string> using namespace std; int main() { // ── SCRITTURA ────────────────────────────────────── ofstream out("voti.txt"); if (!out.is_open()) { cerr << "Errore: impossibile creare il file." << endl; return 1; } out << "Anna Bianchi" << endl << 8 << endl; out << "Mario Rossi" << endl << 7 << endl; out << "Luca Verdi" << endl << 9 << endl; out.close(); // ── LETTURA RIGA PER RIGA ────────────────────────── ifstream in("voti.txt"); if (!in.is_open()) { cerr << "Errore: file non trovato." << endl; return 1; } string nome; int voto; while (getline(in, nome)) { // legge il nome if (!(in >> voto)) break; // legge il voto in.ignore(); // NECESSARIO: scarta il '\n' lasciato da operator>> dopo il numero, altrimenti il getline successivo leggerebbe una riga vuota cout << nome << ": " << voto << endl; } in.close(); return 0; }
Modalità append — aggiungere senza sovrascrivere
// ios::app posiziona il cursore in fondo prima di ogni scrittura ofstream out("voti.txt", ios::app); if (!out.is_open()) { cerr << "Errore apertura." << endl; return 1; } out << "Sara Neri" << endl << 9 << endl; out.close(); cout << "Dati aggiunti con successo." << endl; // il file ora contiene i dati precedenti + Sara Neri
🔧 Simulatore File di Testo
Aggiungi righe al file simulato, scegli se sovrascrivere o aggiungere in fondo, poi simula la lettura con getline().
File binari
In un file binario i dati vengono scritti byte per byte esattamente come sono in memoria, senza alcuna conversione. Un int occupa sempre 4 byte, un float sempre 4 byte. Risultato: file più compatti, lettura/scrittura più veloci, contenuto non leggibile con un editor.
Scrittura binaria — esempio base
#include <fstream> using namespace std; int main() { int dati[] = {10, 25, 7, 42}; // scrittura binaria: ios::binary disabilita le conversioni ofstream out("numeri.bin", ios::binary); out.write(reinterpret_cast<const char*>(dati), sizeof(dati)); out.close(); // lettura binaria int letti[4]; ifstream in("numeri.bin", ios::binary); in.read(reinterpret_cast<char*>(letti), sizeof(letti)); in.close(); return 0; }
Con gestione degli errori
#include <iostream> #include <fstream> using namespace std; int main() { int dati[] = {10, 25, 7, 42}; const int N = sizeof(dati) / sizeof(dati[0]); // ── SCRITTURA ────────────────────────────────────── ofstream out("numeri.bin", ios::binary); if (!out.is_open()) { cerr << "Errore: impossibile aprire in scrittura." << endl; return 1; } out.write(reinterpret_cast<const char*>(dati), sizeof(dati)); if (!out.good()) { cerr << "Errore durante la scrittura." << endl; return 1; } out.close(); // ── LETTURA ──────────────────────────────────────── int letti[N]; ifstream in("numeri.bin", ios::binary); if (!in.is_open()) { cerr << "Errore: file non trovato." << endl; return 1; } in.read(reinterpret_cast<char*>(letti), sizeof(letti)); if (in) { // lettura riuscita: lo stream è ancora in stato valido for (int i = 0; i < N; i++) cout << letti[i] << " "; cout << endl; } else { cerr << "Errore durante la lettura del file binario." << endl; return 1; } in.close(); return 0; }
write() e read() lavorano su sequenze di char (byte grezzi). reinterpret_cast dice al compilatore di trattare il puntatore al nostro dato come un puntatore a byte, senza modificarne il contenuto. È il modo corretto per accedere alla rappresentazione binaria di qualsiasi tipo.
File di record
Il caso d'uso più comune in ambito SIA: salvare su file un intero array di struct e rileggerlo in un'altra esecuzione. Ogni struct è un record a dimensione fissa, il che permette l'accesso diretto per posizione senza leggere tutto il file.
Scrittura e lettura sequenziale
#include <fstream> using namespace std; typedef struct { int matricola; char nome[30]; float media; } Studente; int main() { Studente classe[3] = { {101, "Anna Bianchi", 8.5f}, {102, "Mario Rossi", 7.2f}, {103, "Luca Verdi", 9.1f} }; // scrittura: ogni struct è scritta come blocco di byte ofstream out("studenti.dat", ios::binary); for (int i = 0; i < 3; i++) out.write(reinterpret_cast<const char*>(&classe[i]), sizeof(Studente)); out.close(); // lettura sequenziale Studente s; ifstream in("studenti.dat", ios::binary); while (in.read(reinterpret_cast<char*>(&s), sizeof(Studente))) cout << s.matricola << " " << s.nome << " " << s.media << endl; in.close(); return 0; }
Accesso diretto per posizione
Poiché ogni record ha dimensione fissa (sizeof(Studente) byte), l'offset del record di indice n è esattamente n × sizeof(Studente) byte dall'inizio del file. Con seekg() si salta direttamente a quella posizione senza leggere tutto il file — O(1) invece di O(n).
#include <iostream> #include <fstream> using namespace std; typedef struct { int matricola; char nome[30]; float media; } Studente; // legge il record in posizione n (indice base 0) bool leggiRecord(ifstream& in, int n, Studente& s) { in.clear(); // resetta eventuali flag di errore (es. EOF da lettura precedente) in.seekg((long)n * sizeof(Studente), ios::beg); in.read(reinterpret_cast<char*>(&s), sizeof(Studente)); return (bool)in; // vero se la lettura è riuscita } // modifica il record in posizione n bool modificaRecord(fstream& f, int n, const Studente& s) { f.clear(); // resetta eventuali flag di errore prima del seek f.seekp((long)n * sizeof(Studente), ios::beg); f.write(reinterpret_cast<const char*>(&s), sizeof(Studente)); return f.good(); // good() è vero solo se lo stream non ha errori E non è EOF: più rigoroso di (bool)stream } int main() { // lettura diretta del secondo record (indice 1) Studente s; ifstream in("studenti.dat", ios::binary); if (!in.is_open()) { cerr << "File non trovato." << endl; return 1; } if (leggiRecord(in, 1, s)) cout << s.nome << " | " << s.media << endl; // Mario Rossi | 7.2 in.close(); // modifica del terzo record (indice 2) con fstream fstream f("studenti.dat", ios::binary | ios::in | ios::out); if (!f.is_open()) { cerr << "Errore apertura." << endl; return 1; } Studente nuovo = {103, "Luca Verdi", 9.8f}; // aggiorno la media modificaRecord(f, 2, nuovo); f.close(); return 0; }
🔧 Visualizzatore Record
Aggiungi record, poi usa l'accesso diretto per selezionarne uno per indice. Il visualizzatore mostra l'offset in byte calcolato con n × sizeof(Studente).
| Indice | Matricola | Nome | Media | Offset nel file |
|---|---|---|---|---|
| Nessun record — aggiungi il primo | ||||
{int matricola; char nome[30]; float media;}: int = 4 byte, char[30] = 30 byte, float = 4 byte, più eventuali 2 byte di padding per allineare al confine di 4 byte. Risultato tipico: 40 byte. Il visualizzatore usa questo valore. Nella pratica, usare sempre sizeof(Studente) e mai un numero fisso.