C++ / MEMORY OWNERSHIP AND SMART POINTERS
The stack versus the heap and object lifetimes
Predict when every object is created and destroyed by naming its storage duration, and choose automatic or dynamic storage on purpose rather than by habit.
What you will learn
- Automatic objects die at their block's closing brace, in reverse construction order.
- Track two lifetimes for every new: the pointer variable and the object it points to.
- Reach for dynamic storage only when a lifetime must outlive the scope that starts it.
- Spot static storage duration: built once, destroyed after main in reverse order.
Understanding The stack versus the heap and object lifetimes
A local variable's storage is part of its function's frame, and the compiler computes the size and layout of that frame while compiling. Entering a block moves the stack pointer by a known amount, leaving it moves the pointer back, and on the way out the compiler emits destructor calls for exactly the objects declared in that block, in reverse order of construction. That is why automatic lifetimes are precise to the closing brace and cost almost nothing to manage: there is no runtime bookkeeping, only pointer arithmetic and a fixed list of destructor calls that was decided at compile time.
A `new` expression differs in kind, not just in speed. It asks an allocator for a suitably aligned block at an address nobody knew at compile time, and the object constructed there has a lifetime that begins at that call and ends only when some `delete` runs, possibly in another function, on another thread, or never. From that point on you are tracking two objects with two different lifetimes: the pointer variable, which is automatic and disappears at the brace, and the pointee, which the language will never touch on its own.
Choose by the lifetime you need, then by size and type. If the object is only needed inside a scope, declare it there; if the caller needs the value, return it by value and let copy elision or a move carry it out, neither of which needs `new`. Reach for dynamic storage when a lifetime genuinely has to cross a scope boundary, when the size or the concrete derived type is only known at runtime, or when the object is far too big for the fixed few megabytes a thread's stack region gets. The two regions also mix inside a single object: a local `std::vector<int>` keeps its bookkeeping pointers in the frame and its elements in dynamic storage, and the destructor call at the closing brace is what releases those elements.
<iostream>
struct Tracer {
const char* name;
explicit Tracer(const char* n) : name(n) { std::cout << "ctor " << name << '\n'; }
~Tracer() { std::cout << "dtor " << name << '\n'; }
};
Tracer* make_one() {
Tracer local{"local"}; // automatic storage duration
Tracer* dyn = new Tracer{"dynamic"}; // dynamic storage duration
std::cout << "about to leave make_one\n";
return dyn; // the pointer is copied out, the object stays put
}
int main() {
Tracer* p = make_one();
std::cout << "back in main, still alive: " << p->name << '\n';
delete p; // the dynamic lifetime ends exactly here
std::cout << "after delete\n";
}
An object's creation and destruction points are fixed by its storage duration rather than by where you wrote the code: automatic objects end at the closing brace of their block, while dynamic objects end only when something explicitly destroys them.
Worked examples
Scopes, not functions, end automatic lifetimes
Shows that each loop iteration and each bare block is its own lifetime boundary, while the first object declared is destroyed last.
<iostream>
struct Marker {
int id;
explicit Marker(int i) : id(i) { std::cout << "(+" << id << ')'; }
~Marker() { std::cout << "(-" << id << ')'; }
};
int main() {
Marker outer{0};
for (int i = 1; i <= 3; ++i) {
Marker loop{i}; // a different object on every pass
}
std::cout << '\n';
{
Marker inner{9}; // a bare block is a scope too
}
std::cout << "\nlast line of main";
}
Example explained
Line 1`Marker loop{i};` sits in the loop body, which is a block, so construction and destruction pair up inside every iteration: (+1)(-1)(+2)(-2)(+3)(-3).
Line 2The bare `{ ... }` around `inner` ends its lifetime before the next statement runs, which is why (+9)(-9) completes on its own line.
Line 3`outer` is declared first in main, so it is destroyed last, after the final output statement, putting (-0) at the end of the text.
Line 4Successive loop objects may well occupy the same stack address, but nothing here depends on that; what the language guarantees is the pairing and the ordering.
Three storage durations in one program
Contrasts a namespace-scope object, a function-local static, and an ordinary local to show when each is built and destroyed.
<iostream>
struct Noisy {
const char* tag;
explicit Noisy(const char* t) : tag(t) { std::cout << "ctor " << tag << '\n'; }
~Noisy() { std::cout << "dtor " << tag << '\n'; }
};
Noisy global{"global"}; // static storage duration
void called_twice() {
static Noisy once{"function static"}; // built on the first call only
Noisy each{"automatic"}; // built on every call
std::cout << "-- inside called_twice\n";
}
int main() {
std::cout << "main starts\n";
called_twice();
called_twice();
std::cout << "main ends\n";
}
Example explained
Line 1`global` has static storage duration and is initialized before main's first statement, so "ctor global" precedes "main starts".
Line 2`static Noisy once` is initialized the first time control passes its declaration and skipped afterwards, hence one "ctor function static" against two "ctor automatic" lines.
Line 3`each` is automatic, so its destructor runs at the end of each call instead of waiting for program exit.
Line 4Static objects are destroyed after main returns in reverse order of construction, so `once` (built during the first call) is destroyed before `global`.
Important notes
"Stack" and "heap" are implementation vocabulary; the standard speaks of automatic, static, thread-local, and dynamic storage duration, and it promises no size for any of them.
The explicit `delete` in these examples is there to mark the precise end of a dynamic lifetime: nothing runs it when the scope ends, and nothing runs it when an exception unwinds the scope either, while automatic objects are destroyed in both cases.
Common mistakes
Putting a large buffer such as `double buf[2000000];` in a function as a local: that is 16 MB of automatic storage against a fixed stack region of a few megabytes, so the program dies with a stack overflow, usually a bare segfault rather than a catchable std::bad_alloc.
Treating `Widget* w = new Widget;` as one object with one lifetime: at the closing brace only the eight-byte pointer is reclaimed, and the Widget is still sitting in dynamic storage with nothing referring to it.
Assuming a variable declared inside an `if` or loop body lives until the function returns; it is destroyed at the end of that inner block, so anything its destructor does happens far earlier than the code after the loop expects.
Try it yourself
Change, predict, then run
Write a struct whose constructor and destructor each print a label, then create one instance directly in main, one inside a bare `{ }` block, and one with `new` that you delete on the last line of main. Write down the expected order of the six lines before running it, then compare with the real output.
Open the C++ workspaceCheck your understanding
A function body contains `std::string s;` followed by `std::string* p = new std::string;`. What happens when control reaches the function's closing brace?
- Both string objects are destroyed, because the compiler destroys everything the block created.
- Neither is destroyed; destruction of locals is deferred until the calling function returns.
- s is destroyed and its character buffer released, while the object at p stays alive and only the pointer variable's storage is reclaimed.
- Destroying p also destroys the string it points to, since p was the only handle to that object.
Show answer
Scope exit destroys the objects with automatic storage duration declared in that block: `s` and the pointer variable `p`. Destroying `s` runs std::string's destructor, which frees the characters it allocated, while destroying `p` does nothing at all because a raw pointer has a trivial destructor. Option 3 is tempting because `p` really is the last handle, but being the only pointer to an object carries no meaning in the language; the dynamic object's lifetime ends only at a `delete`, and losing the pointer just makes it unreachable.