C++ course menu
C++ Constructors and Destructors: Initializer Lists, explicit and Object Lifetime
Learn C++ constructors and destructors: default and parameterized constructors, member initializer lists, delegating constructors, explicit, = default, copy constructors, the order of construction and destruction, and RAII.
On this page
A constructor runs automatically when an object is created, and its job is to put the object into a valid state. A destructor runs automatically when the object is destroyed, and releases anything the object holds. Together they are the foundation of safe resource management in C++.
A constructor with parameters#
A constructor has the same name as the class and no return type:
#include <iostream>
#include <string>
class Student {
public:
Student(std::string name, int year) : name_(std::move(name)), year_(year) {}
void print() const {
std::cout << name_ << " (year " << year_ << ")\n";
}
private:
std::string name_;
int year_;
};
int main() {
Student a("Asha", 2);
Student b{"Ravi", 3}; // brace syntax works too
a.print();
b.print();
}Asha (year 2)
Ravi (year 3)Because the only constructor needs a name and a year, it is now impossible to create a Student without them. Constructors let a class guarantee its objects start valid.
Member initializer lists#
The part after the colon, : name_(std::move(name)), year_(year), is the member initializer list. It initializes members directly, before the constructor body runs. Assigning in the body instead first default-constructs each member and then overwrites it, which is slower, and impossible for const members, references and members without a default constructor.
Members are always initialized in the order they are declared in the class, not the order you list them. Keep both orders the same to avoid surprises (-Wall warns when they differ).
std::move(name) moves the string into the member instead of copying it; see Move Semantics.
Default constructors and default member values#
A default constructor takes no arguments. If you write no constructors at all, the compiler generates one. Combined with default member initializers, this is often all you need:
#include <iostream>
class Settings {
public:
Settings() = default; // use the defaults below
Settings(int volume) : volume_(volume) {}
void print() const {
std::cout << "volume " << volume_ << ", brightness " << brightness_ << '\n';
}
private:
int volume_ = 50;
int brightness_ = 70;
};
int main() {
Settings normal;
Settings quiet(10);
normal.print();
quiet.print();
}volume 50, brightness 70
volume 10, brightness 70Once you write any constructor, the compiler no longer generates the default one; = default asks for it back.
Delegating constructors#
One constructor can call another of the same class, so the setup code is written once:
#include <iostream>
#include <string>
class Timer {
public:
Timer(int minutes, int seconds) : total_seconds_(minutes * 60 + seconds) {}
explicit Timer(int seconds) : Timer(0, seconds) {} // delegates
void print() const {
std::cout << total_seconds_ / 60 << "m " << total_seconds_ % 60 << "s\n";
}
private:
int total_seconds_;
};
int main() {
Timer a(2, 30);
Timer b(95);
a.print();
b.print();
}2m 30s
1m 35sexplicit#
A constructor with one parameter also acts as an automatic conversion. Without explicit, Timer t = 95; or passing 95 to a function that expects a Timer would silently build a Timer. Marking single-argument constructors explicit prevents surprising conversions; it is good practice unless the conversion is genuinely natural.
Destructors#
A destructor is named ~ClassName, takes no parameters, and runs when the object's lifetime ends: at the end of its scope, when deleted, or when its container is destroyed:
#include <iostream>
#include <string>
class Connection {
public:
explicit Connection(std::string host) : host_(std::move(host)) {
std::cout << "open " << host_ << '\n';
}
~Connection() {
std::cout << "close " << host_ << '\n';
}
private:
std::string host_;
};
int main() {
Connection db("database");
{
Connection cache("cache");
std::cout << "working\n";
}
std::cout << "end of main\n";
}open database
open cache
working
close cache
end of main
close databaseObjects are destroyed in the reverse order of their creation. This is RAII in action: a resource opened in a constructor is always closed by the destructor, even if the function exits early or throws. You will rarely need to write a destructor yourself when members are standard types (std::string, std::vector, smart pointers), because they clean themselves up.
Copying objects#
When you copy an object (Student b = a;, or pass it by value), the copy constructor runs. The compiler-generated one copies each member, which is correct for classes made of standard types:
#include <iostream>
#include <string>
#include <vector>
class Playlist {
public:
explicit Playlist(std::string name) : name_(std::move(name)) {}
void add(const std::string& song) { songs_.push_back(song); }
std::size_t size() const { return songs_.size(); }
private:
std::string name_;
std::vector<std::string> songs_;
};
int main() {
Playlist original("Road trip");
original.add("Song A");
Playlist copy = original; // copies name_ and songs_
copy.add("Song B");
std::cout << original.size() << ' ' << copy.size() << '\n';
}1 2The copy is fully independent. Classes that own a raw resource (a raw pointer from new, a file handle) need custom copy behaviour, or must forbid copying, which is the Rule of Three/Five covered in Move Semantics. Using standard types and smart pointers as members avoids that work (the "Rule of Zero").
Common mistakes#
Practice#
Practice exercise
Write a class Fraction with a constructor Fraction(int numerator, int denominator = 1) that throws std::invalid_argument if the denominator is 0, and stores the fraction in lowest terms (use std::gcd from <numeric>). Print Fraction(6, 8) and Fraction(5).
Show solution
#include <iostream>
#include <numeric>
#include <stdexcept>
class Fraction {
public:
Fraction(int numerator, int denominator = 1) {
if (denominator == 0) {
throw std::invalid_argument("denominator is zero");
}
int g = std::gcd(numerator, denominator);
num_ = numerator / g;
den_ = denominator / g;
}
void print() const { std::cout << num_ << '/' << den_ << '\n'; }
private:
int num_ = 0;
int den_ = 1;
};
int main() {
Fraction(6, 8).print();
Fraction(5).print();
}3/4
5/1The constructor body is used here (rather than only the initializer list) because the values need to be calculated and checked first.
Key takeaway#
Finished this lesson?