Data Structures for MCA
Dynamic Memory Management
Memory: Stack vs. Heap
When your program runs, the operating system gives it a block of memory to work with. This memory is organized into a few key areas, but for now, we'll focus on two: the stack and the heap. You've already been using the stack, perhaps without realizing it. Every time you declare a variable like int score = 100; inside a function, that variable is placed on the stack.
The stack is fast and managed automatically. When a function is called, its variables are "pushed" onto the stack. When the function finishes, they are automatically "popped" off. This is efficient, but also rigid. The size of everything on the stack must be known when you compile the program. You can't decide to create a much larger array at runtime just because a user entered a big number.
The heap is the other major region. It's a large pool of memory available for your program to use as it sees fit. Unlike the stack, the heap is not managed automatically. You must explicitly request a chunk of memory, use it, and then explicitly release it back to the system when you're done. This is called dynamic memory allocation because you can 'dynamically' decide how much memory you need while the program is running.
Allocating Memory
To request memory from the heap, you use a set of functions defined in the <stdlib.h> library. The most common one is malloc, which stands for memory allocation.
#include <stdlib.h>
// Request space for 10 integers from the heap
int *my_array;
my_array = (int *)malloc(10 * sizeof(int));
// Always check if malloc was successful
if (my_array == NULL) {
// Allocation failed, handle the error
return 1;
}
// ... use the allocated memory ...
// Release the memory when done
free(my_array);
Let's break that down. sizeof(int) calculates how many bytes one integer takes up. We multiply by 10 to get the total size we need. malloc then finds a contiguous block of that size on the heap and returns a void pointer to the first byte. Since we want to treat this memory as an array of integers, we cast the pointer to int *.
Crucially, malloc can fail if the system is out of memory. In that case, it returns NULL. You must always check for this NULL return value. Forgetting to do so and then trying to use the pointer leads to a crash.
The pointer
my_arraylives on the stack, but the large block of memory it points to lives on the heap.
Two other allocation functions are calloc and realloc.
calloc (contiguous allocation) is similar to malloc, but it has two key differences:
- It takes two arguments: the number of elements and the size of each element.
- It initializes the allocated memory to zero.
mallocleaves the memory uninitialized, containing whatever garbage data was there before.
realloc is used to change the size of a previously allocated memory block. You can use it to make a block larger or smaller.
// Allocate space for 5 floats, initialized to 0.0
float *data = (float *)calloc(5, sizeof(float));
// ... later, we need more space ...
// Resize the block to hold 10 floats instead of 5
float *new_data = (float *)realloc(data, 10 * sizeof(float));
if (new_data != NULL) {
data = new_data; // Point to the new, larger block
} else {
// realloc failed, original block is still valid
free(data);
}
The Dangers of Manual Management
Dynamic allocation is powerful. It allows us to create flexible data structures like linked lists and trees that can grow and shrink as needed. But this power comes with responsibility. Because you are managing memory manually, you are also responsible for the errors that can occur.
The two most common errors are memory leaks and dangling pointers.
Memory Leak
noun
Occurs when a program allocates memory on the heap but fails to free it when it's no longer needed. The program loses the pointer to that memory, making it impossible to ever release. Over time, these leaks can consume all available memory and crash the system.
A leak happens if you allocate memory and then lose the only pointer to it, often by reassigning the pointer or letting it go out of scope.
A dangling pointer is the opposite problem. It's a pointer that continues to exist even after the memory it points to has been deallocated with free. If you try to access the memory through a dangling pointer, your program's behavior is undefined. It might crash, or it might silently corrupt other data by overwriting the memory that has since been reallocated for another purpose. This can lead to incredibly hard-to-find bugs.
A good habit is to set a pointer to
NULLimmediately after freeing it. This prevents it from becoming a dangling pointer.free(ptr); ptr = NULL;
Managing memory correctly is critical for building robust and efficient software. While modern languages often handle this for you with systems, understanding the manual process in C gives you a deeper appreciation for what's happening under the hood. It's an essential skill for performance-critical applications, operating systems, and embedded systems, where every byte and every CPU cycle counts.
Now that you understand the fundamentals of heap allocation, let's test your knowledge.
What is the primary difference between how memory is managed on the stack versus the heap?
After executing the following line of code, what is the most critical immediate next step?
int *arr = (int *)malloc(10 * sizeof(int));
