Phase 2 · Arrays, Strings & MemoryModule 10~50 min read

Dynamic Memory Management

Allocate storage at runtime with malloc, calloc, realloc, and free while preventing leaks and lifetime bugs.

What you'll learn

Dynamic allocation creates storage whose size and lifetime are decided at runtime. It powers flexible collections and long-lived objects, but every successful allocation creates a cleanup responsibility.

By the end, you'll be able to:

  • Distinguish automatic, static, and allocated lifetimes
  • Allocate, initialize, resize, and free checked storage
  • State ownership rules and avoid leaks, double-free, and dangling pointers
  • Grow a dynamic array without losing the original allocation

Storage durations

Scope controls name visibility; storage duration controls object lifetime. Allocated objects are different from automatic locals: leaving the creating function does not release them.

Three lifetimes used throughout C

Static storage

Entire program

globals · static locals

Automatic storage

One block/call

ordinary local objects

Allocated storage

malloc → free

runtime-sized objects

Stack & heap model

Implementations commonly place automatic call frames on a stack and allocated storage in a heap. These are useful engineering models, though the C standard defines storage durations rather than requiring those exact regions.

PropertyAutomatic objectAllocated object
Size decisionUsually fixed at block entryChosen at runtime
Lifetime endsAutomatically at block exitOnly when released with free
Typical accessDirect nameThrough a pointer
Main riskReturning its addressLeak, double-free, use-after-free

malloc & calloc

malloc returns uninitialized storage suitably aligned for any object type.calloc multiplies count and element size and zeroes the resulting bytes. Both return NULL on failure.

malloc.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t count = 5;
    int *values = malloc(count * sizeof *values);
    if (values == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }

    for (size_t i = 0; i < count; i++) {
        values[i] = (int) (i + 1) * 10;
    }
    for (size_t i = 0; i < count; i++) {
        printf("%d ", values[i]);
    }
    putchar('\n');

    free(values);
    values = NULL;
    return 0;
}
calloc.c
#include <stdlib.h>

size_t count = 100;
int *counters = calloc(count, sizeof *counters);
if (counters == NULL) {
    /* handle allocation failure */
}
/* every byte in the allocated region initially contains zero */
free(counters);

Tip

Write sizeof *pointer instead of repeating the type. If the pointer type changes, the allocation expression stays correct.

Watch out

Before multiplying user-controlled counts by element size, checkcount <= SIZE_MAX / sizeof *pointer to prevent size overflow.

Resize with realloc

realloc may extend the existing region or move it. On success, the old pointer is invalid; on failure, the old allocation remains valid.

realloc.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t count = 3;
    int *values = malloc(count * sizeof *values);
    if (values == NULL) return 1;

    values[0] = 10;
    values[1] = 20;
    values[2] = 30;

    size_t new_count = 6;
    int *resized = realloc(values, new_count * sizeof *values);
    if (resized == NULL) {
        free(values);             // original allocation is still valid
        return 1;
    }
    values = resized;

    for (size_t i = count; i < new_count; i++) values[i] = 0;
    printf("%d %d\n", values[2], values[5]);
    free(values);
    return 0;
}

Key idea

Store the result in a temporary pointer. Assigningvalues = realloc(values, ...) directly would lose the only pointer to the original allocation when resizing fails.

Ownership & cleanup

Ownership is a design rule: the owner is responsible for eventually callingfree. APIs should state whether they borrow a pointer, take ownership, or return ownership to the caller.

  • Exactly one clear owner for each allocation
  • Borrowed pointers never outlive their owner
  • Every successful allocation has a reachable cleanup path
  • After free, do not read, write, compare as an array cursor, or free again
cleanup.c
#include <stdio.h>
#include <stdlib.h>

int process(size_t count) {
    int result = 1;
    int *left = NULL;
    int *right = NULL;

    left = malloc(count * sizeof *left);
    if (left == NULL) goto cleanup;

    right = malloc(count * sizeof *right);
    if (right == NULL) goto cleanup;

    /* perform work */
    result = 0;

cleanup:
    free(right); // free(NULL) is safe
    free(left);
    return result;
}

int main(void) {
    return process(100);
}

Note

A single cleanup block is one legitimate use of goto in C. It avoids duplicating release logic across many early-error branches.

Failure patterns

BugWhat happenedPrevention
LeakThe final pointer to live allocated storage was lostOne owner and cleanup on every exit
Double-freeThe same live allocation was released twiceTransfer ownership explicitly
Use-after-freeCode dereferenced a pointer after lifetime endedInvalidate borrows; structure cleanup
Invalid freePointer was not returned by an allocation functionFree only owned allocation bases
Size overflowByte calculation wrapped before allocationCheck count before multiplication

Build a dynamic array

A dynamic array tracks three facts: its element pointer, current size, and allocated capacity. Growing geometrically makes repeated append operations efficient.

vector.c
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} IntVector;

bool push(IntVector *vector, int value) {
    if (vector->size == vector->capacity) {
        size_t next = vector->capacity == 0 ? 4 : vector->capacity * 2;
        if (next > SIZE_MAX / sizeof *vector->data) return false;

        int *grown = realloc(vector->data, next * sizeof *vector->data);
        if (grown == NULL) return false;
        vector->data = grown;
        vector->capacity = next;
    }
    vector->data[vector->size++] = value;
    return true;
}

int main(void) {
    IntVector values = {0};
    for (int value = 10; value <= 50; value += 10) {
        if (!push(&values, value)) {
            free(values.data);
            return 1;
        }
    }
    printf("size=%zu capacity=%zu last=%d\n",
           values.size, values.capacity, values.data[values.size - 1]);
    free(values.data);
    return 0;
}

Key idea

size counts constructed values; capacity counts available slots. Code may access only indexes below size, even when more storage has been allocated.

Recap & quick check

Key takeaways

  • Allocated storage lives from a successful allocation until free, independent of block scope.
  • Check allocation failure and multiplication overflow before using storage.
  • Use a temporary pointer with realloc so failure cannot lose the original allocation.
  • Ownership identifies who must release a resource; borrowed pointers must not outlive it.
  • Dynamic arrays separate logical size from allocated capacity and grow geometrically.

Quick check

1. What are malloc's newly allocated bytes initialized to?

2. What remains true when realloc returns NULL?

3. What does free(NULL) do?

4. Who should call free?

Phase 2 complete. Phase 3 begins with Module 11 — Structs, Unions, Enums & typedef, where related values become expressive domain types.