Why this matters
Every variable has two properties that are easy to confuse: where in the source its name can be used (its scope) and when its memory exists while the program runs (its lifetime, or storage duration). Getting them straight explains why a variable declared inside a loop "forgets" its value, why two functions can both have a variable called i without interfering, how a function can remember something between calls, and why experienced C programmers are wary of global variables. It also tells you exactly which region of memory each variable lives in.
Scope: where a name is visible
Scope is a compile-time idea: it is about the text of your program.
Block scope. A name declared inside a block { ... } (including a function body) is visible from its declaration to the closing brace of that block. Function parameters have the scope of the function body. A variable declared in a for header is visible only in that loop.
int main(void)
{
int a = 1; /* visible until the end of main */
if (a > 0) {
int b = 2; /* visible only inside this if block */
printf("%d %d\n", a, b);
}
/* b does not exist here: using it gives "error: 'b' undeclared" */
for (int i = 0; i < 3; i++) {
/* i visible here */
}
/* and not here */
return 0;
}
File scope. A name declared outside every function is visible from its declaration to the end of the file. Functions themselves have file scope, which is why a function defined above main can be called from main.
Shadowing. An inner block may declare a name that already exists in an outer scope. Inside the inner block, the new name hides the outer one:
int x = 1;
{
int x = 2; /* a different variable that shadows the outer x */
printf("inner x = %d\n", x);
}
printf("outer x = %d\n", x);
inner x = 2
outer x = 1
Shadowing is legal and -Wall -Wextra do not mention it, but it is a common source of bugs (see the quiz). The extra flag -Wshadow reports it:
z.c:10:13: warning: declaration of 'total' shadows a previous local [-Wshadow]
z.c:8:9: note: shadowed declaration is here
Lifetime: when the memory exists
Lifetime is a run-time idea. C has three storage durations you will use (a fourth, thread storage, is for multi-threaded programs):
- Automatic: variables declared in a block without
static. Created when execution enters the block, destroyed when it leaves. They live in the current stack frame, as you saw two lessons ago. Each call, and each pass through a loop body, gets fresh ones. - Static: variables declared at file scope, or with the keyword
staticinside a block. They exist for the whole run of the program, from beforemainstarts until the program ends, and are initialized exactly once. They live in the program's data region, not on the stack. - Allocated: memory you request with
mallocand release withfree, on the heap. That is module 8.
process memory
+----------------------+
| stack | automatic variables: one frame per active call
| tick(): b |
| main(): x |
+----------------------+
| ... |
+----------------------+
| heap | allocated (module 8)
+----------------------+
| data / bss | static storage: globals and static locals
| total_calls, calls | exist for the whole run
+----------------------+
| code |
+----------------------+
Initialization differs too
Automatic variables are not initialized unless you initialize them. They start with whatever bytes were left in that part of the stack by earlier calls: garbage. Reading one before assigning it is undefined behavior. gcc catches the simple cases:
u2.c:5:5: warning: 'x' is used uninitialized [-Wuninitialized]
but not all of them: int sum; for (...) sum += i; produces no warning at all at -O0, and only gets one when compiling with optimization (-O2), because the deeper analysis runs as part of the optimizer. Make it a habit to initialize every variable when you declare it.
Static-storage variables are always initialized, exactly once, before main runs. If you give no initializer, they start as zero. The initializer must be a constant expression (you cannot write static int n = some_function(); in C). Zero-initialized statics go into a section called .bss, which takes no space in the executable file; the kernel simply provides zeroed memory.
static local variables: memory that survives calls
Putting static on a local variable keeps its scope local (only that function can name it) but gives it static lifetime: one variable for the whole program, kept between calls.
#include <stdio.h>
int total_calls = 0; /* global: file scope, static storage */
int never_set; /* global with no initializer: starts at 0 */
int count_calls(void)
{
static int calls = 0; /* initialized once, keeps its value */
int temp = 0; /* automatic: fresh on every call */
calls++;
temp++;
total_calls++;
printf("calls = %d, temp = %d\n", calls, temp);
return calls;
}
int main(void)
{
count_calls();
count_calls();
count_calls();
printf("total_calls = %d, never_set = %d\n", total_calls, never_set);
return 0;
}
calls = 1, temp = 1
calls = 2, temp = 1
calls = 3, temp = 1
total_calls = 3, never_set = 0
The line static int calls = 0; is not an assignment executed on each call; it describes the initial value the variable has when the program starts. That surprises people coming from other languages.
Static locals are useful for counters, ID generators, caches of an expensive result, and "initialize on first use" flags. Their downside: the function now has hidden state, so calling it twice with the same arguments can give different results, which makes it harder to test and unsafe to call from several threads at once.
Global variables
A variable declared at file scope is often called a global. Any function in the file can read and change it (and, as module 11 shows, functions in other files too). That convenience is exactly the problem:
- Any function might change it, so understanding one function requires reading all of them.
- Functions that use globals depend on hidden inputs, which makes them hard to test and reuse.
- Name clashes appear when programs grow.
Reasonable uses exist: a program-wide configuration flag set once at startup, or a table of constants. But the default should be: pass data in through parameters and out through return values, and when a function truly needs private memory between calls, use a static local, which at least limits who can touch it.
You can see where the compiler put each variable with nm on the object file:
0000000000000008 b calls.0
0000000000000000 T count_calls
0000000000000058 T main
0000000000000004 B never_set
U printf
0000000000000000 B total_calls
B is a global in .bss; b (lowercase) is a symbol private to this file, here the static local calls, renamed calls.0 so it cannot clash with other functions' statics. T marks functions. The exercises in this lesson use exactly this output to check that you used a static local rather than a global.
static on functions and globals: a preview
The keyword static has a second meaning at file scope. A function or global declared static is private to its file: other source files cannot see or call it. This is called internal linkage:
static int helper(int x) /* usable only within this .c file */
{
return x * 2;
}
In a single-file program this changes nothing visible, but in multi-file programs (module 11) it is how C does encapsulation: everything not meant to be called from outside is static.
So static means two different things depending on where it appears:
| where | effect |
|---|---|
| inside a function, on a variable | static lifetime (survives calls), scope still local |
| at file scope, on a variable or function | internal linkage: private to this file |
Common mistakes
- Using an uninitialized local. Garbage values, different from run to run. Initialize at the declaration, and try
-O2once in a while to get the deeper warnings. - Declaring a variable inside a loop when it should accumulate across passes. It is recreated on each pass. Declare it before the loop.
- Accidental shadowing:
int totalagain in an inner block. Use-Wshadowwhen in doubt. - Expecting
static int n = 0;to resetnon each call. It is initialized only once. - Reaching for a global to "share" a value between two functions. Pass it as a parameter instead.
Recap
- Scope is where a name can be used (block or file); lifetime is when its memory exists.
- Automatic locals live in the stack frame, are recreated on each entry and start as garbage.
- Static storage (globals and
staticlocals) lives for the whole run in the data region, is initialized once, and starts at zero if no initializer is given. - A
staticlocal keeps a function's private state between calls. - Inner declarations shadow outer ones;
-Wshadowwarns. - At file scope,
staticmakes a function or variable private to its file.
Going further: compile a program with a few globals, static locals and functions with gcc -c, then run nm and size on the object file. Initialize one global to a nonzero value and watch it move from B (bss) to D (initialized data).