Hello. In the previous lesson, you used GDB’s stack frames to see which function calls are active and to inspect each call’s local state. That model is useful here, but it is not the whole story: a stack frame is an implementation and debugging concept, while an object lifetime is a C++ language rule.
This lesson makes that distinction concrete. You will trace objects created in ordinary blocks, objects created with new, and objects that persist for the whole program. By the end, you should be able to look at a declaration and state when the object begins its lifetime, when it is destroyed, and who is responsible for ending that lifetime.
Storage duration, scope, and lifetime
Three related ideas are easy to conflate:
- Scope answers: Where can I write this name in the source code?
- Storage duration classifies how long the storage associated with an object can persist.
- Object lifetime is the interval from construction/initialization until destruction.
Consider:
void process() {
int count = 0;
{
int temporary = 42;
++count;
}
++count;
}
Both count and temporary have automatic storage duration because they are ordinary local variables. But their scopes and lifetimes differ:
countis constructed when execution reaches its declaration and is destroyed whenprocess’s body ends.temporaryis constructed when execution enters its inner block and is destroyed at that inner block’s closing brace.
The central rule is:
An automatic object is destroyed when execution leaves the block in which it was declared.
This includes leaving through return or an exception, not just reaching the final closing brace normally. For class objects, destruction means that the destructor runs.
The word “automatic” does not literally guarantee physical placement in a particular hardware memory region. In ordinary implementations, automatic locals are commonly associated with stack frames, dynamically allocated objects with the heap, and static objects with a static-data region. That is a useful model for debugging, but C++ specifies lifetimes rather than exact memory layout.

The diagram also highlights a crucial point for pointers: the pointer and the object it points to can have different storage durations.
void example() {
int* p = new int{42};
}
Here, p itself is an automatic local variable. It typically lives in example’s stack frame and is destroyed when the function ends. The int{42} is a separate dynamically allocated object. Its lifetime does not end merely because p goes out of scope.
A compact reference: the three durations in this lesson
| Storage duration | Typical declaration or creation | Object lifetime ends | Usual responsibility |
|---|---|---|---|
| Automatic | Ordinary local variable, such as Trace local{"x"}; | When its enclosing block is exited | C++ destroys it automatically |
| Dynamic | new Trace{"x"} | When the corresponding delete is executed | Code that owns the allocation must arrange cleanup |
| Static | Namespace-scope object, or local declared static | During normal program termination | C++ destroys it automatically |
For this lesson, use “global” informally to mean an object defined at namespace scope, outside every function:
int requests_seen = 0; // static storage duration
This object has static storage duration whether or not you write the keyword static. At namespace scope, that keyword has additional linkage implications, but its presence is not what gives the object its long lifetime. We will keep linkage separate from the storage-duration question for now.
Watch the lifetime model once
The first few minutes of The Cherno’s Object Lifetime in C++ (Stack/Scope Lifetimes) give a clear visual treatment of block-based automatic lifetime and contrast it with a heap allocation that survives the block.
Object Lifetime in C++ (Stack/Scope Lifetimes)
Watch Object Lifetime in C++ (Stack/Scope Lifetimes) by The Cherno to reinforce the distinction between leaving a scope and explicitly releasing a dynamic allocation.
Start with the stack model, which connects scopes to automatic local objects. Then watch the constructor trace to see a destructor run at block exit. Finish with the heap contrast: notice that creating an object with new does not schedule its destructor at the end of the surrounding block.
The “stack of books” picture is helpful, provided you retain the more precise rule: the language promises destruction at scope exit for automatic objects; the stack is the usual implementation strategy.
For a compact written reference, read the opening material and the section on static locals and new in the CSE 202 notes.
Read Storage duration from CSE 202 for a concise statement of the default durations of local and namespace-scope objects, followed by examples of static locals and dynamic allocation.
In the opening discussion, read the duration overview. Focus on the distinction between local scope and namespace scope, and on the fact that a class destructor is invoked at the end of its lifetime. Then continue to the following discussion, beginning “A local variable declared with the static specifier.” Read static locals and dynamic allocation. Pay particular attention to the fact that a static local is initialized once, whereas an object allocated with new remains alive until explicit destruction.
Lab: make constructions and destructions visible
Rather than inferring lifetimes from addresses, instrument the program so that every construction and destruction announces itself.
Create lifetimes.cpp:
#include <cstdio>
struct Trace {
const char* name;
explicit Trace(const char* label) : name(label) {
std::printf("construct %-10s at %p\n",
name, static_cast<void*>(this));
}
~Trace() {
std::printf("destroy %-10s at %p\n",
name, static_cast<void*>(this));
}
};
Trace global_trace{"global"};
void visit() {
Trace outer{"outer"};
static Trace remembered{"remembered"};
Trace* dynamic = new Trace{"dynamic"};
{
Trace inner{"inner"};
std::puts(" using objects inside the inner block");
}
std::puts(" inner block has ended");
delete dynamic;
dynamic = nullptr;
std::puts(" leaving visit");
}
int main() {
std::puts("main begins");
std::puts("first visit");
visit();
std::puts("second visit");
visit();
std::puts("main ends");
}
Build it with the familiar warning baseline:
g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O0 lifetimes.cpp -o lifetimes
./lifetimes
The addresses will differ on your machine and may differ between runs. Ignore their numerical values; the relative order of the messages is what matters.
A simplified trace should look like this:
construct global at ...
main begins
first visit
construct outer at ...
construct remembered at ...
construct dynamic at ...
construct inner at ...
using objects inside the inner block
destroy inner at ...
inner block has ended
destroy dynamic at ...
leaving visit
destroy outer at ...
second visit
construct outer at ...
construct dynamic at ...
construct inner at ...
using objects inside the inner block
destroy inner at ...
inner block has ended
destroy dynamic at ...
leaving visit
destroy outer at ...
main ends
destroy remembered at ...
destroy global at ...
Read this output as an execution trace, not as a list of unrelated events.
1. global_trace: static storage duration
Trace global_trace{"global"};
global_trace is defined outside any function, so it has static storage duration.
- It is constructed before
mainbegins. - It remains alive during both calls to
visit. - It is destroyed during normal program termination, after
mainhas finished.
The name global_trace is visible in the rest of this translation unit after its declaration. Its broad visibility is separate from its long lifetime, though broad visibility is one reason mutable global state can make programs harder to reason about.
2. outer and inner: automatic storage duration
Trace outer{"outer"};
{
Trace inner{"inner"};
}
Both objects are automatic locals.
outerbegins its lifetime on each entry tovisit.innerbegins when execution enters the nested block.inneris destroyed immediately on leaving the nested block.outerremains alive afterinneris gone, then is destroyed whenvisitreturns.
The reverse destruction order is deliberate. Within a given scope, later-constructed automatic objects are destroyed first. This property becomes essential when one object depends on another during cleanup.
3. remembered: static storage duration, narrow scope
static Trace remembered{"remembered"};
This declaration is inside visit, so its name is visible only inside visit. However, static changes the object’s storage duration:
rememberedis initialized only on the first call that reaches its declaration.- The second call to
visitdoes not construct anotherremembered. - The same object persists until normal program termination.
- It is destroyed near the end of the program, after
mainends.
A static local is therefore useful for state that must persist across calls while not being globally visible. A counter is the classic small example:
int next_id() {
static int last_id = 0;
return ++last_id;
}
Each call updates the same last_id; it is not recreated as zero on every invocation.
4. dynamic: dynamic storage duration
Trace* dynamic = new Trace{"dynamic"};
This single line creates two distinct objects:
dynamic, an automatic pointer variable local tovisit.- A
Traceobject with dynamic storage duration, created bynew.
The pointer is merely a value that can refer to the dynamically allocated object. It does not determine the dynamic object’s lifetime automatically.
delete dynamic;
dynamic = nullptr;
delete dynamic does two things:
- Calls the dynamic
Traceobject’s destructor. - Releases its dynamically allocated storage.
After delete, dereferencing dynamic would be undefined behavior because the Trace object no longer exists. Assigning nullptr makes the pointer’s non-owning, no-object state explicit. The pointer variable still exists until visit returns, but it no longer refers to a live Trace.
Trace the program against a lifetime timeline
This table condenses the lab into the exact events that determine each object’s lifetime.
| Program event | Objects constructed | Objects destroyed |
|---|---|---|
Before main starts | global_trace | — |
First entry to visit | outer, then remembered, then dynamic Trace, then inner | — |
| First inner block ends | — | inner |
First delete dynamic | — | first dynamic Trace |
First visit returns | — | first outer |
Second entry to visit | second outer, second dynamic Trace, second inner | — |
| Second inner block ends and function returns | — | inner, dynamic Trace, outer |
| Normal program termination | — | remembered, then global_trace |
Two observations should stand out:
- The
rememberedobject has function-local scope, but it outlives every call tovisit. - Each dynamically allocated
Traceoutlives neither the first nor second call by accident; it ends exactly wheredeleteis executed.
If you set a breakpoint at visit in GDB, the first and second stops are also a useful continuation of the previous lesson:
gdb ./lifetimes
break visit
run
At each stop, outer and the pointer variable dynamic belong to the current visit stack frame. The static local remembered, once initialized, is not recreated with that frame. The dynamically allocated Trace is separate again: its address is stored in dynamic, but it is not part of the frame’s automatic object set.
The failure modes this model prevents
Returning an address of an automatic object
This function is incorrect:
int* broken() {
int local = 42;
return &local;
}
local is destroyed when broken returns. The returned pointer may still contain an address-like value, but it does not point to a live int. Dereferencing it is undefined behavior.
A valid design normally returns the value itself:
int make_value() {
return 42;
}
For collections or objects whose result should have independent ownership, C++ value types such as std::vector usually express the design more safely. The next lesson will address the ownership side directly with RAII and smart pointers.
Forgetting to destroy a dynamic object
This is a leak:
void leaking_function() {
Trace* p = new Trace{"leaked"};
// No delete p.
}
At the closing brace, p is destroyed because it is automatic. But destroying a raw pointer does not call delete on the object it points to. The dynamically allocated Trace remains alive and unreachable: a memory leak.
As a short experiment, comment out delete dynamic; in the lab program and build with AddressSanitizer:
g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O0 \
-fsanitize=address -fno-omit-frame-pointer \
lifetimes.cpp -o lifetimes_asan
ASAN_OPTIONS=detect_leaks=1 ./lifetimes_asan
On a typical Linux/WSL setup, LeakSanitizer reports the unreleased allocation at program exit. Restore the delete afterward. Importantly, the operating system reclaiming process memory after termination does not make a leak correct: destructors and resource cleanup may have been skipped, and a long-running program can exhaust memory.
Mismatching array allocation and destruction
For raw dynamic arrays, the allocation and destruction forms must match:
int* values = new int[100];
// use values
delete[] values;
Using plain delete values; here is undefined behavior. In modern C++, prefer std::vector for dynamically sized collections; it gives the collection automatic lifetime management without manual new[] and delete[].
A reliable classification routine
When reading unfamiliar C++ code, classify each object using this sequence:
- Find the object itself. Do not confuse a pointer with the pointee.
- Locate its declaration or creation. Is it an ordinary local, a namespace-scope object, a static local, or an object created with
new? - Identify its construction point. For an automatic local, execution reaches the declaration; for a static local, the first execution reaches the declaration; for
new, the allocation expression runs. - Identify its destruction point. A block exit, program termination, or an explicit
delete. - Check whether a pointer, reference, iterator, or callback could outlive the object it refers to.
This is more dependable than asking only, “Is it on the stack or heap?” The latter question can be a useful debugging shortcut, but lifetime analysis catches the actual correctness issue: whether the object exists at the point where code uses it.
Takeaways
- Automatic storage duration is the default for ordinary local objects. Their lifetimes end at block exit, and destructors run automatically in reverse construction order.
- Dynamic storage duration comes from explicit allocation such as
new. The pointer variable and the dynamically allocated object are separate objects with separate lifetimes;deleteends the pointee’s lifetime. - Static storage duration applies to namespace-scope objects and local
staticobjects. Namespace-scope objects are generally initialized beforemain; static locals initialize once on first use. Both persist until normal program termination. - Scope, storage duration, and lifetime are related but different. A static local has narrow scope and long lifetime; a local pointer can have short automatic lifetime while referring to a longer-lived dynamic object.
- Constructors and destructors provide a practical way to trace lifetime events, while GDB can show which automatic locals belong to the currently active frame.
Next, you will use this model to apply RAII and smart pointers to repair a resource leak. The key idea will be to place an owning object with automatic lifetime around a dynamically allocated resource, so cleanup occurs reliably at scope exit.
Can't find a good explanation? Sign up and we'll make it for you
Sign up