C Notes
Complete guide to memory management best practices in C — preventing memory leaks, using Valgrind, ownership patterns, RAII-like cleanup, common pitfalls, and professional coding patterns for safe dynamic memory.
Memory bugs are among the most insidious problems in C programming. They can hide for months, then suddenly crash production systems. Unlike type errors that the compiler catches, memory errors are runtime problems — buffer overflows, use-after-free, double-free, and leaks. This guide covers professional patterns to prevent these issues.
The Four Deadly Sins of Memory Management
| Sin | Description | Consequence |
|---|---|---|
| Memory leak | Allocated but never freed | Gradual memory exhaustion |
| Use after free | Accessing freed memory | Crashes or silent corruption |
| Double free | Freeing the same block twice | Heap corruption |
| Buffer overflow | Writing past allocated bounds | Data corruption, security exploit |
Rule 1: Every malloc() Must Have a Matching free()
Think of allocations as opening parentheses — every one must be closed:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *create_greeting(const char *name) {
size_t len = strlen("Hello, ") + strlen(name) + strlen("!") + 1;
char *msg = (char *)malloc(len);
if (msg == NULL) return NULL;
snprintf(msg, len, "Hello, %s!", name);
return msg; // Caller owns this memory
}
int main() {
char *greeting = create_greeting("World");
if (greeting != NULL) {
printf("%s\n", greeting);
free(greeting); // Caller frees it
greeting = NULL; // Prevent dangling pointer
}
return 0;
}Hello, World!
Rule 2: Establish Clear Ownership
Every allocated block should have exactly one "owner" responsible for freeing it:
Rule 3: Always Check malloc() Return Values
Rule 4: Set Pointers to NULL After free()
free(ptr);
ptr = NULL; // Now any accidental access is catchable
// Check before use
if (ptr != NULL) {
// Safe to use
}Rule 5: Use goto for Cleanup in C (RAII-like Pattern)
When a function makes multiple allocations, use goto for centralized cleanup:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int process_data(const char *filename) {
FILE *file = NULL;
char *buffer = NULL;
int *results = NULL;
int status = -1; // Assume failure
file = fopen(filename, "r");
if (file == NULL) goto cleanup;
buffer = (char *)malloc(1024);
if (buffer == NULL) goto cleanup;
results = (int *)calloc(100, sizeof(int));
if (results == NULL) goto cleanup;
// ... do actual work ...
printf("Processing complete.\n");
status = 0; // Success
cleanup:
free(results); // free(NULL) is safe
free(buffer);
if (file) fclose(file);
return status;
}
int main() {
process_data("data.txt");
return 0;
}Using Valgrind to Detect Memory Bugs
Valgrind is the gold standard tool for finding memory issues on Linux:
Running with Valgrind:
==12345== HEAP SUMMARY: ==12345== in use at exit: 200 bytes in 1 blocks ==12345== total heap usage: 2 allocs, 1 frees, 600 bytes allocated ==12345== ==12345== 200 bytes in 1 blocks are definitely lost ==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/...) ==12345== by 0x40054E: main (leaky_program.c:5) ==12345== ==12345== LEAK SUMMARY: ==12345== definitely lost: 200 bytes in 1 blocks ==12345== indirectly lost: 0 bytes in 0 blocks
Memory Debugging Tools
| Tool | Platform | What It Detects |
|---|---|---|
| Valgrind | Linux/macOS | Leaks, invalid reads/writes, uninit values |
| AddressSanitizer | GCC/Clang | Buffer overflow, use-after-free, leaks |
| Dr. Memory | Windows/Linux | Similar to Valgrind |
| Visual Studio Debugger | Windows | Leaks, corruption |
| Electric Fence | Linux | Buffer overflows (crashes immediately) |
Using AddressSanitizer (ASan)
Pattern: Tracking Allocations
For debugging, wrap malloc/free to track allocations:
[ALLOC #1] 40 bytes at 0x55a1e3b012a0 (program.c:27) [ALLOC #2] 100 bytes at 0x55a1e3b012d0 (program.c:28) [FREE] 0x55a1e3b012a0 (program.c:30) — 1 remaining [FREE] 0x55a1e3b012d0 (program.c:31) — 0 remaining Final allocation count: 0 (should be 0)
Pattern: Arena Allocator
For many small allocations with the same lifetime, use an arena:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *buffer;
size_t capacity;
size_t used;
} Arena;
Arena *arena_create(size_t capacity) {
Arena *a = (Arena *)malloc(sizeof(Arena));
a->buffer = (char *)malloc(capacity);
a->capacity = capacity;
a->used = 0;
return a;
}
void *arena_alloc(Arena *a, size_t size) {
if (a->used + size > a->capacity) return NULL;
void *ptr = a->buffer + a->used;
a->used += size;
return ptr;
}
void arena_destroy(Arena *a) {
free(a->buffer); // One free for all allocations!
free(a);
}
int main() {
Arena *arena = arena_create(1024);
// Many allocations, no individual frees needed
int *x = (int *)arena_alloc(arena, sizeof(int));
int *y = (int *)arena_alloc(arena, sizeof(int));
char *name = (char *)arena_alloc(arena, 50);
*x = 42;
*y = 99;
strcpy(name, "Arena allocated!");
printf("x=%d, y=%d, name=%s\n", *x, *y, name);
printf("Arena used: %zu / %zu bytes\n", arena->used, arena->capacity);
arena_destroy(arena); // Single cleanup!
return 0;
}x=42, y=99, name=Arena allocated! Arena used: 58 / 1024 bytes
Checklist: Before Submitting Code
- [ ] Every
malloc/calloc/reallochas a correspondingfree - [ ] Every allocation is checked for NULL
- [ ] No pointer is used after being freed
- [ ] Pointers are set to NULL after free
- [ ] No function returns a pointer to a local variable
- [ ] Temporary pointers used for
reallocresults - [ ] Complex structures have constructor/destructor pairs
- [ ] Code compiles cleanly with
-Wall -Wextra - [ ] Valgrind or ASan reports zero errors
Interview Questions
Q1: How do you detect memory leaks in a C program?
Use tools like Valgrind (--leak-check=full), AddressSanitizer (-fsanitize=address), or custom allocation tracking wrappers. In production, monitor process RSS (Resident Set Size) over time — a steady increase suggests a leak.
Q2: What is the difference between a memory leak and a dangling pointer?
A memory leak means allocated memory is never freed (wasted but not immediately dangerous). A dangling pointer means you're using memory that was already freed (immediately dangerous — undefined behavior, crashes, security exploits).
Q3: Explain the concept of memory ownership in C.
Memory ownership means that for every allocated block, there is exactly one entity (function, module, or struct) responsible for freeing it. The owner allocates and eventually frees. If ownership transfers (e.g., returning a pointer), the new owner takes responsibility. Clear ownership prevents both leaks (nobody frees) and double-frees (two entities try to free).
Q4: What is an arena allocator and when would you use one?
An arena allocator pre-allocates a large buffer, then hands out portions of it for individual allocations. When all the work is done, the entire arena is freed at once. This is ideal when many small allocations share the same lifetime (e.g., parsing a document, handling a request). It eliminates individual free() calls, prevents fragmentation, and is very fast.
Summary
- Establish clear ownership: every allocation has exactly one owner responsible for freeing it
- Use constructor/destructor pairs for complex types
- Use
goto cleanupfor safe multi-resource functions - Set pointers to NULL after freeing
- Use Valgrind or AddressSanitizer to catch bugs early
- Consider arena allocators for groups of allocations with the same lifetime
- Always compile with warnings enabled (
-Wall -Wextra -Werror)
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Memory Management Best Practices in C.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this C Programming topic.
Search Terms
c-programming, c programming, programming, dynamic, memory, allocation, management, best
Related C Programming Topics