Se serializzare (scrivere su file) dati in puro testo è però operazione piuttosto comune e ben nota, fare lo stesso con file binari è spesso un’opzione poco esplorata. I file di testo, o ASCII, hanno principalmente tre grandi vantaggi: sono semplici, umanamente comprensibili e facilmente modificabili all’esterno dell’applicazione con un qualsiasi editor di testo. Presentano però anche alcuni svantaggi, in particolare la difficoltà nel salvare strutture dati complesse, e ancor più rileggerle. Per questi scenari occorrono infatti dei parser di stringhe la cui complessità va rapidamente fuori controllo.
Se da una parte potremmo uscirne elegantemente utilizzando XML, dall’altra dobbiamo poter contare anche su una alternativa altrettanto valida: utilizzare file binari.
Il file binario presenta l’indubbia scomodità di essere illeggibile a occhio nudo e modificabile solo tramite le procedure scritte nel nostro programma. Accanto a questi due limiti, però, bisogna elencare le sue caratteristiche positive:
- Possibilità di serializzare e rileggere facilmente strutture complesse
- Dimensioni ridotte del file prodotto (rispetto al corrispettivo ASCII)
Volendo poi, la caratteristica di essere illeggibili e difficilmente modificabili può divenire, a volte, una cosa positiva. Pensate di scrivere un videogioco e di serializzare i dati dei vostri livelli su file: non sarebbe il massimo se chiunque potesse aprire il file con notepad e cambiare il vostro curatissimo design!
Lo scenario
Ma torniamo a noi. Il C++ ci mette a disposizione la libreria standard fstream per lavorare su file, sia di testo che binari.
Supponiamo di voler serializzare una struttura dati così definita:
struct PlayerData { unsigned int energy; unsigned int score; double x; double y; double z; }
La nostra struct
PlayerData
contiene le informazioni di base del giocatore di un ipotetico sparatutto in 3D: l’energia, il punteggio e le tre coordinate di spazio che definiscono la sua posizione nel mondo.Se volessimo salvare questi dati utilizzando un file di testo saremmo costretti a scegliere qualche strategia, tipo: un valore su ogni riga, utilizzo di un carattere separatore, valori in posizioni assolute nel file ecc.
Oltre a portare a facili errori, queste strategie sono anche rigide ai cambiamenti, avendo natura posizionale.
Vediamo invece come possiamo raggiungere lo stesso obiettivo con un file binario.
Scrittura binaria
Iniziamo con l’includere la nostra libreria standard:
#include <fstream>
L’oggetto che dobbiamo istanziare è di tipo
ofstream
(output file stream). Ecco il codice:PlayerData data; data.energy = 100; data.score = 12090; data.x = 34.56778; data.y = -87.24573; data.z = 487.2198; ofstream file; file.open(“player.dat”, ios_base::binary); if(file.good()) { file.write((char*)&data, sizeof(PlayerData)); } else { cout << “Errore durante l’apertura in scrittura del file player.dat!” << endl; } file.close();
Una volta popolata una struttura
PlayerData
andiamo ad aprire il nostro file specificandone il nome (l’estensione .dat è arbitraria: potete chiamarlo anche .pippo ;)) e, cosa importante, la modalità binaria.Se la open va a buon fine la
good()
restituirà true
e andremo a gestire la scrittura vera e propria. Come vedete è solo una riga! Ma può apparire un po’ criptica a prima vista. Partiamo quindi col vedere come è dichiarata la funzione write della classe ofstream:ostream& write ( const char* s , streamsize n );
Accetta in input due argomenti: una stringa di caratteri e un valore che ne indica la lunghezza. Piuttosto lineare, ed infatti è la stessa funzione che useremmo per scrivere un semplice file di testo.
Quel che facciamo è un piccolo trucchetto: passiamo sì come primo parametro un puntatore, ma non ad un array di caratteri, bensì alla nostra struct. Per farlo digerire alla
write
la rassicuriamo tramite un cast di tipo, dicendole di trattarlo come fosse un array di caratteri. La seconda fase è recuperare la lunghezza (in byte) della nostra struttura. Per fare questo utilizziamo la funzione sizeof. Stiamo barando, ma solo un po’: dopo tutto il nostro è un flusso di byte, e un byte ha la stessa dimensione di un char!Lettura binaria
Se la scrittura è stata piuttosto diretta, ci aspettiamo che anche la lettura lo sia. Ancora una volta vi invito a pensare a come risolvere la cosa con i file di testo...
L’oggetto che useremo sarà questa volta un
ifstream
(input file stream). Esiste in effetti anche fstream
, di derivazione da ifstream
e ofstream
, e sarebbe possibile utilizzarlo direttamente, specificando di volta in volta se accedere in lettura o in scrittura, ma solitamente preferisco eliminare situazioni di ambiguità. Se devo leggere uso ifstream
, se devo scrivere ofstream
.Ecco il codice per rileggere il nostro file player.dat e popolare con i suoi dati la nostra struttura:
PlayerData data; ifstream file; file.open(“player.dat”, ios_base::binary); if(file.good()) { file.read((char*)&data, sizeof(PlayerData)); } else { cout << “Errore durante l’apertura in lettura del file player.dat!” << endl; } file.close(); cout << “Player energy: “ << data.energy << endl; cout << “Player score: “ << data.score << endl; cout << “Player x: “ << data.x << endl; cout << “Player y: “ << data.y << endl; cout << “Player z: “ << data.z << endl;
Ora che abbiamo già visto nel dettaglio la
write
, la lettura della read
è piuttosto semplice. Quel che accade è una piccola magia: i byte vengono letti dal file e vanno a popolare correttamente i singoli campi della nostra struttura. Dopo aver chiuso lo stream, non ci rimane che verificare la lettura stampando a video i valori contenuti in data.Buon coding!
Riferimenti:
Documentazione fstream
Grazie mille per questa guida! Metodo molto piu veloce e meno seccante che salvar su .txt!
RispondiEliminaFigurati! Anzi, approfitto per una precisazione: nell'articolo si parla di "serializzazione" quando in realtà si tratta di banale "scrittura" binaria. La vera serializzazione è indipendente dalla piattaforma e capace di gestire puntatori e collezioni, come liste, array, vettori ecc.
RispondiEliminaQuesto metodo, più casareccio e spicciolo, si limita a scrivere dati primitivi... il quick and dirty, come dicono ;)
grazie, mi hai tolto metà del lavoro!
RispondiElimina