20 topics
← Back to Quick Reference/
Topic 03
VVaarriiaabblleess && CCoonnssttaannttss
Storage Duration · Initialization · Scope · const · constexpr · volatile
C++17 · Advanced ReferenceStorage Duration
01Four storage durations in C++
Every variable has a storage duration that determines when its memory is allocated and released. This is separate from scope (where the name is visible) — a static local has local scope but static duration.
Durations
- 1.
automatic— stack; created on block entry, destroyed on exit. Default for locals. - 2.
static— lives for the entire program. Globals andstaticlocals. - 3.
thread_local— one instance per thread; lifetime matches the thread. - 4.
dynamic— heap; you control lifetime withnew/delete(or RAII).
Key rules
- 1.Globals and
staticlocals are zero-initialized before any other init runs. - 2.
staticlocal variables are initialized exactly once — on first call (C++11: thread-safe). - 3.
thread_localcan be combined withstaticorextern. - 4.Prefer
unique_ptr/shared_ptrover rawnewfor dynamic storage.
// ── Automatic storage (stack) — destroyed when scope ends ── void foo() { int x = 10; // created on entry, destroyed on exit { int y = 20; // y destroyed here ↓ } // ← y's lifetime ends } // ── Static storage — lives for entire program duration ────── static int fileCounter = 0; // internal linkage (file-scope) int globalCount = 0; // external linkage void bar() { static int callCount = 0; // initialized once, persists across calls callCount++; } // ── Thread-local storage (C++11) ──────────────────────────── thread_local int threadId = 0; // each thread gets its own copy // ── Dynamic storage (heap) — manual or RAII lifetime ──────── int* p = new int(42); // you control the lifetime delete p; // must free manually // Prefer: std::unique_ptr<int> p = std::make_unique<int>(42);
Initialization Forms
02// ── Zero-initialization (before any other init) ───────────── // Globals and static locals are zero-initialized automatically int g; // g == 0 (guaranteed) static int s; // s == 0 (guaranteed) // ── Default-initialization ─────────────────────────────────── int x; // ❌ indeterminate (local, non-class) — garbage! std::string s; // ✅ default-constructed — empty string // ── Value-initialization (with empty {}) ──────────────────── int a{}; // 0 double b{}; // 0.0 int* p{}; // nullptr int arr[5]{}; // {0,0,0,0,0} // ── Direct / copy initialization ──────────────────────────── int i = 5; // copy-initialization int j(5); // direct-initialization int k{5}; // direct-list-initialization (C++11) ← prefer // Brace init PREVENTS narrowing: int n{3.9}; // ❌ error — narrowing double→int not allowed int m = 3.9; // ✅ compiles (silently truncates to 3)
| int x; | Default-init — local non-class types are indeterminate (garbage). Always initialize locals. |
| int x{}; | Value-init — zero for scalars, default-construct for classes. Prevents garbage. |
| int x{5}; | Direct-list-init (brace init). Preferred — prevents narrowing conversions at compile time. |
| int x = 5; | Copy-init — fine for simple types but allows narrowing silently. |
| int x(5); | Direct-init — same as copy-init for scalars; can call explicit constructors. |
Prefer brace init
{} everywhere. It is the only form that makes narrowing conversions a compile error, and it works uniformly across scalars, aggregates, and containers.Scope & Name Lookup
03int x = 1; // file scope (global) namespace Demo { int x = 2; // namespace scope } void func() { int x = 3; // local scope — shadows global { int x = 4; // inner block — shadows outer local std::cout << x; // 4 } std::cout << x; // 3 std::cout << ::x; // 1 — :: accesses global scope std::cout << Demo::x; // 2 } // Name lookup order: // 1. Current block → 2. Enclosing blocks → 3. Namespace → 4. Global
| Block scope | Variable declared inside { }. Destroyed at closing brace. |
| Namespace scope | Declared outside all functions/classes. Lives until program ends. |
| Class scope | Members — accessible via object, pointer, or (static) class name. |
| ::x | Global scope operator — bypasses local shadowing to reach file-scope name. |
Avoid shadowing. Enable
-Wshadow to catch variables that silently hide outer names — a common source of subtle bugs.const · constexpr · constinit
04// const — immutable after initialization; value may be runtime const int port = 8080; const int limit = getEnvLimit(); // runtime const — OK // constexpr — must be compile-time; enables use as template arg constexpr int CACHE_SIZE = 1024; constexpr double PI = 3.14159265358979; int arr[CACHE_SIZE]; // ✅ valid array size template<int N> struct Buffer {}; Buffer<CACHE_SIZE> buf; // ✅ valid template arg // constexpr function — evaluated at compile time if possible constexpr int square(int n) { return n * n; } constexpr int S = square(8); // 64 — compile time int s2 = square(rand()); // runtime — also fine // constinit (C++20) — must init at compile time, but not const constinit int g_flags = 0; // compile-time init, mutable g_flags = 1; // ✅ allowed (not const) // constinit int bad = getFlags(); // ❌ runtime init — compile error
| const | Immutable after init. Value may come from runtime. Cannot use as template arg or array size. |
| constexpr | Must evaluate at compile time. Implies const. Usable as template arg, array size, case label. |
| constinit | C++20: guarantees compile-time initialization but the variable stays mutable. Prevents SIOF. |
volatile
05// volatile tells the compiler: don't optimize away reads/writes // Used for memory-mapped hardware registers or signal handlers volatile int* statusReg = reinterpret_cast<volatile int*>(0xDEAD0); int val = *statusReg; // read is NOT cached — always hits memory *statusReg = 1; // write is NOT reordered or elided // Common use in embedded / systems code: volatile bool interruptFlag = false; void interruptHandler() { interruptFlag = true; // without volatile, optimizer may remove this } void loop() { while (!interruptFlag) { /* spin */ } // must re-read each iteration } // ⚠ volatile does NOT make code thread-safe. // Use std::atomic<> for inter-thread communication.
volatile ≠ atomic.
volatile only prevents the compiler from caching or reordering accesses — it gives no guarantees about CPU cache coherency or memory ordering between threads. Use std::atomic<T> for thread-safe shared state.Structured Bindings (C++17)
06Decompose aggregates, pairs, tuples
// Structured bindings (C++17) — name elements of aggregate/pair/tuple // std::pair auto [host, port] = std::make_pair("localhost", 8080); // Struct (binds in declaration order) struct Point { double x, y, z; }; Point p{1.0, 2.0, 3.0}; auto [px, py, pz] = p; // std::map / std::unordered_map iteration std::map<std::string, int> scores; for (auto& [name, score] : scores) { score += 10; // modifies map in-place (ref binding) } // const binding — both names become const const auto [lo, hi] = std::make_pair(0, 100); // std::array / C-array int rgb[3] = {255, 128, 0}; auto [r, g, b] = rgb; // copies; use auto& to avoid copy
| auto [a, b] = x | Copies x then binds a and b to the copy's members. |
| auto& [a, b] = x | Binds by reference — modifies the original. |
| const auto& [a, b] | Read-only reference binding — no copy, no mutation. |