Abstract out allocators and deallocators for each type. Given a type definition
typedef struct foo
{
int x;
double y;
char *z;
} Foo;
create an allocator function
Foo *createFoo(int x, double y, char *z)
{
Foo *newFoo = NULL;
char *zcpy = copyStr(z);
if (zcpy)
{
newFoo = malloc(sizeof *newFoo);
if (newFoo)
{
newFoo->x = x;
newFoo->y = y;
newFoo->z = zcpy;
}
}
return newFoo;
}
a copy function
Foo *copyFoo(Foo f)
{
Foo *newFoo = createFoo(f.x, f.y, f.z);
return newFoo;
}
and a deallocator function
void destroyFoo(Foo **f)
{
deleteStr(&((*f)->z));
free(*f);
*f = NULL;
}
Note that createFoo()
in turn calls a copyStr()
function that is responsible for allocating memory for and copying the contents of a string. Note also that if copyStr()
fails and returns a NULL, then newFoo
will not attempt to allocate memory and return a NULL. Similarly, destroyFoo()
will call a function to delete the memory for z before before freeing the rest of the struct. Finally, destroyFoo()
sets the value of f to NULL.
The key here is that the allocator and deallocator delegate responsibility to other functions if member elements also require memory management. So, as your types get more complicated, you can reuse those allocators like so:
typedef struct bar
{
Foo *f;
Bletch *b;
} Bar;
Bar *createBar(Foo f, Bletch b)
{
Bar *newBar = NULL;
Foo *fcpy = copyFoo(f);
Bletch *bcpy = copyBar(b);
if (fcpy && bcpy)
{
newBar = malloc(sizeof *newBar);
if (newBar)
{
newBar->f = fcpy;
newBar->b = bcpy;
}
}
else
{
free(fcpy);
free(bcpy);
}
return newBar;
}
Bar *copyBar(Bar b)
{
Bar *newBar = createBar(b.f, b.b);
return newBar;
}
void destroyBar(Bar **b)
{
destroyFoo(&((*b)->f));
destroyBletch(&((*b)->b));
free(*b);
*b = NULL;
}
Obviously, this example assumes that members do not have a lifetime outside of their containers. That's not always the case, and you'll have to design your interface accordingly. However, this should give you a flavor of what needs to be done.
Doing this allows you to allocate and deallocate memory for objects in a consistent, well-defined order, which is 80% of the battle in memory management. The other 20% is making sure every allocator call is balanced by a deallocator, which is the really hard part.
edit
Changed the calls to the delete*
functions so that I'm passing the right types.