Dynamic Memory Allocation
Motivation
Suppose we wanted to write a function that takes an integer, creates an array of the size specified by the integer, initializes each field, and returns the array back to the caller. Given the tools we have thus far, our code might look like this:
// Broken code! Do not do this!
int *initArray(int howLarge) {
int myArray[howLarge];
for (int i = 0; i < howLarge; i++) {
myArray[i] = i;
}
return myArray;
}
The reason this code will not work is that the array is created on the stack. Variables on the stack exist only until the function ends, at which point the stack frame is popped. You can’t use the memory for that stack frame anymore, and it will get reused for some other data.
Dynamic memory allocation lets you obtain memory on the heap instead of the stack. Unlike stack frames, the heap is forever: it remains even when the function returns. Instead, you have to remember to explicitly free the memory when you are done using it.
Both the stack and the heap can grow and shrink over time, as the program creates and destroys stack frames and heap-allocated memory regions. Typically, systems lay out the stack at higher addresses in memory and the heap at lower addresses in memory; as they grow, the stack grows “down” and the heap grows “up.” Here’s a diagram that depicts this growth in the address space:
The diagram also includes static data (globals and constants) and code, which are other memory regions distinct from the heap and stack.
malloc
To use dynamic memory allocation functions, #include <stdlib.h>
.
Check out the reference for the stdlib.h
header.
To allocate memory on the heap, use the malloc
function.
Here’s its declaration:
void* malloc(size_t size);
The return type of malloc
is void*
, which looks a little weird, but it means “a pointer to some type but I’m not sure which.”
The only argument is a size: the number of bytes you want to allocate.
(size_t
is an unsigned integer type.)
How do you know how many bytes you need?
The best way is to use C’s sizeof
operator.
Use sizeof(int)
, for example, to get the number of bytes that an int
occupies.
For example, here’s how to allocate space for an int
on the heap:
int* intPtr = malloc(sizeof(int));
If you want to get fancy, you can even avoid repeating the int
type by using sizeof
’s ability to get the type of a variable for you:
int* intPtr = malloc(sizeof(*intPtr));
And here’s how to allocate space for an array of 500 float
s:
float* floatArray = malloc(500 * sizeof(*floatArray));
(Please use sizeof
instead of guessing the sizes of things, even if you think you know that an int
occupies 4 bytes. Because types can be different sizes on different platforms, using sizeof
will make your code portable.)
free
Unlike stack variables, you are responsible for freeing memory that you malloc
!
You do that with the free
function.
free
just takes one argument: the pointer to some memory previously allocated with malloc
.
Remember this rule: every time you call malloc
, remember to put a free
somewhere to balance it out.
initArray
Revisited
Here’s a fixed version of the code above:
int *initArray(int howLarge) {
int *array = malloc(howLarge * sizeof(*array));
if (array != NULL) {
for (int i = 0; i < howLarge; i++) {
array[i] = i;
}
}
return array;
}
Of course, the caller of initArray
will need to call free
when it is finished with the memory.
Notice how the above code checks whether malloc
returns NULL
. It is possible that the heap could run out of space and that there is not enough memory to fulfill the current request. In such cases, malloc
will return NULL
instead of a valid pointer to a location on the heap. It is a good idea to check the value returned by malloc
and make sure that it is not NULL
before trying to use the pointer. If the value is NULL
, the program should gracefully abort with an error message explaining that a call to malloc
failed (or if it can recover from the situation and continue—that is even better).
realloc
The realloc
function can reallocate a block of memory at a different size.
In general, realloc
might
allocate a new (larger or smaller) block of memory, copy the contents of the original to the new one, and free the old one.
(But it might do something faster if it can avoid it, e.g., if there is room to expand the allocated region “in place.”)