Scope and storage
A name in C is visible inside a region of the source, and the object it names lives for a period of the program. Scope and lifetime are two different questions, and the keyword static changes the second without changing the first. Lesson 17 introduced static on a function; here it is applied to objects.
Block scope
A variable declared inside braces is visible from its declaration to the closing brace. A second declaration of the same name in an inner block hides the outer one, and the outer object comes back when the block ends. Each if, for and function body is such a block.
#include <stdio.h>
int main(void)
{
int value = 10;
printf("outer: %d\n", value);
{
int value = 20;
printf("inner: %d\n", value);
}
printf("outer again: %d\n", value);
return 0;
}
gcc -Wall -Wextra blocks.c -o blocks
./blocks
outer: 10
inner: 20
outer again: 10
The inner declaration creates a new object; it does not assign to the outer one. A block used only for its scope is legal, but a named function usually explains the intent better.
static locals keep state
A static local is created once, before the program starts, instead of on entry to the block. It keeps its value between calls, so a function can count its calls without a global name. Its scope is still the block: no other function can see it.
#include <stdio.h>
static void count(void)
{
static int calls;
calls++;
printf("call %d\n", calls);
}
int main(void)
{
count();
count();
count();
return 0;
}
gcc -Wall -Wextra counter.c -o counter
./counter
call 1
call 2
call 3
Without static every call would start again from an undefined value. A static local is not thread-safe: two threads inside the same function share it, a problem the part on POSIX threads comes back to.
File scope and extern
A variable declared outside every function has file scope: it is visible from its declaration to the end of the file. The definition reserves storage and may initialise it; an extern declaration only promises that the object exists somewhere, which lets code above the definition use it.
#include <stdio.h>
extern int total;
int total = 0;
static void add(int value)
{
total += value;
}
int main(void)
{
add(4);
add(6);
printf("total: %d\n", total);
return 0;
}
gcc -Wall -Wextra globals.c -o globals
./globals
total: 10
In a multi-file program the extern declaration usually sits in a header and the definition in one source file, which is the pattern of lesson 19. A static file-scope object is the opposite: it is private to its file and cannot be named from another translation unit.
Initialisation and lifetime
Objects with static storage duration, which means file-scope objects and static locals, start as zero. Automatic objects, the ordinary locals inside a block, start with an indeterminate value unless the declaration has an initialiser. The program shows both rules on values that are defined.
#include <stdio.h>
static int total;
static double ratio;
int main(void)
{
int local = 7;
printf("static int: %d\n", total);
printf("static double: %.1f\n", ratio);
printf("local: %d\n", local);
return 0;
}
gcc -Wall -Wextra zeroed.c -o zeroed
./zeroed
static int: 0
static double: 0.0
local: 7
The zeroing is done by the runtime before main, so it costs nothing per call. Relying on it for an automatic variable is the mistake that follows: an uninitialised local is not zero, it is whatever the stack memory held.
Shadowing and uninitialised reads
An inner declaration with the same name as an outer one compiles and works on the inner object, but the reader may believe the outer one was changed. -Wshadow is not part of -Wall; ask for it when a project uses many globals:
#include <stdio.h>
int value = 10;
int main(void)
{
int value = 20;
printf("%d\n", value);
return 0;
}
gcc -Wall -Wextra -Wshadow shadow.c -o shadow
./shadow
shadow.c: In function 'main':
shadow.c:7:9: warning: declaration of 'value' shadows a global declaration [-Wshadow]
7 | int value = 20;
| ^~~~~
shadow.c:3:5: note: shadowed declaration is here
3 | int value = 10;
| ^~~~~
20
The second mistake is reading an automatic variable before giving it a value. The output would be whatever happened to be in that memory, so the program is not run here; the compiler reports the read when optimisation lets it follow the value:
#include <stdio.h>
int main(void)
{
int total;
printf("%d\n", total);
return 0;
}
gcc -O2 -Wall -Wextra uninit.c -o uninit
uninit.c: In function 'main':
uninit.c:7:5: warning: 'total' is used uninitialized [-Wuninitialized]
7 | printf("%d\n", total);
| ^~~~~~~~~~~~~~~~~~~~~
uninit.c:5:9: note: 'total' was declared here
5 | int total;
| ^~~~~~
The warning is not guaranteed at -O0, because the compiler does not track every value there. The rule is independent of warnings: every object is initialised where it is declared, the habit from lesson 5.
Exercises
- Write a function with a static local that returns the next value of a sequence, and call it five times from main.
- Move that counter to file scope and make it static, then explain which names in the file can still see it.
- Declare a local with the same name as a file-scope object, compile with and without -Wshadow, and describe the difference.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
How far does a variable declared inside a block remain visible?
Answer
To the closing brace of that block
Block scope starts at the declaration and ends at the closing brace, and an inner declaration can hide an outer one.
-
What is special about the lifetime of a
staticlocal variable?Answer
It lives for the whole program
A static local has static storage duration: it is created once, keeps its value, and its name is still limited to the block.
-
What is the difference between a file-scope
staticobject and anexterndeclaration?Answer
The static one is private to its file
staticgives internal linkage and hides the name from other files;externpromises that the object exists somewhere else. -
Which objects start as zero before
mainruns?Answer
Objects with static storage duration
File-scope objects and static locals are zeroed by the runtime; automatic objects without an initialiser stay indeterminate.
-
What does this program print?
#include <stdio.h> int main(void) { int value = 10; { int value = 20; printf("inner: %d\n", value); } printf("outer again: %d\n", value); return 0; }Answer
inner: 20 / outer again: 10
The inner declaration is a separate object, so the outer variable is unchanged and still holds 10.
-
What does this program print?
#include <stdio.h> static void count(void) { static int calls; calls++; printf("call %d\n", calls); } int main(void) { count(); count(); count(); return 0; }Answer
call 1 / call 2 / call 3
The static local keeps its value between calls, so the three increments print 1, 2 and 3.
-
What does this program print?
#include <stdio.h> int total = 0; static void add(int value) { total += value; } int main(void) { add(4); add(6); printf("total: %d\n", total); return 0; }Answer
total: 10
Both calls update the same file-scope object, which starts at zero and ends at 10.
-
What does this program print?
#include <stdio.h> static int total; static double ratio; int main(void) { printf("static int: %d\n", total); printf("static double: %.1f\n", ratio); return 0; }Answer
static int: 0 / static double: 0.0
Objects with static storage duration are zeroed before
main, so both print zero. -
Which flag reports an inner variable that hides a file-scope object?
Answer
-Wshadow-Wshadowis not enabled by-Wall, so it has to be requested when a project uses many globals. -
This program is built with
gcc -std=c17 -O2 -Wall -Wextra uninit.c -o uninit. What does GCC warn about?#include <stdio.h> int main(void) { int total; printf("%d\n", total); return 0; }Answer
'total' is used uninitialized [-Wuninitialized]
At
-O2the compiler follows the value and reports the read before any assignment.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev