Memory leaks are among the most insidious bugs in C programming. They don't crash your program immediately—instead, they silently consume resources until your application grinds to a halt or the system runs out of memory. Detecting and fixing memory leaks requires discipline, tools, and a deep understanding of how dynamic memory works in C. This guide covers everything from manual techniques to advanced automated tools for memory leak detection.
What Is a Memory Leak?
A memory leak occurs when a program allocates memory but never frees it, losing all references to that memory. The memory remains allocated until the program terminates, wasting system resources.
void leak_example() {
int *ptr = malloc(sizeof(int) * 100);
// Do something with ptr...
// Forgot to free(ptr) - memory leak!
} // ptr goes out of scope, memory lost forever
Basic Causes of Memory Leaks
1. Forgetting to Free
#include <stdlib.h>
void simple_leak() {
int *data = malloc(1000 * sizeof(int));
// Use data...
// Missing free(data)
} // 4000 bytes leaked (on 32-bit int)
2. Losing the Pointer
void lost_pointer_leak() {
int *ptr = malloc(sizeof(int));
ptr = malloc(sizeof(int)); // First allocation lost!
free(ptr); // Only frees the second allocation
}
3. Not Freeing All Members of Structures
typedef struct {
int *data;
int size;
} Array;
Array* create_array(int size) {
Array *arr = malloc(sizeof(Array));
arr->data = malloc(size * sizeof(int));
arr->size = size;
return arr;
}
void bad_free(Array *arr) {
free(arr); // Frees the structure but not arr->data!
}
void good_free(Array *arr) {
free(arr->data); // Free internal data first
free(arr); // Then free the structure
}
4. Leaks in Error Paths
int risky_function() {
int *data = malloc(1000 * sizeof(int));
FILE *file = fopen("data.txt", "r");
if (!file) {
// Error: forgot to free data before returning
return -1; // Leak!
}
// ... use file and data ...
fclose(file);
free(data);
return 0;
}
Manual Detection Techniques
1. Code Review Checklist
// Look for these patterns: malloc() / calloc() - needs matching free() strdup() - needs free() fopen() - needs fclose() *_alloc functions - need corresponding *_free functions // Every allocation should have a clear ownership and cleanup path
2. Tracking Allocations Manually
#include <stdio.h>
#include <stdlib.h>
#define MAX_ALLOCS 10000
typedef struct {
void *ptr;
size_t size;
const char *file;
int line;
int freed;
} AllocationRecord;
AllocationRecord records[MAX_ALLOCS];
int alloc_count = 0;
void* tracked_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr && alloc_count < MAX_ALLOCS) {
records[alloc_count].ptr = ptr;
records[alloc_count].size = size;
records[alloc_count].file = file;
records[alloc_count].line = line;
records[alloc_count].freed = 0;
alloc_count++;
}
return ptr;
}
void tracked_free(void *ptr, const char *file, int line) {
for (int i = 0; i < alloc_count; i++) {
if (records[i].ptr == ptr && !records[i].freed) {
records[i].freed = 1;
free(ptr);
return;
}
}
fprintf(stderr, "Warning: Attempt to free unknown pointer at %s:%d\n",
file, line);
}
void print_leaks() {
int leaks = 0;
size_t total_leaked = 0;
printf("\n=== Memory Leak Report ===\n");
for (int i = 0; i < alloc_count; i++) {
if (!records[i].freed) {
printf("Leak: %zu bytes at %p (allocated at %s:%d)\n",
records[i].size, records[i].ptr,
records[i].file, records[i].line);
leaks++;
total_leaked += records[i].size;
}
}
if (leaks == 0) {
printf("No memory leaks detected!\n");
} else {
printf("Total: %d leaks, %zu bytes\n", leaks, total_leaked);
}
}
#define MALLOC(size) tracked_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) tracked_free(ptr, __FILE__, __LINE__)
int main() {
int *p1 = MALLOC(100 * sizeof(int));
int *p2 = MALLOC(200 * sizeof(int));
int *p3 = MALLOC(300 * sizeof(int));
FREE(p1);
FREE(p3);
// Forgot to free p2 - will show as leak
print_leaks();
return 0;
}
3. Reference Counting
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int ref_count;
int data;
} RefCountedObject;
RefCountedObject* create_object(int value) {
RefCountedObject *obj = malloc(sizeof(RefCountedObject));
obj->ref_count = 1;
obj->data = value;
return obj;
}
void retain_object(RefCountedObject *obj) {
if (obj) {
obj->ref_count++;
}
}
void release_object(RefCountedObject *obj) {
if (obj) {
obj->ref_count--;
if (obj->ref_count == 0) {
printf("Freeing object with data %d\n", obj->data);
free(obj);
}
}
}
typedef struct {
RefCountedObject *obj;
// other fields
} Container;
int main() {
RefCountedObject *obj = create_object(42);
Container *c1 = malloc(sizeof(Container));
c1->obj = obj;
retain_object(obj); // c1 holds reference
Container *c2 = malloc(sizeof(Container));
c2->obj = obj;
retain_object(obj); // c2 holds reference
release_object(obj); // obj still has 2 references
release_object(c1->obj); // 1 reference left
release_object(c2->obj); // 0 references - freed
free(c1);
free(c2);
return 0;
}
Using Valgrind
Valgrind is the most powerful tool for memory leak detection on Linux.
Basic Usage:
# Compile with debug symbols gcc -g -o program program.c # Run under Valgrind valgrind --leak-check=full ./program # More detailed output valgrind --leak-check=full --show-leak-kinds=all --verbose ./program # Generate suppression file valgrind --gen-suppressions=all --leak-check=full ./program 2> suppressions.txt
Example Program with Leaks:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char* create_string(const char *s) {
char *str = malloc(strlen(s) + 1);
strcpy(str, s);
return str;
}
void leak_function() {
int *data = malloc(1000 * sizeof(int));
// data not freed
}
int main() {
char *str = create_string("Hello");
printf("%s\n", str);
// str not freed
leak_function();
return 0;
}
Valgrind Output:
==12345== Memcheck, a memory error detector ==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==12345== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==12345== Command: ./program ==12345== Hello ==12345== ==12345== HEAP SUMMARY: ==12345== in use at exit: 1,016 bytes in 2 blocks ==12345== total heap usage: 3 allocs, 1 frees, 2,048 bytes allocated ==12345== ==12345== 16 bytes in 1 blocks are definitely lost in loss record 1 of 2 ==12345== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x109182: create_string (program.c:5) ==12345== by 0x1091F2: main (program.c:14) ==12345== ==12345== 1,000 bytes in 1 blocks are definitely lost in loss record 2 of 2 ==12345== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x1091C0: leak_function (program.c:9) ==12345== by 0x109207: main (program.c:17) ==12345== ==12345== LEAK SUMMARY: ==12345== definitely lost: 1,016 bytes in 2 blocks ==12345== indirectly lost: 0 bytes in 0 blocks ==12345== possibly lost: 0 bytes in 0 blocks ==12345== still reachable: 0 bytes in 0 blocks ==12345== suppressed: 0 bytes in 0 blocks ==12345== ==12345== For lists of detected and suppressed errors, rerun with: -s ==12345== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
AddressSanitizer (ASan)
A modern alternative to Valgrind, integrated with GCC and Clang:
#include <stdlib.h>
int main() {
int *leak = malloc(100 * sizeof(int));
// leak not freed
int *out_of_bounds = malloc(10 * sizeof(int));
out_of_bounds[10] = 42; // Buffer overflow
return 0;
}
Compilation and Execution:
# Compile with ASan gcc -fsanitize=address -g -o program program.c # Run ./program
ASan Output:
================================================================= ==12345==ERROR: LeakSanitizer: detected memory leaks Direct leak of 400 byte(s) in 1 object(s) allocated from: #0 0x7f8b3c0b3bc8 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x10dbc8) #1 0x558a1f4b0160 in main /home/user/program.c:4 #2 0x7f8b3be56082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).
Electric Fence
A library that helps catch memory leaks and overruns:
#include <stdlib.h>
#include <stdio.h>
#include <efence.h>
int main() {
// Compile with -lefence
int *data = malloc(100 * sizeof(int));
// Use data...
// Forgot to free - Electric Fence will report
return 0;
}
gcc -g -o program program.c -lefence ./program
Custom Memory Pool with Leak Detection
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define POOL_SIZE 1024 * 1024 // 1MB
#define ALIGNMENT 8
#define ALIGN_SIZE(size) (((size) + (ALIGNMENT - 1)) & ~(ALIGNMENT - 1))
typedef struct BlockHeader {
struct BlockHeader *next;
size_t size;
const char *file;
int line;
uint32_t magic; // For corruption detection
} BlockHeader;
typedef struct {
char *memory;
size_t total_size;
size_t used;
BlockHeader *free_list;
int allocation_count;
size_t peak_used;
} MemoryPool;
MemoryPool* create_pool(size_t size) {
MemoryPool *pool = malloc(sizeof(MemoryPool));
pool->memory = malloc(size);
pool->total_size = size;
pool->used = 0;
pool->free_list = NULL;
pool->allocation_count = 0;
pool->peak_used = 0;
return pool;
}
void* pool_alloc_debug(MemoryPool *pool, size_t size,
const char *file, int line) {
size = ALIGN_SIZE(size);
size_t total_needed = size + sizeof(BlockHeader);
if (pool->used + total_needed > pool->total_size) {
fprintf(stderr, "Pool out of memory at %s:%d\n", file, line);
return NULL;
}
BlockHeader *header = (BlockHeader*)(pool->memory + pool->used);
header->next = NULL;
header->size = size;
header->file = file;
header->line = line;
header->magic = 0xDEADBEEF;
pool->used += total_needed;
pool->allocation_count++;
if (pool->used > pool->peak_used) {
pool->peak_used = pool->used;
}
return (char*)header + sizeof(BlockHeader);
}
void pool_free(MemoryPool *pool, void *ptr) {
if (!ptr) return;
BlockHeader *header = (BlockHeader*)((char*)ptr - sizeof(BlockHeader));
if (header->magic != 0xDEADBEEF) {
fprintf(stderr, "Memory corruption detected: invalid magic\n");
return;
}
header->magic = 0;
pool->allocation_count--;
}
void pool_check_leaks(MemoryPool *pool) {
printf("\n=== Memory Pool Leak Report ===\n");
printf("Total pool size: %zu bytes\n", pool->total_size);
printf("Currently used: %zu bytes\n", pool->used);
printf("Peak usage: %zu bytes\n", pool->peak_used);
if (pool->allocation_count == 0 && pool->used == 0) {
printf("No memory leaks in pool!\n");
return;
}
printf("WARNING: %d active allocations\n", pool->allocation_count);
// Scan for unfreed allocations
char *ptr = pool->memory;
while (ptr < pool->memory + pool->used) {
BlockHeader *header = (BlockHeader*)ptr;
if (header->magic == 0xDEADBEEF) {
printf("Leak: %zu bytes at offset %ld (allocated at %s:%d)\n",
header->size, ptr - pool->memory,
header->file ? header->file : "unknown",
header->line);
}
ptr += sizeof(BlockHeader) + header->size;
}
}
void destroy_pool(MemoryPool *pool) {
pool_check_leaks(pool);
free(pool->memory);
free(pool);
}
#define POOL_ALLOC(pool, type) \
(type*)pool_alloc_debug(pool, sizeof(type), __FILE__, __LINE__)
#define POOL_ALLOC_ARRAY(pool, type, count) \
(type*)pool_alloc_debug(pool, sizeof(type) * (count), __FILE__, __LINE__)
int main() {
MemoryPool *pool = create_pool(10000);
// Allocate some memory
int *p1 = POOL_ALLOC(pool, int);
*p1 = 42;
double *p2 = POOL_ALLOC_ARRAY(pool, double, 100);
p2[0] = 3.14;
char *p3 = POOL_ALLOC_ARRAY(pool, char, 50);
strcpy(p3, "Hello, pool!");
printf("p1: %d\n", *p1);
printf("p2[0]: %f\n", p2[0]);
printf("p3: %s\n", p3);
// Free only some allocations
pool_free(pool, p1);
// p2 and p3 not freed - will show as leaks
destroy_pool(pool);
return 0;
}
Leak Detection in Long-Running Programs
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// Global allocation tracking
typedef struct AllocNode {
void *ptr;
size_t size;
const char *file;
int line;
struct AllocNode *next;
} AllocNode;
AllocNode *alloc_list = NULL;
int total_allocs = 0;
size_t total_bytes = 0;
void* tracked_malloc_detailed(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr) {
AllocNode *node = malloc(sizeof(AllocNode));
node->ptr = ptr;
node->size = size;
node->file = file;
node->line = line;
node->next = alloc_list;
alloc_list = node;
total_allocs++;
total_bytes += size;
}
return ptr;
}
void tracked_free_detailed(void *ptr) {
if (!ptr) return;
AllocNode **curr = &alloc_list;
while (*curr) {
if ((*curr)->ptr == ptr) {
AllocNode *to_free = *curr;
*curr = (*curr)->next;
total_bytes -= to_free->size;
total_allocs--;
free(to_free);
free(ptr);
return;
}
curr = &(*curr)->next;
}
fprintf(stderr, "Warning: freeing untracked pointer %p\n", ptr);
free(ptr);
}
void print_stats(int signum) {
printf("\n=== Memory Stats at signal %d ===\n", signum);
printf("Active allocations: %d\n", total_allocs);
printf("Total bytes: %zu\n", total_bytes);
if (total_allocs > 0) {
printf("\nActive allocations:\n");
AllocNode *curr = alloc_list;
while (curr) {
printf(" %zu bytes at %p (allocated at %s:%d)\n",
curr->size, curr->ptr,
curr->file ? curr->file : "unknown",
curr->line);
curr = curr->next;
}
}
}
void signal_handler(int signum) {
print_stats(signum);
exit(0);
}
#define MALLOC(size) tracked_malloc_detailed(size, __FILE__, __LINE__)
#define FREE(ptr) tracked_free_detailed(ptr)
int main() {
signal(SIGINT, signal_handler); // Ctrl+C
signal(SIGTERM, signal_handler);
// Simulate a long-running program with leaks
for (int i = 0; i < 10; i++) {
int *p1 = MALLOC(100 * sizeof(int));
double *p2 = MALLOC(50 * sizeof(double));
// Use memory...
// Free only one
FREE(p1);
// p2 leaks
printf("Iteration %d done\n", i);
sleep(1);
}
print_stats(0);
return 0;
}
Static Analysis Tools
1. Using Clang Static Analyzer
// compile with: clang --analyze program.c
#include <stdlib.h>
void leak_example() {
int *x = malloc(sizeof(int) * 10);
// no free - analyzer will warn
}
2. Using CPPCheck
cppcheck --enable=all program.c
Example warnings:
[program.c:5]: (error) Memory leak: x [program.c:10]: (error) Resource leak: fp
3. Using Splint
splint +checks program.c
Real-World Leak Detection Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Complex program with multiple potential leaks
typedef struct {
char *name;
int *scores;
int num_scores;
} Student;
typedef struct {
Student **students;
int count;
int capacity;
} Class;
Class* create_class(int initial_capacity) {
Class *c = malloc(sizeof(Class));
if (!c) return NULL;
c->students = malloc(initial_capacity * sizeof(Student*));
if (!c->students) {
free(c);
return NULL;
}
c->count = 0;
c->capacity = initial_capacity;
return c;
}
Student* create_student(const char *name, int num_scores) {
Student *s = malloc(sizeof(Student));
if (!s) return NULL;
s->name = malloc(strlen(name) + 1);
if (!s->name) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->scores = malloc(num_scores * sizeof(int));
if (!s->scores) {
free(s->name);
free(s);
return NULL;
}
s->num_scores = num_scores;
return s;
}
void add_student(Class *c, Student *s) {
if (c->count >= c->capacity) {
c->capacity *= 2;
c->students = realloc(c->students,
c->capacity * sizeof(Student*));
}
c->students[c->count++] = s;
}
// BAD: Missing cleanup functions
// void free_student(Student *s) { ... }
// void free_class(Class *c) { ... }
int main() {
Class *my_class = create_class(5);
Student *s1 = create_student("Alice", 5);
Student *s2 = create_student("Bob", 4);
Student *s3 = create_student("Charlie", 6);
add_student(my_class, s1);
add_student(my_class, s2);
add_student(my_class, s3);
printf("Class has %d students\n", my_class->count);
// OOPS! Forgot to free everything
// free_class(my_class);
return 0; // Multiple leaks!
}
Proper cleanup version:
void free_student(Student *s) {
if (s) {
free(s->name);
free(s->scores);
free(s);
}
}
void free_class(Class *c) {
if (c) {
for (int i = 0; i < c->count; i++) {
free_student(c->students[i]);
}
free(c->students);
free(c);
}
}
// In main, add:
// free_class(my_class);
Leak Detection Checklist
- [ ] Every
malloc/callochas a matchingfree - [ ] Error paths free allocated resources
- [ ] Structures with nested allocations have proper destructors
- [ ] Functions that allocate memory document ownership
- [ ] Strings created with
strdupare freed - [ ] Files opened with
fopenare closed - [ ] Thread-local storage is cleaned up
- [ ] Container structures free all elements
- [ ] Reference counting is implemented correctly
- [ ] Pool allocators track all allocations
Best Practices to Prevent Leaks
- Establish clear ownership rules - Document who frees what
- Use consistent allocation/deallocation pairs - If you write
create_x, writedestroy_x - Initialize pointers to NULL - Makes freeing safer
- Free in reverse order of allocation - Nested structures: free innermost first
- Use static analysis tools - Integrate into CI pipeline
- Test with Valgrind/ASan - Regularly run under leak detectors
- Implement reference counting for shared objects
- Use memory pools for many small allocations
- Set freed pointers to NULL - Prevents double frees
- Write unit tests for error paths
Common Leak Patterns to Watch For
// Pattern 1: Reassigning pointer without freeing
void *ptr = malloc(100);
ptr = malloc(200); // First allocation leaked!
// Pattern 2: Leaks in loops
for (int i = 0; i < 100; i++) {
char *str = malloc(100);
// Only free the last one!
if (i == 99) free(str);
}
// Pattern 3: Hidden allocations
char *str = strdup("hello"); // Need to free!
char *path = realpath("file", NULL); // Need to free!
// Pattern 4: Early returns
void *ptr = malloc(100);
if (some_error) {
return; // Forgot to free ptr!
}
free(ptr);
// Pattern 5: Leaks in data structures
struct Node *node = malloc(sizeof(struct Node));
node->data = malloc(100);
// Need to free node->data before freeing node
Conclusion
Memory leak detection is a critical skill for C programmers. Key takeaways:
- Leaks are cumulative - small leaks in loops become big problems
- Tools are essential - Valgrind, ASan, and static analyzers catch what humans miss
- Prevention is better than detection - Clear ownership rules and consistent patterns
- Test error paths - leaks often hide in error handling code
- Document ownership - every allocation should have a clear owner
- Regular monitoring - check long-running programs for growth
Mastering memory leak detection and prevention will make your C programs more reliable, efficient, and maintainable. It's not just about finding bugs—it's about developing habits that prevent them from occurring in the first place.