CodeGym /Courses /C++ SELF /Storage duration in C++ — automatic / static / dynamic

Storage duration in C++ — automatic / static / dynamic

C++ SELF
Level 38, Lesson 0
Available

1. Introduction

When you start programming, a variable seems like just a "box with a value". But very quickly you find out that every "box" has another parameter: how long it exists at all. This is critical, because a program may try to read or modify something that no longer exists, or, conversely, accidentally retain something for too long and get "weird memory effects".

Today we'll introduce a convenient educational classification that helps predict code behavior without mysticism and coffee-ground fortune-telling: storage duration. It's not about "where it lives in OS-level memory", but about "when it's created and when it disappears" (in a simplified model). And yes, this is one of those cases where knowing "how variables live" saves hours of debugging.

2. Terms: scope, lifetime and storage duration

Let's carefully sort out the terms, because beginners very often mix them into one "word soup", and then the soup starts to bite back.

Scope (visibility) — answers the question: where can I refer to the name? For example, the name x is visible inside the block { ... }, but not outside. This is about name accessibility.

Lifetime — answers the question: when does the object exist as an object? That is, when it can be used legally.

Storage duration — a "categorical" classification of lifetime: the object lives until the end of the block, until the end of the program, or until someone frees/loses the allocated memory.

In the C++ standard you will see phrases like "object with automatic storage duration" — this is not just casual wording, but an established term.

Today three kinds of storage duration are enough for us: automatic, static, dynamic.

Cheatsheet: storage duration in one table

Before diving deeper, it's useful to see the whole picture at a glance. Below is an educational cheatsheet. You don't need to memorize it like a poem (we are not in literature class), but it's useful to return to it from time to time.

Kind of storage duration Where usually declared When it's created When it's destroyed Typical example
automatic inside a function / inside {} when entering the block when exiting the block
int x = 0;
inside main()
static globally or local with static once per program at program termination
static int counter;
dynamic "lives separately from the stack" (in examples: inside containers) when a container needs memory when the container frees the memory (usually in its destructor) elements of
std::vector
, buffer of
std::string

Important: the phrase "dynamic storage duration" applies to objects, not to "the heap as a place". This is a nuance of phrasing, but useful to remember: we talk about objects and their life.

3. Automatic storage duration: local variables until }

You already live with automatic storage duration every day, even if you didn't know its name. Almost any variable you create inside a function is automatic. It appears when entering the code region and disappears when exiting. No magic, just discipline: "enter — create, exit — destroy".

The point of automatic variables is that they are cheap and predictable: you can almost always tell where their life ends — at the nearest closing }. This is a basic building block for local computations: compute, check, parse, compare, temporarily assemble a string.

Example: a variable is created anew on every function call

#include <iostream>

void demo() {
    int x = 0;          // automatic
    ++x;
    std::cout << x << '\n'; // 1
}

int main() {
    demo();
    demo();
}

Note: the program will print 1 and 1, not 1 and 2. Because x is created anew each time. demo() finished — x disappeared. Then demo() started again — a new x appeared.

Example: a nested block shortens a variable's life

#include <iostream>
#include <string>

int main() {
    std::cout << "start\n"; // start

    {
        std::string tmp = "temporary"; // automatic
        std::cout << tmp << '\n';      // temporary
    } // tmp is destroyed here

    std::cout << "end\n";   // end
}

Practical meaning: sometimes it's convenient to create "heavy" objects (for example, a large std::string or std::vector) as late as possible and destroy them as early as possible — simply by placing them in a separate block. This is not a "micro-optimization", but a way to make code clearer: it's visible where the object is needed and where it is not.

4. Static storage duration: the object lives the whole program

Now a very useful and also very tricky tool. Static storage duration means: the object exists for almost the entire program runtime. The simplest example is a global variable. But more often in real code you see another variant: a local static variable inside a function.

And here the main point begins: scope and lifetime are not the same. A name can be visible only inside a function (local scope), but the object can live the whole program (static storage duration).

Example: a function call counter

#include <iostream>

void hits() {
    static int counter = 0; // static storage duration
    ++counter;
    std::cout << counter << '\n';
}

int main() {
    hits(); // 1
    hits(); // 2
    hits(); // 3
}

counter is created once, and its value is not "reset" between calls. That's why static is often used for "function memory".

In the C++ standard you will find the term "object with static storage duration" used in the same "official style" as for automatic.

When it's convenient

Imagine you build a generator of unique ids for tasks. You want the "next id" number not to reset each time you add a task. Here static looks logical: it's a long-lived counter/configuration hidden inside a function (not exposed globally).

But static is like garlic: overall useful, but too much of it burdens the surroundings. Too many static variables create hidden dependencies, make program behavior less obvious, and make functions harder to test (because they have memory that doesn't reset automatically between tests).

5. Dynamic storage duration: dynamic memory via containers

Beginners often perceive the word "dynamic" as scary, meaning there must be new/delete and nightmarish bugs. Good news: today we don't do manual memory management. We use a more mature approach: dynamic memory comes through standard containers, and they manage it themselves.

The idea is simple: the stack (automatic) is good when data size is small and known in advance. But strings, lists, user input — these have sizes known only at runtime. So std::string and std::vector typically use dynamic memory for their internal data.

Key point: the container object itself can be automatic (for example, std::vector<int> v; inside main()), but its elements can be stored in dynamic memory.

Example: std::vector grows — and needs dynamic memory

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;               // v — automatic object
    std::cout << v.size() << '\n';    // 0

    v.push_back(10);                  // elements usually in dynamic memory
    std::cout << v.size() << '\n';    // 1
}

We don't see the "heap" with our eyes, but we see the effect: the vector can grow as needed.

Example: size and capacity — about "how many are present" and "how much is allocated"

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    v.reserve(10);

    std::cout << v.size() << '\n';     // 0
    std::cout << v.capacity() << '\n'; // 10
}

reserve(10) does not add 10 elements. It says: "allocate space so you can grow later without extra moves". This is a direct bridge to dynamic storage duration: memory for elements is allocated separately, and the container manages this resource.

Important confusion: dynamic doesn't mean "lives long"

I'll pause here intentionally, because this is one of the main "mental bugs" beginners have.

When you hear "dynamic memory", it may seem: "Oh, so it lives long". In fact no. Dynamic memory is not about "long", it's about "not tied to a block {} and can have variable size".

For example, std::vector<int> v; inside a function can live exactly until the }. And when v is destroyed, it will free its dynamic memory (internally). So dynamic memory can be used very briefly, just because a variable size is needed.

Conversely, a static variable can live a long time but have nothing to do with dynamic memory.

This idea helps you correctly answer "who is responsible for the data" and "why data disappeared after leaving the function".

Intuitive scheme: "office" and flowchart

To fix the intuition, imagine the program as a small office:

  • automatic — a "paper on an employee's desk". While the employee is at the desk (while the block executes), the paper exists. When the shift is over (block exited) — the paper is thrown away.
  • static — "information on the company board". It hangs all working day (the whole program).
  • dynamic — the "warehouse". You can bring boxes of different sizes there, but someone must manage the warehouse (in our case — std::vector/std::string).

You can even draw a simple flowchart:

flowchart TD
    A["Where was the object declared?"] --> B{"Inside a block `{}`?"}
    B -->|Yes| C["Usually automatic (lives until `}`)"]
    B -->|No| D{"Global/`static`?"}
    D -->|Yes| E["static (lives until program end)"]
    D -->|No| F["dynamic (in examples: memory inside containers)"]

This diagram is not "the formal truth of the universe", but a handy compass: it helps quickly estimate what to expect from a variable.

6. Practical example: the mini-app TaskBook

To avoid leaving the topic in vacuum, let's continue our learning line: a small console app where we keep a list of tasks. We won't build a full CLI parser (that will come much later), just show how storage duration appears in ordinary code.

Task model: an automatic object and dynamics inside std::string

The Task struct itself is a normal data model. When we create a variable Task t; inside a function, it's an automatic object. But the std::string inside it can use dynamic memory for the text.

#include <string>

struct Task {
    int id = 0;
    std::string title;
};

Here it's important to get used to the idea of a "layered cake": the Task object follows its own rules, and its fields (for example, the string) can have internal resources and manage them.

Id generator: a local static variable

Now we need to issue unique ids. The simplest educational implementation is a function that remembers a counter between calls. This is a classic case of static storage duration.

int generate_task_id() {
    static int next_id = 1;  // lives for the whole program
    return next_id++;        // 1, then 2, then 3...
}

The name next_id is visible only inside generate_task_id(), but it will live until program termination. This nicely illustrates that scope and storage duration are different "axes".

Creating a task: an automatic object returned by value

The function creates a local Task, fills it and returns it. For us it's not important "how exactly the return is optimized" now, but that the local object t is automatic and will disappear when exiting the function, while the result goes outside as a value.

#include <string>

Task make_task(const std::string& title) {
    Task t;
    t.id = generate_task_id();
    t.title = title;
    return t;
}

Yes, here we use const std::string& as a parameter — we already know how to do that to avoid unnecessary string copies (this comes from earlier topics about parameters). We won't dive deeper into reference rules today — we use it as a familiar tool.

Task storage: std::vector<Task> and dynamic memory for elements

Now let's create a list of tasks. The variable tasks can be local in main() — so the container object itself is automatic. But memory for the elements inside the vector is dynamic.

#include <vector>

int main() {
    std::vector<Task> tasks;     // tasks — automatic object
    tasks.push_back(make_task("Read C++ book"));

    // ... later we'll add and print
}

And this is the central idea of the day: a "local variable" and "dynamic memory" coexist nicely in the same code without new/delete. The container owns the memory, you own the container.

Printing tasks: local variables in a loop are also automatic

Let's make a small print function. Its local variables (the const Task& t in the range-for, temporary strings for formatting, etc.) are automatic.

#include <iostream>
#include <vector>

void print_tasks(const std::vector<Task>& tasks) {
    for (const Task& t : tasks) {
        std::cout << t.id << ": " << t.title << '\n';
    }
}

If you see for (...) { ... } — almost certainly there are automatic variables inside that will disappear when the loop ends.

7. Common mistakes

Mistake #1: confusing name visibility (scope) with object lifetime.
Beginners often reason: "The name is not visible — so the object is gone." That's incorrect. A name may be not visible, but the object can still live (for example, a static variable in another function, or an object referenced through other mechanisms). And vice versa: a name can be visible, but the object must not be used anymore (we will cover this in detail later when pointers/references appear). For now keep a simple rule: scope answers access to the name, while storage duration and lifetime are about the object's existence.

Mistake #2: expecting a local variable to "remember the value" between function calls.
If a variable is automatic, it is created anew each time you enter the block. So code like "increment a local counter and expect it to grow" yields surprisingly identical results. If you really need memory between calls — consider static (and at the same time think whether you are introducing hidden state that will be hard to test and explain).

Mistake #3: thinking that std::vector is "entirely on the heap".
A vector has a wrapper object (size, capacity, pointer to buffer, etc.) and a buffer of elements. The std::vector object can be automatic (a local variable), while elements are in dynamic memory. Misunderstanding this "two-layer" structure often leads to odd expectations: "why did the vector get destroyed and everything disappeared, even though there was dynamic memory?" Because the vector owned the dynamic memory and its lifetime ended.

Mistake #4: using static as a "universal crutch" because "it works".
static can indeed "fix" a situation when you need to preserve a value. But it can quietly turn a function into an object with hidden state. Later you add tests, a second use case, parallel runs, and find that "it behaves strangely". A good habit: before writing static, articulate in words why the object must live the whole program, and who will be responsible for the correctness of that long-lived state.

1
Task
C++ SELF, level 38, lesson 0
Locked
Temporary counter
Temporary counter
1
Task
C++ SELF, level 38, lesson 0
Locked
Persistent counter
Persistent counter
1
Task
C++ SELF, level 38, lesson 0
Locked
Heavy String
Heavy String
1
Task
C++ SELF, level 38, lesson 0
Locked
Task list
Task list
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION