Dataslope logoDataslope

Dynamic Memory

malloc, free, the heap, and why managing memory by hand is both powerful and dangerous

So far, every variable we've used has been a local variable (sitting on the stack) or a fixed-size array known at compile time. What if you don't know how much memory you'll need until the program is already running?

That's what the heap is for. The heap is a big pool of memory your program can grab chunks from and give back as it sees fit. The controls are malloc and free.

Stack vs heap, in one picture

StackHeap
Automatic — frame per function callManual — malloc / free
Size fixed at compile timeSize decided at runtime
Very fastFast, but slower than stack
Freed automatically on returnLives until you free it
Small (typically a few MB)Large (often many GB)

If a value is small and short-lived, put it on the stack. If a value must outlive the function that created it, or must be sized at runtime, put it on the heap.

malloc and free

malloc(n) asks the operating system for a chunk of n bytes and returns a pointer to it (or NULL if it failed). Always include <stdlib.h>.

int *nums = malloc(10 * sizeof(int));
if (nums == NULL) {
    // out of memory — handle it
}
// use nums[0]..nums[9]
free(nums);
nums = NULL;       // optional but smart

The shape malloc(N * sizeof(T)) is the canonical form. Read it as "give me room for N things of type T".

Code Block
C 17 (201710L)

The point is the runtime-decided size. You could not have written int nums[n]; at the top of main portably; with malloc you can ask for exactly as much as you need.

What free does (and doesn't do)

free(p) returns the chunk p points at to the heap, so other parts of the program can reuse it. It does not:

  • Set p to NULL — the variable p still holds the same address.
  • Erase the memory — the bytes are still there until something overwrites them.
  • Tell you whether you'd already freed the chunk.

That's why dangling pointers — pointers to memory that has already been freed — are one of the most dangerous bug types in C. A self-discipline that helps: after free(p), immediately do p = NULL;.

calloc and realloc

Two close cousins of malloc are worth knowing:

  • calloc(count, size) — like malloc(count * size) but zero-initializes the memory. Useful when you want a clean slate.
  • realloc(p, new_size) — resize an existing allocation. May move it; always reassign: p = realloc(p, new_size); and check the result before using.

A classic pattern is a growable buffer: start with a small malloc, realloc to double the capacity when full.

Lifetimes and ownership

Every chunk of malloc-ed memory belongs to someone. That someone is responsible for eventually calling free. C doesn't track this for you; it's a convention your code has to make obvious.

A useful rule: wherever you create a heap allocation, decide who will free it before you write another line. The most common discipline is "the function that allocates is also the function that frees", but for larger structures, you often have an explicit destructor function:

struct Foo *foo_new(void);
void        foo_free(struct Foo *foo);

A function called _new allocates, the matching _free releases. Every _new call must be balanced by exactly one _free call.

The three memory bugs you must recognize

These are the classics. Every C programmer will write them at some point. The skill is recognizing them quickly.

1. Memory leak

You allocated memory and never freed it. Each leak is a slow drip; in a long-running program they add up until the system runs out of RAM.

int *p = malloc(1000 * sizeof(int));
// ...use p...
// forgot to free(p)

2. Use-after-free

You freed memory, then used the pointer anyway:

free(p);
p[0] = 5;       // UB — that memory is no longer yours

3. Double-free

You freed the same pointer twice:

free(p);
free(p);        // UB — corruption of the heap's bookkeeping

Tools like Valgrind and AddressSanitizer can catch all three automatically. We'll mention them again in the debugging chapter.

Returning a heap-allocated array from a function

This is the canonical reason for malloc: you can't return a local array (it'd die when the function returns), but you can return a pointer to heap memory.

Code Block
C 17 (201710L)

Notice the comment in the source code is doing real work: it tells the reader that main must free what make_squares returned.

Multi-file challenge: a growable integer buffer

Build a tiny "dynamic array" in two files. The header declares the type and operations, the implementation manages a malloc-ed buffer. main.c exercises it.

Challenge
C 17 (201710L)
Build a tiny dynamic array

Implement a small dynamic array of ints split across three files.

dynarr.h declares:

typedef struct {
  int   *data;
  size_t length;
  size_t capacity;
} DynArr;

void da_init(DynArr *a);
void da_push(DynArr *a, int value);
void da_free(DynArr *a);

dynarr.c implements them. da_push must double the capacity (starting from 1) whenever length == capacity.

main.c pushes 1..5 and prints them space-separated on one line, then frees.

The program must print exactly:

1 2 3 4 5

The doubling strategy is what std::vector does in C++ and what Python's list does. Allocating one element at a time would make every push O(n); doubling makes each push amortized O(1).

QuestionSelect one

What is wrong with this code?

int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);

It will fail to compile.

The malloc will fail because 4 bytes is too small.

*p is a use-after-free; the memory was returned to the heap and may now belong to something else.

The printf requires %p, not %d.

QuestionSelect one

Which of the following allocations leaves you with zero-initialized memory without a separate loop?

malloc(n * sizeof(int))

calloc(n, sizeof(int))

realloc(NULL, n * sizeof(int))

alloca(n * sizeof(int))

On this page