lunedì 20 giugno 2011

HowTo: Serializzare strutture su file binari in C++

La serializzazione dei dati è un aspetto spesso cruciale in una applicazione. E’ utile sia per lo svolgimento del normale flusso logico del programma (salvare e caricare record di dati, ad esempio) sia per scopi di debug e diagnostica (file di log di errori ed eccezioni).

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

Forse ti interessa anche...


3 commenti:

  1. Grazie mille per questa guida! Metodo molto piu veloce e meno seccante che salvar su .txt!

    RispondiElimina
  2. Figurati! 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.
    Questo metodo, più casareccio e spicciolo, si limita a scrivere dati primitivi... il quick and dirty, come dicono ;)

    RispondiElimina
  3. grazie, mi hai tolto metà del lavoro!

    RispondiElimina