Dynamic Memory Allocation in C
Static memory allocation in C requires knowing the size of data at compile time. But real-world programs often need to allocate memory based on runtime conditions — user input, file sizes, network data, etc. Dynamic memory allocation solves this by requesting memory from the heap at runtime using four standard library functions: malloc, calloc, realloc, and free.
Table of Contents
- Why Dynamic Memory?
- The Heap vs Stack
- malloc()
- calloc()
- realloc()
- free()
- malloc vs calloc vs realloc
- Common Memory Bugs
- Memory Leak Detection
- Best Practices
- Advanced Patterns
- Interview Questions
1. Why Dynamic Memory?
Static arrays have a fixed size determined at compile time:
int arr[100]; // What if we need 101 elements? What if we only need 10?
Problems with fixed-size arrays:
- Wasted memory: Array too large for actual needs
- Buffer overflow: Array too small for actual data
- Inflexibility: Cannot adapt to runtime requirements
Dynamic allocation solves all three:
int n;
scanf("%d", &n); // size known only at runtime
int *arr = malloc(n * sizeof(int)); // allocate exactly n ints
2. The Heap vs Stack
Understanding where memory comes from is crucial.
Stack Memory
- Automatically managed (allocated on function entry, freed on exit)
- Fast allocation
- Limited size (typically 1-8 MB)
- Stores local variables and function call frames
void func() {
int x = 10; // stack memory
int arr[1000]; // stack memory — but limited!
}
Heap Memory
- Manually managed (you allocate, you free)
- Slower allocation
- Much larger (limited only by system RAM + swap)
- Stores dynamically allocated data
void func() {
int *p = malloc(sizeof(int)); // heap memory
free(p); // must free manually
}
Memory Layout Visualization
High Address
+------------------+
| Command-line |
| arguments |
+------------------+
| Stack | <- Grows downward
| (local vars) |
| | |
| v |
| |
| ^ |
| | |
| Heap | <- Grows upward
| (malloc/free) |
+------------------+
| BSS | <- Uninitialized globals (zeroed)
+------------------+
| Data | <- Initialized globals
+------------------+
| Text | <- Program code
+------------------+
Low Address
3. malloc()
malloc (memory allocate) allocates a block of uninitialized memory.
Syntax
void *malloc(size_t size);
size: number of bytes to allocate- Returns: pointer to allocated memory, or
NULLon failure - Memory contents: uninitialized (garbage values)
Basic Usage
#include <stdlib.h>
int *p = malloc(sizeof(int)); // allocate 1 int
if (p == NULL) {
fprintf(stderr, "Allocation failed
");
exit(EXIT_FAILURE);
}
*p = 42;
printf("%d
", *p); // 42
free(p);
Allocating Arrays
int n = 100;
int *arr = malloc(n * sizeof(int)); // allocate n ints
if (arr == NULL) { /* handle error */ }
for (int i = 0; i < n; i++)
arr[i] = i;
free(arr);
The sizeof Pattern
Always use sizeof rather than hardcoding sizes:
// Good: portable and clear
int *arr = malloc(100 * sizeof(int));
// Better: ties sizeof to the pointer type
int *arr = malloc(100 * sizeof(*arr));
// Bad: assumes int is 4 bytes
int *arr = malloc(100 * 4); // wrong on systems where int is 2 or 8 bytes
Casting malloc’s Return
In C, casting malloc’s return is optional (unlike C++ where it’s required):
int *p = malloc(sizeof(int)); // C style — preferred
int *p = (int*)malloc(sizeof(int)); // also valid, but unnecessary in C
The C standard guarantees that void* can be implicitly converted to any object pointer type.
4. calloc()
calloc (contiguous allocate) allocates memory for an array and initializes all bytes to zero.
Syntax
void *calloc(size_t nmemb, size_t size);
nmemb: number of elementssize: size of each element- Returns: pointer to zero-initialized memory, or
NULLon failure
Basic Usage
int *arr = calloc(100, sizeof(int)); // 100 ints, all set to 0
if (arr == NULL) { /* handle error */ }
printf("%d
", arr[50]); // guaranteed to be 0
free(arr);
malloc vs calloc
| Feature | malloc() | calloc() |
|---|---|---|
| Arguments | 1 (total bytes) | 2 (count, size) |
| Initialization | Garbage | Zero-initialized |
| Speed | Faster | Slightly slower |
| Use case | When you’ll overwrite all values | When you need zero initialization |
When to Use calloc
- Allocating arrays of pointers (automatically NULL-initialized)
- Counting arrays (start from zero)
- Safety-critical code where uninitialized reads would be dangerous
// calloc ensures all pointers start as NULL
char *strings[100] = {0}; // static arrays can be initialized this way
// but for dynamic:
char **strings = calloc(100, sizeof(char*));
// all 100 pointers are NULL — safe to check before use
5. realloc()
realloc resizes a previously allocated block, preserving existing data.
Syntax
void *realloc(void *ptr, size_t size);
ptr: pointer to previously allocated memory (orNULLto act likemalloc)size: new size in bytes- Returns: pointer to resized memory, or
NULLon failure
Basic Usage
int *arr = malloc(5 * sizeof(int));
arr[0] = 1; arr[1] = 2; arr[2] = 3; arr[3] = 4; arr[4] = 5;
// Need more space
int *temp = realloc(arr, 10 * sizeof(int));
if (temp != NULL) {
arr = temp;
// arr[0] through arr[4] still contain 1-5
// arr[5] through arr[9] are uninitialized
}
The realloc Trap
Never assign realloc’s result directly to the original pointer:
// DANGEROUS:
int *arr = malloc(100);
arr = realloc(arr, 200); // If this fails, arr becomes NULL
// and the original 100 bytes are leaked!
Always use a temporary pointer:
// SAFE:
int *arr = malloc(100);
int *temp = realloc(arr, 200);
if (temp != NULL) {
arr = temp;
} else {
// realloc failed, but arr is still valid
// handle error without losing the original memory
}
realloc with NULL
Passing NULL to realloc makes it behave like malloc:
int *arr = realloc(NULL, 100 * sizeof(int)); // equivalent to malloc
realloc with Size 0
Passing size 0 is implementation-defined — it may free the memory and return NULL, or return a unique pointer that can be freed.
int *p = realloc(arr, 0); // Don't rely on this behavior
6. free()
free deallocates memory previously allocated by malloc, calloc, or realloc.
Syntax
void free(void *ptr);
ptr: pointer to allocated memory (orNULL— freeing NULL is a no-op)- Returns: nothing
Basic Usage
int *p = malloc(sizeof(int));
*p = 42;
free(p);
p = NULL; // prevent dangling pointer
What free() Actually Does
free does not:
- Set the pointer to
NULL - Erase the memory contents
- Change the pointer variable itself
free does:
- Mark the memory block as available for reuse
- Return the memory to the heap allocator
After free(p), p still holds the old address — this is a dangling pointer.
7. malloc vs calloc vs realloc
Comparison Table
| Function | Purpose | Args | Initialization | Returns NULL when |
|---|---|---|---|---|
malloc(n) | Allocate memory | total bytes | Garbage | Allocation fails |
calloc(n, size) | Allocate zeroed array | count, element size | Zero | Allocation fails |
realloc(ptr, n) | Resize existing block | old ptr, new size | Old data preserved, new = garbage | Allocation fails |
Decision Flowchart
Need memory?
├── Know exact size?
│ ├── Need zero initialization? -> calloc
│ └── Will overwrite immediately? -> malloc
└── Have existing block to resize? -> realloc
8. Common Memory Bugs
Memory Leak
Memory that is allocated but never freed, and whose address is lost.
void leaky_function() {
int *p = malloc(sizeof(int));
*p = 10;
// p goes out of scope — memory is lost forever!
}
Detection: Use Valgrind, AddressSanitizer, or static analysis tools.
Fix: Ensure every malloc/calloc has exactly one free.
Dangling Pointer
Using memory after it has been freed.
int *p = malloc(sizeof(int));
free(p);
*p = 10; // Undefined behavior — p is dangling
Fix: Set pointer to NULL after free.
Double Free
Freeing the same memory twice.
int *p = malloc(sizeof(int));
free(p);
free(p); // Undefined behavior — heap corruption
Fix: Set pointer to NULL after free. Freeing NULL is safe (no-op).
Use After Free
Reading or writing freed memory.
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d
", *p); // May print 42, may crash, may print garbage
This is a security vulnerability — attackers can exploit use-after-free bugs.
Buffer Overflow (Heap)
Writing past the end of allocated memory.
int *arr = malloc(5 * sizeof(int));
arr[5] = 10; // UB — writes into unallocated memory
NULL Pointer Dereference
int *p = malloc(1000000000000 * sizeof(int)); // likely fails
*p = 10; // If p is NULL, this segfaults
Fix: Always check return value.
9. Memory Leak Detection
Valgrind (Linux)
valgrind --leak-check=full ./program
AddressSanitizer (GCC/Clang)
gcc -fsanitize=address -g program.c
./a.out
Static Analysis
# Clang Static Analyzer
scan-build gcc program.c
# cppcheck
cppcheck --enable=all program.c
10. Best Practices
1. Always Check for NULL
int *p = malloc(sizeof(int));
if (p == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
2. Free in the Same Scope When Possible
int *create_array(int n) {
int *arr = malloc(n * sizeof(int));
if (arr == NULL) return NULL;
// ... populate array ...
return arr; // caller is now responsible for freeing
}
// Caller:
int *arr = create_array(100);
// ... use arr ...
free(arr); // free in the same "logical scope"
3. Set Pointer to NULL After free
free(p);
p = NULL; // prevents accidental use-after-free
4. Use realloc Safely
void *temp = realloc(ptr, new_size);
if (temp != NULL) ptr = temp;
// else: ptr is still valid, handle error
5. Avoid Magic Numbers
// Bad
int *arr = malloc(100 * 4);
// Good
int *arr = malloc(100 * sizeof(int));
// Better
int *arr = malloc(100 * sizeof(*arr));
6. Document Ownership
// Function returns dynamically allocated string
// Caller must free the returned pointer
char *read_file(const char *filename);
11. Advanced Patterns
Growing Arrays (Dynamic Arrays)
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
void da_init(DynamicArray *da) {
da->data = NULL;
da->size = 0;
da->capacity = 0;
}
void da_append(DynamicArray *da, int value) {
if (da->size >= da->capacity) {
size_t new_cap = da->capacity == 0 ? 4 : da->capacity * 2;
int *temp = realloc(da->data, new_cap * sizeof(int));
if (temp == NULL) return; // handle error
da->data = temp;
da->capacity = new_cap;
}
da->data[da->size++] = value;
}
void da_free(DynamicArray *da) {
free(da->data);
da->data = NULL;
da->size = da->capacity = 0;
}
2D Array (Jagged)
int **create_matrix(int rows, int cols) {
int **mat = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
mat[i] = malloc(cols * sizeof(int));
return mat;
}
void free_matrix(int **mat, int rows) {
for (int i = 0; i < rows; i++)
free(mat[i]);
free(mat);
}
2D Array (Contiguous)
int *create_contiguous_matrix(int rows, int cols) {
return malloc(rows * cols * sizeof(int));
}
// Access: mat[i * cols + j]
12. Interview Questions
Q1: What is the difference between malloc() and calloc()?
malloc takes one argument (total bytes) and returns uninitialized memory. calloc takes two arguments (count, size), allocates memory for an array, and initializes all bytes to zero.
Q2: Why check realloc’s return with a temporary pointer?
If realloc fails, it returns NULL but does not free the original block. Assigning directly to the original pointer would lose the only reference to valid memory, causing a leak.
Q3: What happens if you free a NULL pointer?
Nothing. free(NULL) is defined as a no-op.
Q4: Is it mandatory to cast malloc’s return in C?
No. void* is implicitly convertible to any object pointer type in C. Casting is required in C++ but optional in C.
Q5: What does realloc(ptr, 0) do?
Implementation-defined. It may free the memory and return NULL, or return a unique non-NULL pointer. Portable code should not rely on this behavior.
Q6: What is a memory leak?
Allocated memory that is never freed and whose address is lost, making it impossible to reclaim. The memory remains reserved until the program exits.
Q7: What is the output of malloc(0)?
Implementation-defined. It may return NULL or a unique pointer that must be freed. Never dereference the result.
Key Takeaways
malloc(size)allocates uninitialized memory; always check forNULL.calloc(n, size)allocates and zero-initializes memory.realloc(ptr, size)resizes a block — always use a temporary pointer.free(ptr)releases memory — setptr = NULLimmediately after.- Every allocation must eventually be matched with exactly one
free. - Double
freeand use-after-free are undefined behavior and security risks. - Memory leaks occur when allocated memory is never freed.
- Use tools like Valgrind and AddressSanitizer to detect memory issues.
Related Topics: Pointers, Arrays, Structures, Memory Layout, Common Pitfalls