Constructors and destructors
A constructor runs when an object is created and leaves it in a valid state. A destructor runs when the object dies and releases what it owns. Between the two, every object in a C++ program has a defined beginning and a defined end, and the compiler inserts the calls.
Lesson 024 used a default member initialiser to give the counter its zero. This lesson writes the constructors explicitly: the default one, the one that takes an argument, the copy one, and the destructor that prints when an object goes out of scope.
The default constructor
A constructor has the class name and no return type. counter() : total_(0) { ... } is the default constructor: it takes no arguments, and the part between the colon and the brace is the member initialiser list.
The list is the place where members are initialised, and it runs before the body. A member left out of the list is default-initialised, which for an int means no value at all; that is why the list is not optional in practice.
The body runs after every member exists, so it is for work that needs the members, not for setting them. Lesson 026 goes through the order and the cases where the list is the only option.
#include <iostream>
class counter
{
public:
counter() : total_(0)
{
std::cout << "constructed\n";
}
int total() const
{
return total_;
}
private:
int total_;
};
int main()
{
counter c;
std::cout << c.total() << '\n';
}
g++ -std=c++23 -Wall -Wextra default.cpp -o default_ctor
./default_ctor
constructed
0
A constructor with an argument converts
A constructor that takes one argument also defines a conversion from that argument's type to the class. The function print takes a meter, and print(42) builds a meter from 42 and passes it.
The conversion is convenient and often unintended: a function that takes a meter can be called with any int, including one that means seconds. The program calls print with a literal and with an explicit temporary, and both work.
The temporary meter(7) is the explicit form of the same thing: a constructor call written as a function call. The next section shows the keyword that stops the implicit version.
#include <iostream>
class meter
{
public:
meter(int value) : value_(value) {}
int value() const
{
return value_;
}
private:
int value_;
};
void print(meter m)
{
std::cout << m.value() << " m\n";
}
int main()
{
print(42);
print(meter(7));
}
g++ -std=c++23 -Wall -Wextra converting.cpp -o converting
./converting
42 m
7 m
explicit stops the conversion
Marking the constructor explicit removes the implicit conversion and keeps the direct construction. print(meter(7)) still works, and print(42) no longer compiles.
The error says the int cannot be converted to meter and points at the call. The note mentions the explicit constructor, which is the compiler telling you why the conversion is not available.
Use explicit for every constructor that takes a single argument unless the conversion is the point of the type. A meter built silently from a duration, or a file opened silently from a string, is a bug that the keyword prevents at compile time.
#include <iostream>
class meter
{
public:
explicit meter(int value) : value_(value) {}
int value() const
{
return value_;
}
private:
int value_;
};
void print(meter m)
{
std::cout << m.value() << " m\n";
}
int main()
{
print(42);
print(meter(7));
}
g++ -std=c++23 -Wall -Wextra explicit.cpp -o explicit_demo
explicit.cpp: In function 'int main()':
explicit.cpp:24:11: error: could not convert '42' from 'int' to 'meter'
24 | print(42);
| ^~
| |
| int
The copy constructor
Passing an object by value copies it with the copy constructor. The compiler writes one that copies every member, and the program below replaces it with one that prints, so the copy becomes visible.
The parameter of take is a payload, so the call take(p) copies p into the parameter. The copy constructor receives a const reference to the original and initialises the new object from it; the body then prints the id.
The compiler-generated copy is correct for members that own nothing, which is most classes. A class that owns a pointer or a handle needs a copy that duplicates the resource, and lesson 031 is about writing that one.
#include <iostream>
class payload
{
public:
payload() = default;
payload(const payload& other) : id_(other.id_)
{
std::cout << "copy " << id_ << '\n';
}
int id() const
{
return id_;
}
private:
int id_ = 1;
};
void take(payload p)
{
std::cout << "inside " << p.id() << '\n';
}
int main()
{
payload p;
take(p);
std::cout << "after\n";
}
g++ -std=c++23 -Wall -Wextra copy.cpp -o copy_demo
./copy_demo
copy 1
inside 1
after
The destructor
The destructor has the class name with a tilde and takes no arguments. It runs when the object goes out of scope, in the reverse order of construction, and it is where a class releases what it owns.
The program creates one object in main and one inside a block. The inner one is destroyed at the closing brace, the outer one at the end of main, and the prints show both moments. The order itself was introduced in lesson 016; here it is the destructor that produces it.
A class that needs no cleanup should not declare a destructor at all. Declaring an empty one is not free: it stops the compiler from moving the object efficiently in some cases, which is part of the rule of zero in lesson 033.
#include <iostream>
class tracer
{
public:
explicit tracer(const char* name) : name_(name) {}
~tracer()
{
std::cout << "destroy " << name_ << '\n';
}
private:
const char* name_;
};
int main()
{
tracer a{"a"};
{
tracer b{"b"};
std::cout << "inner block\n";
}
std::cout << "end of main\n";
}
g++ -std=c++23 -Wall -Wextra destructor.cpp -o destructor
./destructor
inner block
destroy b
end of main
destroy a
Exercises
- Give counter a constructor that takes an initial total and use it in main. Which constructor does `counter c;` call then?
- Remove explicit from meter and print 42 through the function. How many meters are constructed for one call?
- Add a copy constructor to tracer that prints "copy" and pass a tracer to a function by value. Which destructor runs first?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a constructor do?
Answer
It initialises an object when it is created
The constructor runs at the creation of the object and leaves it in a valid state.
-
What does a destructor do?
Answer
It releases what the object owns when it dies
The destructor runs at the end of the object's lifetime, which is the RAII pattern.
-
What does
expliciton a constructor prevent?Answer
An implicit conversion from the argument type
The keyword removes the conversion from the candidate set, so a call must name the type.
-
When is the copy constructor called?
Answer
When an object is initialised from another object of the same type
Copy initialisation, passing by value and inserting into a container all use it.
-
What does this program print?
#include <iostream> class value { public: value() { std::cout << "constructed\n"; } int get() const { return 0; } }; int main() { value v; std::cout << v.get(); }Answer
constructed / 0
The constructor prints its line when the object is created, and the member returns zero.
-
What does this program print?
#include <iostream> class meter { public: explicit meter(int value) : value_(value) {} void show() const { std::cout << value_ << " m\n"; } private: int value_; }; int main() { meter a{42}; meter b = static_cast<meter>(7); a.show(); b.show(); }Answer
42 m / 7 m
Two objects are built from integers, and show prints the value with its unit.
-
What does this program print?
#include <iostream> class item { public: item() = default; item(const item&) { std::cout << "copy 1\n"; } }; void take(item) { std::cout << "inside 1\n"; } int main() { item original; take(original); std::cout << "after"; }Answer
copy 1 / inside 1 / after
The by-value parameter copies the object, then the function and main print their lines.
-
What is the order of the lines?
#include <iostream> struct named { const char* name; ~named() { std::cout << "destroy " << name << '\n'; } }; int main() { named a{"a"}; { named b{"b"}; std::cout << "inner block\n"; } std::cout << "end of main\n"; }Answer
inner block / destroy b / end of main / destroy a
The inner object dies at the end of its block, and the outer one dies when main returns.
-
What does
print(42)report when meter's constructor is explicit?Answer
could not convert '42' from 'int' to 'meter'
The explicit constructor is not considered for the implicit conversion.
-
In what order are an object's members destroyed?
Answer
In reverse order of construction
The members are destroyed in the reverse of their construction, after the destructor body runs.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev