Memory Management & Object Lifetime
Every object in C++ has a storage duration that governs when its memory exists and a lifetime that governs when it's safe to use -- confusing the two is the root cause of most dangling-pointer and dangling-reference bugs.
C++ gives the programmer far more control over an object's lifetime than most languages -- and far more ways to get it wrong. An object's storage duration (automatic, static, dynamic, or thread-local) determines which region of memory it lives in and when that memory is allocated and released; its lifetime is the narrower window, starting when construction completes and ending when destruction begins, during which the object can actually be used. Reading or writing an object outside its lifetime -- before construction finishes, after destruction starts, or through a pointer to memory that's already been reclaimed -- is undefined behavior, whether or not the bytes still look valid. This page works through the four storage durations, exactly what construction and destruction guarantee, why temporaries have their own special lifetime-extension rule, how dangling pointers and references arise, and the low-level tools (placement new, explicit destructor calls) that exist for the rare cases where you need to separate allocation from construction entirely.
Storage Duration: Where and When Memory Exists
C++ defines four storage durations. Automatic storage duration applies to ordinary local variables -- memory is allocated when execution enters their scope and released when it leaves. Static storage duration applies to global variables, namespace-scope variables, and variables declared static inside a function or class -- memory exists for the entire program run. Dynamic storage duration applies to objects created with new -- memory persists until explicitly released with delete. Thread-local storage duration (thread_local) applies to variables where each thread gets its own independent instance, existing for the lifetime of that thread.
local is recreated fresh on every call to demo(), so it's always 5. call_count is static, so its storage (and value) survives between calls -- it keeps incrementing. heap_value points at memory that exists independently of demo()'s scope and must be deleted explicitly, or it leaks.
#include <iostream>
int global_counter = 0; // static storage duration
void demo()
{
int local = 5; // automatic storage duration
static int call_count = 0; // static storage duration (persists across calls)
int *heap_value = new int(42); // dynamic storage duration
++call_count;
std::cout << local << " " << call_count << " " << *heap_value << "\n";
delete heap_value; // must release manually
}
int main()
{
demo(); // prints: 5 1 42
demo(); // prints: 5 2 42 -- call_count persisted, local did not
}Construction and Destruction: The Lifetime Boundaries
An object's lifetime begins exactly when its constructor finishes running (more precisely, when initialization is complete) and ends exactly when its destructor begins running. This is a stricter window than 'storage exists' -- storage for an automatic variable is reserved the moment its scope is entered, but the object itself doesn't exist yet until the constructor completes.
Output: before scope / A constructed / B constructed / inside scope / B destroyed / A destroyed / after scope. Destruction always happens in the reverse order of construction for objects in the same scope, which is what guarantees that if B's constructor relied on A already existing, A is still alive (not yet destroyed) when B is torn down.
Entering the inner block reserves storage for a and b, but neither exists yet as an object.
Logger a("A") constructs a -- a's lifetime begins, 'A constructed' prints.
Logger b("B") constructs b -- b's lifetime begins, 'B constructed' prints.
At the closing brace, b is destroyed first (reverse order), then a.
Once a's destructor finishes, a's lifetime -- and its storage -- both end.
#include <iostream>
class Logger
{
public:
Logger(const std::string &name) : name_(name)
{
std::cout << name_ << " constructed\n";
}
~Logger()
{
std::cout << name_ << " destroyed\n";
}
private:
std::string name_;
};
int main()
{
std::cout << "before scope\n";
{
Logger a("A");
Logger b("B");
std::cout << "inside scope\n";
} // b destroyed, then a destroyed -- reverse of construction order
std::cout << "after scope\n";
}Temporary Objects and Lifetime Extension
A temporary object -- the result of a function call returning by value, or the target of an implicit conversion -- normally lives only until the end of the full expression that created it. Binding a temporary to a const lvalue reference (or an rvalue reference) is a special case: the standard extends the temporary's lifetime to match the reference's lifetime, so it doesn't vanish immediately.
Because ref is a const std::string&, the compiler extends the returned temporary's lifetime to match ref's own scope -- it's destroyed when ref goes out of scope, not at the end of the initializing expression. This is one of the few lifetime-extension rules in C++, and it applies only to the temporary bound directly, not to sub-objects reached through it later.
#include <iostream>
#include <string>
std::string make_greeting() { return "hello"; }
int main()
{
const std::string &ref = make_greeting(); // temporary's lifetime extended
std::cout << ref << "\n"; // safe: temporary still alive
// Without a reference, the temporary would be destroyed immediately:
// std::string &&bad = make_greeting().append("!").substr(0); // not the issue here,
// but note that a *member* accessed off a temporary and stored as a reference is NOT extended.
}Dangling Pointers and Dangling References
A dangling pointer or reference refers to memory whose object has already been destroyed (or, for a pointer, memory that was never a live object of the pointed-to type). Using it -- dereferencing it, calling a member function through it -- is undefined behavior, regardless of whether the memory happens to still contain recognizable-looking bytes.
local in each function has automatic storage duration -- its lifetime ends when the function returns. Both functions hand back a pointer or reference to memory that's already been reclaimed by the time main() tries to use it. Most compilers warn loudly about exactly this pattern (returning the address of a local), because it's one of the most common lifetime bugs in the language.
delete p destroys the pointed-to int and returns its memory, but p itself still holds the old address -- it's now a dangling pointer. Dereferencing it afterward is a use-after-free. Setting p = nullptr; immediately after delete is a defensive habit that at least turns a later accidental use into a clear, debuggable null-pointer dereference instead of silent memory corruption.
#include <iostream>
int *dangling_pointer_example()
{
int local = 42;
return &local; // BUG: returns address of a variable about to be destroyed
}
int &dangling_reference_example()
{
int local = 99;
return local; // BUG: same problem, via a reference
}
int main()
{
int *p = dangling_pointer_example();
int &r = dangling_reference_example();
std::cout << *p << "\n"; // undefined behavior -- local no longer exists
std::cout << r << "\n"; // undefined behavior -- same problem
}#include <iostream>
int main()
{
int *p = new int(10);
delete p;
std::cout << *p << "\n"; // dangling pointer: use-after-free
p = nullptr; // too late -- the bad read already happened above
}Placement new: Separating Allocation from Construction
Ordinary new does two things at once: it allocates raw memory and it constructs an object in that memory. Placement new (#include <new>) lets you separate those steps -- construct an object at a specific, already-allocated address, without asking for new memory. It's the mechanism std::vector and other containers use internally to construct elements only in the already-reserved storage they manage.
new (buffer) Point(3, 4) constructs a Point object directly inside the raw buffer array instead of asking the allocator for fresh memory. Because that memory was never obtained via a matching new expression, calling delete p would be wrong -- delete would try to deallocate buffer itself, which is a stack array, not heap memory. Instead the destructor must be called directly and explicitly.
#include <new>
#include <iostream>
class Point
{
public:
Point(int x, int y) : x_(x), y_(y) { std::cout << "Point constructed\n"; }
~Point() { std::cout << "Point destroyed\n"; }
int x_, y_;
};
int main()
{
alignas(Point) unsigned char buffer[sizeof(Point)]; // raw, uninitialized storage
Point *p = new (buffer) Point(3, 4); // placement new: construct IN buffer
std::cout << p->x_ << ", " << p->y_ << "\n";
p->~Point(); // must call the destructor explicitly -- no delete for placement new
}Explicit Destructor Calls
Calling a destructor directly (p->~Point();) is legal C++, but it's almost never needed in ordinary code -- normal objects are destroyed automatically when they go out of scope, and delete already calls the destructor for you as part of destroying a heap object. An explicit destructor call only makes sense paired with placement new, where the object's lifetime is being managed manually and there's no delete to call the destructor implicitly.
Object Lifetime Rules, Summarized
Putting the pieces together: storage duration answers 'when does the memory exist', and lifetime answers the narrower question 'when is it valid to use the object'. A local variable's storage and lifetime happen to start and end together in simple cases, which is why beginners often conflate the two -- but placement new (construct after allocating), a union's active member (only one member has a live object at a time), and manual delete followed by continued pointer use all show the two concepts pulling apart. The core rule that avoids nearly every lifetime bug on this page: never access an object before its constructor has finished, and never access it after its destructor has started (or after its owning storage duration has ended) -- track exactly what currently owns each object's lifetime, and make sure nothing reaches for it once that owner has let it go.
What is the difference between an object's storage duration and its lifetime?
Storage duration describes which memory region an object lives in and when that memory is allocated and released (automatic, static, dynamic, or thread-local). Lifetime is the narrower window during which the object actually exists as a valid, usable object -- it begins when construction completes and ends when destruction begins. Storage can exist before an object's lifetime starts (e.g. raw memory before placement new) or continue after it ends.
Why does binding a temporary to a const reference extend its lifetime, and what is the one thing that extension does NOT cover?
The standard specifically extends a temporary's lifetime to match the const (or rvalue) reference's own lifetime when the reference is bound directly to that temporary, preventing it from being destroyed at the end of the full expression as it normally would be. What it does NOT cover is a temporary reached indirectly -- if a reference is bound to a member or return value obtained through a temporary rather than the temporary itself, that underlying temporary is still destroyed at the end of the expression, leaving the reference dangling.
Why is it a bug to return a pointer or reference to a local variable from a function?
A local variable has automatic storage duration, so its storage (and lifetime) ends when the function returns. Returning its address or a reference to it hands the caller a pointer or reference to memory that's already been reclaimed by the time the caller can use it, making any subsequent access undefined behavior even though the bytes might briefly look unchanged.
What two things does ordinary new do that placement new separates, and why must an object created with placement new be destroyed with an explicit destructor call rather than delete?
Ordinary new both allocates raw memory and constructs an object in it; placement new only constructs an object at an address you already have, without allocating new memory. Because that memory wasn't obtained through the placement-new expression itself, delete would incorrectly try to deallocate it (e.g. a stack buffer or memory owned by something else); the object still needs its destructor run, so it's called explicitly (p->~T();) instead, leaving the underlying storage's release to whatever mechanism actually allocated it.
After delete p;, why is p itself still dangerous even though the memory it pointed to has been freed?
delete destroys the pointed-to object and releases its memory, but it does not change the value stored in p -- p still holds the old address, now dangling. Any subsequent dereference of p is a use-after-free, which is undefined behavior; setting p = nullptr immediately after delete is a common defensive habit that turns an accidental later use into an obvious null-pointer dereference instead of silent corruption.
What does this program print, and why is ref still safe to use even though make_value() returns by value?
It prints 100. make_value() returns a temporary int, and binding it directly to the const int& ref triggers lifetime extension: the standard extends the temporary's lifetime to match ref's own scope instead of destroying it at the end of the initializing expression. So by the time std::cout << ref runs, the temporary is still alive -- it will be destroyed only when ref itself goes out of scope at the end of main.
Write a C++ program that demonstrates a dangling reference bug by returning a reference to a local variable from a function, then fix it by returning by value instead, and print the (correct) result from the fixed version.
The buggy version returns int& bound to a local variable whose automatic storage duration ends the moment the function returns, so any use of that reference by the caller is undefined behavior. The fixed version returns int by value: the return statement copies local's value out before local is destroyed, so main() ends up with its own independent int that has no connection to the now-destroyed local, and printing it correctly shows 10.
Write a C++ program that uses placement new to construct a std::string in a manually-allocated char buffer, uses the string, then destroys it with an explicit destructor call, verifying with std::cout that no memory is leaked or corrupted.
alignas(std::string) buffer[sizeof(std::string)] reserves correctly sized and aligned raw storage without constructing anything in it. new (buffer) std::string(...) then constructs a real std::string object at that address, which is used normally through s. Because the storage came from a stack array rather than from a matching new expression, delete s would be wrong -- instead s->~basic_string() is called explicitly, which runs std::string's destructor (releasing any heap memory the string itself allocated for its characters) without trying to deallocate buffer, which the stack cleans up on its own when main() exits.
#include <iostream>
int make_value() { return 100; }
int main()
{
const int &ref = make_value();
std::cout << ref << "\n";
}