Storage Classes and Variable Scope in C++
A storage class determines a variable's lifetime (how long the variable lives), scope (where the variable can be accessed), and linkage (whether the variable can be accessed from other files). C++ has several storage classes: auto, register, static, extern, mutable, and thread_local.
auto (C++11)
In C++11, auto is used for type deduction — the compiler automatically determines the variable's type from its initialization. This replaced the old use of auto (which was the same as the default and was never used).
register (deprecated)
register is a hint for the compiler to store the variable in a CPU register. Since C++11, modern compilers ignore this hint and it is considered deprecated.
static
static has two different meanings depending on context:
- Local scope: A static local variable lives for the entire program (not just within the block), but can only be accessed within that block. Initialization is done only once.
- File scope: Sets internal linkage. A static function or variable at file scope can only be accessed from that file (internal linkage).
extern
extern declares a variable that is defined elsewhere. It tells the compiler: "this variable exists, its definition is in another file/location." Useful for global variables shared between files.
mutable
mutable allows a struct/class member to be modified even if the object is const. Useful for caching or logging that does not change the object's logical state.
thread_local (C++11)
thread_local gives a variable thread storage duration. Each thread has its own independent copy of the variable. Useful for thread-specific data without race conditions.
Lifetime vs Visibility
- Lifetime: How long a variable holds its value in memory. It can be static (the whole program), automatic (block scope), or dynamic (heap via
new). - Visibility (scope): Where in the code the variable can be accessed. It can be global, local, or block-scoped.
Code Example
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std;
// Extern: defined elsewhere (could be in another file)
extern int sharedData;
// File-scope static: can only be accessed in this file
static int fileStatic = 100;
static void fileFunction() {
cout << "This function is internal, only for this file" << endl;
}
// Mutable struct
struct CachedData {
mutable int hitCount = 0;
string data;
string getData() const {
hitCount++; // allowed because mutable
return data;
}
};
// Thread-local
thread_local int threadSpecific = 0;
void autoFunction() {
// auto type deduction
auto x = 42; // int
auto y = 3.14; // double
auto z = "Hello"; // const char*
auto w = string("Hi"); // string
cout << "auto x: " << x << " (" << typeid(x).name() << ")" << endl;
cout << "auto y: " << y << endl;
cout << "auto z: " << z << endl;
}
void counterStatic() {
static int count = 0; // initialized only once
count++;
cout << "Function called " << count << " times" << endl;
}
void demonstrasiScope() {
int lokal = 10; // automatic storage duration
{
int dalamBlok = 20; // block scope
cout << "lokal: " << lokal << ", dalamBlok: " << dalamBlok << endl;
}
// dalamBlok cannot be accessed here
static int staticLokal = 0; // static local: lives forever
staticLokal++;
cout << "staticLokal: " << staticLokal << endl;
}
void threadFunction(int id) {
threadSpecific = id * 100;
cout << "Thread " << id << ": threadSpecific = " << threadSpecific << endl;
}
int main() {
// auto
cout << "--- auto ---" << endl;
autoFunction();
// Static local
cout << "\n--- Static Local ---" << endl;
counterStatic(); // count = 1
counterStatic(); // count = 2
counterStatic(); // count = 3
// Scope demonstration
cout << "\n--- Scope ---" << endl;
demonstrasiScope();
demonstrasiScope(); // staticLokal continues from 1
// Extern
cout << "\n--- Extern ---" << endl;
sharedData = 42;
cout << "sharedData: " << sharedData << endl;
// Mutable
cout << "\n--- Mutable ---" << endl;
CachedData cache = {"sensitive data"};
const CachedData& ref = cache;
cout << "Data: " << ref.getData() << endl;
cout << "Data: " << ref.getData() << endl;
cout << "Hit count: " << ref.hitCount << endl;
// Thread-local
cout << "\n--- Thread Local ---" << endl;
thread t1(threadFunction, 1);
thread t2(threadFunction, 2);
t1.join();
t2.join();
// Static global
cout << "\n--- Static Global ---" << endl;
cout << "fileStatic: " << fileStatic << endl;
fileFunction();
// Lifetime demonstration
cout << "\n--- Lifetime ---" << endl;
int* ptr;
{
int dynamic = 42; // automatic: dies at the end of the block
ptr = new int(100); // dynamic: lives until delete
}
// dynamic has already died, but *ptr is still alive
cout << "*ptr = " << *ptr << endl;
delete ptr;
// Static vs auto
cout << "\n--- Static vs Auto ---" << endl;
for (int i = 0; i < 3; i++) {
int autoVar = 0; // re-initialized every iteration
static int staticVar = 0; // initialized only once
autoVar++;
staticVar++;
cout << "Iterasi " << i << ": auto=" << autoVar
<< ", static=" << staticVar << endl;
}
return 0;
}
// Extern definition (could be in another file, but here for demonstration)
int sharedData = 0;
Code Explanation
auto x = 42;— The compiler determines thatxis of typeintfrom the initialization value 42. Saves writing the type explicitly, especially for complex types such as iterators or templates.auto y = 3.14;—yis automaticallydouble.auto z = "Hello"is automaticallyconst char*(notstring).static int count = 0;— A static local variable. Initialized only once the first time the function is called. The value persists across function calls. Useful for counters, caching, or state that needs to be remembered.demonstrasiScope()called twice —staticLokalis initialized once (initial value 0) and increments on every call. Output: 1, then 2.extern int sharedData;— Declares a variable defined elsewhere. Here,sharedDatais defined at the end of the file. Useful for sharing data between files.static int fileStatic— Static at file scope. Internal linkage. Can only be accessed from this file. Another function in a different file can have its ownfileStaticvariable without conflict.mutable int hitCount— Inside aconstmethod, buthitCountcan still be modified because ofmutable. Useful for logging or memoization that does not affect the logical state.thread_local int threadSpecific— Each thread has its own copy. Thread 1 changesthreadSpecificto 100, thread 2 to 200. They do not affect each other.int autoVarvsstatic int staticVarinside a loop —autoVaris re-initialized every iteration (always 1).staticVaris initialized only once (keeps increasing: 1, 2, 3).int* ptr = new int(100)— Dynamic allocation. Lives untildelete ptr. Different from automatic variables that die when leaving scope.
Everyday Use Cases
Singleton pattern: A static local variable inside a function ensures the object is created only once. Every call gets the same object.
Database& getDB() {
static Database instance; // created once
return instance;
}
Thread pool: thread_local stores a worker ID or thread-specific context. Each thread has its own data without a race condition.
Global counter: extern allows several source files to share a counter variable. File A declares extern int totalRequest;, File B defines int totalRequest = 0;.
Caching: mutable on a class member allows caching in a const method. The method does not change the logical state but can store previously computed results.
Conclusion
Storage classes control a variable's lifetime and visibility. auto (C++11) is for type deduction. A local static persists for the entire program. extern is for sharing variables between files. mutable allows modification on const objects. thread_local (C++11) is for thread-specific data. Understand the difference between lifetime and scope: a variable can live a long time but have limited scope, or vice versa. Choosing the right storage class results in an efficient, maintainable program.