Skip to content
Laviyx Learn

Try

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.

Intermediate12 min readUpdated
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:

C++
#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();
}
Output
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:

C++
#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();
}
Output
volume 50, brightness 70
volume 10, brightness 70

Once 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:

C++
#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();
}
Output
2m 30s
1m 35s

explicit#

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:

C++
#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";
}
Output
open database
open cache
working
close cache
end of main
close database

Objects 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:

C++
#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';
}
Output
1 2

The 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
C++
#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();
}
Output
3/4
5/1

The 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?