Stack vs heap in C: difference, lifetime and size

Stack vs heap in C: how they differ in lifetime, size and speed, where globals and string literals live, and why returning a stack address is a bug.

By
Published on

"Put it on the heap", "that variable is on the stack": the two words come up all the time when you write C, often without anyone being quite sure what separates them. The difference is concrete. It decides how long your data lives, how big it can get, and who has to free it.

In short

The stack and the heap are two areas of a program's memory. The stack holds each function's local variables: it's fast, limited in size (often 8 MB), and the compiler manages it for you, so every variable disappears at the end of its block. The heap is for the blocks you request with malloc: it's large, but freeing it with free is your job, and a block stays there until you give it back. Global variables and string literals are on neither: they live in a static area, allocated once for the whole run of the program.

Stack vs heap: the difference in one table

Four criteria are enough to tell them apart.

StackHeap
Lifetimethe function (the block)until your free
Who allocates and freesthe compiler, on its ownyou, with malloc and free
Sizelimited (often 8 MB)large, limited by RAM
Speedvery fastslower (a free block has to be found)

The stack grows and shrinks with function calls: entering a function reserves a frame (its local variables), leaving it releases the whole frame in one go. That's why it's fast, and why a local variable stops existing as soon as its function returns. The heap doesn't follow calls: a block you allocate in one function can be used much later, somewhere else, and only goes away when you free it.

Where each variable lives

The simplest way to find out is to ask the variables for their addresses. This program declares one of each kind and prints its address with %p:

zones.c
#include <stdio.h>#include <stdlib.h>
int counter = 42;       /* initialized global */int total;              /* uninitialized global */
int main(void){    int local = 7;              /* on the stack */    static int calls = 0;       /* local, but static */    char *message = "hello";    /* read-only literal */    int *heap = malloc(sizeof(int)); /* block on the heap */
    if (heap == NULL)        return 1;    *heap = 5;    printf("local     %p\n", (void *)&local);    printf("heap      %p\n", (void *)heap);    printf("total     %p\n", (void *)&total);    printf("calls     %p\n", (void *)&calls);    printf("counter   %p\n", (void *)&counter);    printf("message   %p\n", (void *)message);    printf("main      %p\n", (void *)main);    free(heap);    return 0;}
Terminal
$ gcc -Wall -Wextra -g zones.c -o zones$ ./zoneslocal     0xffffed597a7cheap      0x31b692a0total     0x420048calls     0x42004ccounter   0x420040message   0x4007c8main      0x4006c4

Sort these addresses by size and the memory map appears. At the very top, local, at 0xffff...: that's the stack, which starts from the high addresses. Much lower down, heap, at 0x31b692a0: the heap, which begins above the static area at a distance that changes from run to run. Then a tight little group, total, calls and counter, around 0x420040: the static area, where globals and static variables live. Finally message and main, around 0x400...: the string literal and the code, in the read-only part of the program.

The areas, from high addresses to low

Stack

main() (stack, high addresses)
intlocal
7
0xffff...7a7c
int *heap
block

Heap

blockintheap, 0x31b692a0
5

Static data

counterintglobal, 0x420040
42
message_datachar[6]literal, read-only
'h''e''l''l''o''\0'

Run it twice in a row and a useful detail shows up: the stack and heap addresses change on every run (that's ASLR, which moves those areas around for security), while the addresses of the static area and the code stay the same.

To follow this layout step by step on a real program, the Grademe visualizer shows the stack, the heap and the pointers between them at every line:

lifetime.cVisualize
#include <stdlib.h>
int global = 42;
int main(void){    int local = 7;    int *copy = malloc(sizeof(int));
    if (copy == NULL)        return 1;    *copy = local;    free(copy);    return 0;}
Run your code step by step in the visualizer

Size: why a big local array crashes

The stack has a maximum size, which ulimit -s prints in kilobytes:

Terminal
$ ulimit -s8192

8192 KB, so 8 MB. That's plenty for ordinary variables, but not for a big local array. This program declares a 16 MB array on the stack:

big.cAvoid
#include <stdio.h>#include <string.h>
int main(void){    char image[16 * 1024 * 1024];
    memset(image, 0, sizeof(image));    printf("image ready: %zu bytes\n", sizeof(image));    return 0;}
Terminal
$ gcc -Wall -Wextra -g big.c -o big$ ./bigSegmentation fault

The array is bigger than the 8 MB stack, and the first access past the limit triggers the signal. The same data on the heap goes through without a problem, because the heap doesn't have that tight limit:

Terminal
$ ./big-heapimage ready: 16777216 bytes

big-heap.c is the same program with a malloc(16 * 1024 * 1024) in place of the local array. The rule of thumb: as soon as a piece of data is large, or its size is only known at run time, it belongs on the heap, not on the stack.

Returning a stack address: the classic bug

Since a local variable disappears at the end of its function, returning its address gives you a pointer to a slot that no longer belongs to you. It's one of the most common causes of a segmentation fault, and GCC often catches it:

return-local.cAvoid
#include <stdio.h>#include <string.h>
char *copy(const char *source){    char buffer[32];
    strcpy(buffer, source);    return buffer;      /* buffer dies here */}
int main(void){    char *s = copy("hello");
    printf("%s\n", s);    return 0;}
Terminal
$ gcc -Wall -Wextra -g return-local.c -o return-localreturn-local.c: In function 'copy':return-local.c:9:12: warning: function returns address of local variable [-Wreturn-local-addr]    9 |     return buffer;      /* buffer dies here */      |            ^~~~~~$ ./return-localSegmentation fault
After the return, the frame of copy() is gone

Stack

main()
char *s
? indeterminate
points into the released frame of copy()

The pointer s in main points to a piece of stack that has already been given back: using it is undefined behavior. The fix is to put the result on the heap, with malloc, and let the caller free it. The two exercises below work on both sides of that line: one hands out memory by hand, the other makes the stack of frames visible.

C

arena_alloc

Write a "bump" allocator that serves 8-byte-aligned slices from a 4096-byte static pool, without ever calling malloc. You manage by hand the boundary between what has been handed out and what is still free: the best way to understand what an allocator does to the heap.

Open the exercise
C

descend

descend(n, ...) calls itself from n down to 1, reporting each level on the way down and again on the way back up. The calls coming back up leave in the reverse order of the ones going down: that's the stack of function frames, made visible.

Open the exercise

Frequently asked questions

What's the difference between the stack and the heap?

The stack holds local variables and is managed by the compiler: each variable is created when its function is entered and destroyed when it returns. The heap holds the blocks you request with malloc: they live until your free, whatever the functions do. The stack is fast and small; the heap is large and your responsibility.

Are global variables stored on the heap?

No. Globals and static variables have static storage duration: they take up a separate area, set up at startup for the whole run. The heap only holds what malloc, calloc or realloc put there.

Why does a big local array crash the program?

Because the stack has a maximum size, often 8 MB (ulimit -s). A larger local array goes past it, and the first access beyond the limit triggers a segmentation fault. Allocate that data on the heap with malloc.

Where are string literals stored?

In a read-only area of the program, alongside the code, not on the stack or the heap. That's why char *s = "text"; s[0] = 'X'; crashes: you're writing to an area that can only be read. For a string you can modify, declare an array: char s[] = "text";.

Should I always use the heap, just to be safe?

No. The stack is faster and frees itself, so it's the right choice for data of known, moderate size. Keep the heap for data that is large, varies in size, or has to outlive the function that creates it.

Sources

Now write it yourself.