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

  1. Why Dynamic Memory?
  2. The Heap vs Stack
  3. malloc()
  4. calloc()
  5. realloc()
  6. free()
  7. malloc vs calloc vs realloc
  8. Common Memory Bugs
  9. Memory Leak Detection
  10. Best Practices
  11. Advanced Patterns
  12. 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 NULL on 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 elements
  • size: size of each element
  • Returns: pointer to zero-initialized memory, or NULL on 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

Featuremalloc()calloc()
Arguments1 (total bytes)2 (count, size)
InitializationGarbageZero-initialized
SpeedFasterSlightly slower
Use caseWhen you’ll overwrite all valuesWhen 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 (or NULL to act like malloc)
  • size: new size in bytes
  • Returns: pointer to resized memory, or NULL on 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 (or NULL — 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

FunctionPurposeArgsInitializationReturns NULL when
malloc(n)Allocate memorytotal bytesGarbageAllocation fails
calloc(n, size)Allocate zeroed arraycount, element sizeZeroAllocation fails
realloc(ptr, n)Resize existing blockold ptr, new sizeOld data preserved, new = garbageAllocation 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 for NULL.
  • calloc(n, size) allocates and zero-initializes memory.
  • realloc(ptr, size) resizes a block — always use a temporary pointer.
  • free(ptr) releases memory — set ptr = NULL immediately after.
  • Every allocation must eventually be matched with exactly one free.
  • Double free and 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