C Stack vs Heap

C programs use memory in different ways depending on how long an object must live and who controls its storage. The stack is the usual home for local variables with automatic lifetime, while the heap is used for dynamically allocated objects requested with functions such as malloc. Understanding the difference helps you avoid dangling pointers, memory leaks, stack overflows, and confusing pointer bugs.

Overview: How Stack and Heap Memory Work

The terms stack and heap are common implementation names. The C standard describes storage duration more formally: automatic, allocated, static, and thread storage. In everyday C programming, local variables inside a function usually have automatic storage and are commonly placed on the call stack. Objects returned by malloc, calloc, or realloc have allocated storage and are commonly placed in the heap.

The stack is tied to function calls. When a function is called, the program creates space for that call’s local variables, parameters, return address, and bookkeeping data. When the function returns, that space is no longer valid for those local variables. This is fast because allocation and release follow a strict last-in, first-out pattern: the most recent function call is the first one to finish.

The heap is not tied to a single function call. A heap object remains alive until your program passes its pointer to free. That means a function may allocate memory and return a pointer to it, but it also means the program must have a clear owner responsible for releasing it. Heap allocation is more flexible than stack allocation, but usually slower and easier to misuse.

Feature Stack Heap
Typical C source int x; inside a block malloc(sizeof *p)
Lifetime Until the block or function ends Until free is called
Size decision Usually compile time or block entry Runtime
Failure mode Too much may overflow the stack Allocation may return NULL
Responsibility Compiler and call flow Programmer ownership

Syntax

Stack variables are ordinary declarations. Heap variables require a pointer, an allocation call, a failure check, and a matching free.

int local = 10;
size_t count = 4;
int *items = malloc(count * sizeof *items);
if (items == NULL) {
    return 1;
}
free(items);
items = NULL;
  • local is an automatic object. It is valid only while its block is active.
  • items is a stack variable that stores an address. The array it points to is on the heap.
  • malloc(count * sizeof *items) requests enough bytes for count integers without repeating the type name.
  • items == NULL checks whether allocation failed.
  • free(items) releases the heap block. Setting the pointer to NULL prevents accidental reuse through that variable.

Examples

Stack memory inside a function

#include <stdio.h>

void print_stack_demo(void) {
    int count = 3;
    int scores[3] = {10, 20, 30};

    printf("count = %d\n", count);
    printf("scores[0] = %d\n", scores[0]);
    printf("scores has %zu bytes\n", sizeof scores);
}

int main(void) {
    print_stack_demo();
    printf("function returned\n");
    return 0;
}

Output:

count = 3
scores[0] = 10
scores has 12 bytes
function returned

count and scores are automatic local variables. They are created when print_stack_demo begins and stop being valid when it returns. The array does not need free because the stack frame is removed automatically.

Heap memory for a runtime-sized array

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

int main(void) {
    size_t count = 5;
    int *values = malloc(count * sizeof *values);

    if (values == NULL) {
        printf("allocation failed\n");
        return 1;
    }

    for (size_t i = 0; i < count; i++) {
        values[i] = (int)((i + 1) * (i + 1));
    }

    int sum = 0;
    printf("values:");
    for (size_t i = 0; i < count; i++) {
        printf(" %d", values[i]);
        sum += values[i];
    }
    printf("\nsum = %d\n", sum);

    free(values);
    return 0;
}

Output:

values: 1 4 9 16 25
sum = 55

The pointer variable values itself is local to main, but the five integers are in allocated storage. This distinction is important: leaving main would destroy the pointer variable, but it would not automatically call free for the heap block. In longer-running programs, forgetting that release becomes a memory leak.

A stack struct that owns heap memory

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

typedef struct {
    char *data;
    size_t length;
} Buffer;

Buffer make_buffer(size_t length) {
    Buffer b;
    b.data = malloc(length + 1);
    b.length = length;

    if (b.data == NULL) {
        b.length = 0;
        return b;
    }

    for (size_t i = 0; i < length; i++) {
        b.data[i] = (char)('A' + i);
    }
    b.data[length] = '\0';
    return b;
}

int main(void) {
    Buffer buffer = make_buffer(6);

    if (buffer.data == NULL) {
        printf("could not create buffer\n");
        return 1;
    }

    printf("text = %s\n", buffer.data);
    printf("length = %zu\n", buffer.length);

    free(buffer.data);
    return 0;
}

Output:

text = ABCDEF
length = 6

This example shows a common real pattern. The Buffer variable in main is a stack object, but one of its members points to heap memory. Returning the structure is safe because the pointer value is copied, and the pointed-to heap block remains alive. The caller must still release buffer.data.

Returning heap memory safely

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

int *make_total(int a, int b) {
    int *result = malloc(sizeof *result);

    if (result != NULL) {
        *result = a + b;
    }

    return result;
}

int main(void) {
    int *total = make_total(40, 2);

    if (total == NULL) {
        printf("allocation failed\n");
        return 1;
    }

    printf("total = %d\n", *total);

    free(total);
    total = NULL;
    printf("pointer released\n");
    return 0;
}

Output:

total = 42
pointer released

A function must not return the address of one of its local variables, because that variable stops existing when the function returns. This version allocates an int on the heap, stores the result there, and returns the pointer. The caller becomes the owner and releases it.

How It Works Step by Step

  1. When main starts, the runtime enters a function call frame. Local variables such as pointers, counters, and small arrays are usually placed in that frame.
  2. When another function is called, a new frame is placed above the current one. Its local variables are separate from the caller’s locals.
  3. When that function returns, its frame is removed. Any pointer to an object inside that frame becomes invalid, even if the address still appears to contain the old bytes.
  4. When malloc is called, the allocator searches its managed heap area for a block large enough for the requested byte count.
  5. If allocation succeeds, malloc returns a pointer to usable storage. If it fails, it returns NULL.
  6. free gives the block back to the allocator. It does not erase every pointer copy, so using an old pointer after free is still a bug.

Do not rely on exact addresses or on stack direction. Some systems grow the stack downward, some details vary by compiler and operating system, and optimization can keep values in registers. The dependable C-level rule is lifetime: automatic objects end with their block, and allocated objects end with free.

Common Mistakes

Returning the address of a stack local

A tempting but broken idea is to create a local variable and return &local. The pointer would refer to a dead object after the function returns. Allocate on the heap when the result must outlive the function, or let the caller pass storage into the function.

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

int *make_answer(void) {
    /* Wrong idea: int answer = 42; return &answer; */
    int *answer = malloc(sizeof *answer);

    if (answer != NULL) {
        *answer = 42;
    }
    return answer;
}

int main(void) {
    int *answer = make_answer();

    if (answer == NULL) {
        printf("allocation failed\n");
        return 1;
    }

    printf("answer = %d\n", *answer);
    free(answer);
    return 0;
}

Output:

answer = 42

Using a pointer after free

After free(p), the heap block no longer belongs to you. Reading or writing through p is undefined behavior. If the pointer variable will remain in scope, assign NULL after freeing and design the program so only one owner frees the block.

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

int main(void) {
    int *number = malloc(sizeof *number);

    if (number == NULL) {
        printf("allocation failed\n");
        return 1;
    }

    *number = 7;
    printf("before free = %d\n", *number);

    free(number);
    number = NULL;

    if (number == NULL) {
        printf("number is no longer available\n");
    }

    return 0;
}

Output:

before free = 7
number is no longer available

Best Practices

  • Use stack storage for small, fixed-size values that do not need to outlive the current function.
  • Use heap storage when the size is known only at runtime, the object is large, or the object must outlive the function that creates it.
  • Check every allocation result before dereferencing it.
  • Write allocation sizes as count * sizeof *pointer to avoid mismatches when the pointed-to type changes.
  • Give every heap allocation a clear owner and exactly one matching free.
  • Do not return addresses of local variables, and do not store such addresses for later use.
  • Avoid very large local arrays; prefer heap allocation when a buffer could be large or user-controlled.
  • After freeing a long-lived pointer variable, set it to NULL if it might be checked or reused later.

Practice Exercises

  1. Write a function that receives an array length, allocates a heap array of double, fills it with halves such as 0.5, 1.0, and 1.5, prints it, and frees it.
  2. Create a small Student structure on the stack with a heap-allocated name member. Print the name, then release the member before the program ends.
  3. Rewrite a function that currently returns a pointer to a local array. Make the caller provide the array instead, or make the function allocate heap storage and document who must call free.

Summary

  • The stack is best for automatic local objects with simple, short lifetimes.
  • The heap is best for dynamic size, large objects, or data that must live beyond one function call.
  • A pointer variable may be on the stack while the object it points to is on the heap.
  • Stack objects are released automatically; heap objects require free.
  • Most stack versus heap bugs are lifetime bugs: dangling pointers, leaks, double frees, and use-after-free errors.