If you are the only client of the custom malloc and free (i.e. you're not trying to monkey patch those methods for code in some other library), then you can use dependency injection.
#ifndef ALLOCATOR_H
#define ALLOCATOR_H
#include <stddef.h>
struct Allocator;
typedef struct {
void *(*allocate)(struct Allocator *allocator, size_t size);
void (*free)(struct Allocator *allocator, void *object);
} AllocatorVTable;
typedef struct Allocator {
const AllocatorVTable *vptr;
} Allocator;
typedef struct {
Allocator super;
char *buffer;
size_t offset;
size_t capacity;
} BufferedAllocator;
void BufferedAllocator_init(BufferedAllocator *allocator, char *buffer, size_t capacity);
typedef Allocator MallocAllocator;
void MallocAllocator_init(MallocAllocator *allocator);
void *Allocator_allocate(Allocator *allocator, size_t size);
void Allocator_free(Allocator *allocator, void *object);
#endif
#include "allocator.h"
#include "malloc.h"
void *Allocator_allocate(Allocator *allocator, size_t size) {
return allocator->vptr->allocate(allocator, size);
}
void Allocator_free(Allocator *allocator, void *object) {
allocator->vptr->free(allocator, object);
}
void *BufferedAllocator_allocate(Allocator *allocator, size_t size) {
BufferedAllocator *bufferedAllocator = (BufferedAllocator *) allocator;
if (bufferedAllocator->offset + size > bufferedAllocator->capacity) {
fprintf(stderr, "buffer overflow: %ld + %ld > %ld\n",
bufferedAllocator->offset, size, bufferedAllocator->capacity);
return NULL;
}
bufferedAllocator->offset += size;
return bufferedAllocator->buffer + bufferedAllocator->offset - size;
}
void BufferedAllocator_free(Allocator *allocator, void *object) {
}
const AllocatorVTable bufferedAllocatorVTable = {
.allocate = BufferedAllocator_allocate,
.free = BufferedAllocator_free,
};
void BufferedAllocator_init(BufferedAllocator *allocator, char *buffer,
size_t capacity) {
allocator->super.vptr = &bufferedAllocatorVTable;
allocator->buffer = buffer;
allocator->offset = 0;
allocator->capacity = capacity;
}
void *MallocAllocator_allocate(Allocator *allocator, size_t size) {
return malloc(size);
}
void MallocAllocator_free(Allocator *allocator, void *object) {
free(object);
}
const AllocatorVTable mallocAllocatorVTable = {
.allocate = MallocAllocator_allocate,
.free = MallocAllocator_free,
};
void MallocAllocator_init(MallocAllocator *allocator) {
allocator->vptr = &mallocAllocatorVTable;
}
#include <assert.h>
#include "allocator_test.h"
#include "allocator.h"
void testAllocator() {
{
BufferedAllocator bufferedAllocator;
char buffer[4];
size_t capacity = sizeof(buffer);
BufferedAllocator_init(&bufferedAllocator, buffer, capacity);
Allocator *allocator = &bufferedAllocator.super;
void *chill = Allocator_allocate(allocator, capacity);
assert(chill == buffer);
void *oops = Allocator_allocate(allocator, 1);
assert(oops == NULL);
}
{
MallocAllocator allocator;
MallocAllocator_init(&allocator);
void *chill = Allocator_allocate(&allocator, 100);
assert(chill != NULL);
void *alsoChill = Allocator_allocate(&allocator, 100);
assert(alsoChill != NULL);
}
}
So you would pass around an Allocator * to whichever piece of code you write that wants to allocate stuff (beyond something like char buf[n] on the stack). You can use a MallocAllocator to just use the system malloc/free, or you could use a BufferedAllocator at the very top of your program. A BufferedAllocator is just an example of a really simple malloc/free. It works well in my use-case because I pretty much know how much memory my program will use in advance, and I don't delete any object until the entire program is done. Using this interface, you could write a more complicated algorithm like one of the ones described in this lecture. There are a lot of different strategies for preventing fragmentation and many trade-offs, so rolling your own malloc/free could be really useful.