The phrase storage class in C++ refers to how and where an object is stored, its lifetime, and its visibility (linkage). In practice you control this with keywords like static
, extern
, and thread_local
, plus your choice of scope and allocation style.
This beginner-friendly guide breaks down every storage class in C++ you’ll actually use, with clear explanations, commented code, outputs, best practices, and “Try it yourself” challenges after each section to lock in your understanding.
🔹 What does “storage class in C++” mean?
In C++, storage class describes three related properties of an object:
- Storage duration: When the object exists (automatic, static, thread, or dynamic).
- Linkage: Whether the name is visible across translation units (external, internal, or none).
- Scope: Where the name can be used in the program (block, class, namespace).
Keywords that influence the storage class in C++ are static
, extern
, and thread_local
. Historically, register
existed but is obsolete. Note: auto
in modern C++ is type deduction, not a storage class.
Keyword | Storage duration | Typical linkage | Common use |
---|---|---|---|
—(local variable) | Automatic | None | Normal block-scope variables |
static (local) | Static | None | Function-level singletons/counters |
static (namespace-scope) | Static | Internal | File-private globals |
extern | Static | External | Cross-file globals (declare in headers, define once) |
thread_local | Thread | Internal or external | Per-thread state (IDs, caches) |
dynamic (new/delete) | Dynamic | — | Heap-allocated lifetime (not a keyword “class”) |
Try it yourself
- Name one example in your app that needs file-private state (use
static
at namespace scope) and one that needs cross-file state (useextern
). - Explain when a variable should have automatic vs static storage duration.
🔹 Automatic storage (default for local variables)
Local (block-scope) variables without any storage specifier have automatic storage duration: they are created when the block is entered and destroyed when it leaves. They have no linkage and exist only within their scope.
#include <iostream>
using namespace std;
void demoAutomatic() {
int x = 0; // automatic storage (born on entry, dies on exit)
x += 5;
cout << "x=" << x << "\n";
}
int main() {
demoAutomatic();
// cout << x; // ERROR: x not visible here (no linkage, out of scope)
}
Output
x=5
Use automatic storage for regular local variables that don’t need to persist or be shared across calls.
Try it yourself
- Create a nested block and shadow a variable (same name). Print both to see scope boundaries.
- Add a
std::string
local and watch its destructor run when leaving scope (add a print in a small wrapper type if you want to observe order).
🔹 Static storage: static local and namespace-scope static
static
gives an object static storage duration (exists for the entire program). Behavior differs by scope:
- static local: One instance across all calls; no linkage.
- static at namespace scope: Internal linkage (visible only in that translation unit).
#include <iostream>
using namespace std;
void hits() {
static int s = 0; // static local: persists across calls
int a = 0; // automatic local: re-initialized every call
++s; ++a;
cout << "s=" << s << ", a=" << a << "\n";
}
// At namespace scope:
static int filePrivate = 42; // internal linkage: only this .cpp can see it
int main() {
hits(); hits(); hits();
cout << "filePrivate=" << filePrivate << "\n";
}
Output
s=1, a=1
s=2, a=1
s=3, a=1
filePrivate=42
Static locals are a clean way to implement function-level singletons or counters. Namespace-scope static
is ideal for file-private helpers that should not leak outside the translation unit.
Try it yourself
- Add another function in the same file that prints
filePrivate
. Then try to access it from a different .cpp—you’ll get a link error (internal linkage). - Turn
hits()
into a unique ID generator using a static counter.
🔹 extern: cross-file variables (external linkage)
extern
declares a variable that is defined in another translation unit. It gives the name external linkage so multiple files can share the same object. You typically place the extern
declaration in a header and define the variable once in a .cpp.
// config.hpp (header)
#pragma once
extern const int kMaxItems; // declaration
// config.cpp (single definition)
#include "config.hpp"
const int kMaxItems = 100; // definition
// main.cpp (use)
#include <iostream>
#include "config.hpp"
int main() {
std::cout << kMaxItems << "\n";
}
Output
100
Always define exactly one instance of an extern
variable. Multiple definitions cause link-time errors. For compile-time constants shared in headers, consider inline
variables (C++17) instead of extern
.
Try it yourself
- Create a non-const global counter with
extern
and increment it in two different .cpp files. Verify the final value reflects both increments. - Replace the pattern with
inline const int
(C++17) in the header and remove the .cpp definition; confirm it links cleanly.
🔹 thread_local: per-thread storage
thread_local
gives an object thread storage duration: each thread gets its own instance, constructed at thread start and destroyed at thread exit. Use it for thread-specific caches or IDs.
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
thread_local int tid = 0; // each thread has its own tid
static atomic<int> nextId{1}; // shared counter to assign per-thread IDs
void worker() {
tid = nextId.fetch_add(1);
cout << "Hello from thread tid=" << tid << "\n";
}
int main() {
thread t1(worker), t2(worker), t3(worker);
t1.join(); t2.join(); t3.join();
}
Possible Output
Hello from thread tid=1
Hello from thread tid=2
Hello from thread tid=3
Each thread writes to its own tid
, so there’s no data race. For shared counters or communication, still use std::atomic
or locks.
Try it yourself
- Create a
thread_local
std::string
buffer per thread and push different data from each thread, then print. - Remove
thread_local
and observe data races or interleaving when multiple threads write the same variable.
🔹 Dynamic storage: heap allocation (not a “class”, but important)
Objects you allocate with new
have dynamic storage duration (heap). You control lifetime manually (or via smart pointers). While not a “storage class specifier,” dynamic storage is part of the storage story in C++.
#include <memory>
#include <iostream>
using namespace std;
int main() {
// Manual lifetime
int* p = new int(42);
cout << *p << "\n";
delete p; // must delete exactly once
// Prefer smart pointers (RAII)
auto arr = make_unique<int[]>(3);
arr[0] = 10; arr[1] = 20; arr[2] = 30;
cout << arr[1] << "\n"; // deletes automatically when arr goes out of scope
}
Output
42
20
Use smart pointers to avoid leaks and double-deletes. Dynamic storage is orthogonal to linkage, but critical for managing lifetimes intentionally.
Try it yourself
- Replace
make_unique
with a rawnew
and forget to delete—run under a leak checker to see why RAII matters. - Create a function that returns
unique_ptr<T>
and observe ownership transfer (no copies of the pointer itself).
🔹 Related/obsolete keywords
- register: Historical hint for CPU registers. Deprecated/removed; compilers ignore it in modern C++.
- auto: In C++11+, this is type deduction, not a storage class.
- mutable: Allows mutation in
const
member functions. It’s not a storage class; it affects const-correctness.
Try it yourself
- Add
register
to a variable in C++20 mode and see that it has no effect (or causes a diagnostic depending on compiler). - Use
mutable
in a class to update a cache inside aconst
method; confirm the compiler allows it.
🔹 Mini project: Putting storage classes together
Let’s combine the key pieces of the storage class in C++ into a small, realistic example: a file-private logger state, an extern
config, a function-level static counter, and a per-thread request ID.
// settings.hpp
#pragma once
extern const int kLogEveryN; // declaration
// settings.cpp
#include "settings.hpp"
const int kLogEveryN = 3; // definition (external linkage)
// logger.cpp
#include
#include
#include
#include "settings.hpp"
using namespace std;
static bool g_enabled = true; // file-private state (internal linkage)
thread_local int t_requestId = 0; // per-thread ID
static atomic g_nextId{1}; // shared counter to assign IDs
void beginRequest() {
t_requestId = g_nextId.fetch_add(1);
}
void logLine(const char* msg) {
static int count = 0; // persists across calls
++count;
if (g_enabled && (count % kLogEveryN == 0)) {
cout << "[req " << t_requestId << "] " << msg << "\n";
}
}
int main() {
auto worker = [] {
beginRequest();
for (int i = 0; i < 5; ++i)
logLine("processing...");
};
thread t1(worker), t2(worker);
t1.join();
t2.join();
}
One of the Possible Output
[req 1] processing...
[req 2] processing...
[req 2] processing...
This demonstrates static local persistence, internal linkage for file-private globals, external linkage via extern
, and distinct thread_local
state per thread.
Try it yourself
- Tweak
kLogEveryN
to 1 and watch every line log; set it to 10 and see fewer logs. - Try to print
g_enabled
from another .cpp and observe that it’s not visible (internal linkage).
🔹 Best practices for storage class in C++
- Prefer local (automatic) variables unless you truly need persistence or sharing.
- Use
static
local for function-level singletons or counters; it’s thread-safe initialization since C++11. - Use
static
at namespace scope for file-private globals; avoid exposing unnecessary globals. - Use
extern
sparingly; prefer passing dependencies explicitly or using singletons with clear ownership. - Use
thread_local
for per-thread state; still use atomics/locks for shared data. - Minimize raw
new
/delete
; prefer smart pointers for dynamic storage. - For constants shared across files, prefer
inline
variables (C++17) orconstexpr
overextern
where appropriate.
Try it yourself
- Refactor a global into a file-private
static
and expose accessor functions instead. - Convert
extern
constants toinline
variables in headers (C++17+) to simplify linkage.
🔹 FAQs about storage class in C++
Q1: Does static
always mean “one copy”?
Yes for storage duration (object lives for the entire program). For namespace-scope static
, linkage is internal (file-private). For static locals, there’s one instance shared across calls (no linkage).
Q2: Why does extern
on a const
matter?
Namespace-scope const
s have internal linkage by default. Add extern
if you want a single shared definition across translation units.
Q3: What’s the difference between static
and thread_local
?
Both have non-automatic lifetimes, but static
objects are shared program-wide, while thread_local
creates a separate instance per thread.
Q4: Are register
and auto
storage classes?
No in modern C++. register
is obsolete and ignored; auto
is type deduction, not storage. Don’t use register
in new code.
Q5: Is dynamic storage a “storage class”?
Not as a keyword class, but it is a storage duration. Objects created with new
live until you delete them (or until the owning smart pointer releases them).
Q6: Do static locals initialize in a thread-safe way?
Yes, since C++11, initialization of function-scope static variables is thread-safe.
Q7: When should I prefer inline
variables over extern
?
For constants defined in headers and shared across TUs, inline
(C++17) simplifies linkage—declare once in the header, no separate .cpp definition needed.
Q8: Does static
change class members’ storage?static
data members have static storage duration and one instance per class, not per object. static
member functions don’t need an object and have no implicit this
.
Q9: Can I put static
on local class definitions?
Classes can’t be static
, but you can define a class inside a function and have static
locals of that type.
Q10: Does static
affect performance?
It can, by changing lifetime and initialization timing. But performance depends on usage patterns; measure if it matters in hot code.
🔹 Wrapping up
The storage class in C++ governs an object’s lifetime, visibility, and sharing. Use automatic storage by default, static
for persistence or file-private globals, extern
for shared definitions across files, thread_local
for per-thread state, and smart pointers for dynamic storage. Practice the “Try it yourself” tasks to get hands-on familiarity and make confident, maintainable choices in your codebase.