Skip to main content
C · lesson 3 of 25
C beginner 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 ClassLifetimeDefault InitVisible From
auto (any plain local)Block durationIndeterminateIts block
static (local)Whole programZeroIts block
static (file scope)Whole programZeroIts source file only
plain global / externWhole programZeroEvery file that declares it
registerBlock durationIndeterminateIts 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

  1. Write int next_id(void) that returns 1000 the first time it is called, then 1001, 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.

  1. Declare a global x = 1, a local x = 2 inside main, and a third x = 3 in an inner block. Predict each printf, 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.

  1. Count how many times a function tick() is called, using a file-scope static int with 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.

  1. Fix the uninitialized-total program so it prints total = 6 under both -O0 and -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.

Frequently Asked Questions

What happens if I don't initialize a variable in C?
A local variable that is never initialized holds an indeterminate value, and reading it is undefined behavior: the program may print garbage, print something that looks correct, or behave differently when optimization is turned on. Global and static variables are different: they are zero-initialized automatically.
What is the difference between declaration and definition?
A declaration tells the compiler a variable exists and its type. A definition also allocates storage. `extern int x;` is a declaration. `int x = 5;` is a definition. A program may declare a name many times but must define it exactly once, or the linker reports an error.
Is the register keyword still useful?
Rarely. Modern compilers are better at register allocation than programmers, so the hint is usually ignored. The one effect it still has is that taking the address of a register variable with & is a compile error.
What does static mean on a global variable in C?
On a variable or function at file scope, static gives it internal linkage: it is visible only inside that one source file. Another file that tries to reach it with extern gets an 'undefined reference' error from the linker. This is C's main tool for hiding implementation details.