There is no stack
Some C programmers often refer to “the stack” or “the machine” as though there is some kind of direct link between C and the computer hardware that a program happens to be running on.
The C programming language does actually target a machine, it’s called the “abstract machine”, C11 §5.1.2.3p1:
The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant.
C11 §5.1.2.3p4:
In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).
C11 §5.1.2.3p6:
The least requirements on a conforming implementation are:
— Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
What this all means is that it is a C implementation’s responsibility to simply make a program appear to execute on your particular computer in the same manner as described for the abstract machine. C does not prescribe the use of registers, stacks, assembly, or any other computer specific concept one might imagine. C can be either compiled or interpreted or anything in between. For example, a human who reads any input from somewhere and writes the output somewhere according to the program’s semantics could be considered a C implementation.
Consider this simple example:
int main(void) {
for (long i = 0; i < 1000000000; i++);
printf("%d\n", i);
return 0;
}
A C compiler could generate some code to store i in a register or memory, increment i, check if i is still less than 1 billion, loop back and increment i until i is big enough. Alternatively it can prove that there are no side effects from this loop and simply output “1000000000\n” without bothering to use a loop or an i variable, or even call a printf function at all. Many compilers will do this with optimization enabled.
There are no “stack variables” or “heap variables”
Although it is common for compilers to utilise things such as stacks and heaps, there is nothing gained by referring to variables based on where/how we think they will be stored when using a given compiler and machine. Even if the machine and compiler you’re using normally results in placing variables on a stack, how does this help you? Optimization might cause the variable to be placed in a register instead, or eliminated entirely because it’s unused or the [sub]expressions it’s used in result in constants. There’s no value in imagining what a compiler is going to do. If you are concerned about performance you can profile and then find out where the bottlenecks are.
A correct C term which sometimes corresponds to how variables are stored is “storage duration”. Unsurprisingly, storage duration determines the lifetime of objects. C11 has four storage durations1:
The explanations below are paraphrased or copied directly from the referred standard sections above.
Automatic storage duration
Automatic storage duration is usually the most common. Variables declared within a block without extern or static and variables declared to be a function parameter have automatic storage duration. These objects’ lifetimes extend until the execution of that block ends. Referring to an object outside its lifetime is undefined behavior.
A common beginner mistake is to return a pointer to an automatic storage object from a function and then refer to that object in the calling code. One approach is to return a pointer to a static object, but this is usually a bad idea — it makes the function nonreentrant:
#include <stdio.h>
#include <string.h>
char *contrived(int n) {
static char result[16];
strcpy(result, n ? "true" : "false");
return result;
}
int main(void) {
char *t = contrived(1);
printf("t = %s\n", t);
char *f = contrived(0);
printf("t = %s\n", t);
printf("f = %s\n", f);
return 0;
}
// output:
// t = true
// t = false
// f = false
Here, because we’re always pointing to the same result, the subsequent calls overwrite the static array in the function. A better approach is for the caller to allocate space and pass a pointer to where it wants the result to be written.
Static storage duration
If an object is declared at file scope, or declared static
as in result
in the above code, and is not declared _Thread_local
then it has static storage duration. Its lifetime is the entire execution of the program.
Allocated storage duration
When a call to aligned_alloc
, calloc
, malloc
, or realloc
succeeds, it returns a pointer to allocated storage. Its lifetime exists until it is deallocated with free or realloc.
Thread storage duration
If an object is declared _Thread_local
(new in C!1, optional), it has thread storage duration. Its lifetime is the entire execution of the thread for which it is created. There is a distinct object per thread, and use of the declared name in an expression refers to the object associated with the thread evaluating the expression.