Kodewick
Lesson 16 of 30Contents

Constructors and destructors

Lesson 16 of 30 · Part 4 · Classes · about 15 minutes

Last lesson's temperature objects all started at 0 degrees and needed a call to set_celsius to get a value. Forget the call and the program carries on with a wrong value. A constructor fixes that: it gives the object its values when the object is created.

main.cpp
1#include <iostream>2#include <string>3 4class Account {5public:6    Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {7        std::cout << "Opened: " << owner_ << '\n';8    }9 10    void show() const {11        std::cout << owner_ << "'s balance: " << balance_ << '\n';12    }13 14private:15    std::string owner_;16    int balance_;17};18 19int main() {20    Account acc{"Ana", 100};21    acc.show();22    return 0;23}
Output
Opened: Ana
Ana's balance: 100

A constructor is a member function with the same name as the class and no return type (not even void). When Account acc{"Ana", 100}; creates the object, the values in the curly braces go to the constructor, which runs by itself; you don't (and can't) call it yourself. Press Step: at this line, the program jumps into the constructor first, and comes back to main only once both fields of acc are filled in.

Initializer lists

The : owner_(owner), balance_(balance) after the constructor's parentheses and before its curly braces is the initializer list: each member gets its value the moment it's created. You could also assign in the curly braces, but then the member is created first and changed after, one step more. And some members can't be changed at all, such as const ones:

main.cpp
1#include <iostream>2 3class Ticket {4public:5    Ticket(int number) {6        number_ = number;7    }8 9    int number() const {10        return number_;11    }12 13private:14    const int number_;15};16 17int main() {18    Ticket t{7};19    std::cout << t.number() << '\n';20    return 0;21}
Output
main.cpp: In constructor 'Ticket::Ticket(int)':
main.cpp:6:17: error: assignment of read-only member 'Ticket::number_'
    6 |         number_ = number;
      |         ~~~~~~~~^~~~~~~~

assignment of read-only member 'Ticket::number_': number_ is const, so once it exists it can't be assigned. Write Ticket(int number) : number_(number) {} and it works. Every constructor in this course uses an initializer list.

Note: Members are initialized in the order they're declared in the class, not the order you list them. Write the initializer list in declaration order, so no member uses another that isn't initialized yet.

Default constructors

Once a class has a constructor you wrote, Fraction f; with nothing given no longer works:

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7    void show() const {8        std::cout << num_ << "/" << den_ << '\n';9    }10 11private:12    int num_;13    int den_;14};15 16int main() {17    Fraction f;18    f.show();19    return 0;20}
Output
main.cpp: In function 'int main()':
main.cpp:17:14: error: no matching function for call to 'Fraction::Fraction()'
   17 |     Fraction f;
      |              ^

no matching function for call to 'Fraction::Fraction()': there's no constructor that takes no arguments. That one is called the default constructor. When you write no constructor at all, the compiler makes one for you; write any, and it leaves it to you.

Constructors can be overloaded (lesson 9): write each kind you want. A constructor is also a good place to keep rules, such as "the denominator isn't 0" and "the minus sign goes on top":

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction() : num_(0), den_(1) {}6 7    Fraction(int num, int den) : num_(num), den_(den) {8        if (den_ == 0) {9            den_ = 1;10        }11        if (den_ < 0) {12            num_ = -num_;13            den_ = -den_;14        }15    }16 17    void show() const {18        std::cout << num_ << "/" << den_ << '\n';19    }20 21private:22    int num_;23    int den_;24};25 26int main() {27    Fraction zero;28    Fraction half{1, 2};29    Fraction odd{3, -4};30    zero.show();31    half.show();32    odd.show();33    return 0;34}
Output
0/1
1/2
-3/4

Fraction zero; uses the default constructor and gets 0/1; odd{3, -4} becomes -3/4 inside the constructor. However an object is created, it follows the rules from birth.

Destructors

What's created also dies. When an object dies, its destructor runs by itself: its name is ~ plus the class name, with no parameters and no return type. Have the constructor and the destructor each show a line, and you can see when an object is born and when it dies:

main.cpp
1#include <iostream>2#include <string>3 4class Guest {5public:6    Guest(const std::string& name) : name_(name) {7        std::cout << name_ << " comes in\n";8    }9 10    ~Guest() {11        std::cout << name_ << " leaves\n";12    }13 14private:15    std::string name_;16};17 18int main() {19    std::cout << "Doors open\n";20    Guest a{"Ana"};21    std::cout << "Doors close\n";22    return 0;23}
Output
Doors open
Ana comes in
Doors close
Ana leaves

After "Doors close", main reaches its end, a dies, and the destructor shows "Ana leaves". Press Step and see the program jump into ~Guest() after return 0;.

You rarely need to write a destructor yourself. std::string and std::vector have their own destructors that give their memory back when they die, which is why you never clean up after them. In lessons 20 and 21, when you ask the system for memory yourself, the destructor is where the cleaning up goes.

End of scope, in reverse order

An object dies at the end of the pair of curly braces it was declared in (its scope). When several die together, the last one created dies first, like a stack of blocks: the last one put on top comes off first.

main.cpp
1#include <iostream>2#include <string>3 4class Guest {5public:6    Guest(const std::string& name) : name_(name) {7        std::cout << name_ << " comes in\n";8    }9 10    ~Guest() {11        std::cout << name_ << " leaves\n";12    }13 14private:15    std::string name_;16};17 18int main() {19    Guest a{"Ana"};20    {21        Guest b{"Ben"};22        Guest c{"Cara"};23        std::cout << "--- inner braces end\n";24    }25    std::cout << "--- main ends\n";26    return 0;27}
Output
Ana comes in
Ben comes in
Cara comes in
--- inner braces end
Cara leaves
Ben leaves
--- main ends
Ana leaves

b and c die where the inner curly braces end, c first because it came last; a lives until main ends. Press Step and go line by line, watching the objects appear and disappear.

Objects in a function die when the function returns, and an object in a loop body is born and dies once per time around:

Guess first

What does this show?

1#include <iostream>2 3class Box {4public:5    Box(int n) : n_(n) {6        std::cout << "+" << n_ << " ";7    }8 9    ~Box() {10        std::cout << "-" << n_ << " ";11    }12 13private:14    int n_;15};16 17int main() {18    for (int i = 1; i <= 2; i++) {19        Box b{i};20    }21    std::cout << '\n';22    return 0;23}

Objects in a vector

Objects in a vector belong to the vector: pop_back removes one and it dies; when the vector dies, everything left in it dies too. std::vector<Seat> row(3); makes 3 with the default constructor:

main.cpp
1#include <iostream>2#include <vector>3 4class Seat {5public:6    Seat() {7        std::cout << "Chair set out\n";8    }9 10    ~Seat() {11        std::cout << "Chair put away\n";12    }13};14 15int main() {16    std::vector<Seat> row(3);17    std::cout << row.size() << " chairs\n";18    row.pop_back();19    std::cout << row.size() << " left\n";20    return 0;21}
Output
Chair set out
Chair set out
Chair set out
3 chairs
Chair put away
2 left
Chair put away
Chair put away

3 chairs are set out; pop_back puts 1 away; main ends, row dies, and the other 2 are put away with it. The order in which a vector's elements are destroyed isn't fixed by the standard (g++ goes front to back), so don't count on it.

Note: When you push_back objects into a vector, you'll see extra constructor and destructor lines: what goes in is a copy or something "moved" in, and a growing vector moves everything to a new place. Lesson 18 covers copying and moving; lesson 28 covers how a vector grows.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

A rectangle's constructor

Write a constructor for Rectangle: take the width and height with an initializer list, then show the line New rectangle: 3 x 4. The program should show the two lines below.

It should print
New rectangle: 3 x 4
Area: 12
exercise1.cpp
1#include <iostream>2 3class Rectangle {4public:5 6    int area() const {7        return width_ * height_;8    }9 10private:11    int width_;12    int height_;13};14 15int main() {16    Rectangle r{3, 4};17    std::cout << "Area: " << r.area() << '\n';18    return 0;19}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: a wallet starts empty

Wallet w; doesn't compile. Add a default constructor to Wallet so a wallet starts with 0 cents. The program should show 350 cents.

It should print
350 cents
exercise2.cpp
1#include <iostream>2 3class Wallet {4public:5    Wallet(int cents) : cents_(cents) {}6 7    void add(int cents) {8        cents_ += cents;9    }10 11    int cents() const {12        return cents_;13    }14 15private:16    int cents_;17};18 19int main() {20    Wallet w;21    w.add(200);22    w.add(150);23    std::cout << w.cents() << " cents\n";24    return 0;25}
Output
Press Run or Ctrl+Enter
Exercise 3

The meeting is over

Write a destructor for Member that shows "name leaves the room", so the output matches. Think about who leaves first.

It should print
Ana joins the meeting
Ben joins the meeting
Meeting over
Ben leaves the room
Ana leaves the room
exercise3.cpp
1#include <iostream>2#include <string>3 4class Member {5public:6    Member(const std::string& name) : name_(name) {7        std::cout << name_ << " joins the meeting\n";8    }9 10private:11    std::string name_;12};13 14int main() {15    Member a{"Ana"};16    Member b{"Ben"};17    std::cout << "Meeting over\n";18    return 0;19}
Output
Press Run or Ctrl+Enter

What you learned

  • A constructor has the class's name and no return type, and runs by itself when an object is created: Account acc{"Ana", 100};.
  • The initializer list : owner_(owner), balance_(balance) gives members their values as they're created; const members can only be set this way. Members are initialized in declaration order.
  • A constructor that takes no arguments is the default constructor. Write any constructor and the compiler stops making the default one; write it yourself if you need it. Constructors can be overloaded and are a good place to keep rules.
  • The destructor is ~ClassName() and runs by itself when an object dies; string and vector use theirs to give memory back.
  • An object dies at the end of its scope, and in one scope the last created dies first; an object in a loop body is born and dies once per round.
  • Objects in a vector die on pop_back or when the vector dies.