The life of an object ends as soon as the block in which the object was declared has been exited. At that point, the compiler does not just forget about the memory that the object had occupied; instead, it searches for a special member function to which responsibility can be delegated to perform any necessary closing down actions for the object. That special function is called the destructor.
A destructor is specified by writing a tilde (~) immediately followed by the name of the class, using exact class name, no qualifiers needed. It can have neither a return type nor any parameters, and is called automatically by the compiler. This implicit call takes place automatically whenever the object leaves scope; the programmer never needs to write the parentheses to make the call in program code. In the program that follows, the automatic call is brought to the reader’s attention by means of output messages in the constructor and the destructor.
Program:
#include <iostream>
class Tracer
{
public:
Tracer()
{
std::cout << "constructor\n";
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "destructor\n";
}
};
int main()
{
std::cout << "entering main\n";
Tracer t{};
std::cout << "leaving main\n";
return 0;
}
Output of program above:
The flow of control begins by printing "entering main". Next, the statement Tracer t{}; allocates the required memory, then invokes the constructor of the Tracer object, causing the message "constructor" to appear. When "leaving main" is printed, control has reached the closing brace of main and, consequently, local object t goes out of scope; the compiler automatically invokes ~Tracer(), and this causes "destructor" to be displayed. In short, the sequence of output could be achieved only by an automatic call to the destructor.
The same automatic call is performed whenever an object of a given class terminates its lifetime, no matter how the object was formed. If multiple objects reside in the same block, they are always destroyed in the reverse of the order in which they were constructed. Next, two objects are constructed; each announces its arrival with its constructor and its departure via its destructor.
#include <iostream>
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "constructor " << id << '\n';
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "destructor " << id << '\n';
}
};
int main()
{
Tracer first{ 1 };
Tracer second{ 2 };
std::cout << "inside main\n";
return 0;
}
The first of the two objects, first, is constructed first, resulting in the display of "constructor 1". The second object, second, is then constructed, displaying "constructor 2". When the output then skips from "inside main" to control's exiting the main block, execution proceeds by destroying the objects in reverse order of their construction.
That sequence is determined by second, printing "destructor 2", and concluding with first, printing "destructor 1".
This reversed sequence is ensured to permit an object created later to have a secure assumption that an object created earlier is still alive when the later object is being destroyed.
It is also permitted to define a destructor in a separate location, so long as it has been declared within the class. The declaration in the class body is essentially a skeleton, simply stipulating that a constructor of a particular type with specific arguments does or can exist. The later definition of such a member is introduced to its corresponding class by means of the scope resolution operator (::).
#include <iostream>
class Counter
{
private:
int value{};
public:
Counter(int start);
~Counter();
// Declare print before code needs to call it
void print();
};
Counter::Counter(int start)
{
value = start;
std::cout << "Counter " << value << " created\n";
}
Counter::~Counter()
{
std::cout << "Counter " << value << " destroyed\n";
}
void Counter::print()
{
std::cout << value << '\n';
}
int main()
{
Counter c{ 7 };
c.print();
return 0;
}
The constructor saves the supplied value into value and prints a message announcing its construction. The destructor announces the completion of the object's life, and is still capable of accessing value, because it is a member of the same class; subsequently, as control exits main, the destructor causes final output to be written.
The destructor being a special member that is automatically called means that it is only ever called explicitly as an exception to normal practice. Writing t.~Tracer(); even if t hasn't left scope leads to an explicit and an automatic call.
Such double calls can be problematic for members whose destructors free system resources.
Safe practice is to rely on the automatically scheduled call instead.
Because the destructor executes only when the object itself is no longer accessible by conventional program code, while still being guaranteed access to the data members that it once occupied, it offers opportunity for executing final operations upon the data as the object ceases to exist. After the execution of a destructor, the object has completed its existence and there can be no further access to its name.
Exercise: DestructorsStarter code, hints and solutionOpenClose
Practice: Destructors
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Create a third Tracer with id 3 after the two existing objects. Predict its constructor message and the destruction order at main exit.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "constructor " << id << '\n';
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "destructor " << id << '\n';
}
};
int main()
{
Tracer first{ 1 };
Tracer second{ 2 };
std::cout << "inside main\n";
return 0;
}Hints
first and second are constructed in declaration order. Their destructor calls happen automatically when their lifetimes end; do not call the destructors manually.
Place the third Tracer after second. At the closing brace, reverse the construction order of all three automatic objects.
Worked solution
third is initialized with id 3 after the existing two objects. At main exit the automatic objects are destroyed in reverse construction order: third, second, first.
#include <iostream>
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "constructor " << id << '\n';
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "destructor " << id << '\n';
}
};
int main()
{
Tracer first{ 1 };
Tracer second{ 2 };
std::cout << "inside main\n";
Tracer third{3};
return 0;
}
Example output
constructor 1
constructor 2
inside main
constructor 3
destructor 3
destructor 2
destructor 1
0 comments
No comments yet. Be the first.