Memory Management in C
Introduction
Welcome to Dynamic Memory Management. If learning C was a superhero origin story, this is the moment you discover your true powers—and the catastrophic consequences of using them irresponsibly.
Imagine you are hosting a dinner party. You have a fixed-size dining table (this is the Stack). If you know exactly how many guests are coming, you can set the table perfectly. But what if you don't know how many guests will show up until they arrive? If 100 people show up, your table overflows.
To solve this, you rent chairs from a massive warehouse nearby (this is the Heap). When a guest arrives, you call the warehouse, rent a chair (malloc), and place it in the room. When the guest leaves, you absolutely MUST return the chair to the warehouse (free). If you keep renting chairs and never return them, the warehouse eventually runs out, and you go bankrupt. This is a memory leak.
In C, you have manual control over renting and returning memory. With great power comes great responsibility.
Core Concept: First Principles
When a C program runs, its memory is typically divided into several segments. The two most important for our discussion are:
- The Stack: Used for local variables declared inside functions. It operates automatically (LIFO - Last In, First Out). When a function is called, memory for its variables is allocated. When the function returns, that memory is automatically destroyed. The stack is fast but limited in size.
- The Heap: A large pool of memory used for dynamic allocation. Memory here must be requested manually and released manually. It persists across function calls until you explicitly destroy it.
To interact with the Heap, C provides four core functions in the <stdlib.h> library:
malloc(size_t size): (Memory Allocation) Requests a block of memory ofsizebytes. It returns a pointer to the beginning of the block. The memory is uninitialized (contains garbage data).calloc(size_t num, size_t size): (Contiguous Allocation) Allocates memory for an array ofnumelements, each ofsizebytes. Unlikemalloc, it initializes all bytes to zero.realloc(void *ptr, size_t new_size): (Re-allocation) Resizes a previously allocated block of memory pointed to byptrtonew_size. It may move the memory block to a new location if there isn't enough space to expand it in place.free(void *ptr): Deallocates the memory block pointed to byptr, returning it to the Heap.
Memory Diagram: Stack vs Heap
+----------------------+ High Memory Address
| Command Line Args |
+----------------------+
| Stack | <- Local variables (grows downward)
| | |
| v |
+----------------------+
| ^ |
| | |
| Heap | <- Dynamic memory via malloc (grows upward)
+----------------------+
| Uninitialized Data | <- Global/static variables (BSS)
+----------------------+
| Initialized Data | <- Global/static variables with initial values
+----------------------+
| Text | <- Compiled machine code
+----------------------+ Low Memory Address
Beginner Code Example
Let's use malloc to allocate memory for a single integer on the Heap, use it, and then free it.
#include <stdio.h>
#include <stdlib.h> // Required for memory management functions
int main() {
// 1. Allocate memory on the heap for one integer
// sizeof(int) ensures we get the exact number of bytes needed for an int on this system
int *ptr = (int *)malloc(sizeof(int));
// 2. ALWAYS check if the allocation was successful
// malloc returns NULL if the heap is out of memory
if (ptr == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
// 3. Use the memory
*ptr = 42; // Dereference the pointer to store a value
printf("Value stored in heap: %d\n", *ptr);
// 4. Free the memory! If you forget this, it's a memory leak.
free(ptr);
// 5. Best practice: Set pointer to NULL after freeing so you don't accidentally use it again
ptr = NULL;
return 0;
}
Intermediate Code Example
Dynamic memory is most useful when you don't know the size of data at compile time. Let's ask the user how many numbers they want to store, and use calloc to create a dynamically sized array.
#include <stdio.h>
#include <stdlib.h>
int main() {
int num_elements;
printf("How many integers do you want to store? ");
scanf("%d", &num_elements);
// Allocate memory for 'num_elements' integers
// calloc initializes all memory to 0
int *array = (int *)calloc(num_elements, sizeof(int));
if (array == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
// Populate the array
for (int i = 0; i < num_elements; i++) {
array[i] = i * 10; // Array indexing works normally with pointers
}
// Print the array
printf("Array contents: ");
for (int i = 0; i < num_elements; i++) {
printf("%d ", array[i]);
}
printf("\n");
// Free the dynamically allocated array
free(array);
array = NULL;
return 0;
}
Advanced Code Example
What if our array fills up and we need more space? We can use realloc to resize it dynamically without losing the existing data. This is how dynamic data structures like vectors (in C++) or ArrayLists (in Java) work under the hood!
#include <stdio.h>
#include <stdlib.h>
int main() {
int initial_size = 2;
int *dynamic_arr = (int *)malloc(initial_size * sizeof(int));
if (dynamic_arr == NULL) return 1;
// Fill initial array
dynamic_arr[0] = 10;
dynamic_arr[1] = 20;
printf("Initial array: %d, %d\n", dynamic_arr[0], dynamic_arr[1]);
// We need space for 3 more elements! Let's resize to 5.
int new_size = 5;
// IMPORTANT: Store the result of realloc in a temporary pointer.
// If realloc fails, it returns NULL. If we assigned it directly back to dynamic_arr,
// we would overwrite our only pointer to the original memory, causing a leak!
int *temp = (int *)realloc(dynamic_arr, new_size * sizeof(int));
if (temp == NULL) {
printf("Reallocation failed! Original data is safe.\n");
free(dynamic_arr);
return 1;
}
// Safe to update the main pointer
dynamic_arr = temp;
// Add new data
dynamic_arr[2] = 30;
dynamic_arr[3] = 40;
dynamic_arr[4] = 50;
printf("Resized array: ");
for (int i = 0; i < new_size; i++) {
printf("%d ", dynamic_arr[i]);
}
printf("\n");
// Cleanup
free(dynamic_arr);
return 0;
}
Common Mistakes
1. Memory Leaks (Forgetting to Free)
If you allocate memory and lose the pointer to it before calling free, that memory is "leaked." It remains unusable until the program terminates.
Wrong:
void memory_leak() {
int *ptr = malloc(sizeof(int));
*ptr = 5;
// Function ends, ptr is destroyed, but the heap memory remains allocated forever!
}
Correct:
void no_leak() {
int *ptr = malloc(sizeof(int));
*ptr = 5;
free(ptr); // Returned to the system
}
2. Use After Free (Dangling Pointers)
Accessing memory after it has been freed is undefined behavior and often leads to catastrophic crashes. Wrong:
int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 10; // CRASH! The memory no longer belongs to you.
Correct:
int *ptr = malloc(sizeof(int));
free(ptr);
ptr = NULL; // Set to NULL immediately
// if (ptr != NULL) { ... } // Now we can safely check
3. Double Free
Attempting to free the exact same memory block twice will corrupt the heap manager. Wrong:
int *ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // CRASH! Double free corruption.
4. Returning Pointers to Local Variables
A classic Stack vs Heap mistake. Returning a pointer to a local stack variable is invalid because the stack frame is destroyed when the function returns. Wrong:
int* get_number() {
int local_num = 42;
return &local_num; // BUG! local_num is destroyed here.
}
Correct:
int* get_number() {
int *heap_num = malloc(sizeof(int));
*heap_num = 42;
return heap_num; // Safe. Heap memory persists.
}
Interview Q&A
Q1: What is the difference between malloc and calloc?
A: malloc(size) allocates a single block of memory of the specified size and leaves it uninitialized (containing garbage). calloc(num, size) allocates memory for an array of num elements of size bytes each, and initializes all bytes to zero.
Q2: What is a memory leak, and how can you detect it? A: A memory leak occurs when dynamically allocated memory is not freed before all pointers to it are lost. The memory becomes permanently unavailable for the program's lifespan. They can be detected using profiling tools like Valgrind on Linux.
Q3: Explain what realloc does under the hood.
A: realloc attempts to resize an existing memory block. If there is enough contiguous free space immediately following the block, it expands it in place. If not, it allocates a completely new block of the requested size, copies the old data over to the new block, frees the old block, and returns the pointer to the new block.
Q4: Why should you cast the return value of malloc in C++ but not necessarily in C?
A: malloc returns a void* (a generic pointer). In C, a void* is automatically and safely promoted to any other pointer type (e.g., int* ptr = malloc(...)). In C++, implicit conversion from void* is not allowed, so a cast is strictly required. While some C programmers cast it for clarity, others argue it can hide failure to include <stdlib.h>.
Q5: What is a dangling pointer?
A: A dangling pointer is a pointer that points to a memory location that has been freed or deleted. Accessing it leads to undefined behavior. Setting a pointer to NULL immediately after freeing it is the standard defense against dangling pointers.
Practice Problems
-
Dynamic String: Write a program that asks the user for the length of their name. Use
mallocto allocate achararray of that length + 1 (for the null terminator). Read the name into the array, print it, and free the memory. -
The Leaky Function: Write a small function that allocates an integer array of 1000 elements but does not free it. Call this function inside a loop 1,000,000 times in
main(). Monitor your computer's RAM usage while the program runs. (Make sure to kill the program before it crashes your machine!). This visually demonstrates a memory leak. -
Custom Realloc: Write your own simplified version of
reallocusingmallocandfree. Assume you are only increasing the size. Hint: Your function should take the old pointer, old size, and new size. Allocate a new block, copy data using a loop, free the old block, and return the new block.
Summary
- The Stack manages memory automatically for local variables. It is small and fast.
- The Heap manages memory dynamically. It is large but requires manual management.
mallocallocates uninitialized memory.callocallocates memory and initializes it to zero.reallocresizes an existing block of memory.freereturns memory to the system. Failing to callfreecauses memory leaks.- Always check if allocation functions return
NULL. - Never access memory after it has been freed (dangling pointers).