Concepts / Scope and Lifetime of Objects

Scope and Lifetime of Objects

A container is an object holding a collection of elements, and Vector is the example used to introduce cleanup.

  • Programming

A Container That Never Gives Memory Back

A container is an object that holds a collection of elements. The Vector class is the running example in this article. Vector is a useful container: it is simple to understand, it provides range-checked access, and it can report its size so that you can loop over its elements. It has one serious flaw, though. Vector obtains memory for its elements with new, and nothing in the class ever gives that memory back.

Keep that flaw in mind as you read. The rest of this article shows how C++ closes the gap. The answer is not to ask every user of Vector to remember a cleanup step. Instead, the language gives each object a way to tie its cleanup to its own lifetime, and that mechanism is the destructor.

Why does the language need a destructor at all? C++ defines an interface for a garbage collector, but you cannot count on one being present. Some environments do not have a collector, and sometimes a programmer wants precise control over when memory is freed, for logical or performance reasons. So the language needs another way to guarantee that memory obtained in a constructor is eventually released.

Tracing One Vector From Creation to Cleanup

Follow a single example through its life. Suppose a program creates a Vector with three elements inside a function, uses it, and then the function returns. The object is not a permanent fixture of the program. It has a beginning, a period of use, and an end, and each of those moments has a specific effect on the memory the Vector depends on.

Vector createdconstructor runsArray acquirednew allocates the elementsElements usedprogram reads and writesFunction returnsobject lifetime endsArray releaseddestructor runs deletethenthenthenthen
In what order do construction, use, and destruction happen, and at which points is the array acquired and released?

Read the timeline from left to right. Memory is acquired at the first step, while the constructor runs, and it is released at the last step, when the object's lifetime ends. The program never has to name the release step itself. It happens as a consequence of the object reaching the end of its life.

How the Constructor and Destructor Pair Up

Read the class in two halves. The constructor does the acquiring. It asks the free store for an array of doubles, stores the pointer in a member named elem, records the number of elements in a member named sz, and fills every slot with zero. The destructor does the releasing. Its name is a tilde, written ~, followed by the class name, so it is ~Vector for a class named Vector. It hands the same array back with delete[]. The two functions work as a pair: whatever the constructor takes from the free store, the destructor is responsible for returning.

Code walkthrough
class Vector {
public:
    Vector(int s) : elem{new double[s]}, sz{s} {
        for (int i = 0; i != s; ++i) elem[i] = 0;
    }
    ~Vector() { delete[] elem; }
    double get(int n) const { return elem[n]; }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
  1. 1
    The constructor runs when a Vector is created and sets up its members.
  2. 2
    The loop fills every slot with zero, so the elements start in a known state.
  3. 3
    The destructor runs when the object's lifetime ends and releases the array through elem.
  4. 4
    get and size only read from the object and do not change its resource.
  • The constructor Vector(int s) has the same name as the class, and it is the acquiring half.
  • new double[s] asks the free store for s doubles, and the pointer is stored in elem.
  • The destructor ~Vector() has the class name preceded by a tilde.
  • delete[] elem returns the same array that the constructor obtained.

Why Users Never Call the Destructor

Users of Vector never call the destructor themselves. They create and use Vector objects much as they would variables of built-in types, and the cleanup happens without their intervention. The rules for naming, scope, allocation, and lifetime that apply to an int or a char also apply to a Vector. The destructor therefore runs as the object's lifetime ends, and the programmer's job is to let the object's scope do the work.

Nested Scopes and When Objects Are Destroyed

A local object lives for the scope in which it is declared. When a block ends, the objects declared inside it reach the end of their lifetime, and their destructors run. The example below uses a small class whose constructor and destructor each print one line, so you can see the order of events directly. The class is written to make the timing visible and is not taken from the source's Vector.

cpp
Notes on the code
  • outer is declared in main and lives until main ends.
  • inner is declared inside the inner block and lives only until that block closes.
  • The destructor of inner runs at the closing brace of the inner block.

What do you think happens?

Before reading on, predict the order in which these five lines print: construct 1, construct 2, inside block, destroy 2, after block, destroy 1.

  • construct 1, construct 2, inside block, destroy 2, after block, destroy 1

  • construct 1, construct 2, destroy 2, inside block, after block, destroy 1

  • construct 1, inside block, construct 2, destroy 2, after block, destroy 1

Reveal answer
Answer:

construct 1, construct 2, inside block, destroy 2, after block, destroy 1

The outer object is constructed first. The inner object is constructed when the inner block begins, and it is destroyed when that block closes, which is before the line after the block prints. The outer object is destroyed last, when main ends.

Construct outerouter lives in mainConstruct innerinner begins its blockInside blockboth objects are aliveDestroy innerinner block closesAfter blockonly outer is aliveDestroy outermain endsnextnextblock endsnextfunction ends
When the inner block ends, which object is destroyed first, and when does the outer object end?

The diagram shows the principle the example depends on. An object's destructor runs when its own scope ends, not when the program happens to finish. The inner object is therefore released while the outer object is still alive, and the outer object is released later, at the end of main.

Resource Acquisition Is Initialization

The constructor and destructor combination is the basis of many techniques for managing resources in C++. The best-known is Resource Acquisition Is Initialization, usually abbreviated RAII. RAII is the technique of acquiring a resource in a constructor and releasing it in a destructor. The name is a little misleading at first, because the important half of the idea is the release, which the destructor guarantees as the object's lifetime ends.

Enter functionlocal object is declaredConstruct localobjectconstructor acquires theresourceUse the resourcecode works with the dataLeave the scopeobject lifetime endsDestructor runsdestructor releases theresourceResource releasedno cleanup call in usercodenextnextnextlifetime endsnext
How does the resource get released automatically once a local object's scope is left?

In the flowchart, the programmer writes no release statement in the function body. The cleanup is attached to the object, so it runs at the point where the object's lifetime ends. This is the reason RAII reduces leaks: the release is no longer a separate step that every piece of code must remember.

Mistakes Beginners Make With Lifetimes

  • Common mistake

    Calling the destructor by hand to free a Vector

    What it looks like

    v.~Vector(); followed later by the end of the scope

    Why it fails

    Users of Vector never call the destructor themselves. The destructor runs as the object's lifetime ends, so an explicit call risks releasing the same array twice.

    Do this instead

    Create the object in the scope where you need it and let the scope end.

  • Common mistake

    Assuming the destructor runs at the end of the program only

    What it looks like

    Expecting an inner-block object to print its destroy message after the final line of main

    Why it fails

    A destructor runs when its own object's lifetime ends, which is the end of its scope, not the end of the program.

    Do this instead

    Track the closing brace of the block where the object was declared.

  • Common mistake

    Thinking a destructor name can be chosen freely

    What it looks like

    Writing a destructor named release() inside Vector

    Why it fails

    The destructor is named by a tilde followed by the class name, so it pairs with the constructor by name.

    Do this instead

    Write ~Vector() for a class named Vector.

Practice Tracing a Lifetime

MEDIUM

Using the Probe program from the nested scopes section, change main so that it declares a third object named 3 inside the inner block after inner is constructed. Write down the full print order, and identify which object's destructor runs first when the inner block closes.

Hints
  • Each constructor prints when its object is created, and each destructor prints when its scope ends.
  • Objects declared inside the inner block all reach the end of their lifetime at its closing brace.
  • Compare your answer with the timeline diagram of nested blocks.

Key Takeaways

  • A constructor acquires a resource, such as an array allocated with new, and the destructor, named a tilde followed by the class name, releases it with delete.
  • A Vector object is a small handle that holds a pointer and a count, while its elements live on the free store, and the destructor releases that block in one step.
  • Destructors run when an object's lifetime ends, which for a local object is the end of its scope, so users never call them directly.
  • Resource Acquisition Is Initialization acquires resources in constructors and releases them in destructors, which lets code avoid naked new and naked delete and reduces resource leaks.

Related concepts

More concepts starting with S