Lesson 03 / 14
Constructors and Destructors
Measures that an argument implicitly built from an int is an unnamed, temporary object, and measures its lifetime with a constructor/destructor pair.
Published Updated
In this lesson
- Measures that an argument built through an implicit type conversion is a temporary object
- Applies how the explicit keyword prevents implicit construction
- Shows the order of the member initializer list and the constructor chain
Contents
The previous lesson measured which object a member function works with, through this
— but that object always already existed. This lesson asks the same question at the
moment an object comes to exist: when is a constructor (a member function
carrying the same name as the class, run once while the object is being built) called,
and when does a destructor (a member function prefixed with ~, run once while the
object is being destroyed) run? This time the measurement kind is temporary: it will
track the lifetime of an unnamed object, implicitly built from an argument of a function
call.
Constructor and Destructor
A constructor carries the same name as the class, has no return type (not even void
is written), and is called by the compiler while an object is being built. A destructor
takes the form ~ClassName(), takes no arguments, and is called by the compiler when an
object reaches the end of its lifetime — the programmer never calls it by hand. If a
class writes no constructor at all, the compiler generates a default constructor
(one taking no arguments, building each data member with its own default value); once a
constructor is written, this generation stops.
A class can have more than one constructor — as long as their parameter count or type differs, they are distinguished by the same overload rules as any function. A class can, by contrast, have only one destructor; because a destructor takes no arguments, it cannot be overloaded.
The biggest difference between a constructor and an ordinary member function is that,
while the constructor is running, the object is not yet complete. While
Quantity(int value) runs, value_ is built by the member initializer list’s
value_(value); once the body begins running, value_ already holds a valid value and
can be read inside the body, but until the body runs, the object’s other members (this
class has only value_, but in a class with more members, the members that follow) may
not yet be built. For this reason, calling another member function through this inside
a constructor’s body — especially a virtual member function, a topic outside this
course’s scope — carries the risk of reaching a not-yet-fully-built state of the object;
this lesson makes no such call.
The destructor’s position is symmetric: while the destructor is running, the object is
still complete and valid, but once the destructor returns the object is considered gone
— the memory for its member variables may still physically remain, but the program
accessing it is undefined behavior (a rule this course inherits from M11/K02: undefined
behavior is never run, it is only mentioned in prose). Quantity::~Quantity‘s body reads
and prints value_ one last time; no code after that line should access the same
object’s value_.
// quantity.hpp — a constructor, a destructor, and a constructor chain
#ifndef QUANTITY_HPP
#define QUANTITY_HPP
#include <cstdio>
class Quantity {
public:
Quantity(int value) : value_(value) {
std::printf("constructor ran value=%d\n", value_);
}
~Quantity() {
std::printf("destructor ran value=%d\n", value_);
}
int value() const { return value_; }
private:
int value_;
};
#endif
CR5. Quantity(int value) takes an argument, and : value_(value), which runs
before its body, is a member initializer list — it builds the data member value_
with the value value before the body runs. Writing value_ = value inside the body
would give the same result here too, but the initializer list separates construction
from assignment: by the time the body runs, value_ already exists, it is only being
modified.
Implicit Construction and explicit
Quantity(int value) is a constructor that takes exactly one int argument; C++
also uses a constructor like this for implicit type conversion. If a function like
show(Quantity q) is called as show(5), the compiler implicitly calls this
constructor to convert 5 into a Quantity — the constructed object has no name, no
variable is visible in main, but the object genuinely exists and its constructor
genuinely runs.
// temporary-measurement.cpp — is a quantity implicitly built from an int a temporary
#include <cstdio>
#include "quantity.hpp"
static void show(Quantity q) {
std::printf("inside show value=%d\n", q.value());
}
int main() {
std::printf("before call\n");
show(5);
std::printf("after call\n");
return 0;
}
before call constructor ran value=5 inside show value=5 destructor ran value=5 after call
CR6. In the call show(5), 5 is first converted into a Quantity object through
Quantity(5) — this object has no name, and is therefore temporary. This temporary
is copied into show‘s parameter q (or built directly in its place — a compiler
optimization outside this course’s scope); when show returns, q‘s destructor is
called, and the output order shows this: the constructor runs right after the call, the
destructor runs right after show returns, but before the after call line — the
temporary’s lifetime is bounded by the expression show(5); alone, it does not spill
into the rest of main.
This implicit conversion is not always wanted: when show(5) is written, it is not
obvious to a reader whether 5 is meant to pass as a Quantity or is an integer passed
by mistake. The explicit keyword prevents this: had explicit Quantity(int value)
been written, show(5) would not compile, and only an explicit construction such as
show(Quantity(5)) or show(Quantity{5}) would compile. This lesson leaves Quantity
without explicit because what it wants to measure is exactly the temporary that
implicit construction produces; but a class author should consider marking every
single-argument constructor explicit — the constructors written in this course’s later
lessons are also written as explicit unless stated otherwise.
The risk of implicit conversion is not only readability. When a chain of two arguments
allows implicit construction — for example, if Quantity implicitly converted from
another class’s constructor as well — the compiler could compile a call through an
unexpected chain; the C++ standard limits this by allowing at most one implicit
user-defined conversion per call, but even a single-step implicit conversion can hide a
constructor call the reader did not expect. In the show(5) example this risk is small,
because Quantity has only one constructor and the intent is clear; in a large class
hierarchy, the same implicit construction can silently add the cost of building an
object (a constructor running, a destructor running) that the caller did not notice.
explicit removes this silence: every construction is then forced to appear in the
source code as a visible Type(...) call.
Constructor Chains and Reverse-Order Destruction
In a class with more than one data member, the member initializer list ensures that members are built in declaration order — not the order written in the list, the order of declaration in the class body governs. Destruction happens in exactly the opposite order: the member built last is destroyed first.
// pair.cpp — the construction and destruction order of more than one member
#include <cstdio>
#include "quantity.hpp"
class Pair {
public:
Pair() : first_(1), second_(2) {
std::printf("pair constructor ran\n");
}
private:
Quantity first_;
Quantity second_;
};
int main() {
std::printf("block started\n");
{
Pair p;
std::printf("pair constructed\n");
}
std::printf("block ended\n");
return 0;
}
block started constructor ran value=1 constructor ran value=2 pair constructor ran pair constructed destructor ran value=2 destructor ran value=1 block ended
CR7. Before Pair‘s own constructor body runs, first_ and second_ are built in
the class body’s declaration order (not the order in the member initializer list — here
the two coincide, so no difference is visible, but the order is always determined by
declaration); each build triggers its own Quantity constructor, which is why two
separate constructor ran lines appear before the pair constructor ran line. When p
is destroyed as the inner block closes, Pair’s own destructor (generated by the
compiler because none is written here, printing nothing) runs first, and then its
members are destroyed in reverse order: second_ first, first_ after. Construction
and destruction mirror each other.
In M10/K02 (Rust), a value going out of scope ran Drop; C++’s destructor answers the
same idea, but the triggering rules differ, and this lesson does not deepen that
comparison.
Declaration order being binding means the member initializer list’s written order is
ignored — some compilers report as a warning when the list’s order does not match the
declaration order, because the two orders differing can mislead a reader: even if the
list were written as second_(2), first_(1), construction order would still put
first_ first, because first_ is declared first in the class body. Pair writes the
list in the same order as the declaration for this lesson, so the two orders coincide
and no confusion arises — but the rule is always declaration order, never the order
written in the list.
Reverse-order destruction has another consequence: a member’s constructor can safely
use a resource built by an earlier member — while second_ is being built, first_ is
already fully built, because it precedes it in declaration order. The same guarantee
runs in reverse during destruction: while second_ is being destroyed, first_ has not
yet been destroyed, so second_‘s destructor can, if needed, still safely access
first_. This symmetry is the foundation the next lesson’s subject, RAII (Resource
Acquisition Is Initialization), is built on: a member holding a resource is not
destroyed before the object holding it goes out of scope.
Constructors Calling Each Other
When a class has more than one constructor, one constructor can call another by
delegating to it, to avoid duplicating code between them: writing Quantity() : Quantity(0) {} defines a constructor that, when called with no arguments, delegates its
work to the single-argument constructor. Delegation is written by calling the class’s own
name in the member initializer list — here Quantity(0) calls a constructor of the
class itself, not of another member. The delegated-to constructor runs completely
(including its own body), and the delegating constructor’s body runs after it.
Quantity does not use delegation within this lesson’s scope — it has only one
constructor — but this technique is the standard way to keep shared construction logic
between multiple constructors in a single place; it must not be confused with building a
member in the initializer list, since one calls the class’s own name and the other calls
a member’s name.
The Default Constructor and Default Values
Quantity does not write a no-argument constructor, because it wants a quantity to
always be built with a starting value — writing Quantity t; does not compile for this
class, because its one constructor requires an int. If a class writes no constructor
at all, the compiler generates a no-argument default constructor; this generated
constructor builds every data member with its own default value — members of class type
run their own default constructors, while members of primitive types such as int
(unless an initializer is given in the member declaration) are left uninitialized,
meaning they carry a random memory value. This is a behavior inherited from C and
unchanged in C++: a class like Pair that only has members of class type has a safe
default constructor, because every member runs its own constructor; but a member such as
int amount_; left without an initializer is left uninitialized by the default
constructor. The classes written in this course give every member of a primitive type an
explicit starting value, either in the declaration (int amount_ = 0;) or in the member
initializer list; reading an uninitialized member is also undefined behavior and is not
run.
The Bounding Measurement
The expression this lesson measures — the implicitly built Quantity inside show(5) —
is of kind temporary: it has no name, and it ceases to exist at the end of the
expression show(5);. But a temporary’s lifetime does not always stay this short: a
temporary, if bound to a const reference, has its lifetime extended to match the
reference’s. Had const Quantity& r = Quantity(7); been written, the temporary on the
right would normally cease to exist at the end of the expression, but because it is
bound to r, its lifetime is extended to the end of r‘s scope — the compiler does this
automatically, the programmer writes nothing extra. This extension only applies to
const references (and to rvalue references, outside this course’s scope); writing
Quantity& r = Quantity(7); does not compile, because a non-const reference cannot
bind to a temporary. That the reference itself cannot be rebound to another temporary
afterward — that a reference, once built, always refers to the same object — is what the
references lesson in the next topic measures.
This boundary explains why temporaries must be used carefully in chains of function
calls: in a call such as show(Quantity(5)), the temporary lives only for the duration
of the expression in which show runs; if a pointer to the temporary, or a non-const
reference to it, is carried out of that expression, that pointer or reference becomes
dangling once the expression ends — the memory it points at is no longer valid. That
Quantity’s value() function returns an int, not a const Quantity& or a
Quantity*, exists exactly to avoid this risk: an int is copied as a value, and even
if the temporary itself ceases to exist, the copied value remains valid.
These three rules together — that implicit construction produces a temporary, that
declaration order governs construction and destruction, and that a const reference can
extend a temporary’s lifetime — show that an object “existing” and an object “being
named” are not the same thing. The Quantity inside show(5) is built without ever
being assigned to a variable, its constructor runs, it is used, its destructor runs — and
throughout this entire process no name ever points at it. The next lesson’s RAII uses
exactly this observation: releasing a resource does not require an object to be named,
only that its lifetime reach its end — the compiler always knows that moment, whether the
object has a name or not.
Summary
- The expression this lesson measures is of kind temporary: the
Quantityimplicitly built insideshow(5)has no name and ceases to exist at the end of the expressionshow(5);. - A constructor carries the same name as the class; a destructor is written with a
~prefix; a destructor takes no arguments and cannot be overloaded. - A single-argument constructor allows implicit type conversion;
explicitprevents this and allows only explicit construction. - The member initializer list builds members in declaration order; destruction happens in the reverse order.
- A temporary bound to a
constreference has its lifetime extended to match the reference’s scope.
Next Step
This lesson measured when an object comes to exist and ceases to exist; the next lesson uses that knowledge to safely release a resource (such as memory or a file handle) — measuring how a destructor releases a member, by separating a guard from the member inside it.
To keep your progress and take notes, Log in
My notes
Log in to take notes.