Lesson 3 of 12
A constructor runs when an object is created. Its job is to leave the object in a valid state, so that no other member function ever has to wonder.
#include <iostream>
#include <string>
class Person {
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {}
void print() const {
std::cout << name << " is " << age << "\n";
}
};
int main() {
Person p("Sokha", 22);
p.print();
return 0;
}
Sokha is 22
A constructor has the class's name and no return type. It is not called; it runs automatically when an object is created.
The member initializer list
That : name(n), age(a) between the parameters and the body is the member initializer list,
and it is not a stylistic alternative to assignment in the body.
#include <iostream>
#include <string>
class Widget {
std::string label;
public:
// Initialisation: label is constructed directly from `text`.
Widget(const std::string& text) : label(text) {}
// Assignment: label is first default-constructed, then overwritten.
// Two operations where one would do.
// Widget(const std::string& text) { label = text; }
const std::string& get() const { return label; }
};
int main() {
Widget w("hello");
std::cout << w.get() << "\n";
return 0;
}
hello
Members are constructed before the constructor body runs. So assigning in the body means the
member is built twice: default-constructed, then assigned over. For an int that is free; for a
std::string or std::vector it is a wasted allocation.
Three cases where the body will not work at all
For these, the initializer list is the only option:
const members cannot be assigned:
class Config {
const int max_retries;
public:
Config(int n) : max_retries(n) {} // only way
// Config(int n) { max_retries = n; } error: assignment of read-only member
};
Reference members must be bound at construction (lesson 7.3):
class Logger {
std::ostream& out;
public:
Logger(std::ostream& o) : out(o) {} // only way
};
Members with no default constructor have nothing to default-construct:
class Engine {
public:
Engine(int power) { (void)power; } // no Engine() exists
};
class Car {
Engine engine;
public:
Car() : engine(150) {} // only way
};
Initialisation order is declaration order
This one causes real bugs:
#include <iostream>
class Confusing {
int a;
int b;
public:
// Written b first, but members initialise in DECLARATION order:
// a is initialised before b, using b's value — which is garbage.
Confusing(int v) : b(v), a(b) {}
void print() const { std::cout << "a=" << a << " b=" << b << "\n"; }
};
int main() {
Confusing c(5);
c.print(); // b is 5; a read b before b existed
return 0;
}
Members initialise in the order they are declared in the class, not the order written in the
list. a is declared first, so a(b) runs while b is still uninitialised.
Compile that with -Wall and GCC tells you:
warning: 'Confusing::b' will be initialized after [-Wreorder]warning: 'int Confusing::a' [-Wreorder]
Keep the list in declaration order and the warning never appears. Better still, do not initialise one member from another.
Multiple constructors
Constructors overload like any function (lesson 5.6):
#include <iostream>
#include <string>
class Rect {
double w;
double h;
public:
Rect() : w(1.0), h(1.0) {} // default
Rect(double side) : w(side), h(side) {} // square
Rect(double width, double height) : w(width), h(height) {}
double area() const { return w * h; }
};
int main() {
Rect a;
Rect b(3.0);
Rect c(2.0, 5.0);
std::cout << a.area() << " " << b.area() << " " << c.area() << "\n";
return 0;
}
1 9 10
The default constructor, and when you lose it
If you write no constructors, the compiler generates a default one. Write any constructor and that free default disappears:
#include <iostream>
class NeedsArgs {
int value;
public:
NeedsArgs(int v) : value(v) {} // writing this removes the default
int get() const { return value; }
};
int main() {
NeedsArgs a(5);
// NeedsArgs b; error: no matching function for call to 'NeedsArgs::NeedsArgs()'
std::cout << a.get() << "\n";
return 0;
}
5
Lesson 9.12 shows how to ask for the default back with = default.
explicit: stopping surprise conversions
A constructor taking one argument doubles as an implicit conversion, which is rarely what you want:
#include <iostream>
class Meters {
double value;
public:
explicit Meters(double v) : value(v) {}
double get() const { return value; }
};
void travel(const Meters& m) {
std::cout << "travelling " << m.get() << "m\n";
}
int main() {
travel(Meters(100.0)); // explicit and clear
// travel(100.0); // error, thanks to `explicit`.
// // Without it this compiles, and 100.0 silently
// // becomes Meters — even if it was seconds.
return 0;
}
travelling 100m
Mark single-argument constructors explicit unless you specifically want the conversion. The
cost of being wrong in the other direction is a program that compiles while meaning something you
did not write.
Try it
Write a class Fraction with a numerator and denominator, a constructor that rejects a zero
denominator, and a value() returning the division. Add a second constructor for whole
numbers. Make sure both initialise via the list.
Checkpoint
- A constructor runs on creation and leaves the object valid.
- The member initializer list initialises members; assigning in the body constructs twice.
constmembers, reference members, and members without a default constructor require the list.- Members initialise in declaration order, not list order —
-Wreorderwarns. - Writing any constructor removes the compiler-generated default one.
- Mark single-argument constructors
explicitto prevent silent conversions. - A constructor can delegate to another by naming it in the initializer list.
Questions about this lesson
Log in to ask a question.