C · lesson 3 of 25
Variables and Storage Classes in C
Declare and initialize C variables, then see scope and the storage classes auto, static, extern and register in real compiled examples.
By SkillByExample · Updated
Declaring and Initializing Variables
A variable is a named storage location with a fixed type. Initialize it where you declare it whenever you can:
#include <stdio.h>
int main(void) {
int age; /* declaration only: no value yet */
int score = 100; /* declaration + initialization */
double pi = 3.14159;
char grade = 'A';
age = 25; /* assignment after declaration */
printf("Age: %d, Score: %d, Pi: %.2f, Grade: %c\n",
age, score, pi, grade);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o declare declare.c
$ ./declare
Age: 25, Score: 100, Pi: 3.14, Grade: A
%.2f rounds pi to two decimal places for display. The variable still holds 3.14159. age is safe to print because it was assigned before it was read.
Reading a Variable Before It Has a Value
This loop forgets to initialize total. Here is what the same code does at two optimization levels:
#include <stdio.h>
int main(void) {
int total; /* never initialized */
for (int i = 1; i <= 3; i++) {
total += i; /* reads garbage on the first pass */
}
printf("total = %d\n", total);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o uninit uninit.c
$ ./uninit
total = 32772
$ gcc -Wall -Wextra -std=c17 -O2 -o uninit uninit.c
uninit.c: In function 'main':
uninit.c:6:15: warning: 'total' is used uninitialized [-Wuninitialized]
6 | total += i; /* reads garbage on the first pass */
| ~~~~~~^~~~
uninit.c:4:9: note: 'total' was declared here
4 | int total; /* never initialized */
| ^~~~~
$ ./uninit
total = 6
The unoptimized build compiled without a single warning and printed 32772, whatever was left in that stack slot. The -O2 build printed the “right” answer, 6, but only by luck. Reading an uninitialized local is undefined behavior, so neither result can be relied on. GCC’s uninitialized-variable check depends on analysis that runs during optimization, so it can miss this bug in a plain -O0 build. The fix is int total = 0;.
Variable Naming Rules
A name must start with a letter or underscore, can contain letters, digits and underscores, is case-sensitive, and cannot be a keyword. Breaking the rules is a compile error:
int main(void) {
int player_score = 0; /* valid: snake_case is idiomatic C */
int 2fast = 1; /* invalid: starts with a digit */
int for = 2; /* invalid: reserved keyword */
return player_score;
}
$ gcc -Wall -Wextra -std=c17 -o names names.c
names.c: In function 'main':
names.c:3:9: error: invalid suffix "fast" on integer constant
3 | int 2fast = 1; /* invalid: starts with a digit */
| ^~~~~
names.c:3:9: error: expected identifier or '(' before numeric constant
names.c:4:9: error: expected identifier or '(' before 'for'
4 | int for = 2; /* invalid: reserved keyword */
| ^~~
The first error message looks strange until you think like the lexer. It reads 2fast as the number 2 followed by a suffix fast (like the u in 2u), so it complains about an invalid number, not an invalid name. Names starting with an underscore followed by a capital letter or a second underscore (_Count, __x) are legal syntax but reserved for the compiler and standard library, so avoid them.
Scope: Where Variables Live
Block scope: a variable declared inside {} exists only within that block. Using it afterwards is a compile error:
#include <stdio.h>
int main(void) {
int x = 10;
{
int y = 20; /* y exists only inside this block */
printf("%d\n", x + y); /* x is visible from the outer block */
}
printf("%d\n", y); /* y is gone here */
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o scope scope.c
scope.c: In function 'main':
scope.c:11:20: error: 'y' undeclared (first use in this function)
11 | printf("%d\n", y); /* y is gone here */
| ^
scope.c:11:20: note: each undeclared identifier is reported only once for each function it appears in
Inner blocks can see outer variables, but outer code cannot see inner ones. Delete the last printf and the program compiles and prints 30.
File scope: a variable declared outside every function is visible to all functions below it in that file, and is zero-initialized even without an initializer:
#include <stdio.h>
int global_counter; /* file scope: zero-initialized automatically */
void increment(void) {
global_counter++; /* every function in this file can see it */
}
int main(void) {
printf("Before: %d\n", global_counter);
increment();
increment();
printf("After: %d\n", global_counter);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o global global.c
$ ./global
Before: 0
After: 2
Unlike total in the uninitialized example, global_counter is guaranteed to start at 0. Objects with static storage duration (globals and static locals) are zeroed before main runs. Automatic locals are not.
The Four Storage Classes
Storage classes control a variable’s lifetime (how long it exists), its default value, and its linkage (whether other source files can see it).
auto — The Default for Locals
Every local variable is auto unless you say otherwise, so int y = 10; inside a function means the same as auto int y = 10;. Nobody writes the keyword in C17 code. C23 goes further and reuses auto for type inference, the way C++ does. Automatic variables are created when their block is entered and destroyed when it exits.
static Local — Survives Between Calls
A static local is initialized once and keeps its value across calls:
#include <stdio.h>
void counter(void) {
static int count = 0; /* initialized once, keeps its value */
int fresh = 0; /* re-created on every call */
count++;
fresh++;
printf("count=%d fresh=%d\n", count, fresh);
}
int main(void) {
counter();
counter();
counter();
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o static_local static_local.c
$ ./static_local
count=1 fresh=1
count=2 fresh=1
count=3 fresh=1
Both variables are incremented once per call, yet only count climbs. The line static int count = 0; is not a statement that runs on every call. It describes the variable’s starting value, applied once before the program starts. fresh is a new automatic variable each time, so it is always 1.
extern and File-Level static — Sharing Across Files
extern declares a variable defined in another source file. A static at file scope does the opposite: it hides a name inside its own file. Here are two files compiled together:
/* globals.c */
int shared_value = 42; /* definition: storage lives here */
static int hidden_value = 7; /* internal linkage: this file only */
int get_hidden(void) {
return hidden_value;
}
/* main.c */
#include <stdio.h>
extern int shared_value; /* declaration: defined in globals.c */
int get_hidden(void); /* function declared, defined in globals.c */
int main(void) {
printf("shared_value = %d\n", shared_value);
printf("get_hidden() = %d\n", get_hidden());
shared_value++;
printf("after ++ = %d\n", shared_value);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o program main.c globals.c
$ ./program
shared_value = 42
get_hidden() = 7
after ++ = 43
main.c never defines shared_value. The linker connects the extern declaration to the definition in globals.c. hidden_value can only be reached through get_hidden(). Here is what happens if main.c tries to reach it directly with extern int hidden_value;:
$ gcc -Wall -Wextra -std=c17 -o bad main_bad.c globals.c
/usr/bin/ld: /tmp/cc4XFiyW.o: in function `main':
main_bad.c:(.text+0x6): undefined reference to `hidden_value'
collect2: error: ld returned 1 exit status
This error comes from ld, the linker, not the compiler. Each file compiled fine on its own, because an extern declaration only promises the name exists somewhere. The promise failed at link time, because a static global is invisible outside its file. In real projects, extern declarations go in a header file so every source file shares one declaration.
register — A Hint the Compiler May Ignore
register asks the compiler to keep a variable in a CPU register. Modern compilers allocate registers better than people do and generally ignore the hint, but one rule is still enforced:
#include <stdio.h>
int main(void) {
register int total = 0; /* a hint the compiler may ignore */
int values[] = {3, 5, 7};
for (int i = 0; i < 3; i++) {
total += values[i];
}
printf("Sum: %d\n", total);
int *p = &total; /* not allowed for register variables */
return *p;
}
$ gcc -Wall -Wextra -std=c17 -o register register.c
register.c: In function 'main':
register.c:10:5: error: address of register variable 'total' requested
10 | int *p = &total; /* not allowed for register variables */
| ^~~
A register has no memory address, so C forbids & on a register variable even when the compiler actually keeps it in memory. That restriction is the keyword’s only observable effect today.
Storage Class Summary
| Storage Class | Lifetime | Default Init | Visible From |
|---|---|---|---|
auto (any plain local) | Block duration | Indeterminate | Its block |
static (local) | Whole program | Zero | Its block |
static (file scope) | Whole program | Zero | Its source file only |
plain global / extern | Whole program | Zero | Every file that declares it |
register | Block duration | Indeterminate | Its block (no & allowed) |
Putting It Together
One program, three calls, and each kind of variable behaves differently:
#include <stdio.h>
int program_runs = 0; /* external linkage, zero-initialized */
static int module_private = 100; /* internal linkage: this file only */
void demonstrate(void) {
int local = 42; /* automatic: re-created each call */
static int call_count = 0; /* static: survives between calls */
call_count++;
local++;
printf("local=%d, calls=%d, private=%d\n",
local, call_count, module_private);
}
int main(void) {
program_runs++;
demonstrate();
demonstrate();
demonstrate();
printf("program_runs=%d\n", program_runs);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o all all.c
$ ./all
local=43, calls=1, private=100
local=43, calls=2, private=100
local=43, calls=3, private=100
program_runs=1
local++ runs on every call, yet local is 43 every time: each call starts from a fresh 42. call_count accumulates because it lives for the whole program. That difference between lifetimes is the core idea behind storage classes.
Practice
- Write
int next_id(void)that returns1000the first time it is called, then1001,1002, and so on, without any global variables.
Solution
#include <stdio.h>
int next_id(void) {
static int id = 1000;
return id++;
}
int main(void) {
printf("%d\n", next_id());
printf("%d\n", next_id());
printf("%d\n", next_id());
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p1 p1.c
$ ./p1
1000
1001
1002
A static local gives the function private memory that survives between calls. No other function can touch id, which is safer than a global counter.
- Declare a global
x = 1, a localx = 2insidemain, and a thirdx = 3in an inner block. Predict eachprintf, then compile with-Wshadow.
Solution
#include <stdio.h>
int x = 1; /* file scope */
int main(void) {
printf("outer x = %d\n", x);
int x = 2; /* shadows the global */
printf("local x = %d\n", x);
{
int x = 3; /* shadows the local */
printf("inner x = %d\n", x);
}
printf("local x = %d\n", x);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -Wshadow -o p2 p2.c
p2.c: In function 'main':
p2.c:7:9: warning: declaration of 'x' shadows a global declaration [-Wshadow]
7 | int x = 2; /* shadows the global */
| ^
p2.c:3:5: note: shadowed declaration is here
3 | int x = 1; /* file scope */
| ^
p2.c:10:13: warning: declaration of 'x' shadows a previous local [-Wshadow]
10 | int x = 3; /* shadows the local */
| ^
p2.c:7:9: note: shadowed declaration is here
7 | int x = 2; /* shadows the global */
| ^
$ ./p2
outer x = 1
local x = 2
inner x = 3
local x = 2
The first printf sees the global because the local x isn’t declared yet at that point. Shadowing is legal, so -Wall -Wextra stays silent. -Wshadow is the flag that catches this common source of confusion.
- Count how many times a function
tick()is called, using a file-scopestatic intwith no initializer.
Solution
#include <stdio.h>
static int calls; /* zero-initialized, private to this file */
void tick(void) { calls++; }
int main(void) {
for (int i = 0; i < 5; i++) {
tick();
}
printf("tick called %d times\n", calls);
return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p3 p3.c
$ ./p3
tick called 5 times
No = 0 was needed, because static-duration variables start at zero. The static keeps calls private to this file, so another file in the same program could define its own calls without a link-time clash.
- Fix the uninitialized-
totalprogram so it printstotal = 6under both-O0and-O2.
Solution
Change the declaration to int total = 0; and rebuild both ways:
$ gcc -Wall -Wextra -std=c17 -o uninit uninit.c
$ ./uninit
total = 6
$ gcc -Wall -Wextra -std=c17 -O2 -o uninit uninit.c
$ ./uninit
total = 6
With the initializer in place, the -O2 warning is gone and both builds agree. When optimization levels give different answers, uninitialized memory is one of the first things to suspect.