Constructors and Initialization
Learning Objectives
Use default, parameterized, copy, move, delegating, converting and initializer-list constructors.
Use member initializer lists and know why declaration order matters.
Explain explicit, factory functions and named construction.
Predict construction and destruction order, and explain why virtual calls in constructors are hazardous.
Prerequisites: Modules 2–4.
Intuition and Definition
A constructor turns raw memory into a valid object: it must establish every invariant before anyone can use the object. A destructor releases what the object owns when its lifetime ends.
C++ constructor kind Create with default values Point(int x, int y) Create from given values Point(const Point&) Create a copy of another object Steal resources from a temporary Point(): Point(0, 0) {} Reuse another constructor Meters(double) without explicit
Allows implicit conversion from double
Why It Matters
Half-constructed objects are a classic source of bugs. If the constructor validates and initializes everything, the rest of the class can assume a valid state and skip defensive checks.
Syntax and Language Notes
class Point {
int x_, y_;
public:
Point() : Point(0, 0) {} // delegating
Point(int x, int y) : x_(x), y_(y) {} // member initializer list
};
class Meters {
double v_;
public:
explicit Meters(double v) : v_(v) {} // no implicit double -> Meters
};// Java (not run): constructors chain with this(...) and super(...)
public class Point {
private final int x, y;
public Point() { this(0, 0); }
public Point(int x, int y) { this.x = x; this.y = y; }
}public class Point
{
public int X { get; init; } // object initializer: new Point { X = 1, Y = 2 }
public int Y { get; init; }
public Point() : this(0, 0) { }
public Point(int x, int y) { X = x; Y = y; }
}class Point:
def __new__(cls, *args): # allocates the object (rarely overridden)
return super().__new__(cls)
def __init__(self, x=0, y=0): # initializes it
self.x, self.y = x, yclass Point {
readonly x: number;
readonly y: number;
constructor(x = 0, y = 0) { this.x = x; this.y = y; }
static origin(): Point { return new Point(); } // named factory
}Complete Example
The compiler warns about the misleading initializer order (shown as part of the output):
explicit stops accidental conversions:
#include <iostream>
#include <string>
struct Tracer {
std::string name;
explicit Tracer(std::string n) : name(std::move(n)) { std::cout << " construct " << name << "\n"; }
~Tracer() { std::cout << " destroy " << name << "\n"; }
};
class Base {
protected:
Tracer b_{"Base::member"};
public:
Base() { std::cout << " Base body, calling describe(): "; describe(); }
virtual void describe() const { std::cout << "Base::describe\n"; }
virtual ~Base() { std::cout << " ~Base body\n"; }
};
class Derived : public Base {
Tracer first_;
Tracer second_;
public:
// initializer list order is written "wrong" on purpose; members are still
// initialized in DECLARATION order: first_, then second_
Derived() : second_("Derived::second"), first_("Derived::first") {
std::cout << " Derived body, calling describe(): ";
describe();
}
void describe() const override { std::cout << "Derived::describe\n"; }
~Derived() override { std::cout << " ~Derived body\n"; }
};
int main() {
std::cout << "creating Derived:\n";
{
Derived d;
std::cout << "leaving scope:\n";
}
}compiler output:
m05_ctor_order.cpp:25:17: warning: field 'second_' will be initialized after field 'first_' [-Wreorder-ctor]
25 | Derived() : second_("Derived::second"), first_("Derived::first") {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
| first_("Derived::first") second_("Derived::second")
1 warning generated.
program output:
creating Derived:
construct Base::member
Base body, calling describe(): Base::describe
construct Derived::first
construct Derived::second
Derived body, calling describe(): Derived::describe
leaving scope:
~Derived body
destroy Derived::second
destroy Derived::first
~Base body
destroy Base::memberclass Meters {
double v_;
public:
explicit Meters(double v) : v_(v) {}
double value() const { return v_; }
};
double twice(Meters m) { return m.value() * 2; }
int main() {
return static_cast<int>(twice(3.5)); // error: no implicit double -> Meters
}m05_explicit_fail.cpp:9:29: error: no matching function for call to 'twice'Step-by-Step Walkthrough
Construction goes base-first, then members in declaration order, then the constructor body. Base::member is built, the Base body runs, then first_ and second_ (declaration order, not the order written in the initializer list), then the Derived body.
While the Base constructor runs, the object *is* a Base — the Derived part does not exist yet — so the virtual call describe() dispatches to Base::describe. In the Derived body it dispatches to Derived::describe.
Destruction is the exact reverse: Derived body, its members in reverse order, then Base body, then base members.
-Wreorder-ctor flags the misleading initializer list; the fix is to write initializers in declaration order.
sequenceDiagram
participant B as Base part
participant M as Derived members
participant D as Derived body
Note over B: Base::member constructed
Note over B: Base body (virtual call -> Base)
Note over M: first_, then second_ (declaration order)
Note over D: Derived body (virtual call -> Derived)
Note over D: destruction runs in exact reverseCommon Mistakes
Assigning in the constructor body instead of initializing. const members, references and members without default constructors *must* be initialized in the initializer list.
Relying on initializer-list order. Members are always initialized in declaration order; using one member to initialize another declared earlier reads garbage.
Calling overridable functions from constructors/destructors expecting the derived behaviour. C++ calls the current class's version; Java and C# call the *derived* override on a not-yet-initialized object — even more dangerous.
Single-argument constructors without explicit, which create surprising implicit conversions.
Throwing from a destructor. Destructors are noexcept by default; a throw during stack unwinding calls std::terminate.
Trade-offs and Alternatives
Factory functions / named constructors (Temperature::fromFahrenheit(98.6)) make intent clear when several constructors would have the same parameter types, and can return std::expected/optional instead of throwing.
Builder (Module 30) for objects with many optional parameters.
Default arguments reduce overloads but bake values into call sites (they are compiled into callers).
Real-World Use
ECU software often forbids exceptions; classes then use a private constructor plus a static create() factory that validates configuration and returns an error code or an empty optional, so an invalid object can never be produced.
Practice
Q1. (Easy) In which order are a class's members initialized?
Answer: In the order they are declared in the class, regardless of the initializer-list order.
Q2. (Medium) Why must a const int member or a reference member be set in the initializer list?
Answer: They cannot be assigned after construction; the body runs after all members already exist, so only the initializer list can give them their value.
Q3. (Advanced) Write a Temperature class with a private constructor and two named factories fromCelsius and fromFahrenheit, rejecting values below absolute zero by returning std::optional.
#include <iostream>
#include <optional>
class Temperature {
double c_;
explicit Temperature(double c) : c_(c) {}
public:
static std::optional<Temperature> fromCelsius(double c) {
if (c < -273.15) return std::nullopt;
return Temperature(c);
}
static std::optional<Temperature> fromFahrenheit(double f) { return fromCelsius((f - 32) * 5 / 9); }
double celsius() const { return c_; }
};
int main() {
auto a = Temperature::fromFahrenheit(212);
auto b = Temperature::fromCelsius(-300);
std::cout << (a ? a->celsius() : -1) << " " << (b.has_value() ? "valid" : "rejected") << "\n";
}100 rejectedInterview Question
Q1. "What does this print, and why?" — a base constructor calls a virtual function overridden in the derived class.
Answer: In C++ it calls the base version: during the base constructor the object's dynamic type is the base class (the vptr points at the base vtable), because the derived members do not exist yet. Calling a *pure* virtual function this way is undefined behaviour (often "pure virtual function called" and abort). In Java and C# the derived override runs, but it sees derived fields with default values (0/null) because their initializers have not run yet — a well-known source of bugs. Either way: don't call overridable methods from constructors.
Quick Quiz
Q1. What is the destruction order relative to construction?
Answer: Exactly the reverse.
Q2. What does explicit prevent?
Answer: Implicit conversions through that constructor (and copy-list-initialization like Meters m = 3.5;).
Q3. What does a delegating constructor do?
Answer: It forwards to another constructor of the same class (Point() : Point(0, 0) {}).
Summary
Constructors establish invariants; destructors release resources in reverse order.
Initialize members in the initializer list, in declaration order.
Mark single-argument constructors explicit; prefer named factories for clarity and error reporting.
Don't rely on virtual dispatch inside constructors/destructors.
Related: Module 12 (copy/move), Module 13 (RAII), Module 14 (constructor failures), Module 30 (Builder).