C++ provides a great deal of flexibility.
That includes how and where objects are created: in automatic storage or dynamically allocated storage.
Of course, great power comes with great responsibility.
It is the developer’s responsibility to choose an appropriate lifetime and ownership model.
This article aims to bring more clarity to that decision.
It explains what developers usually mean by the stack and the heap, how storage duration differs from object lifetime, and when dynamic allocation is appropriate.
What Is Usually Called the Stack and the Heap
In everyday C++ discussions, the terms stack and heap are convenient shorthand.
Strictly speaking, the C++ standard defines storage durations rather than requiring objects to be placed in a physical stack or heap.
Implementations commonly place local objects with automatic storage duration on the call stack and use the heap, sometimes called the free store, for dynamically allocated objects.
However, the compiler may also keep a local object in a register or optimize it away completely.
An object is commonly described as being created on the heap when its storage is obtained dynamically.
For example, an object of type Foo can be created with a new expression:
auto* heapFoo = new Foo{};
The storage for the Foo object is dynamically allocated.
The object remains alive until it is explicitly destroyed:
delete heapFoo;
Without a corresponding delete, the dynamically allocated object and its storage are leaked.
Direct use of new and delete is shown here only to explain the underlying model. In modern C++, ownership should normally be represented by standard containers or smart pointers.
On the other hand, an object declared as a local variable usually has automatic storage duration:
void bar()
{
Foo stackFoo{};
}
The lifetime of stackFoo ends automatically when control leaves the function.
The important difference is therefore not only the physical location of the object.
It is how the object’s storage and lifetime are managed.
In the context of C++, it usually makes more sense to discuss storage duration instead of only asking whether an object is constructed on the stack or the heap.
Storage Duration Instead of Construction Place
Storage duration determines the minimum period during which storage for an object is guaranteed to exist.
C++ defines four storage duration categories:
- static storage duration for namespace-scope variables and variables declared with
static; - thread storage duration for variables declared with
thread_local; - automatic storage duration for non-static block-scope variables and function parameters;
- dynamic storage duration for objects whose storage is obtained through an allocating
newexpression.
This article focuses mainly on automatic and dynamic storage duration.
Objects with automatic storage duration are destroyed automatically when control leaves the block in which they were created.
Objects with dynamic storage duration require an explicit ownership strategy.
That does not necessarily mean that application code must call delete manually. In modern C++, dynamic objects are usually managed by containers and smart pointers.
Consider this example:
void bar()
{
Foo automaticFoo{};
auto* dynamicFoo = new Foo{};
delete dynamicFoo;
}
automaticFoo has automatic storage duration and is destroyed when bar() returns.
There is also an important detail here:
auto* dynamicFoo = new Foo{};
The pointer variable dynamicFoo itself has automatic storage duration.
The Foo object created by the new expression has dynamic storage duration.
When bar() returns, the pointer variable is destroyed automatically.
The dynamically allocated Foo object, however, would continue to exist if delete dynamicFoo were omitted.
This distinction is one reason why talking only about “stack variables” and “heap variables” can be misleading.
Storage Duration Is Not Object Lifetime
Storage duration and object lifetime are closely related, but they are not the same thing.
Storage duration determines how long the storage containing an object may exist.
The lifetime of an object can begin after that storage has been obtained and end before the storage itself is released.
For example, a Foo object can be constructed inside a statically allocated byte buffer using placement new:
alignas(Foo) static std::byte storage[sizeof(Foo)];
void bar()
{
Foo* foo = ::new (storage) Foo{};
// Use foo...
std::destroy_at(foo);
}
The array storage has static storage duration and exists for the entire execution of the program.
The lifetime of the Foo object begins when placement new completes.
Its lifetime ends when std::destroy_at(foo) invokes its destructor.

Therefore, the lifetime of foo is much shorter than the lifetime of the storage containing it.
Placement new is a low-level tool and should not be the default way to create objects. It is commonly used inside containers, allocators, memory pools, and other infrastructure that manages raw storage explicitly.
How One Object Manages the Lifetime of Another
An object with dynamic storage duration requires an explicit ownership strategy.
A forgotten delete may cause a memory leak.
And making this mistake is easier than it may seem.
For example, consider an unexpected exception:
void handleFoo(const Foo* foo)
{
throw std::runtime_error("Exception");
}
void bar()
{
auto* foo = new Foo{};
// Throws an exception
handleFoo(foo);
delete foo;
}
In this example, handleFoo() throws an exception.
If the exception propagates outside bar(), execution never reaches:
delete foo;
The local pointer variable is destroyed during stack unwinding, but the dynamically allocated Foo object remains alive. As a result, the program leaks memory.
The proper C++ solution is not to surround every suspicious function call with try/catch blocks.
Instead, ownership of the object with dynamic storage duration should be transferred to an object with automatic storage duration.
When the owning object is destroyed automatically, it also destroys the object it manages.
For example, FooHandler can store and handle a pointer to object:Foo
class FooHandler {
public:
FooHandler()
: m_foo(new Foo())
{
}
~FooHandler(){
delete m_foo;
}
Foo* get() const noexcept
{
return m_foo;
}
private:
Foo * m_foo;
};
FooHandler manages a raw resource directly, its special member functions must be designed carefully according to the Rule of Five and define move/copy constructor and assign operators.

If you are interested in learning more about special member functions, read my free guide:
If Special Member Functions Are Unclear, Your C++ Code Will Stay Fragile
Now bar() can create FooHandler as a local object:
void bar()
{
FooHandler fooHandler;
// Throws an exception
handleFoo(fooHandler.get());
}
Because fooHandler has automatic storage duration, it does not require explicit deletion.
If handleFoo() throws, stack unwinding destroys fooHandler.
Its std::unique_ptr member then destroys the managed Foo object and releases its dynamically allocated storage.
This is how RAII works:
Resource Acquisition Is Initialization.
A resource is owned by an object, and the object’s destructor releases that resource automatically.
Standard library containers and smart pointers are built around the same principle.
When Should You Use Dynamic Allocation?
Objects with automatic storage duration should usually be the default.
They have clear lifetimes, require no separate ownership model, and are destroyed automatically when their scope ends.
Dynamic allocation becomes useful when the program needs a lifetime, ownership model, runtime size, or level of indirection that automatic storage cannot conveniently express.
Several Objects Share Ownership of the Same Instance
Several components may genuinely share ownership of the same object.
For example, multiple request controllers may share the lifetime of one database instance:
auto database = std::make_shared<MySQLDatabase>();
UserRequestController userRequestController{database};
ShopRequestController shopRequestController{database};

The database object is dynamically allocated and managed by a std::shared_ptr.
Both controllers participate in its ownership.
The database remains alive until the last owning std::shared_ptr is destroyed.
However, shared access does not always require shared ownership.
If an application object clearly owns the database and the controllers only use it temporarily, references or non-owning pointers may express the design more clearly.
Use std::shared_ptr only when ownership is genuinely shared.
The Amount of Storage Is Known Only at Runtime
Containers such as std::vector manage dynamically allocated storage internally:
std::vector<Message> messages(messageCount);
The messages object itself may have automatic storage duration.
Its elements are stored separately in dynamically allocated storage managed by the vector.
This is an important distinction:
an object does not always keep all of its managed data in the same storage area as the object itself.
The vector object stores information such as the address, size, and capacity of its element storage.
Its destructor releases that storage automatically.
The Design Requires Indirection
Dynamic allocation is also common when the concrete object type is selected at runtime.
Consider an abstract database interface:
class Database {
public:
virtual ~Database() = default;
virtual bool executeUpdate(std::string_view sql) = 0;
};
A concrete implementation can provide the actual behavior:
class MySQLDatabase final : public Database {
public:
bool executeUpdate(std::string_view sql) override
{
// Execute the query...
return true;
}
};
A controller can own the implementation through the abstract interface:
class UserRequestController {
public:
explicit UserRequestController(std::unique_ptr<Database> database)
: m_database(std::move(database))
{
}
private:
std::unique_ptr<Database> m_database;
};

The concrete implementation is selected when the controller is created:
UserRequestController controller{std::make_unique<MySQLDatabase>()};
The pointer to the base class Database provides a stable interface, while the concrete implementation may vary.
This hides implementation details and allows the program to select an implementation at runtime.
Similar indirection is used in object graphs, recursive data structures, and the PImpl pattern.
Dynamic allocation is not required for every use of polymorphism or indirection, but it is a common way to represent them when object lifetime and concrete type must be decided at runtime.
A Practical Rule
Prefer objects with automatic storage duration by default.
Use dynamic allocation when lifetime, ownership, runtime size, or indirection requires it.
When dynamic allocation is necessary, prefer standard containers and smart pointers over direct calls to new and delete.
The question is usually not:
Should this object be on the stack or the heap?
A better question is:
Who owns this object, and how long must it remain alive?
Final Thoughts
The terms stack and heap are useful for a quick explanation, but they do not describe the complete C++ object model.
A more accurate way to reason about object creation is to separate four questions:
- Where does the storage come from?
- When does the object’s lifetime begin and end?
- Who owns the object?
- Who is responsible for releasing its resources?
For most objects, automatic storage duration should be the default.
It provides a clear lifetime, automatic destruction, and no separate ownership management.
Dynamic allocation becomes useful when the object must outlive the current scope, when its size or concrete type is determined at runtime, or when the design requires ownership transfer, shared ownership, or indirection.
Even then, direct calls to new and delete should rarely appear in application code.
Standard containers and smart pointers already provide the ownership and cleanup mechanisms required by most programs.
So instead of asking:
Should this object be created on the stack or the heap?
Ask:
What lifetime and ownership model does this object require?
Once that question is answered, the storage choice usually becomes much clearer.
Build More Than Small C++ Examples
Understanding storage duration, ownership, and object lifetime becomes much easier when you apply these ideas in a real project.
In my book, No More Helloworlds — Build a Real C++ App, I show how these concepts connect with practical C++ development:
- object ownership and RAII
- project structure
- modern CMake
- testing
- dependencies
- architecture decisions and trade-offs
The goal is not only to make the code compile, but to build a C++ application that remains understandable and maintainable as it grows.