Skip to main content
C beginner Lesson 3 of 23

Variables and Storage Classes in C

Learn variable declaration, initialization, scope, and the four storage classes: auto, static, extern, and register.

Declaring and Initializing Variables

A variable is a named storage location in memory. Before you can use a value in C, you must declare the variable that will hold it — telling the compiler what type of data it stores. Initialization gives the variable its first value at the point of declaration, which is almost always preferable to declaring and assigning in two steps.

#include <stdio.h>

int main(void) {
    int age;          /* declaration — value is garbage (uninitialized) */
    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;
}

Always initialize variables before reading them. Reading an uninitialized local variable is undefined behavior — the program may produce random output, crash, or behave inconsistently across runs and compilers.

Variable Naming Rules

C variable names must follow specific rules. Violating them is a compile error; ignoring conventions makes your code harder to read.

  • Must start with a letter or underscore (_)
  • Can contain letters, digits, and underscores
  • Case-sensitive: count, Count, and COUNT are three different variables
  • Cannot be a C keyword (int, while, return, etc.)
int player_score = 0;   /* valid — snake_case is idiomatic C */
int _internal = 1;      /* valid — but leading underscore is reserved for library use */
int 2fast;              /* INVALID — starts with a digit */
int for;                /* INVALID — reserved keyword */

Scope: Where Variables Live

Scope determines where a variable is visible and accessible. Understanding scope prevents accidental name collisions and makes it clear which code can affect which data.

Block scope: Variables declared inside {} exist only within that block.

#include <stdio.h>

int main(void) {
    int x = 10;

    {
        int y = 20;       /* y is only visible inside this block */
        printf("%d\n", x + y);  /* OK — x is visible from the outer block */
    }

    /* printf("%d\n", y); */  /* ERROR: y is out of scope here */
    return 0;
}

File scope: Variables declared outside all functions are visible throughout the entire file (and potentially other files, if not static). They are zero-initialized automatically.

#include <stdio.h>

int global_counter = 0;   /* file scope, zero-initialized automatically */

void increment(void) {
    global_counter++;   /* every function in this file can see and modify it */
}

int main(void) {
    increment();
    increment();
    printf("Counter: %d\n", global_counter);  /* prints 2 */
    return 0;
}

The Four Storage Classes

Storage classes control a variable’s lifetime (how long it exists), its default initial value, and its linkage (which other files can see it). They are specified with a keyword before the type.

auto — Automatic (Default for Locals)

auto is the default storage class for all local variables. You almost never need to write it explicitly — it’s there for completeness.

void foo(void) {
    auto int x = 5;  /* 'auto' is implicit — you almost never write it */
    int y = 10;      /* identical to 'auto int y = 10' */
}

auto variables live on the stack. They are created when the block is entered and destroyed when it exits.

static — Persistent Storage

static has two distinct uses depending on where it appears. In both cases, the variable lives in the data segment (not the stack) and is zero-initialized if you don’t provide a value.

Static local variable: Persists its value across function calls. Initialized only once, the first time the function runs. This is useful for counters, caches, and state that must survive between calls.

#include <stdio.h>

void counter(void) {
    static int count = 0;  /* initialized once, retains value between calls */
    count++;
    printf("Called %d times\n", count);
}

int main(void) {
    counter();  /* Called 1 times */
    counter();  /* Called 2 times */
    counter();  /* Called 3 times */
    return 0;
}

Static global variable / function: Restricts visibility to the current translation unit (source file). This is the primary way to achieve encapsulation in C — hiding implementation details from other files.

/* utils.c */
static int helper_state = 0;  /* not visible outside utils.c */

static void internal_helper(void) {  /* not visible outside utils.c */
    helper_state++;
}

void public_api(void) {  /* visible to other files — no 'static' */
    internal_helper();
}

extern — External Linkage

extern declares that a variable or function is defined in another translation unit. It does not allocate storage — it tells the compiler “this name exists somewhere else, trust me.” The linker resolves it when combining object files.

/* globals.c */
int shared_value = 42;   /* definition — storage allocated here */

/* main.c */
extern int shared_value;  /* declaration — no storage, just a reference */

int main(void) {
    printf("%d\n", shared_value);  /* uses the variable from globals.c */
    return 0;
}

Compile both files together: gcc main.c globals.c -o program

In practice, extern declarations belong in header files so every file that needs the variable includes the same declaration.

register — Register Hint

register suggests the compiler store the variable in a CPU register for faster access. The compiler is free to ignore this hint — and modern compilers usually do, since they optimize register allocation automatically.

void sum_array(int *arr, int n) {
    register int i;      /* hint: keep loop counter in a CPU register */
    register int total = 0;

    for (i = 0; i < n; i++) {
        total += arr[i];
    }
    printf("Sum: %d\n", total);
}

The key side effect of register: you cannot take the address of a register variable (&i is a compile error). This keyword is largely historical today.

Storage Class Summary

Storage ClassWhere StoredLifetimeDefault InitScope
autoStackBlock durationGarbageBlock
static (local)Data segmentProgram durationZeroBlock
static (global)Data segmentProgram durationZeroFile
externElsewhereProgram durationZeroFile/program
registerRegister/stackBlock durationGarbageBlock

Practical Example: Using All Storage Classes

This example puts all four storage classes together so you can see their different behaviors side by side:

#include <stdio.h>

/* File-scope variable — zero-initialized, external linkage */
int program_start_count = 0;

/* File-scope variable — internal linkage only, invisible to other .c files */
static int module_private = 100;

void demonstrate(void) {
    /* auto: lives on the stack, garbage unless initialized */
    auto int local = 42;

    /* static local: lives in data segment, retains value between calls */
    static int call_count = 0;
    call_count++;

    /* register: hint to keep in CPU register, cannot take its address */
    register int fast_counter;
    for (fast_counter = 0; fast_counter < 3; fast_counter++) {
        /* do work */
    }

    printf("local=%d, calls=%d, private=%d\n",
           local, call_count, module_private);
}

int main(void) {
    program_start_count++;
    demonstrate();  /* local=42, calls=1, private=100 */
    demonstrate();  /* local=42, calls=2, private=100 */
    return 0;
}

Notice that local is always 42 (re-initialized each call), while call_count accumulates across calls because it’s static. That difference is the essence of understanding storage classes.

Frequently Asked Questions

What happens if I don't initialize a variable in C?
Local variables contain garbage values — whatever bytes happened to be in that memory location. Global and static variables 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.
Is the register keyword still useful?
Rarely. Modern compilers are better at register allocation than programmers. The keyword is mostly a historical artifact, though it still prevents you from taking the address of a variable.