I had previously only ever taken the "call the language's default dynamic allocator whenever I want" approach to memory management, but I recently decided to try rewriting one of my existing programs in C, and figured it would be a good time to try out a more controlled approach to memory management at the same time. It would be helpful to get some outside thoughts on what I've considered so far.
The program is a computer algebra system. If you dig down far enough, almost everything in the program is done in terms of an Integer type, which contains an array of uint32_t's, which is dynamically-sized so that it can represent however large a value turns out to be necessary in order to complete the calculation. I usually want Integers to behave as if they were on the stack - in C++, I could make Integer a class, where the destructor cleans up the array on the heap, and call it good. My understanding is that the functional equivalent of this in C would be to allocate the array using malloc when the Integer is created, and call free on it when I'm done with it, but at least in the common case, I think I can do better.
What I have in mind is to allocate a big block of memory at startup, like Handmade Hero does. Every time someone needs a new Integer, they would put the dynamic array at the location of the memory pointer, then increment the pointer by the size of the array. Functions that need to create Integers for use in internal calculations but don't return anything that contains an Integer would take a pointer to the memory - that is, a local copy of the caller's memory pointer, which can be incremented in the function body without affecting the caller's copy. That way, any Integers the caller allocates after calling the function will overwrite the ones allocated inside the function.
On the other hand, functions that do return something that contains one or more Integers would take a pointer to the memory pointer, so that they could increment the caller's pointer to the end of the last array that the return value references. Often, generating the return value requires allocating additional Integers that are not themselves part of the return value. Ideally, the caller's memory pointer would not be incremented in such a way that the heap memory for these temporary values falls before the pointer, preventing the caller from overwriting it. If an upper bound on the size of the return value's heap data is known a priori, this could be done by leaving an empty block of that size at the initial location of the memory pointer before allocating the local values. If no such bound is known, all Integer heap data could be allocated in the order that it's needed for the calculation, and then the parts that are needed for the return value could be copied to the initial location of the memory pointer.
One uncertainty I have about this is how big a block of memory to allocate initially. In a video game like Handmade Hero, it makes sense for the creator to decide what they want the game to be like, then choose an amount of memory that will suffice to deliver that experience. The idea of choosing an amount of memory that will suffice to deliver the kinds of calculations I want the user to be able to do doesn't seem as sensible. I'd rather the user be able to do as intensive a calculation as is possible on their hardware. The simplest approach would seem to be allocating the largest block the OS will allow. That seems rude to say the least. Alternatively, I could do something like a C++ Vector, where if the currently-allocated block gets filled, I could copy the data to a new, larger one. If I did that, I suppose I'd want to allocate out of the block using offsets from the start rather than using pointers so all the pointers to parts of the old block wouldn't have to be updated.
Another thing I'm not sure about is a case that doesn't seem as well-behaved as the Integer one. There is a Number type, which is capable of representing various irrational numbers. This type includes a rational_approximation field, which contains two Integer ratios - a lower bound and an upper bound. Whenever someone happens to need a rational approximation, they access it through a function that takes a parameter that specifies an upper bound on what the distance between the upper and lower bound of rational_approximation needs to be. If the existing value of rational_approximation doesn't satisfy the bound, then the function replaces it with a new one that does before returning it. The problem is that, unlike Integers themselves, whose heap memory will always stay the same size once they have been created, the heap memory of a rational approximation might need to grow, if the Integers in its ratios need to be replaced with ones with larger arrays. Given that there is no upper bound on the amount of heap memory a given Number's rational approximation might need, I don't see a reasonable way to reconcile this with the stacklike approach I described for Integers. I could allocate a second large memory block for such cases, with a more complicated custom allocator that does searches for holes that can be filled and such, but at that point, I don't know if there would be any advantage to writing the allocator myself over just using malloc.