20 topics
/
← Back to Quick Reference
Topic 03

Variables & Constants

Storage Duration · Initialization · Scope · const · constexpr · volatile

C++17 · Advanced Reference

Storage Duration

01

Four 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. 1.automatic — stack; created on block entry, destroyed on exit. Default for locals.
  2. 2.static — lives for the entire program. Globals and static locals.
  3. 3.thread_local — one instance per thread; lifetime matches the thread.
  4. 4.dynamic — heap; you control lifetime with new / delete (or RAII).

Key rules

  1. 1.Globals and static locals are zero-initialized before any other init runs.
  2. 2.static local variables are initialized exactly once — on first call (C++11: thread-safe).
  3. 3.thread_local can be combined with static or extern.
  4. 4.Prefer unique_ptr / shared_ptr over raw new for 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

03
int 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 scopeVariable declared inside { }. Destroyed at closing brace.
Namespace scopeDeclared outside all functions/classes. Lives until program ends.
Class scopeMembers — accessible via object, pointer, or (static) class name.
::xGlobal 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
constImmutable after init. Value may come from runtime. Cannot use as template arg or array size.
constexprMust evaluate at compile time. Implies const. Usable as template arg, array size, case label.
constinitC++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)

06
Decompose 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] = xCopies x then binds a and b to the copy's members.
auto& [a, b] = xBinds by reference — modifies the original.
const auto& [a, b]Read-only reference binding — no copy, no mutation.