C / DYNAMIC MEMORY
The stack versus the heap and choosing between them
Decide whether each object belongs in automatic or allocated storage by reasoning about its lifetime, its size bound, and the real cost of malloc.
What you will learn
- Decide storage from two questions: how long must it live, and is its size bounded?
- Keep buffers sized from input off the stack even when their lifetime is short.
- Return small fixed-size structs by value instead of malloc-ing them.
- Take a destination pointer and capacity so the caller picks stack or heap.
Understanding The stack versus the heap and choosing between them
Every object in a C program has a storage duration, and two of them matter here. A local variable has automatic storage duration: the compiler already knows how many bytes the function's frame needs, so entering the function moves the stack pointer once and returning moves it back. An object obtained from malloc has allocated storage duration: the allocator keeps its own bookkeeping, hands out blocks in any order, and each block stays alive until you release it. The stack therefore supports only strictly nested lifetimes while the heap supports arbitrary ones, and that is the whole difference.
Two questions decide where an object goes: does anything need to read it after the creating function returns, and is its maximum size a compile-time constant small enough to reserve on every call? A yes to the first rules out automatic storage no matter how tiny the object is, because the frame is gone. A no to the second rules out the stack even for a buffer that dies three lines later, since the stack is a single fixed reservation, commonly 8 MiB for the main thread on Linux and often 512 KiB or less for other threads. Overrunning it is a segmentation fault rather than a NULL return you can check, so a length that comes from a file, a socket, or argv must never size a local array.
When both answers permit the stack, use it. A frame slot costs nothing to allocate, nothing to release, cannot leak, and is almost certainly already in cache, whereas malloc walks its bookkeeping structures, may take a lock, may ask the kernel for more pages, and adds both a failure path and an ownership obligation to your function's contract. The habit to build is automatic by default, allocated when lifetime or size forces it, and static when there is exactly one of the thing for the entire program.
Cost differences show up most in hot loops and in leaf functions called millions of times, where a malloc/free pair can dominate the useful work.
<stdio.h>
<stdlib.h>
CAP/* capacity fixed at compile time */
/* Automatic storage: the frame supplies the array and the return
statement reclaims it, but the capacity is baked in. */
static long long sum_on_stack(int n)
{
int a[CAP];
long long total = 0;
if (n > CAP) n = CAP; /* the caller cannot ask for more */
for (int i = 0; i < n; i++) a[i] = i;
for (int i = 0; i < n; i++) total += a[i];
return total;
}
/* Allocated storage: n is a run-time value and 16 MB will not fit in a
frame, so the block is requested and released explicitly. */
static long long sum_on_heap(size_t n)
{
int *a = malloc(n * sizeof *a);
long long total = 0;
if (a == NULL) return -1;
for (size_t i = 0; i < n; i++) a[i] = (int)i;
for (size_t i = 0; i < n; i++) total += a[i];
free(a);
return total;
}
int main(void)
{
printf("stack sum: %lld\n", sum_on_stack(100));
printf("heap sum: %lld\n", sum_on_heap(4000000));
printf("frame array: %zu bytes, heap block: %zu bytes\n",
CAP * sizeof(int), 4000000 * sizeof(int));
return 0;
}
Automatic storage is the default; only an object that must outlive its function, or whose size is not a small compile-time constant, justifies the heap.
Worked examples
Return the value, not a pointer to it
A small fixed-size struct needs neither the heap nor an ownership rule, because returning it copies it into the caller.
<stdio.h>
struct point { double x, y; };
static struct point midpoint(struct point a, struct point b)
{
struct point m = { (a.x + b.x) / 2, (a.y + b.y) / 2 };
return m;
}
int main(void)
{
struct point p = midpoint((struct point){0, 0}, (struct point){3, 5});
printf("midpoint = (%.1f, %.1f), struct is %zu bytes\n", p.x, p.y, sizeof p);
return 0;
}
Example explained
Line 1struct point is 16 bytes, so copying it out is a couple of moves, far cheaper than a malloc and free pair.
Line 2m lives in midpoint's frame, but return copies its value before the frame disappears, so nothing dangles.
Line 3Because there is no allocation, midpoint has no failure case and cannot return NULL for the caller to test.
Line 4sizeof p works here because p has array-free struct type in scope; the size was known when the code was compiled.
Let the caller choose the storage
A function that takes a destination pointer and a capacity works unchanged whether the buffer is a local array or a malloc'd block.
<stdio.h>
<stdlib.h>
static int render(char *dst, size_t cap, int id)
{
return snprintf(dst, cap, "item-%04d", id);
}
int main(void)
{
char small[16]; /* the caller chose the stack */
int n1 = render(small, sizeof small, 7);
printf("stack: %s (%d chars)\n", small, n1);
char *big = malloc(64); /* the caller chose the heap */
if (big == NULL) return 1;
int n2 = render(big, 64, 12345);
printf("heap: %s (%d chars)\n", big, n2);
free(big);
return 0;
}
Example explained
Line 1render never allocates, so it imposes no ownership rule and the same code serves both storage kinds.
Line 2sizeof small is the array's capacity only in main; inside render, dst is a plain pointer and the size must be passed in.
Line 3snprintf returns the length it wanted to write, which is how the caller learns that the storage it picked was too small.
Line 4Only main knows how long the text must live, which is exactly why the storage decision belongs to main.
Important notes
The standard says automatic and allocated storage duration; stack and heap describe how implementations usually provide them, and it is the lifetime rules, not the layout, that your code may rely on.
A variable-length array such as int a[n] does not escape the stack limit. It puts a run-time size into the frame with no way to report failure, and it is optional since C11, so prefer malloc when n comes from input.
Common mistakes
Declaring char buf[1048576] as a local 'to be safe': every call reserves a megabyte of frame, and the overflow inside a thread or a recursion arrives as a SIGSEGV with no diagnostic pointing at the array.
Calling malloc for a 16-byte struct that dies at the end of the function, which buys a NULL check, a free, and an ownership question in exchange for nothing.
Expecting stack exhaustion to report itself the way malloc does; there is no return value to test, so the only defence is bounding frame sizes before the program runs.
Try it yourself
Change, predict, then run
Write reverse_into(char *dst, size_t cap, const char *src) and a reverse_dup(const char *src) that returns a malloc'd copy, then call the first with a local char buf[64] and the second with a string built at run time. Print both results and state which one you would export from a library, and why.
Open the C workspaceCheck your understanding
A function needs a temporary buffer whose length equals one line of user input, anywhere from 1 byte to several megabytes. Nothing reads the buffer after the function returns. Where should it live?
- On the stack, because it dies when the function returns and lifetime is the only criterion
- On the stack as a variable-length array, since a VLA lets the frame hold any run-time size
- On the heap, because the size has no small compile-time bound and a huge frame overruns the stack with no detectable error
- In static storage, so one shared buffer avoids both the stack limit and the cost of malloc
Show answer
Lifetime is one criterion and a bounded compile-time size is the other; either one alone can force the heap. The first option is tempting because the buffer really is short-lived, but a several-megabyte frame blows past the typical 8 MiB main-thread limit and far past a thread's limit, and that overrun is a crash rather than a value you can check. The VLA option moves the same unbounded reservation into the frame instead of avoiding it, and a static buffer still needs a fixed bound while making the function unsafe to re-enter.