Storage Classes in C
There are four storage class in c language.
- auto
- register
- static
- extern
Let's look into each and context of their uses.
auto — The Default Local Variable
The auto keyword is the default storage class for any variable declared inside a function or block. You almost never write it explicitly because the compiler assumes it when no other specifier is given.
#include <stdio.h>
void demo(void) {
auto int x = 10; /* same as: int x = 10; */
int y = 20; /* implicitly auto */
printf("%d %d\n", x, y);
} /* x and y are destroyed here */
Scope: within the block where declared.
Lifetime: created when the block is entered, destroyed when it exits.
Storage: stack frame.
Default value: undefined (garbage).
register — Hint for CPU Register Allocation
The register keyword asks the compiler to place the variable in a CPU register (e.g., r0–r12 on ARM) rather than in RAM. This eliminates a memory load/store on every access, giving a speed boost for tight loops.
#include <stdio.h>
int sum_array(const int *arr, int n) {
register int i, total = 0; /* hint: keep these in registers */
for (i = 0; i < n; i++)
total += arr[i];
return total;
}
Important rules:
- The compiler may ignore the hint if no register is available — the variable falls back to the stack.
- In C, you cannot take the address of a register variable (
&ais a compile-time error). - In C++, taking the address is allowed (the compiler demotes it to memory automatically).
- Modern compilers with
-O2typically do better register allocation than manual hints; useregistermainly in C89/C90 codebases or bare-metal environments without optimisation.
/* C — address of register not allowed */
#include <stdio.h>
int main(void) {
register int a = 10;
// printf("%p", &a); /* error: address of register variable requested */
printf("%d\n", a);
return 0;
}
static — Persistent Local Variables and File Scope
The static keyword has two distinct uses depending on where it appears:
1. Static local variable
The variable is allocated once in the BSS/data segment and survives across function calls. It is initialised only the first time the function is entered.
#include <stdio.h>
void counter(void) {
static int count = 0; /* initialised once; persists across calls */
count++;
printf("called %d time(s)\n", count);
}
int main(void) {
counter(); /* called 1 time(s) */
counter(); /* called 2 time(s) */
counter(); /* called 3 time(s) */
return 0;
}
2. Static global variable / function (internal linkage)
When applied to a global variable or function, static restricts its visibility to the translation unit (the .c file). This is the standard way to create "private" module-level state in C.
/* module.c */
static int error_count = 0; /* not visible outside this file */
static void log_error(const char *msg) { /* internal helper */
error_count++;
/* ... */
}
void public_api(void) { log_error("oops"); } /* visible everywhere */
extern — Share Variables Across Translation Units
extern declares that a variable or function exists but is defined (memory allocated) in another translation unit. No storage is reserved by the declaration itself.
/* config.c — definition: memory is allocated here */
int baud_rate = 115200;
/* main.c — declaration: tells the compiler it exists elsewhere */
extern int baud_rate;
int main(void) {
/* Use baud_rate — linker resolves the reference to config.c */
return 0;
}
Common patterns:
- Put
extern int baud_rate;in a shared header (config.h). - Define
int baud_rate = 115200;in exactly one.cfile. extern int d = 0;is a special case — adding an initialiser makes it a definition too (memory is allocated).
extern int a; /* declaration only — no memory allocated */ int b; /* definition — memory allocated */ a = 10; /* ERROR — a has no definition yet */ extern int d = 0; /* definition + declaration (initialiser present) */
Comparison Table
| Storage Class | Scope | Lifetime | Default Init | Linkage | Storage Location |
|---|---|---|---|---|---|
auto |
Block | Block duration | Undefined | None | Stack |
register |
Block | Block duration | Undefined | None | CPU register / stack |
static (local) |
Block | Entire program | Zero | None | BSS / Data segment |
static (global) |
File (translation unit) | Entire program | Zero | Internal | BSS / Data segment |
extern |
Program-wide | Entire program | Zero (at definition) | External | BSS / Data segment |
Memory Layout — Stack vs Data Segment
Understanding where a variable lives helps you avoid common bugs like returning a pointer to a local variable:
[ High address ] ┌─────────────────────────────────┐ │ Stack │ ← auto / register variables │ grows downward ↓ │ destroyed on function return ├─────────────────────────────────┤ │ Heap │ ← malloc / calloc │ grows upward ↑ │ ├─────────────────────────────────┤ │ BSS segment │ ← static / extern uninitialised │ (zero-filled by OS loader) │ live for entire program ├─────────────────────────────────┤ │ Data segment │ ← static / extern initialised │ (loaded from binary) │ live for entire program ├─────────────────────────────────┤ │ Text (code) segment │ ← read-only instructions [ Low address ]
#include <stdio.h>
static int s_global = 0; /* data segment — initialised */
static int s_uninit; /* BSS segment — zero-filled */
int *danger(void) {
int local = 42;
return &local; /* WRONG: stack frame gone after return */
}
int *safe(void) {
static int persistent = 42;
return &persistent; /* OK: data segment, survives return */
}
Embedded Systems Use Cases
On microcontrollers (ARM Cortex-M, AVR, RISC-V) memory is scarce and deterministic behaviour matters more than on a desktop. Storage classes become engineering decisions, not just style choices.
| Scenario | Best Choice | Reason |
|---|---|---|
| ISR state counter (e.g., tick count) | static local + volatile |
Persists between ISR invocations; volatile prevents register caching |
| HAL configuration struct shared across files | extern declaration + one .c definition |
Single source of truth, avoids linker conflicts |
| Private module state (not exported in header) | static global |
Prevents accidental access from other modules |
| Loop index in tight DSP inner loop | register (or rely on -O2) |
Keeps index in CPU register, reduces load/store cycles |
| Bootloader vector table pointer (read-only ROM) | const + static |
Placed in flash, zero RAM cost at runtime |
As an embedded developer, choosing the right storage class is a real memory-budget decision. A
staticvariable that lives forever consumes precious RAM on a device with 4 KB of SRAM. Design intentionally.