Constructors and destructors guarantee that an object is fully built before anyone uses it and fully cleaned up when it goes out of scope. The constructor establishes the invariant; the destructor releases whatever the object owns. Understanding the order in which they run, and what the compiler generates for you when you write none, is what makes resource handling predictable.
The default constructor
A class with no user-declared constructor gets one automatically. It default-initialises every member, which for a built-in type means leaving it indeterminate and for a class type means calling that type's constructor.
#include
#include
class Vehicle {
public:
std::string registration;
int mileage;
void report() const {
std::cout << registration << ": " << mileage << " miles\n";
}
};
int main() {
Vehicle van;
van.registration = "VN-4471";
van.mileage = 18200;
van.report();
return 0;
}The output shows the object working after the implicit constructor ran:
VN-4471: 18200 milesThe std::string member was constructed properly, which is why it can be assigned a string at all. The int member, however, was left indeterminate, so assigning it afterwards was necessary.
Parameterised constructors
Once you declare any constructor, the implicit default constructor disappears. That is a frequent surprise: adding a constructor that takes arguments means Vehicle van; no longer compiles.
#include
#include
class Vehicle {
public:
std::string registration;
int mileage;
Vehicle() : registration("UNASSIGNED"), mileage(0) {}
Vehicle(const std::string& reg, int miles)
: registration(reg), mileage(miles) {}
};
int main() {
Vehicle van;
Vehicle truck("TR-0093", 74000);
std::cout << van.registration << " " << van.mileage << '\n';
std::cout << truck.registration << " " << truck.mileage << '\n';
return 0;
}The output shows both objects in a known state:
UNASSIGNED 0
TR-0093 74000Declaring both constructors is called *overloading the constructor*, and it is how a class offers a sensible default alongside specific configurations.
The member initialiser list
The colon after the parameter list starts the member initialiser list, and it is the only correct way to initialise a member that is not a built-in type.
#include
#include
#include
class Fleet {
public:
std::string name;
std::vector mileages;
int size_cache;
Fleet(const std::string& fleet_name, const std::vector& data)
: name(fleet_name), mileages(data), size_cache(static_cast(data.size())) {}
void report() const {
std::cout << name << " holds " << mileages.size() << " vehicles\n";
std::cout << "cached size: " << size_cache << '\n';
}
};
int main() {
Fleet fleet("north", {12000, 45000, 8000});
fleet.report();
return 0;
}The output shows the vector copied into the member and the size computed from it:
north holds 3 vehicles
cached size: 3The alternative — assigning in the body — would default-construct the vector first and then copy over it, doing the work twice and failing outright for members that cannot be assigned, such as const members or references.
Destruction happens automatically
The destructor runs when an object goes out of scope, whether that is at the end of a block, when the owning container is destroyed, or when the program ends. You do not call it by hand.
#include
#include
class Vehicle {
public:
std::string registration;
explicit Vehicle(const std::string& reg) : registration(reg) {
std::cout << "created " << registration << '\n';
}
~Vehicle() {
std::cout << "destroyed " << registration << '\n';
}
};
int main() {
{
Vehicle van("VN-4471");
Vehicle truck("TR-0093");
std::cout << "in scope\n";
}
std::cout << "out of scope\n";
return 0;
}The output shows the objects created in order and destroyed in the reverse order:
created VN-4471
created TR-0093
in scope
destroyed TR-0093
destroyed VN-4471
out of scopeThat reverse order matters: the truck was constructed after the van, so it must be destroyed first, since it might depend on the van still existing.
What the destructor is really for
The destructor releases resources the object owns. When the compiler generates one for you, it destroys each member. When you write one, you are responsible for releasing anything the members do not know about.
#include
class Buffer {
public:
int* data;
std::size_t size;
explicit Buffer(std::size_t bytes) : data(new int[bytes]), size(bytes) {
std::cout << "allocated " << size << " ints\n";
}
~Buffer() {
delete[] data;
std::cout << "released " << size << " ints\n";
}
};
int main() {
{
Buffer buffer(1024);
buffer.data[0] = 7;
std::cout << "first value: " << buffer.data[0] << '\n';
}
return 0;
}The output shows the allocation and the matching release:
allocated 1024 ints
first value: 7
released 1024 intsThis is the pattern that motivates the rule of five, covered in the next module. Writing a destructor is the point at which you take on responsibility for memory management, and the safe alternative is to not own raw memory at all.
The order rules you must remember
- Members are initialised in declaration order, not in the order they appear in the initialiser list. The compiler warns when these differ.
- The base class is constructed before the derived class, and destroyed after it.
- Members are destroyed in reverse declaration order.
- A constructor that throws still destroys the members that were already constructed.
Quick reference
- The default constructor is generated only if you declare no constructor of your own.
- The member initialiser list is the only correct way to initialise members.
- Members are initialised in declaration order regardless of the list order.
- A destructor runs automatically when the object goes out of scope.
- Objects are destroyed in reverse order of construction.
- Writing a destructor means taking responsibility for what the members do not release.
One more habit: read the constructor list of every class you use and check that each member appears exactly once. A member that is missing from the list is default-constructed, and for a resource-holding type that is almost always a mistake waiting to happen.
Try it yourself: write a class that owns a dynamically allocated buffer, with a constructor that allocates and a destructor that frees, and add printing to both so you can watch the lifetime. Then put one of these objects inside a [[std::vector]] and see the order in which the vector destroys its elements. Finally, comment out the destructor and rebuild with the address sanitizer to see the leak reported.