Memory Management in C
malloc, calloc, realloc, free, structs, function pointers, and avoiding a real use-after-free bug.
Dynamic memory: malloc, calloc, realloc, free
C has no garbage collector — every byte you request from the heap with malloc/calloc/realloc must eventually be released with a matching free, or it leaks for the lifetime of the process.
#include <stdlib.h>
#include <stdio.h>
int main(void) {
// malloc: allocates raw, UNINITIALIZED memory for 5 ints
int *nums = malloc(5 * sizeof(int));
if (nums == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
for (int i = 0; i < 5; i++) {
nums[i] = i * 10;
}
printf("%d\n", nums[2]); // 20
free(nums); // release the memory back to the system
nums = NULL; // good practice: avoid leaving a dangling pointer around
return 0;
}
Always check the return value of malloc — it returns NULL if the allocation fails (out of memory), and dereferencing a NULL pointer is undefined behavior.
int *zeros = calloc(5, sizeof(int)); // allocates AND zero-initializes 5 ints
// zeros[0..4] are all guaranteed to be 0, unlike malloc's uninitialized memory
int *bigger = realloc(zeros, 10 * sizeof(int)); // grows (or shrinks) an existing allocation
if (bigger == NULL) {
// realloc failed — the ORIGINAL 'zeros' block is still valid and must still be freed
free(zeros);
return 1;
}
// bigger may be the same pointer as zeros, or a brand-new address — always use the return value
free(bigger);
| Function | Purpose | Initializes memory? |
|---|---|---|
malloc(size) |
Allocate size bytes |
No — contents are garbage |
calloc(count, size) |
Allocate count * size bytes |
Yes — zero-filled |
realloc(ptr, newSize) |
Resize an existing allocation | Existing bytes preserved; new bytes uninitialized |
free(ptr) |
Release an allocation back to the system | N/A |
Structs
A struct groups related fields together under one type — C's basic tool for modeling a record or object's data (with no built-in methods; behavior is just ordinary functions that take the struct as a parameter).
typedef struct {
char name[50];
int age;
double salary;
} Employee;
int main(void) {
Employee e = {"Ada", 30, 85000.0};
printf("%s is %d\n", e.name, e.age); // Ada is 30
Employee *ePtr = &e;
printf("%s\n", ePtr->name); // "->" dereferences a struct pointer and accesses a field
printf("%s\n", (*ePtr).name); // equivalent, but -> is idiomatic and far more common
return 0;
}
Structs are frequently allocated on the heap when their lifetime needs to outlive the function that creates them:
Employee *createEmployee(const char *name, int age) {
Employee *e = malloc(sizeof(Employee));
if (e == NULL) return NULL;
strncpy(e->name, name, sizeof(e->name) - 1);
e->name[sizeof(e->name) - 1] = '\0'; // ensure null termination even if name was truncated
e->age = age;
e->salary = 0.0;
return e;
}
int main(void) {
Employee *e = createEmployee("Grace", 45);
if (e != NULL) {
printf("%s\n", e->name); // Grace
free(e); // the caller is responsible for freeing what createEmployee allocated
}
return 0;
}
Function pointers
A function pointer stores the address of a function, letting you pass behavior around as data — the foundation of callbacks in C (and how C simulates what other languages do with lambdas or first-class functions).
#include <stdio.h>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int apply(int (*operation)(int, int), int x, int y) { // takes a function pointer as a parameter
return operation(x, y);
}
int main(void) {
int (*opPtr)(int, int) = add; // opPtr points to the add function
printf("%d\n", opPtr(3, 4)); // 7
printf("%d\n", apply(add, 3, 4)); // 7
printf("%d\n", apply(multiply, 3, 4)); // 12
return 0;
}
The standard library's qsort is a classic real-world use of function pointers — you pass it a comparison function, and it sorts any array using that comparison logic.
A realistic bug: use-after-free
Freeing memory doesn't erase the pointer's value — it just tells the allocator that memory is available for reuse. Using a pointer after freeing it ("use-after-free") is undefined behavior: it might still appear to work, might crash, or might silently read/write memory that's since been reused by something else entirely.
#include <stdlib.h>
#include <stdio.h>
int *createValue(int v) {
int *p = malloc(sizeof(int));
*p = v;
return p;
}
int main(void) {
int *p = createValue(42);
printf("%d\n", *p); // 42 — fine so far
free(p);
// ... p still holds the OLD address, but that memory is no longer ours ...
printf("%d\n", *p); // BUG: use-after-free — undefined behavior, may print garbage or crash
return 0;
}
How to avoid it:
- Set a pointer to
NULLimmediately afterfreeing it — dereferencingNULLcrashes predictably and loudly, instead of silently corrupting memory. - Never return or store a pointer to memory you've already freed, and be especially careful with pointers copied to more than one variable (freeing through one leaves the others dangling).
- Use a tool like Valgrind or AddressSanitizer (
gcc -fsanitize=address) during development — they catch use-after-free and leaks that are otherwise very easy to miss in manual testing.
free(p);
p = NULL; // now dereferencing p crashes immediately and obviously, instead of silently corrupting data
if (p != NULL) { // defensive checks like this become meaningful once you adopt this habit
printf("%d\n", *p);
}
Common mistakes
- Forgetting to
freean allocation on every exit path (including earlyreturns and error branches) — a memory leak. - Using a pointer after
freeing it (use-after-free) instead of nulling it out immediately. - Passing the wrong size to
malloc— a classic mistake ismalloc(sizeof(ptr))instead ofmalloc(sizeof(*ptr))when allocating for a struct through a pointer variable. - Forgetting that
realloccan return a different address than its input, and continuing to use the old pointer after a successfulrealloc.
Interview questions
Q: What's the difference between malloc and calloc?
Both allocate heap memory, but malloc(size) leaves the memory uninitialized (containing whatever garbage was previously there), while calloc(count, size) additionally zero-initializes every byte of the allocation. calloc is the safer default when you need the initial contents to be predictable (e.g., a fresh array of counters).
Q: What is a use-after-free bug, and how do you defend against it?
It happens when a pointer is dereferenced after the memory it points to has already been freed — the memory may have been reused by something else entirely, so the read/write has undefined, unpredictable effects. The standard defense is disciplined ownership (know exactly who is responsible for freeing each allocation), setting a pointer to NULL immediately after freeing it, and running tools like AddressSanitizer or Valgrind during development to catch it before it ships.