Don't forget that the size of each primitive will vary from architecture to architecture, as will alignment requirements and the endian representation of multibyte data, if platform portability is important.
In short, never rely on simply dumping a struct as binary data even if you are careful when handling pointer data. Even changing the compiler settings on the same target could cause changes in alignment which in turn could seriously alter the topology of your structure.
A much better approach, IMHO for plain C is to add a function pointer to your struct that points to the appropriate serialization functions to load/save that structure to an open filestream. A factory function that creates the structure would ensure that this function pointer is set:
typedef Foo struct {
int someInteger;
float someFloat;
char someFixedCharData[8];
Foo* next;
Foo* prev;
/* serialisation handlers, point to proper function at instansiation */
int (*serialize)(const Foo* foo, FILE* fp);
int (*unserialize) (Foo* foo, FILE* fp);
} Foo_t;
/* implementation detail */
int serializeFooInstance(const Foo* foo, FILE *fp)
{
/* serialization code specific to Foo here */
}
int unserializeFooInstance(Foo* foo, FILE *fp)
{
/* unserialization code specific to Foo here */
}
Foo* makeFoo(void)
{
Foo* foo = (Foo*) malloc(sizeof(Foo));
if (foo) {
/* initialise all the members to safe defaults... */
foo->serialize = serializeFooInstance;
foo->unserialize = unserializeFooInstance;
return foo;
}
return NULL;
}
You can then do a poor man's method call with the struct, rather like this:
FILE* fp;
Foo* myFoo;
....
myFoo = makeFoo();
...
if (fp = fopen(......)) {
myFoo->serialise(myFoo, fp);
}
and so on.
The main point about this is that each structure type knows how to un/serialize an instance of itself.
Of course, if you follow this idea to it's logical conclusion, you'll ditch C and start C++ which gives you much more elegant ways of doing it ;-)