Scope and Lifetime of Objects
A container is an object holding a collection of elements, and Vector is the example used to introduce cleanup.
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.
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.
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;
};- 1The constructor runs when a Vector is created and sets up its members.
- 2The loop fills every slot with zero, so the elements start in a known state.
- 3The destructor runs when the object's lifetime ends and releases the array through elem.
- 4get 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.
- 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.
Reveal 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.
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.
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 likev.~Vector(); followed later by the end of the scope
Why it failsUsers 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 insteadCreate 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 likeExpecting an inner-block object to print its destroy message after the final line of main
Why it failsA destructor runs when its own object's lifetime ends, which is the end of its scope, not the end of the program.
Do this insteadTrack the closing brace of the block where the object was declared.
Common mistake Thinking a destructor name can be chosen freely
What it looks likeWriting a destructor named release() inside Vector
Why it failsThe destructor is named by a tilde followed by the class name, so it pairs with the constructor by name.
Do this insteadWrite ~Vector() for a class named Vector.
Practice Tracing a Lifetime
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.