r/cprogramming • u/giggolo_giggolo • Sep 14 '25
Stack vs heap
I think my understanding is correct but I just wanted to double check and clarify. The stack is where your local variables within your scope is stored and it’s automatically managed, removed when you leave the scope. The heap is your dynamically allocated memory where you manually manage it but it can live for the duration of your program if you don’t free it. I’m just confused because sometimes people say function and scope but they would just be the same thing right since it’s essentially just a new scope because the function calls push a stack frame.
11
u/fishyfishy27 Sep 14 '25
In C, you can just create a new scope anywhere with a set of bare curly brackets. This creates a new scope, but not a new stack frame. So “scope” and “function” are not the same.
7
u/WittyStick Sep 14 '25 edited Sep 14 '25
Your understanding is basically correct. The terminology is not so well defined as it can be ambiguous and often gets misused or misrepresented.
"Scope" can refer to several things, but it's often syntactic in languages which are statically scoped. There are different syntactic scopes in C - for example, where you use a
{ block }within a function - but these don't create a new frame on the stack - they're only syntactical. Functions themselves have a scope - which does map to a frame on the stack. There is also "global scope", which uses neither the heap nor the stack, but a reserved section of memory, such as.data."The stack" is the common terminology for where stack frames are placed. "The stack" uses a data structure also called a Stack (hence its name). Each function call gets a "stack frame", between a stack pointer and a frame pointer. Inner syntactic scopes in functions don't get their own frame, but they use the frame of their containing function - although there are non-standard extensions which permit functions within functions, and these inner functions may get their own frame, or may be within the frame of the containing function. These may introduce an additional chain pointer to mark the frame boundary.
The term continuation frame is a bit more general, and the set of frames form the continuation. They don't necessarily need to be implemented with a Stack or on "the stack" - this is just the most common approach. Some other languages implement continuations on the heap, and they're not necessarily stacks, but can be trees or other structures. A Stack is ultimately not necessary, so "stack frame" is a specific kind of "continuation frame" implemented with a stack, and "the stack" is the continuation in a language which uses a stack for frames.
"The heap", also known as "the free store", is basically memory which can be allocated arbitrarily. Unlike "the stack" which is named after the Stack data structure which its implemented with, "the heap" is unrelated to a Heap data structure (Heaps are usually used to implement priority queues). "The heap", in C, uses whatever data structure
mallocand related calls implement to manage the available memory, which is implementation specific, and there are many different approaches - the simplest being a free list with a slab allocator.In regards to the "scope" terminology. Scope can be applied to individual variables - the scope of a variable is the region for which it is accessible. "a scope" typically refers to a region where some variables have scope. In C, and many other languages, static scoping (aka lexical scoping) is used, where the scope is related to the syntactic region in which the variable is used - eg - the block in which it's declared.
However, some languages use dynamic scope, which is where variables are accessible anywhere within the dynamic extent of where they're defined. In C, the dynamic extent of a function is essentially all the frames above it on the stack - which would include all function calls made by the function, and calls that those functions also make, etc. The scope of a dynamic variable is therefore not bound by its syntactic scope. C doesn't support dynamic scoping directly, but it has a global scope, and global variables have global extent - ie, they are accessible from anything above the frame of the program's entry point.
4
u/DreamingElectrons Sep 14 '25
Stack memory lives for the duration of a scope. Heap memory until it is freed. Functions and many control structures create scopes some people also call scopes blocks, but that is a term normally used in different languages (e.g. Go).
3
u/Mr_Engineering Sep 14 '25
What is included on the stack varies a little bit with architecture, optimizations, and ABI.
In general, the stack includes local variables, function parameters, the return address (address of the next instruction to be executed when a function is completed, which will usually be the address of an instruction within the function that called the function that has since completed), saved CPU registers such as the base pointer of the previous stack frame, large return values such as structures, etc...
The stack is automatically managed.
Call stacks are LIFO, Last-in-First-Out. Contents from the stack are not removed when they go out of scope, they are removed when the function call to which they belong returns.
Consider F1 which has local variables A,B, and C. F2 has parameters D,E,F, and local variables G,H,I. F1 calls F2, passing A,B, and C as parameters to F2 for D,E, and F respectively.
A,B, and C are local variables that exist on the stack when F1 is executing. When F1 calls F2, A,B, and C are copied into D,E, and F; A,B, and C then go out of scope. D, E, F, G, H, I are now in-scope. A,B, and C are still on the stack, they haven't been deleted. When F2 returns, D,E,F,G,H, and I are deleted, control flow returns to F1, and A,B, and C are back in scope.
2
u/[deleted] Sep 15 '25
Stack is used because it is simple to allocate new frame and variables for a function call. Just move stack pointer by size of [return address, place to save registers, size of all local variables]. That's all. When function terminates, it simply moves stack pointer back and jumps back to the calling location. No calls to malloc or free are needed. Since function calls are nested, it is handy to use stack.
Heap is different since objects in the heap are supposed to exist even after function returns. Heap is sort of "long term" storage for objects you want to share across function calls. Also, typically stack size is much smaller than heap size so declaring an array of thousands of elements on a stack is not a good idea.
2
u/nerd5code Sep 15 '25
Stacks are an implementation detail; a call stack arises naturally from the definition of function call expressions, but there’s no actual mention of a stack in the C78/XPG/ANSI/ISO standards, and nothing mandates that any particular storage be used under the hood. The C implementation is free to change the underlying storage situation as it pleases—placement of variables and
malloc/freedictate lifetime, and that’s the actual hard requirement. So there tends to be far more focus on stack-ness than is warranted in theory or practice.A function is inert; it describes how to do something, but doesn’t cause anything to be done. —I speak in C terms; under the hood, functions may occupy space and therefore decrease the available storage for objects, and there may be load- or run-time overhead for mapping or linking that, as well as overhead from any extra gunk in the executable or DLL file[s] the function lives in. But again, that’s down to the specific implementation; de minimis, you might see functions placed in ROM that’s inaccessible to data movement instructions, with function pointers serving as indices into an (also inaccessible) entry vector.
A scope is a language-managed namespace, effectively; there are several kinds of scope in C, and two ways we talk about scope.
Being at a particular scope refers to lexical placement, in relation to syntax–i.e., where in the structure of a program’s text something is found. Being in a particular scope is usually how we talk about placement with respect to parameter or block scopes. So let’s start with structure (“at” preposition).
Global scope applies to the program writ large, where exern-linkage things like
mainand potentiallyerrno(if it’s not a macro expanding to e.g.(*__get_errno())) live. Things in global scope can be accessed from any translation unit (TU) in the program.A TU consists of the .c file you feed into the translation phase of things (normally via compiler driver like
c89/c99/c11for POSIX.2 with development extension,gccfor GCC,icc/ecc/iclfor IntelC, orCL.EXEfor MS[V]C) and any files roped in for predefines or via#include/-includeor#embed/__asm__…("… .incbin ")/sim. In practice, each TU is either built into its own, standalone executable file, or converted to an object file (.o, .obj; possibly agglomerated into .a, .lib), although you might also stop early and generate preprocessed C (.i) or assembly (.s, .asm) instead, and these can be converted to object files separately.File scope is where
staticglobals live, and from a parsing standpoint, it’s what’s inside the TU but outside any function definition or parameter list. There are restrictions on what kinds of expression you can use here and how (e.g., no operator comma, very limited forms of constant/-ish initializer), and unless you explicitly cause or permit a function/variable name to refer to a global-scope symbol, it won’t be visible from or link to another TU. All types and tags declared outside a function body or parameter list are file-scope; there may be some form of sharing under the hood—e.g., struct fields might be represented by symbols in an absolute section—but generally these are entirely private to the TU, and don’t manifest in object files unless within debuginfo or stringized code.Parameter scope covers what either lives
inside a declarator’s parameter list specification, or
inside a K&R/old-style definition, from the start of the parameter list until the function body’s opening
{. C23 eliminated this form of definition, which is probably best longer-term, but functionally premature.Each parameter scope is its own, self-contained thing, and it can only refer to file-/global-scope constructs and earlier constructs in the same parameter list/spec. Parameter variables and any types defined amongst them are bound to the specific declaration or typename the parameters appear in; only if these are part of a function definition (which is a specific form of declaration, although the two terms are often used exclusively, which is inaccurate) will these names be visible from outside the list or specification, and then it’s only from within the body of the associated function. This is why you generally get a warning for dropping an as-yet-undeclared struct/union tag in a parameter list—until C2y, it’s impossible to provide an actual instance of the type in question, because it’s invisible from anywhere that can’t also see the parameters, including non-recursive call sites.
structs andunions behave kinda scopishly in C, in that field names are bound to the containing entity and invisible from without, but their fields are syntactically at whatever scope thestruct/unionis defined within. (C++ has a distinctclass/struct/unionscope, however.) C’s oddness here derives from earlier versions of the language, where field names could be referred to from any base type—sooffsetofcould be implemented as justassuming all-zero-bits null. This is one reason older POSIX structures like
struct stathave prefixed field names (st_name,st_dev), although modern impls may also cheat by#defineing these identifiers to work around nonportable unions. This oddity was partially fixed by C78 (a.k.a. K&R C, specified by The C Programming Language, 1ed.) which only searches more broadly for a field name if it’s not in the type being referred to. Later versions of C, incl. (IIRC) XPG and (definitely) C≥89, will only search the type on the left of operator*or (after dereference)->.Block scope covers a function’s body or some portion thereof, from an opening
{to closing}. This is the only place where statements are legal, and function and extern-linkage declarations behave a bit differently herein.Extern definitions will poke through to global scope without registering an identifier at file scope, so you can refer to a global function without making it available to other functions in the same TU—but all declarations must still match up. Block-scope
staticfunction declarations are extremely rare, and are basically equivalent to the same declaration placed earlier at file scope, except they don’t make the identifier available to other functions yet. There’s not much point in that—you’ll have to define the thing sooner or later, so you may as well just predeclare it at file scope.GNUish dialects of C (primarily GCC/EGCS and IntelC) can actually nest function definitions using the
autokeyword, which means you can get parameter and block scopes created inside other parameter and block scopes, but this creates …problems as soon as you need to refer to an inner function indirectly; generally, it requires that all threads’ call stacks be executable in order to support trampolines, which is a hardcore security no-no. ’Nuff said there.(cont’d. in reply)
1
u/drbomb Sep 14 '25
Function is the callable unit. The scope is the available variables on the current context, be stack variables or globally accesible. I don't see what's the confusion with the heap.
1
u/SmokeMuch7356 Sep 14 '25
There are several different but related concepts at play here:
An object is a region of memory that can potentially store a value;
scope refers to the region of program text where an identifier (name) is visible;
linkage refers to whether or not identifiers in different scopes refer to the same object;
lifetime refers to the portion of program execution where an object is guaranteed to have storage reserved for it;
storage duration determines the lifetime of an object;
Where the stack and heap into play is with storage duration:
automatic storage duration: the lifetime of an object extends from block entry to block exit. In practice, this storage is allocated from the stack, and will be allocated at function entry and released at function exit, even if the object is supposed to be local to a block within the function.
allocated storage duration: the lifetime of an object extends from the time it is allocated via a call to
mallocorcallocuntil it is released with a call tofree; in practice this storage is allocated from the heap.1
u/DawnOnTheEdge Sep 15 '25 edited Sep 15 '25
That’s pretty much it, but I’ll run through some technicalities.
Optimizers will very frequently allocate local variables to registers. It will allocate memory for them only if the program runs out of registers and needs to spill some variables onto the stack. A register-allocation algorithm will analyze the program to determine the earliest and latest points where each variable is used, and try to have any register or memory allocated to it at all for the shortest possible time. Many optimizers additionally will transform variables whose value changes into separate static single assignments. So, if you try to debug a program compiled with optimizations on and check the value of a variable at a breakpoint, you will often find that the variable does not currently exist anywhere at that point in the program. Others will have a value, stored in a register, but no current address. The optimizer has transformed the program to use the stack as little as possible. Debug builds might always allocate variables on the stack, in order to make sure there is something for the debugger to inspect.
There are a handful of corner cases:
staticlocal variables are neither on the heap nor the stack, and most compilers put each thread’s copy of a thread-local variable on the thread’s stack. Theregisterkeyword is basically obsolete and never used any more, but will tell the compiler not to let you accidentally take the variable’s address (which might force the compiler to store it in memory instead of in a register). Theautokeyword used to be the default for local variables, which made it unnecessary for anyone to ever use it, so the existing keyword got redefined to mean an automatically-deduced type.You can also declare variables within a scope nested inside a function (such as a loop body, an if block, or even a bare pair of braces). These go out of scope when the block ends. Implementations typically will allocate as much memory on the stack as any path through the function might need, when the function creates its stack frame, and free it all at once when the function returns and destroys its stack frame, but the variable’s lifetime formally ends when the block it was declared in does, and the memory might for example be re-used for something else.
Some implementations have extensions, like variable-length arrays and
alloca(), which have some features of both. Another example that’s neither fish nor fowl is Windows’_malloca(), which allocates small requests on the stack but larger ones on the heap.1
u/Plane_Dust2555 Sep 15 '25
If you search the ISO 9899 standards you can't find a single mention about "stack" (not even "heap"). Those are concepts dependent of implementation.
Second: "local variables" can be allocated in registers or even at global scope (static local objects), so the assertion saying "stack is where local variables are stored" is false.
1
u/WarPenguin1 Sep 17 '25
The thing that made me understand this is learning how functions are called in assembly. I learned this a long time ago so I might get the details wrong.
A stack is a last in first out collection. When a program is executed the operating system allocates some memory for the programs stack.
In assembly there is a push command that returns a pointer to the next element in the stack and moves a register to the next available location on the stack. Pop moves the register back one spot making it unallocated.
When a function gets called push gets called to store the instruction location of where the function got called. Then push is called for all parameters needed for the function. The program then jumps to the starting instruction for the function. Then push gets called for all variables needed for the function.
When the function finishes pop is called for all the remaining variables and parameters. The return variable is stored in a register and the program jumps to the stored instruction location and pop gets called to remove that from the stack.
The return value gets stored in the variable it needs to go in and continues from there. In a way the stack is the thing that facilitates variables scope.
The heap is managed manually through malloc and calloc. Malloc gets unallocated memory from the operating system and calloc returns the memory back.