C++ / FUNCTIONS
Scope, lifetime, and static local variables
Predict when each local object is created and destroyed, and use static locals to carry state between calls without reaching for a global.
What you will learn
- Distinguish scope (where a name is visible) from lifetime (when the object exists)
- Use a static local to keep state across calls without adding a global name
- Say exactly when a static local's initializer and destructor run
- Avoid returning references or pointers to automatic locals
Understanding Scope, lifetime, and static local variables
Scope is a compile-time property of a name: the region of source text in which that identifier refers to a particular declaration. Lifetime is a runtime property of the object: the span between the end of its initialization and the start of its destruction. For ordinary locals the two line up so neatly that beginners treat them as one thing, and it is precisely where they come apart, with static locals and with returned references, that the interesting bugs live.
A local declared without static has automatic storage duration. Space for it belongs to the function's activation record, the object is initialized when control flows through its declaration, and it is destroyed when control leaves the enclosing block, in reverse order of construction. Returning, breaking out of a loop, and propagating an exception all count as leaving the block, which is why destructor-based cleanup is dependable. Every call and every recursion level gets its own frame, so each one gets its own copy of the object.
Writing static in front of a local leaves the scope untouched, the name is still visible only inside that block, but changes the storage duration to static: one single object for the entire run of the program, placed in static storage rather than on the stack. If the initializer is a constant expression the object is set up before main starts; otherwise initialization is deferred until control first reaches the declaration and is protected by a hidden guard so it happens exactly once, and since C++11 that guard is thread-safe. Those objects are destroyed after main returns, in reverse order of their construction.
<iostream>
struct Tracer {
const char* name;
explicit Tracer(const char* n) : name(n) { std::cout << "make " << name << '\n'; }
~Tracer() { std::cout << "drop " << name << '\n'; }
};
int nextId() {
static Tracer kept("static"); // constructed once, on the first call
static int counter = 0; // survives between calls
Tracer temp("auto"); // built and torn down on every call
++counter;
std::cout << "call " << counter << '\n';
return counter;
}
int main() {
nextId();
nextId();
std::cout << "main done\n";
}
static on a local variable changes the object's lifetime to span the whole program while leaving the name's scope confined to its block.
Worked examples
Block scope, shadowing, and a static inside a loop
Shows that an inner declaration hides the outer name and that a static local in a loop body is created only once.
<iostream>
int main() {
int x = 1;
{
int x = 2; // a different object; hides the outer x here
x += 10;
std::cout << "inner x = " << x << '\n';
} // inner x destroyed, outer name visible again
std::cout << "outer x = " << x << '\n';
for (int i = 0; i < 2; ++i) {
int fresh = 0; // new object each iteration
static int sticky = 0; // one object for the whole program
++fresh;
++sticky;
std::cout << "fresh = " << fresh << ", sticky = " << sticky << '\n';
}
}
Example explained
Line 1The inner int x = 2 is a separate object; += 10 touches it and never the outer one.
Line 2At the closing brace the inner x dies and the name x again resolves to the outer declaration, still 1.
Line 3fresh is re-created and re-initialized on every iteration, so it prints 1 twice.
Line 4sticky's initializer runs only on the first pass through its declaration, so increments accumulate.
One static local shared by all recursion levels
Demonstrates that recursion gives each call its own automatic variable but not its own static local.
<iostream>
int depth(int n) {
static int deepest = 0; // shared by every activation of depth
int here = n; // one per activation
if (n > deepest) deepest = n;
if (n < 3) depth(n + 1);
std::cout << "unwinding at here=" << here
<< ", deepest=" << deepest << '\n';
return deepest;
}
int main() {
depth(1);
}
Example explained
Line 1here lives in each call's own frame, so the unwinding prints show 3, then 2, then 1.
Line 2deepest names the same object in all three activations, which is why it reads 3 even in the outermost call.
Line 3The prints happen after the recursive call returns, so the deepest level reports first.
Line 4A second call to depth from main would start with deepest already at 3, since it is never re-initialized.
Returning a reference to a static local
Shows why a reference to a static local is safe to return while a reference to an automatic local would dangle.
<iostream>
<string>
std::string& settingsPath() {
static std::string path = std::string("/etc/") + "app.conf";
return path;
}
int main() {
std::cout << settingsPath() << '\n';
settingsPath() = "/tmp/app.conf";
std::cout << settingsPath() << '\n';
}
Example explained
Line 1The initializer is not a constant expression, so it runs on the first call to settingsPath and never again.
Line 2Returning path by reference is legal because its lifetime ends only after main returns.
Line 3Assigning through the returned reference mutates the single stored object, which the next call observes.
Line 4Drop the static and the same code returns a reference to a destroyed string: undefined behaviour, not a compiler error.
Important notes
static on a local controls storage duration only. At namespace scope the same keyword means internal linkage instead, which is a different idea with the same spelling.
Since C++11 the first-time initialization of a static local is thread-safe, but later reads and writes are not synchronized for you; concurrent mutation still needs a mutex or an atomic.
Common mistakes
Reading static int count = 0; as an assignment that runs on every call: the counter never resets, so a function meant to number items per batch keeps climbing across batches.
Returning &local or a reference to an automatic variable. It usually compiles with at most a warning, then the caller reads storage that has been reused, producing values that look plausible until the code is optimized or the call pattern changes.
Using a static local as scratch space, such as an accumulator or buffer in a recursive or repeatedly called function, so results silently depend on call history and the function is no longer reentrant or thread-safe.
Try it yourself
Change, predict, then run
Write int nextEven() that returns 0, 2, 4, 6 on four successive calls using a single static local, print the four results, then delete the static keyword and confirm the function starts returning 0 every time.
Open the C++ workspaceCheck your understanding
A function body contains static int n = expensive(); where expensive() prints a line and returns an int. The function is called three times. How many lines does expensive() print, and why?
- One, because a static local's initializer runs only on the first pass through its declaration
- Three, because the declaration statement is reached and executed on every call
- One, but before main starts, because objects with static storage duration are always initialized at program startup
- None, because static objects are zero-initialized and a function-call initializer is ignored
Show answer
Control reaches the declaration on all three calls, but a hidden guard flag makes the initialization happen exactly once, on the first pass. Option 3 is tempting because a static local with a constant initializer really is set up before main; expensive() is not a constant expression, so this object needs dynamic initialization, which for a local static is deferred until control first flows through the declaration.