Hello! Welcome to the third lesson in our C programming module.
In our last session, we dove into pointers, learning how they store memory addresses and how pointer arithmetic allows us to navigate through data like arrays. We saw that an array's name is essentially a pointer to its first element.
Today, we're taking a major step forward by learning how to request and manage memory while the program is running. This is called dynamic memory allocation, and it is one of the most powerful and critical concepts for game engine development. When you're building a game, you don't know ahead of time how many enemies will spawn, how many bullets will be fired, or how large a level the player will load. Dynamic memory allocation gives you the flexibility to handle these unpredictable situations.
Our goal for this lesson is to learn how to allocate and deallocate memory from a special memory region called the heap using the C standard library functions malloc and free.
Why Do We Need Dynamic Memory? The Stack vs. The Heap
So far, all the variables we've created, like int x; or float arr[10];, have been stored on the stack. Stack memory is fast and automatically managed. When a function is called, its local variables are "pushed" onto the stack; when the function returns, they are automatically "popped" off and destroyed.
This automatic management has a major limitation: stack-allocated data only exists for the duration of the function call. This leads to a critical rule in C: never return a pointer to a local variable.
The Stack, The Heap, and Dynamic Memory Allocation
This article from Cornell's CS department clearly explains the limitations of the stack and why returning a pointer to a local variable leads to a serious bug. This is the fundamental motivation for using the heap.
Read the section titled 'Limitations of the Stack'. Pay close attention to the buggy create_exp function and the explanation of why the pointer it returns becomes invalid after the function finishes.
The solution to this problem is the heap. The heap is a large pool of memory that is available for your program to use. Unlike the stack, you, the programmer, are in full control of the heap. You can request a block of memory, use it for as long as you need (even across multiple function calls), and then you must explicitly return it to the system when you're done.

To get a better sense of this distinction, let's watch a short video segment.
Dynamic Memory Allocation | C Programming Tutorial
This video provides a great visual overview of how memory is organized in a C program and the conceptual difference between the stack and the heap.
Watch from the beginning to 04:23. The video will introduce dynamic memory and contrast the automatically managed, LIFO (Last-In, First-Out) nature of the stack with the manually managed heap.
The Core Functions: malloc and free
To work with the heap, you'll primarily use two functions, which are declared in the <stdlib.h> header file.
malloc(memory allocation): Requests a block of memory of a specific size.free: Releases a block of memory back to the system.
Allocating Memory with malloc
The malloc function takes one argument: the number of bytes you want to allocate. It returns a "generic" pointer of type void* to the first byte of that new memory block.
void* malloc(size_t size);
Since it returns a void*, you must cast it to the specific pointer type you need. To calculate the correct number of bytes, you should always use the sizeof operator. This makes your code portable and readable.
For example, to allocate memory for an array of 50 integers:
#include <stdlib.h> // Needed for malloc and free
// Calculate bytes needed: 50 integers * size of one integer
int *numbers = (int*)malloc(50 * sizeof(int));
Crucial Point: malloc is not guaranteed to succeed. If the system is out of memory, malloc will return NULL. You must always check for this to prevent your program from crashing when it tries to use a NULL pointer.
if (numbers == NULL) {
// Allocation failed! Handle the error, maybe exit the program.
fprintf(stderr, "Failed to allocate memory!\n");
return 1; // Exit with an error code
}
Releasing Memory with free
Manual memory management is a double-edged sword. The control it gives you is key to C's performance, but it comes with responsibility. For every call to malloc, you must have a corresponding call to free when you are finished with the memory.
void free(void *ptr);
You simply pass the pointer you got from malloc to the free function.
free(numbers);
Forgetting to call free creates a memory leak. The memory remains allocated but your program no longer has a pointer to it, so it can't be used or freed. A slow memory leak can eventually consume all available system memory and crash your program or even the entire OS.
This video covers the mechanics of malloc, free, and demonstrates the dramatic effect of a memory leak. The apartment rental analogy used in the second video is also an excellent way to think about this process.
Dynamic Memory Allocation | C Programming Tutorial
First, watch this detailed explanation of malloc and free, including a powerful demonstration of a memory leak.
Watch from 04:23 to 13:50. This covers using malloc, the critical importance of free, what memory leaks are, and a live demo of a program crashing from one.
Next, watch this shorter video for a very clear analogy and some important best practices.
Watch from 02:31 to 05:19. Focus on the 'renting an apartment' analogy for malloc/free. Also, note the important tips on checking for NULL after malloc and setting a pointer to NULL after freeing it to avoid a 'dangling pointer'.
The Laws of the Heap
Because manual memory management can be tricky, it's helpful to remember a few strict rules, which we'll call the "Heap Laws." Violating them leads to undefined behavior, which can cause crashes, subtle bugs, or even security vulnerabilities.
The Stack, The Heap, and Dynamic Memory Allocation
This section of the Cornell article provides a clear, concise list of the rules you must follow when working with heap memory.
Read the section titled 'The Heap Laws'. Internalize the four main rules: avoid use-after-free, double-free, memory leaks, and out-of-bounds access. The real-world examples mentioned highlight the seriousness of these issues.
Dynamic Memory in a Game Engine
Let's see how this applies directly to your goal of building a game engine. A common component in a game is a "state manager," which handles switching between the main menu, the game world, the pause screen, etc. You might not know how many states you need, so a dynamic array is a perfect fit.
How to write a game engine in pure C: Part 1 – State Manager
This blog post about writing a game engine in C demonstrates a practical use case for dynamic memory. It builds a StateManager that uses malloc and realloc to manage a growing list of game states.
Read the sections describing the STATEMANAGER_init, STATEMANAGER_free, and scale functions. Notice how malloc is used to create the initial array, free is used to clean it up, and realloc (a function to resize an allocation) is used to make the array bigger as needed. This is a very common and efficient pattern.
This example introduces two other useful functions:
calloc: Works likemallocbut initializes the allocated memory to all zeros. Its signature iscalloc(num_items, item_size).realloc: Resizes a previously allocated block of memory. This is very useful for dynamic arrays that need to grow or shrink.
Practice Exercise and a Debugging Superpower
Task:
Write a program that demonstrates the full lifecycle of dynamic memory.
- Include
<stdio.h>and<stdlib.h>. - Write a function
int* create_integer_array(int size)that:- Takes an integer
sizeas an argument. - Uses
mallocto allocate enough memory for an array ofsizeintegers. - Checks if
mallocreturnedNULL. If it did, print an error and returnNULL. - If allocation was successful, use a
forloop to fill the array with values (e.g., fill indexiwith the valuei * i). - Returns the pointer to the newly created array.
- Takes an integer
- In your
mainfunction:- Prompt the user to enter a number for the array size.
- Call
create_integer_arraywith the user's number. - Check if the returned pointer is not
NULL. - If it's valid, loop through the array and print out its contents.
- Finally, and most importantly, call
freeon the pointer to release the memory. - Set the pointer to
NULLafter freeing it.
A Debugging Superpower: AddressSanitizer
Memory bugs are notoriously hard to find. Luckily, modern compilers come with tools to help. GCC and Clang have a tool called the AddressSanitizer that can automatically detect many memory errors at runtime.
To use it, simply add the -fsanitize=address flag when you compile your code.
gcc -g -fsanitize=address your_code.c -o your_program
If you run your program and it attempts something illegal (like reading out of bounds or using memory after it's been freed), the sanitizer will stop the program and print a detailed report of what went wrong and where. This is an invaluable tool for any C programmer.
Conclusion
You've just learned one of the most fundamental skills for system-level programming in C. While it requires more discipline than the automatic memory management you're used to from Python or C#, the control it provides is essential for building high-performance applications like game engines.
Key Takeaways:
- Variables created in functions live on the stack and are destroyed automatically when the function returns.
- The heap is a region of memory for data that needs to live longer than a single function call.
- Use
malloc(size_in_bytes)to request memory from the heap and always check if it returnedNULL. - Use
free(pointer)to release memory back to the system. Everymallocneeds afree. - Forgetting to
freememory causes a memory leak. - Using memory after freeing it, or freeing it twice, causes undefined behavior.
In our next lesson, we will learn how to define structs to create our own custom data types. We will then be able to combine structs with dynamic memory allocation to do things like GameObject *player = (GameObject*)malloc(sizeof(GameObject));, bringing us one step closer to creating and managing objects in our game world.