Archived

This topic is now archived and is closed to further replies.

writing classes to files

This topic is 5429 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hey I just found this place and it looks pretty great, i was just wondering how to write and retrieve a class from a file in binary, im goin to use it so players can save there character to use in other games i make. Thanks Dexman

Share this post


Link to post
Share on other sites
Don''t make the mistake of using memcpy to directly copy the class somewhere, or otherwise treat it as raw data.

Do a websearch. The concepts you are searching for are "serialization" (expressing data of arbitrary complexity as a linear file) and "persistence" (being able to save the state of an object and recreate it later).


Don''t listen to me. I''ve had too much coffee.

Share this post


Link to post
Share on other sites
If you want to port characters from one game to another, why do
you want to save the whole class anyways?

That would require you to have exactly the same character class
in all your games.

Why not just save the date (stats) of the player-character to
a file? And then load those in the other game.

This will still require you to have the same player stats in all
your games, but at least you can have different classes
(different member-functions), in case players have different
abilities in different games.
And you can leave out some stats in games that don''t need them.

For example compare ''Diablo'' and ''Fallout''. Both could share
a character model, but Diablo has a lot less detailed characters.
Having exactly the same player model in both games would waste
space. ''Diablo'' has no use for ''charisma'' or ''inteligense''.

Share this post


Link to post
Share on other sites
Sneftel is right, using memcpy() is an all-too-frequent mistake that will cause you enless trouble.

You usually want to save/restore member-by-member, possibly recursively (if your class include other classes you want to save), and prefixing a class''s data with a ''tag'' of some kind that let your program know what type of object follows.

Assuming you use C++ strings, you end up with something like...

First a pair of helper functions


  
#include <iostream>

#include <string>


// Take care of the std:: prefixes.

using namespace std;

// Function to save strings, will also accept C strings

ostream& save_string( ostream& os, const string& str )
{
// write the string length

string::size_type size = str.size();
os.write( (const char*)&size, sizeof( size ) );

// write the string itself

os.write( str.data(), size );

return os; // just out of habit. :)

}

// Function to load strings, only accepts C++ strings

istream& load_string( istream& is, string& str )
{
// read the string size

string::size_type size;
is.read( (char*)&size, sizeof( size ) );

// I haven''t found anything really simpler

// than dynamically allocating a buffer.

// The other things I can think of are *evil*

// either involving stream iterators,

// direct writes into the string''s memory,

// and other mojo.

// At least this should be understandable.


char* buffer = new char[size];
// There should be some error checking

// in case the memory allocation fails :

// the size parameter might be huge

// - corrupted file ?

// - malicious user ?

// - overloaded system ?


// actually read into the buffer

is.read( buffer, size );

// this will copy the content of the array

// into the C++ string. Note that we pass

// two pointers, and that buffer is NOT

// a null-terminated string.

str.assign( buffer, buffer+size );
delete[] buffer;

return is;
}


Then the actual code that loads and save an object.


  
#include <iostream>

#include <string>


using namespace std;

// An interface, useful later

class Serializable
{
public:
virtual void Save( ostream& os ) = 0;
virtual void Load( istream& is ) = 0;

// We''re going to manipulate derived

// classes through pointers to Serializable.

// Therefore a virtual destructor (even empty)

// is *mandatory*.

virtual ~Serializable() {}
};

// Note that since we''re going to inherit a bunch

// of virtual functions, memcpy() is *definitively*

// out.


// Assume there also is a class Bar derived from Serializable

class Bar;

class Foo : public Serializable
{
int mA,mB;
double mC;

// ** this member must not be saved, it is computed **

int mD;

string mStr;
Bar* mpBar;
Foo* mpFoo;

// ** this member must not be saved, it is computed **

Foo* mpParent

public:
void ~Foo()
{
// clean up the contained objects, if any.

delete mpBar;
delete mpFoo;
}

void Save( ostream& os ) const
// member function is const, because we

// can save const objects too.

{
// Write the class tag

// It might be better to provide the tag

// through a member function (and also

// possibly through a static member function)

save_string( os, "Foo" );

// Save mA, mB, mC

os.write( (const char*)&mA, sizeof( mA ) );
os.write( (const char*)&mB, sizeof( mB ) );
os.write( (const char*)&mC, sizeof( mC ) );

save_string( os, mStr );

// If we have a valid Bar pointer,

// save the object, otherwise write

// a null byte to indicate its absence.


// Note that we would need to be careful,

// if that object had a back pointer to

// the current object to NOT get stuck

// in an infinite loop... saving object

// trees is easy. Saving object graphs

// containing cycles is harder.


if( mpBar != 0 )
mpBar->Save( os );
else
os.put( ''\0'' );

// idem with mpFoo

if( mpBar != 0 )
mpBar->Save( os );
else
os.put( ''\0'' );
}

void Load( istream& is )
// member function is not const.

{
// The tag has already been read,

// otherwise we wouldn''t know we''re

// loading a Foo object.


// load mA, mB, mC in the order in which

// they were saved.


is.read( (char*)&mA, sizeof( mA ) );
is.read( (char*)&mB, sizeof( mB ) );
is.read( (char*)&mC, sizeof( mC ) );

// load mStr using the helper function

load_string( is, mStr );

// initialise mD properly (whatever that means)

mD = mA + mB * mC

// Look ahead for a null byte to see if

// mpBar exists or not, and if so, load it,

// using the top-level Restore function.


if( is.peek() != 0 )
{
// set mpBar to NULL

mpBar = 0;

// We just peeked, so now we must

// consume the null byte and move on.

is.ignore();
}
else
{
// Use Restore() -- see below -- to

// create the object. The tag string

// will ensure that the right type

// of object is created.


Serializable* tmp = Restore( is );

// We got a Serializable, so we need

// to make sure it is really a Bar

// object, or derived from Bar.

// dynamic_cast will return 0 if not.


mpBar = dynamic_cast<Bar*>(tmp);

// If we didn''t get a Bar object, then

// the only thing we can do is clean it

// up, and possibly issue a warning.

// If the tag was provided by a member

// function, we could directly query

// tmp for it.


if( mpBar == 0 )
delete tmp;
}

// idem with mpFoo

if( is.peek() != 0 )
{
mpFoo = 0;
is.ignore();
}
else
{
Serializable* tmp = Restore( is );
mpFoo = dynamic_cast<Foo*>(tmp);
if( mpFoo == 0 )
delete tmp;
}

// Complication ! Because Foo objects

// object have a parent pointer, we must

// make sure it is valid.

// This could also be done later as a fixup,

// but I prefer to do it here.


if( mpFoo )
mpFoo->mpParent = this;

// And we''re done. We reconstructed

// an object entirely from stream data.

}
};


And then the actual Restore function, which is responsible for creating the objects. There exist better design patterns than the simple factory I present here, allowing for automatic registration - using an associative array to link class tags to class creation functions (that just call the approriate ''new X''), but this one should be sufficiently simple. It has the disadvantage that it must know about *all* the types involved.


  
#include <iostream>

#include <string>


using namespace std;

Serializable* CreateFoo() { return new Foo; }
Serializable* CreateBar() { return new Bar; }
Serializable* CreateBaz() { return new Baz; }

Serializable* Restore( istream& is )
{
string tag;
load_string( is, tag );

Serializable* obj = 0;

// note, you can use == with C++ string objects

// even to compare them to null-terminated strings

// however, you cannot write a case statement


if (tag == "Foo") obj = CreateFoo();
else if (tag == "Bar") obj = CreateBar();
else if (tag == "Baz") obj = CreateBaz();
// ...


// If we have a real object, then we initialize it.

if( obj != 0 )
obj->Load( is )

// The calling code should be ready to deal

// with a NULL pointer if no correct object

// tag was found.

return obj;
};


Here you go. It''s a bit complicated, but I hope that it''ll explain some


[ Start Here ! | How To Ask Smart Questions | Recommended C++ Books | C++ FAQ Lite | Function Ptrs | CppTips Archive ]
[ Header Files | File Format Docs | LNK2001 | C++ STL Doc | STLPort | Free C++ IDE | Boost C++ Lib | MSVC6 Lib Fixes ]

Share this post


Link to post
Share on other sites