Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Cleaning Up Dynamic Resources
Release heap memory automatically when objects are destroyed.
Destructors
A destructor is another special kind of class member function that is executed when an object of that class is destroyed. Whereas constructors are designed to initialize a class, destructors are designed to help clean up.
When an object goes out of scope normally, or a dynamically allocated object is explicitly deleted using the delete keyword, the class destructor is automatically called (if it exists) to perform any necessary cleanup before the object is removed from memory. For simple classes (those that just initialize the values of normal member variables), a destructor is not needed because C++ will automatically clean up the memory for you.
However, if your class object is holding any resources (e.g. dynamic memory, or a file or database handle), or if you need to perform any kind of maintenance before the object is destroyed, the destructor is the perfect place to do so, as it is typically the last thing to happen before the object is destroyed.
Destructor naming
Like constructors, destructors have specific naming rules:
- The destructor must have the same name as the class, preceded by a tilde (~).
- The destructor can not take arguments.
- The destructor has no return type.
A class can only have a single destructor.
Generally you should not call a destructor explicitly (as it will be called automatically when the object is destroyed), since there are rarely cases where you'd want to clean up an object more than once. However, destructors may safely call other member functions since the object isn't destroyed until after the destructor executes.
A destructor example
Let's examine a simple class that uses a destructor:
#include <iostream>
#include <cassert>
#include <cstddef>
class NumberList
{
private:
int* m_data{};
int m_size{};
public:
NumberList(int size)
{
assert(size > 0);
m_data = new int[static_cast<std::size_t>(size)]{};
m_size = size;
}
~NumberList()
{
delete[] m_data;
}
void set(int index, int value) { m_data[index] = value; }
int get(int index) { return m_data[index]; }
int getSize() { return m_size; }
};
int main()
{
NumberList list( 12 );
for (int i{ 0 }; i < list.getSize(); ++i)
list.set(i, i * 3);
std::cout << "The value at position 7 is: " << list.get(7) << '\n';
return 0;
}
If you compile the above example and get the following error:
error: 'class NumberList' has pointer data members [-Werror=effc++]| error: but does not override 'NumberList(const NumberList&)' [-Werror=effc++]| error: or 'operator=(const NumberList&)' [-Werror=effc++]|
Then you can either remove the "-Weffc++" flag from your compile settings for this example, or you can add the following two lines to the class:
We discuss =delete for members in the Introduction to the copy constructor lesson.
This program produces the result:
The value at position 7 is: 21
On the first line of main(), we instantiate a new NumberList class object called list, passing in a size of 12. This calls the constructor, which dynamically allocates memory for the data member. We must use dynamic allocation here because we do not know at compile time what the size will be (the caller decides that).
At the end of main(), list goes out of scope. This causes the ~NumberList() destructor to be called, which deletes the array that we allocated in the constructor!
Advanced note: Parentheses based initialization should be used when initializing an array/container/list class with a length (as opposed to a list of elements). For this reason, we initialize NumberList using NumberList list( 12 );.
Constructor and destructor timing
As mentioned previously, the constructor is called when an object is created, and the destructor is called when an object is destroyed. In the following example, we use cout statements inside the constructor and destructor to demonstrate this:
#include <iostream>
class Resource
{
private:
int m_id{};
public:
Resource(int id)
: m_id{ id }
{
std::cout << "Constructing Resource " << id << '\n';
}
~Resource()
{
std::cout << "Destructing Resource " << m_id << '\n';
}
int getId() { return m_id; }
};
int main()
{
Resource stackResource{ 10 };
std::cout << stackResource.getId() << '\n';
Resource* heapResource{ new Resource{ 20 } };
std::cout << heapResource->getId() << '\n';
delete heapResource;
return 0;
}
This program produces the following result:
Constructing Resource 10 10 Constructing Resource 20 20 Destructing Resource 20 Destructing Resource 10
Note that "Resource 10" is destroyed after "Resource 20" because we deleted heapResource before the end of the function, whereas stackResource was not destroyed until the end of main().
Global variables are constructed before main() and destroyed after main().
RAII
RAII (Resource Acquisition Is Initialization) is a programming technique whereby resource use is tied to the lifetime of objects with automatic duration (e.g., non-dynamically allocated objects). In C++, RAII is implemented via classes with constructors and destructors. A resource (such as memory, a file or database handle, etc.) is typically acquired in the object's constructor (though it can be acquired after the object is created if that makes sense). That resource can then be used while the object is alive. The resource is released in the destructor, when the object is destroyed. The primary advantage of RAII is that it helps prevent resource leaks (e.g., memory not being deallocated) as all resource-holding objects are cleaned up automatically.
The NumberList class at the top of this lesson is an example of a class that implements RAII -- allocation in the constructor, deallocation in the destructor. std::string and std::vector are examples of classes in the standard library that follow RAII -- dynamic memory is acquired on initialization, and cleaned up automatically on destruction.
A warning about the std::exit() function
Note that if you use the std::exit() function, your program will terminate and no destructors will be called. Be wary if you're relying on your destructors to perform necessary cleanup work (e.g., write something to a log file or database before exiting).
Summary
Destructors are special member functions that execute when an object is destroyed. They're designed to perform cleanup tasks before the object is removed from memory.
Destructor naming rules: Must have the same name as the class preceded by a tilde (~), cannot take arguments, and has no return type. Each class can have only one destructor.
Automatic destruction: Destructors are called automatically when objects go out of scope or when dynamically allocated objects are explicitly deleted. Generally, you should not call destructors manually.
When destructors are needed: Simple classes that only use normal member variables don't need destructors - C++ automatically cleans up the memory. Destructors are essential for classes that hold resources like dynamic memory, file handles, database connections, or need to perform maintenance before destruction.
Destructor timing: Constructors run when objects are created, destructors run when objects are destroyed. The order of destruction is the reverse of construction (last created, first destroyed).
RAII (Resource Acquisition Is Initialization): A programming technique where resource use is tied to object lifetime. Resources are acquired in the constructor and released in the destructor, ensuring automatic cleanup and preventing resource leaks.
std::exit() warning: Calling std::exit() terminates the program without calling destructors, which can prevent necessary cleanup work from being performed.
Constructors and destructors together: When used in combination, constructors and destructors enable classes to automatically manage their own initialization and cleanup without programmer intervention, reducing errors and making classes easier to use.
As you can see, when constructors and destructors are used together, your classes can initialize and clean up after themselves without the programmer having to do any special work! This reduces the probability of making an error, and makes classes easier to use.
Cleaning Up Dynamic Resources - Quiz
Test your understanding of the lesson.
Practice Exercises
Resource Manager with Destructor
Create a FileManager class that manages file resources using RAII principles. The class should allocate memory for a file buffer in the constructor and clean it up in the destructor.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!