I have been wondering for a long time how you would save a class in binary format if you don't declare everything on the stack. If you try to do this following the procedure shown in most tutorials and books on the subject, it will just save 4 bytes of data representing a pointer to the location of the object in memory.
I finally figured out how to save classes in binary format, even when the use pointers or, as in the example, std::strings or something similar with variable-length containers. I am going to illustrate it here, because I have _never_ been able to find this anywhere on the web. :(
Here it goes, along with an explanation, following here:
The class we are going to save is called Item, a simple class storing a "resource ID", a unique ID string for the item, like "ITEM_WAND_01", a "display name", which would be displayed in-game as the item's name, like "spoon" or "wand of Merlin", a description, like "A powerful staff to conduct magical energies", or "For eating".
Also, it has a weight, in pounds.
The Item class is a bare-bones example of something that might be found in an RPG or some other type of game. We are going to save it in binary format, then load it again and read out the contents. Here we go! (Its a bit rough, I took it straight from one of my
projects, and it hasn't been cleaned up).
Source from the item class:
item.h - the class definition
#ifndef ITEM_CLASS
#define ITEM_CLASS
#include <string>
#include "stringops.h"
class Item
{
public:
Item();
~Item();
//Set member variables
void operator () ( std::string, std::string,
std::string, unsigned int );
//Set member variables
void operator () ( const char*, const char*, const char*, unsigned int );
//Get member variables
std::string & GetResourceName();
std::string & GetDisplayName();
std::string & GetDescription();
unsigned int & GetWeight();
private:
std::string resource_name;
std::string display_name;
std::string description;
unsigned int weight;
};
#endif
item.cpp - the class methods
#ifndef ITEM_CPP
#define ITEM_CPP
#include "item.h"
Item::Item()
: resource_name("UNDEFINED")
{}
Item::~Item()
{}
void Item::operator () ( const char* new_resource_name,
const char* new_display_name,
const char* new_description,
unsigned int new_weight)
{
(*this)(std::string(new_resource_name),
std::string(new_display_name),
std::string(new_description),
new_weight );
}
void Item::operator () ( std::string new_resource_name,
std::string new_display_name,
std::string new_description,
unsigned int new_weight)
{
resource_name = Left( new_resource_name, 25 );
display_name = Left( new_display_name, 25 );
description = Left( new_description, 200 );
weight = new_weight;
}
std::string & Item::GetResourceName()
{
return resource_name;
}
std::string & Item::GetDisplayName()
{
return display_name;
}
std::string & Item::GetDescription()
{
return description;
}
unsigned int & Item::GetWeight()
{
return weight;
}
#endif
Now for the functions which read and write the item to a binary file. There are numerous
ways in which it could be improved, and it works only for items, but you could easily modify it for your own classes (provided you understand what I'm doing) or make it more general.
Here is the source for the read and write item functions - WriteItem will write the item
to a binary file with filename [item.resource_name.RIF]. ReadItem will read from the
file specified by std::string "filename" into "item".
#include <fstream>
#include <string>
#include "item.h"
bool WriteItem( Item & item )
{
//this is the filename we are writing to - [item.resource_name].RIF
std::string filename = item.GetResourceName() + ".RIF";
//open a stream to the file, which will create or copy over the filename you
//specified. Open it in binary format, as "std::ofstream::binary".
std::ofstream fout ( filename.c_str(), std::ofstream::binary );
//Get the size, in bytes, of the actual char* part of "item"'s resource_name.
//Add 1 to it to account for the '\0' character that postpends a char*.
//The size is equal to 1 byte per character, as a char variable is equal to 1 byte.
//If you aren't saving a char, which is shown when we write out the weight, you
//would use this: "sizeof( [variable_type or class_name] )".
//sizeof( char ) returns 1, sizeof( int ) returns 4, etc. You can use this on
//classes you design, as long as they don't contain any variables on the heap.
long size = item.GetResourceName().length() + 1;
//Write out the size of the string you are writing out, so you can load it
//properly later.
fout.write( (char*)&size, sizeof(&size) );
//Now, take out a number of bytes equal to the length of resource_name + 1, as we got
//above. Write out the actual character array to the file, as shown below.
fout.write( (item.GetResourceName().c_str()), size );
////////////////////////////////////////////////////
//Repeat above, with item.display_name.
size = item.GetDisplayName().length() + 1;
fout.write( (char*)&size, sizeof(&size) );
fout.write( (item.GetDisplayName().c_str()), size );
////////////////////////////////////////////////////
//Repeat as above, but with item.description
size = item.GetDescription().length() + 1;
fout.write( (char*)&size, sizeof(&size) );
fout.write( (item.GetDescription().c_str()), size );
////////////////////////////////////////////////////
//Write out item.weight. You cast it to (char*), because this effectively tells it
//to write it out byte by byte. You would do this with anything you use this
//statement on, except for actual (char*)s, as above.
fout.write( (char*)&item.GetWeight(), sizeof(unsigned int) );
//Close the file - we have saved all information from the class!
fout.close();
return true;
}
bool ReadItem( std::string filename, Item & item )
{
std::ifstream fin ( filename.c_str(), std::ofstream::binary );
//this will store the size, in bytes, of each variable to read into.
long size;
//these store the actual variables to read into.
char * resource_name = 0;
char * display_name = 0;
char * description = 0;
unsigned int weight = 0;
//read in the size of the resource name
fin.read( (char*)&size, sizeof( &size ) );
//size resource_name appropriately
resource_name = new char[size];
//read in resource_name, a number of bytes equal to what we wrote out the size was.
fin.read( resource_name, size );
///////////////////////////////////////////
//as above, but for the display name
fin.read( (char*)&size, sizeof( &size ) );
display_name = new char[size];
fin.read( display_name, size );
///////////////////////////////////////////
//as above, but for the description
fin.read( (char*)&size, sizeof( &size ) );
description = new char[size];
fin.read( description, size );
///////////////////////////////////////////
//read directly into "weight"
fin.read( (char*)&weight, sizeof(&weight ) );
//we have read all data! store it in "item".
item( resource_name, display_name, description, weight );
//release memory taken up by char arrays
delete [] resource_name;
delete [] display_name;
delete [] description;
//Close the file - we have loaded in all the member variables. "item" now stores
//an exact copy of the Item that was saved.
fin.close();
return true;
}
The source of the functions should be commented sufficiently. If you have questions or
comments, please post below! Hope it was helpful to at least one person, besides myself.
[edit]Fixed!