Contents
#define N_ENTRIES 5
#define N_INITIALS 3
struct hi_entry
{
int score;
char initials[N_INITIALS];
} hiscores[N_ENTRIES];
void save_hi(struct hi_entry* h, FILE* f)
{
fwrite( h, sizeof(struct hi_entry), 1, f);
}
void load_hi(struct hi_entry* h, FILE* f)
{
fread( h, sizeof(struct hi_entry), 1, f);
}
f is a file descriptor, and we save one structure
with a size of sizeof(struct hi_entry).
What gets written to disk is
FILE* fhi;
int i;
fhi=fopen("hi.dat", "rb"); /* open for reading */
for (i=0; i<N_ENTRIES; ++i)
load_hi(&hiscores[i], fhi);
fclose(fhi);
/* ..rest of program ... */
fhi=fopen("hi.dat", "wb"); /* open for writing */
for (i=0; i<N_ENTRIES; ++i)
save_hi(&hiscores[i], fhi);
fclose(fhi);
fopen,
and if it fails to read, set up the score table to a sensible default.struct hi_entry
{
int score;
char initials[N_INITIALS];
char* planet;
} hiscores[N_ENTRIES];
char* planets[]={"Earth", "Mars", "Venus"};
hiscores[0].planet=planets[2];
and save it, since the planets array is part of the program itself. However,
you can't rely on this at all. What you must do is convert planet to an
identifying number, and save that. In this case, one way is to convert
it to an index in the planets array:
void save_hi(struct hi_entry* h, FILE* f)
{
int index;
fwrite( &h->score, sizeof(int), 1, f);
fwrite( h->initials, sizeof(char), N_INITIALS, f);
index=0;
/* convert pointer to index */
while (planets[index]!=h->planet)
++index;
fwrite( &index, sizeof(int), 1, f);
}
void load_hi(struct hi_entry* h, FILE* f)
{
int index;
fread( &h->score, sizeof(int), 1, f);
fread( h->initials, sizeof(char), N_INITIALS, f);
fread( &index, sizeof(int), 1, f);
h->planet=planets[index];
}

int convert_location_to_identifier(struct location*)
and struct location* convert_identifier_to_location(int).
save_X(struct X*, FILE*); and load_X(struct X*, FILE*)
then it's easy. For example:
struct inner
{
int a,b,c;
};
struct outer
{
int x,y;
struct inner z;
};
void save_outer(struct outer* o, FILE* f)
{
/* save x and y */
fwrite( &o->x, sizeof(int), 1, f);
fwrite( &o->y, sizeof(int), 1, f);
/* go into the 'inner' member */
save_inner(&o->z, f);
}
void save_inner(struct inner* i, FILE* f)
{
/* ... you know how to do this! ... */
}

struct inner
{
int a,b,c;
};
struct outer
{
int x,y;
struct inner *z;
};
void save_outer(struct outer* o, FILE* f)
{
/* save x and y */
fwrite( &o->x, sizeof(int), 1, f);
fwrite( &o->y, sizeof(int), 1, f);
/* go into the 'inner' member */
save_inner(o->z, f);
}
void load_outer(struct outer* o, FILE* f)
{
/* load x and y */
fread( &o->x, sizeof(int), 1, f);
fread( &o->y, sizeof(int), 1, f);
/* allocate a new inner member */
o->z=malloc(sizeof(struct inner));
/* go into the 'inner' member */
load_inner(o->z, f);
}
(on disk, this is identical to the previous one)
As I'm now using malloc to allocate the memory for my inner structure,
I need to use free somewhere to avoid a memory leak (leaving memory allocated
when I'm not using it any more.)
If there's a chance that z could be a NULL pointer, I need to handle that. The code, as it stands,
would crash in the save_inner function. The solution is to write an extra value, which is zero
if there's a NULL pointer and non-zero otherwise.
void save_outer(struct outer* o, FILE* f)
{
int nullp;
/* save x and y */
fwrite( &o->x, sizeof(int), 1, f);
fwrite( &o->y, sizeof(int), 1, f);
if (o->z==NULL)
{
nullp=0;
fwrite(&nullp, sizeof(int), 1, f);
}
else
{
nullp=1;
fwrite(&nullp, sizeof(int), 1, f);
/* go into the 'inner' member */
save_inner(o->z, f);
}
}
void load_outer(struct outer* o, FILE* f)
{
int nullp;
/* load x and y */
fread( &o->x, sizeof(int), 1, f);
fread( &o->y, sizeof(int), 1, f);
fread (&nullp, sizeof(int), 1, f);
if (nullp)
{
/* allocate a new inner member */
o->z=malloc(sizeof(struct inner));
/* go into the 'inner' member */
load_inner(o->z, f);
}
else
o->z=NULL;
}
(when z is not NULL)
(when z is NULL)
struct hi_entry
{
int score;
char* name;
};
I just use an extra number, as before. It holds the length of the string plus 1. The +1 allows me to distinguish between an
empty string "" (in memory {'\0'}), and a null pointer.
void save_string(char* s, FILE* f)
{
size_t l;
if (s)
{
/* not a null string */
l=strlen(s)+1;
fwrite( &l, sizeof(size_t),1, f); /* save length */
fwrite( s, sizeof(char), l, f);
}
else
{
/* null string */
l=0;
fwrite( &l, sizeof(size_t),1, f); /* save length=0 */
}
}
char* load_string(FILE* f)
{
size_t l;
char * str;
fread(&l, sizeof(size_t), 1, f);
if (l)
{
/* not null */
str=malloc(l);
fread(str, sizeof(char), l, f);
}
else
{
/* null pointer */
str=NULL;
}
return str;
}
(the string "Hello")
(a null string)
This made use of the final '\0' to mark the end of the string. This isn't always available.
Suppose, in the game, you are offered a choice of missions at the start.
You can pick any mission, and once it's done, pick another until you have completed
all the missions.
I want to record which missions have been played and what
the score was on each. I have to include an extra value to
keep track of how long the array is.
struct mission
{
int id;
int score;
};
struct hi_entry
{
int score;
struct mission* missions;
int n_missions; /* how many in the array */
char *name;
};
void save_hi(struct hi_entry* h, FILE* f)
{
int i;
fwrite(&h->score, sizeof(int), 1, f);
/* store how long the array is */
fwrite(&h->n_missions, sizeof(int), 1, f);
/* save each mission in turn */
for (i=0; i<h->n_missions; ++i)
save_mission(&h->missions[i], f);
save_string(h->name, f);
}
void load_hi(struct hi_entry* h, FILE* f)
{
int i;
fwrite(&h->score, sizeof(int), 1, f);
/* how many missions to expect */
fwrite(&h->n_missions, sizeof(int), 1, f);
/* create an array */
h->missions=malloc(sizeof (struct mission)*h->n_missions);
/* load each one in turn */
for (i=0; i<h->n_missions; ++i)
load_mission(&h->missions[i], f);
h->name=load_string(f);
}
That just about wraps it up for this section. The main points are:
| Replace this... | ...with this |
FILE | PACKFILE |
fopen(s, "rb") | pack_fopen(s, F_READ_PACKED) |
fopen(s, "wb") | pack_fopen(s, F_WRITE_PACKED) |
fread(p, l, n, f) | pack_fread(p, l*n, f) |
fwrite(p, l, n, f) | pack_fwrite(p, l*n, f) |
fclose(f) | pack_fclose(f) |
fwrite. However, the way data
is held in memory is not defined by the C standard, so you may expect some differences.
int is 4 bytes,
every short is two, and a char is one. The compiler might,
or might not, insert padding to ensure the correct alignment. You can tell it not to,
but then you will pay a performance penalty.
If your structure is
struct s /* 8 bytes long */
{
short a; /* at offset 0 */
char b; /* at offset 2 */
char c; /* at offset 3 */
int d; /* at offset 4 */
};
and you need dword alignment, it may be compiled as
struct s /* 16 bytes long */
{
short a; /* at offset 0 */
short _pad1; /* an invisible member which you can't access */
char b; /* at offset 4 */
char _pad2[3];
char c; /* at offset 8 */
char _pad3[3];
int d; /* at offset 12 */
};
This is why I always use sizeof instead of calculating it by hand.
If you save the structure as one block, using fwrite(p, sizeof(struct s), 1, f);
and read it back on a computer which only needs word alignment, the
two structures will not match up, and disaster will occur. Therefore, it's
better to save each element individually. It takes up less space, and, as we're all
super C programmers, is more correct.int
is a sequence of four bytes on 32 bit machine, and on Intel processors,
the first byte is always the least significant, which is called little endian. So, the sequence {0x12,0x34,0x56,0x78}
represents the integer 0x78563412. On other processors, Motorola's for example, it is the other
way round (big endian): {0x12,0x34,0x56,0x78} represents 0x12345678.pack_iputl to write a 32-bit value,
and pack_igetl to read it back, always using the Intel sequence. If you are primarily a Mac user,
pack_mputl uses the Motorola sequence.
There's also pack_iputw for 16-bit values and pack_putc for 8-bit values.
Note that there's only one variant for bytes. I shan't explain why.
/* this could be any 32-bit number, really */
#define HI_MAGIC 0x99669966
/* save the table */
PACKFILE* f=pack_fopen("hi.sav", F_WRITE_PACKED);
pack_iputl(HI_MAGIC, f); /* write magic number right at the start */
/* ..now save the table itself; */
for (i=0; i<N_ENTRIES; ++i)
save_hi(&hiscores[i], f);
pack_fclose(f);
/* load the table */
PACKFILE* f=pack_fopen("hi.sav", F_READ_PACKED);
if (pack_igetl(f)==HI_MAGIC)
{
/* ..now load the table itself; */
for (i=0; i<N_ENTRIES; ++i)
load_hi(&hiscores[i], f);
}
else
{
/* error - not a hiscore table */
}
pack_fclose(f);
Suppose I released version 1.0 of my game, then changed the definition
of struct hi_entry for version 2.0. I would change the value of HI_MAGIC
to something else, to prevent the program from trying to load an old-format table.
If I were really clever, I would do this
/* this is for version 1.0 */
#define HI_MAGIC_1_0 0x99669966
/* this is for version 2.0 */
#define HI_MAGIC_2_0 0x87654321
/* save the table */
PACKFILE* f=pack_fopen("hi.sav", F_WRITE_PACKED);
pack_iputl(HI_MAGIC_2_0, f); /* write magic no right at the start */
/* ..now save the table itself; */
for (i=0; i<N_ENTRIES; ++i)
save_hi(&hiscores[i], f);
pack_fclose(f);
/* load the table */
PACKFILE* f=pack_fopen("hi.sav", F_READ_PACKED);
long int m=pack_igetl(f);
if (m==HI_MAGIC_2_0)
{
/* ..now load the table itself; */
for (i=0; i<N_ENTRIES; ++i)
load_hi(&hiscores[i], f);
}
else if (m==HI_MAGIC_1_0)
{
/* ..import from old format and convert to new */
for (i=0; i<N_ENTRIES; ++i)
load_hi_1_0(&hiscores[i], f);
}
else
{
/* error - not a hiscore table */
}
pack_fclose(f);
To show my third use for magic numbers, I'll need to jump into C++.
Supposing I have a class Inventory for keeping track of the
player's possessions. Each possession is a subclass of class Item,
with virtual functions. Very briefly:
class Item
{
public:
virtual int id() const=0;
virtual void save(PACKFILE*);
virtual void load(PACKFILE*);
/* rest of class... */
};
#define I_RUBY 0
class Ruby: public Item
{
public:
virtual int id() const {return I_RUBY;}
virtual void save(PACKFILE*);
virtual void load(PACKFILE*);
/* rest of class... */
};
#define I_CHEESE 1
class Cheese: public Item
{
public:
virtual int id() const {return I_CHEESE;}
virtual void save(PACKFILE*);
virtual void load(PACKFILE*);
/* rest of class... */
};
#define I_SWORD 2
class Sword: public Item
{
public:
virtual int id() const {return I_SWORD;}
virtual void save(PACKFILE*);
virtual void load(PACKFILE*);
/* rest of class... */
};
class Inventory
{
public:
Item* items[10];
void save(PACKFILE*);
void load(PACKFILE*);
/* rest of class... */
};
void Inventory::save(PACKFILE* f)
{
for (int i=0; i<10; ++i)
{
pack_iputl(items[i]->id(), f);
items[i]->save(f);
}
/* save rest of this class */
}
void Inventory::load(PACKFILE* f)
{
for (int i=0; i<10; ++i)
{
Item* it;
switch(pack_igetl(f))
/* which class is coming up in the file? */
{
case I_RUBY:
it=new Ruby();
break;
case I_SWORD:
it=new Sword();
break;
case I_CHEESE:
it=new Cheese();
break;
default:
/* error! */
it=0;
break;
}
if (it)
it->load(f);
items[i]=it;
}
/* load rest of this class */
}

static Item* Item::fromID(int) to contain that switch statement.