Skip to content
Home
C Memory Management: malloc, calloc, realloc, and free

C Memory Management: malloc, calloc, realloc, and free

C/C++ C/C++ 9 min read 1809 words Intermediate ExcellentWiki Editorial Team

Memory management is what separates C from higher-level languages. In C, you control when memory is allocated and freed. This control gives you predictable performance and minimal overhead, but it also means every allocation must be paired with a corresponding free. Mistakes cause memory leaks, crashes, and security vulnerabilities.

Stack vs Heap

Every program has two primary memory regions for storing data during execution.

Stack Memory

Stack memory is automatically managed. Local variables live on the stack and are destroyed when their scope exits:

void func(void) {
    int x = 42;      // Allocated on the stack
    char buf[64];    // Also on the stack
---  // x and buf are automatically freed here

Stack allocation is incredibly fast — it is just a pointer adjustment. But stack memory is limited (typically 1-8 MB per thread) and cannot be resized. Large allocations or data that must outlive the function call need the heap.

Heap Memory

Heap memory is managed explicitly. It persists until you free it and is only limited by available system memory:

void func(void) {
    int *p = malloc(sizeof(int));  // Allocated on the heap
    *p = 42;
    // p itself is on the stack — it holds the address of heap memory
    free(p);  // Must free explicitly
---

The pointer p lives on the stack and is destroyed when the function returns. But the heap memory it points to persists until free(p) is called. If you lose the pointer without freeing first, you have a memory leak.

malloc

malloc (memory allocation) allocates a block of uninitialized memory:

#include <stdlib.h>

int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "Memory allocation failed\n");
    exit(1);
---
// Memory is uninitialized — contains garbage values

malloc takes a size in bytes and returns a void* pointing to the allocated block, or NULL on failure. The memory is not zeroed — it contains whatever was previously in that region. Always check the return value; malloc fails when memory is exhausted, which can happen at any time on memory-constrained systems.

Common malloc Mistake

int *p = malloc(sizeof(int));   // Correct: allocates space for one int
int *q = malloc(sizeof(p));     // Wrong: allocates sizeof(pointer), not sizeof(int)

sizeof(p) returns the size of the pointer itself (typically 8 bytes on 64-bit systems), not the size of the pointed-to type. Use sizeof(*p) to get the correct size regardless of type changes.

calloc

calloc (clear allocation) allocates memory and initializes every byte to zero:

int *arr = calloc(10, sizeof(int));
if (arr == NULL) {
    // Handle error
---
// All 10 integers are guaranteed to be zero

calloc takes a count and an element size, making it ideal for allocating arrays. Zero-initialization adds a small runtime cost, but it prevents bugs from uninitialized data. Use calloc when you need zeroed memory — for example, when allocating arrays that will be partially filled.

realloc

realloc (reallocation) resizes an existing allocation, preserving the original content:

int *arr = malloc(5 * sizeof(int));
// ... fill arr ...

int *new_arr = realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
    // Original arr is still valid
    free(arr);
    exit(1);
---
arr = new_arr;

realloc may move the block to a new location if there is not enough contiguous space. The original pointer becomes invalid after a successful realloc — do not use it. If realloc fails, it returns NULL and the original block remains allocated, so always use a temporary variable.

Realloc Idioms

// Correct pattern
int *temp = realloc(arr, new_size);
if (temp == NULL) {
    // arr still valid — handle error
--- else {
    arr = temp;
---

// Wrong pattern — leaks original memory on failure
arr = realloc(arr, new_size);  // If NULL, original pointer is lost

free

free releases heap memory back to the system:

int *p = malloc(sizeof(int));
free(p);
// p is now a dangling pointer — do not dereference it
free(p);  // Undefined behavior — double free

After freeing, the pointer still holds the old address. Dereferencing it accesses freed memory, which may be corrupt or already in use. Freeing it again causes a double-free. Both are undefined behavior that can crash or be exploited by attackers.

Safe Free Pattern

void safe_free(void **ptr) {
    if (ptr != NULL && *ptr != NULL) {
        free(*ptr);
        *ptr = NULL;
    }
---

Setting the pointer to NULL after freeing makes it safe to call free again (free(NULL) is a no-op), and any accidental dereference will crash immediately instead of corrupting data silently.

Memory Leaks

A memory leak occurs when allocated memory is no longer reachable but has not been freed:

void leak(void) {
    int *p = malloc(1000 * sizeof(int));
    // A leak if we return without freeing
---

void leak_by_overwrite(void) {
    int *p = malloc(sizeof(int));
    p = malloc(sizeof(int));  // First allocation is leaked
    free(p);
---

Memory leaks waste system memory. A leaking program will eventually exhaust available memory, causing allocation failures or swapping. Long-running processes like servers and daemons must be especially careful — even small leaks accumulate over time.

Detecting Leaks

Compile with AddressSanitizer for immediate detection:

gcc -fsanitize=address -g program.c -o program
./program

Leaks are reported with stack traces on program exit.

Valgrind for Memory Debugging

Valgrind is a powerful tool for detecting memory errors at runtime:

valgrind --leak-check=full --show-leak-kinds=all ./program

Valgrind detects:

  • Memory leaks (unfreed allocations)
  • Use-after-free
  • Invalid reads and writes (buffer overflows)
  • Mismatched allocation/deallocation functions
int main(void) {
    int *p = malloc(10 * sizeof(int));
    p[10] = 42;  // Invalid write — Valgrind catches this
    free(p);
    p[0] = 10;   // Use-after-free — Valgrind catches this
    return 0;
---

Valgrind slows execution by 10-20x but provides invaluable information. Run it regularly during development and always before shipping.

Memory Management Patterns

Arena Allocator

For performance-critical code, batch allocations in an arena and free everything at once:

typedef struct {
    char *start;
    char *current;
    size_t size;
--- Arena;

Arena *arena_create(size_t size) {
    Arena *a = malloc(sizeof(Arena));
    a->start = malloc(size);
    a->current = a->start;
    a->size = size;
    return a;
---

void *arena_alloc(Arena *a, size_t size) {
    if (a->current + size > a->start + a->size) return NULL;
    void *ptr = a->current;
    a->current += size;
    return ptr;
---

void arena_destroy(Arena *a) {
    free(a->start);
    free(a);
---

Arenas are used in games, compilers, and real-time systems where individual frees would be too slow or complex.

Reference Counting

For shared ownership, implement a simple reference counter:

typedef struct {
    int *data;
    int refs;
--- SharedBuffer;

SharedBuffer *sb_create(size_t size) {
    SharedBuffer *sb = malloc(sizeof(SharedBuffer));
    sb->data = malloc(size);
    sb->refs = 1;
    return sb;
---

void sb_ref(SharedBuffer *sb) {
    sb->refs++;
---

void sb_unref(SharedBuffer *sb) {
    if (--sb->refs == 0) {
        free(sb->data);
        free(sb);
    }
---

This is the manual equivalent of C++’s shared_ptr. Every thread or component that uses the buffer calls sb_ref, and calls sb_unref when done. The buffer is freed automatically when the last reference is released.

Best Practices

Every malloc has a free — Pair every allocation with a corresponding deallocation. Track ownership explicitly in comments or use a static analysis tool.

Initialize pointers to NULL — An uninitialized pointer is a crash waiting to happen. Set every pointer to NULL or a valid address at declaration.

Check allocation failures — malloc returns NULL on failure. Check it every time. On embedded systems without an MMU, malloc can fail at any time.

Zero-initialize structs — Use calloc or memset to zero-initialize structs. Uninitialized struct fields cause nondeterministic behavior.

Use static analysis — Tools like cppcheck, clang-tidy, and valgrind catch memory errors that are invisible during normal testing.

Stack vs Heap Allocation

Understanding the distinction between stack and heap memory is fundamental to C programming. Stack allocation happens automatically when functions are called — local variables live on the stack and are freed when the function returns. Stack allocation is extremely fast because it only requires moving the stack pointer. However, stack space is limited (typically 1-8 MB per thread on modern systems), and the size of stack allocations must be known at compile time.

Heap allocation via malloc() is more flexible — you can allocate arbitrary sizes at runtime and control the lifetime of the memory. The trade-off is performance: heap allocation requires traversing free lists, finding suitable blocks, and managing fragmentation. A malloc() call can be hundreds of times slower than a stack allocation.

Memory Layout of a C Program

A typical C process has five memory segments:

  1. Text segment — compiled machine code, read-only
  2. Data segment — initialized global and static variables
  3. BSS segment — uninitialized global and static variables (zero-filled at load)
  4. Heap — dynamically allocated memory (grows upward)
  5. Stack — function call frames and local variables (grows downward)

Understanding this layout helps debug memory issues. Stack overflow occurs when the stack collides with the heap. Heap fragmentation occurs when repeated allocations and frees create small unusable gaps between allocated blocks.

Alignment and Padding

The malloc() function returns memory aligned to the strictest fundamental alignment of the platform (typically 16 bytes on 64-bit systems). Structures may have padding between members to satisfy alignment requirements:

struct example {
    char c;      // 1 byte + 3 padding
    int i;       // 4 bytes
    double d;    // 8 bytes
---;  // total: 16 bytes (not 13)

Rearranging structure members from largest to smallest minimizes padding and saves memory — a useful optimization when storing millions of records.

Memory Pooling

For applications that allocate many small objects of the same size, a memory pool (or slab allocator) can dramatically improve performance. Instead of calling malloc() for each object, pre-allocate a large block and hand out fixed-size chunks from it. This eliminates fragmentation, reduces allocation overhead, and improves cache locality.

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.


Related: Learn about C pointers, C++ smart pointers, and C vs C++.

Section: C/C++ 1809 words 9 min read Intermediate 756 articles in section Report inaccuracy Back to top