Embercode
✕
Object-Oriented ProgrammingLesson 2 of 17

50 min read

Constructors, destructors and this

Why it matters

An object should start its life in a valid state, and it should release anything it owns when it ends. A bank account should start with a sensible balance. A list should start empty and free its nodes when it is no longer needed. In C++ two special member functions do this work automatically. A constructor runs when an object is created. A destructor runs when an object is destroyed. Get these right and most memory bugs disappear. Get them wrong and you see crashes, leaks and values that change for no visible reason.

Interviewers ask about constructors and destructors almost every time a candidate writes a class. They want to know the order in which things are built and destroyed, how a class with a pointer should clean up, and why a const member cannot be assigned in the body of a constructor. This lesson answers each of those questions with running code.

The idea: setup at check-in, clean-up at check-out

Think of renting a flat. At check-in, someone hands you keys, sets the water and electricity on, and makes sure the place is ready to live in. That is the constructor. You do not live in a flat that has no keys and no power. At check-out, someone reads the meters, returns the keys and cancels the connections. That is the destructor. Nobody checks out by leaving the keys in the lock and walking away, because the next tenant then finds a mess.

A class works the same way. The constructor takes the raw materials you give it and sets the object up so that every member has a valid value. The destructor releases whatever the object took, such as memory, file handles or network connections. The language calls these two functions for you, at exactly the right moments. You write the rules once, and you do not have to remember to call them.

The rest of this lesson covers the details: how a constructor gets its values, the exact order of setup and clean-up, what this means inside a method, and how to write a destructor that frees memory without causing a crash.

Constructors

A constructor is a method that runs when an object is created. It has three rules.

  1. Its name is exactly the class name.
  2. It has no return type, not even void.
  3. It runs automatically, once for each object.

Here is a class with two constructors. The first takes a title and a page count. The second takes nothing and gives defaults. Each constructor prints a line so you can see when it runs.

C++ · editable

Expected output:

created Dune
created default
Dune 412
untitled 0

Trace it. Book a("Dune", 412); picks the constructor whose parameters match the arguments, which is the two-parameter one. It sets title to "Dune" and pages to 412, then prints created Dune. Book b; has no arguments, so it picks the constructor with no parameters. That constructor is called the default constructor, because it is the one the language uses when you give no arguments. It prints created default. The last two lines read the values back.

Having several constructors with different parameter lists is called overloading. It is the same idea as overloading an ordinary function. The compiler chooses the best match from the argument types, and if no constructor matches, it stops with an error. We will see that error in the common mistakes section.

What happens when you write no constructor

If a class has no constructor at all, the compiler writes a default constructor for you. That generated constructor does nothing special. It gives class-type members their own default constructors, but it leaves members of built-in types such as int and double with no value. A class with an int member and no constructor therefore holds garbage in that member, which is the same danger we saw in lesson 1.

Once you write any constructor yourself, the compiler stops generating the default one. If you then need a default object, you must write that constructor too, as the Book class above does. This is a frequent surprise, and it is the reason the array example in the mistakes section fails.

Member initialiser lists

Look at the text after the colon in the Book constructor: title(t), pages(p). This is the member initialiser list. It sets each member directly, before the body of the constructor runs.

There are two ways to set a member inside a constructor:

// Way A: assign in the body. The member is first created empty, then assigned.
Book(string t, int p) {
    title = t;
    pages = p;
}
 
// Way B: the initialiser list. The member is created with the value.
Book(string t, int p) : title(t), pages(p) {
}

For a string the difference is small, but it adds up. Way A builds an empty string and then copies into it. Way B builds the string once, from the argument. For an int the two ways cost the same, and most programmers still prefer Way B out of habit.

The initialiser list is required in two cases.

  • A const member. A const value can be set once, when it is created. Assigning it in the body is too late, and the compiler refuses.
  • A reference member. A reference must be bound to an object when it is created. You cannot bind it later.

The following example shows the first case.

class Point {
    const int x;
public:
    Point(int v) { x = v; }   // error: the member x is set too late
};

The compiler reports:

error: uninitialized const member in 'const int' [-fpermissive]
error: assignment of read-only member 'Point::x'

The fix is Point(int v) : x(v) {}. The second message is the important one. It names the member Point::x and says that it is read-only, which is the clue that the constant must be set in the initialiser list. The first message says the same thing from another angle, and it is only a warning that the compiler accepts as an error here.

The order of members is the declaration order

There is one rule about initialiser lists that catches almost everyone. Members are set up in the order they are declared in the class, not in the order you list them after the colon. If you write : pages(p), title(t) but the class declares title first, the compiler still sets title first. With -Wall the compiler warns about the mismatch (-Wreorder), but the program still compiles.

Why does the order matter? Because one member may depend on another. If area is set from width and width has not been set yet, the value is wrong. Declare members in the order they depend on each other, and keep the initialiser list in the same order to avoid warnings.

What happens inside: the order of setup and clean-up

Setup has a fixed order. When an object is created, the following steps run in order.

  1. Memory for the whole object is reserved.
  2. Each member is built, in the order of declaration. Members with their own constructors run those constructors.
  3. The body of the class's constructor runs.

Clean-up happens in the reverse order. When the object is destroyed, the following steps run.

  1. The body of the destructor runs.
  2. Each member is destroyed, in the reverse order of declaration.
  3. The memory is released.

Inheritance adds one more level, which lesson 3 covers. For now, the next example proves the order for ordinary members.

Worked example 2: the order of parts

The Car below has two members, a Part and an Engine, each with its own constructor and destructor. The class declares wheel first and engine second. Its initialiser list writes engine first on purpose, to show that the list does not control the order.

C++ · editable

Expected output:

make car
  part tyre built
  engine built
car built
car removed
  engine removed
  part tyre removed
done

Read the output as a timeline. make car prints first, from main. Then Car c("tyre") starts. The first member built is wheel, because it is declared first, so part tyre built appears, even though the initialiser list wrote engine() first. Next engine is built. Then the body of Car runs and prints car built. The object is now ready.

The closing brace of the inner block ends c. The destructor of Car runs first, printing car removed. Then the members are destroyed in reverse order. engine was declared second, so it goes first: engine removed. Then wheel goes: part tyre removed. The line done comes last, because the block has ended.

Timeline of building and removing a Car: parts are built in declaration order, and removed in reverse orderTimeline of building and removing a Car: parts are built in declaration order, and removed in reverse order

The diagram above shows the same timeline. Learn the rule it shows, because it applies to every class with members: build in declaration order, remove in reverse.

The this pointer

Inside a method, every member function has a hidden pointer called this. It points to the object the method was called on. When you write a.deposit(5), the compiler passes the address of a as this, and inside deposit a name such as balance means this->balance.

You need this in two common situations.

To tell a member from a parameter with the same name. A constructor often takes parameters named the same as the members, because the names are natural. Inside the body, the parameter hides the member, so x = x assigns the parameter to itself and changes nothing. The fix is this->x = x, which says "the member x gets the value of the parameter x".

To return the object itself. A method can return *this, which means "the object this method was called on". If the return type is a reference, the caller gets the same object back and can call another method on it. This is called chaining, and it is how a stream statement like cout << a << b works.

The following example uses both.

C++ · editable

Expected output:

14,2

Trace the chain. Point p(1, 2) sets x to 1 and y to 2. p.moveBy(3) adds 3 to x, so x becomes 4, and returns *this, which is p itself. The next call is .moveBy(10) on that same object. x becomes 14. The show call prints 14,2. Notice that y never changed.

Two Point objects, each with its own x and y, and the this pointer that a method receives for the object it was called onTwo Point objects, each with its own x and y, and the this pointer that a method receives for the object it was called on

The figure shows where this comes from. Each object has its own x and y. A method call hands the address of one object to the method as this, so the method works on the right data.

A rule to remember: plain x inside a method means this->x, unless a parameter or a local variable with the same name hides it. Many style guides use an underscore suffix for members, such as x_, to avoid the hiding problem. Either style works. Be consistent.

const and this

Inside a const method, this has the type const Point*, which means the pointed-to object cannot be changed through this. That is how the compiler enforces the promise from lesson 1. You do not write this in a const method for that purpose, but it explains why assignments inside the method are rejected.

Destructors

A destructor is the opposite of a constructor. It runs when the object is destroyed. Its name is the class name with a tilde in front, such as ~Buffer. It takes no parameters and returns nothing. A class has at most one destructor.

The destructor runs in three common situations.

  • An object created as a local variable goes out of scope, which means the block that holds it ends.
  • An object created with new is destroyed with delete.
  • An object held inside another object is destroyed when the outer object is.

Destructors matter most for objects that own a resource. The most common resource is memory from new. If the constructor allocated memory, the destructor must release it. Otherwise the memory leaks, which means the program keeps it reserved and can never use it again.

Worked example 3: a buffer that frees its memory

C++ · editable

Expected output:

start
allocated
inside
freed
end

Trace the program. start prints first. The inner block opens. Buffer b(10) runs the constructor, which asks the system for an array of ten int values and remembers its address in data, and then prints allocated. The block prints inside. The block closes, which ends the life of b. The destructor runs: it gives the array back with delete[] and prints freed. The line end prints last.

Two details matter. The destructor uses delete[] because the constructor used new[]. Mixing new[] with plain delete is undefined behaviour, which means the program may do anything. The second detail is the position of the destructor's print. It runs after the memory is released, so you should not read data after delete[]. In this program nothing reads it.

RAII: acquire in the constructor, release in the destructor

The pattern in this program has a name. It is called RAII, which stands for "resource acquisition is initialisation". The phrase is unusual, but the idea is simple. A resource such as memory, a file or a lock is acquired in the constructor, and released in the destructor. Since the destructor runs when the object goes out of scope, the resource is released even when the code leaves the block early, for example through a return or a thrown error.

Compare two ways to write the same job.

void risky() {
    int* data = new int[10];
    if (someCondition()) return;     // leak: delete[] is never reached
    delete[] data;
}
 
void safe() {
    Buffer b(10);
    if (someCondition()) return;     // fine: ~Buffer runs on the way out
}

In risky, the early return skips the delete[], so the memory leaks. In safe, the Buffer object is destroyed on every exit path, so the memory is always released. This is why C++ programmers reach for classes that own resources instead of bare new and delete. The standard library's vector and smart pointers are RAII classes too.

Three ways to create an object

C++ has several ways to write the creation of an object, and they do not all mean the same thing. Learn the three forms below, because each one appears in real code and interviewers like to test the difference.

Book a;          // default-initialised: calls the default constructor
Book b{};        // value-initialised: calls the default constructor, and zeroes plain members
Book c("Dune", 412);   // calls the constructor that takes two arguments

For a class that has a constructor, the first and second forms both call the default constructor. The difference shows for plain members such as int when the class has no constructor at all. With Book a;, a plain member such as int pages holds garbage. With Book b{};, the same member is set to 0. Braces are the safe habit, because they do the right thing in both cases.

The third form calls a constructor with arguments. Parentheses work here, and so do braces in modern C++: Book c{"Dune", 412};. The braces refuse some conversions that parentheses allow, such as turning a double into an int without a warning, so many style guides prefer braces. Either form is correct, and you will see both in real code.

Now the trap from lesson 1, written again so you can recognise it:

Book d();        // not an object: a function declaration named d

The empty parentheses mean "a function that takes nothing and returns a Book". The compiler does not create anything. The fix is Book d; or Book d{};.

Here is what the compiler does for Book c("Dune", 412);, in order.

  1. It reserves enough memory for one Book, which is the sum of the sizes of its members, plus any padding the compiler adds for alignment.
  2. It looks at the argument types, const char* and int here, and chooses the constructor whose parameter types match best.
  3. It converts the const char* to a string, which is a parameter of the chosen constructor, and passes the int unchanged.
  4. It runs the member initialisers, then the body.
  5. The name c now refers to the finished object.

Step 3 hides a copy. The string is created from the text "Dune", and then the constructor copies it into the member title. Lesson 5 explains when such copies happen and how to make them cheap.

A linked list class and its destructor

A linked list is a chain of small boxes called nodes. Each node holds one value and a pointer to the next node. The list keeps a pointer to the first node, called the head. A linked list is a natural place to use a class, because the nodes are allocated with new, and somebody must free every one of them. If the list object is destroyed without freeing its nodes, the nodes leak.

The next example writes a small list. The pushFront method adds a new node at the front. The destructor walks the chain and deletes each node. The printout at the end shows how many nodes were freed.

headnull

Each node holds a value and a pointer (dot and arrow) to the next node. An outlined node is where the walk is now. A filled node is new or found.

The list is empty. head points to nothing.

Step 1 of 16

Step through the visualizer above. Watch the head pointer move each time a node is added, and watch the arrows. Notice that a node points to the next one, not to the head, so the list is a chain that you can only walk from the front.

C++ · editable

Expected output:

list ready
1 -> 2 -> 3
size 3
freed 3 nodes

Look at the destructor. It cannot simply write delete head, because that frees only the first node and the rest of the chain would leak. So it saves the next pointer in a local variable, deletes the current node, and moves on. The local variable next is what makes the walk safe. If the code read cur->next after delete cur, it would read freed memory.

Look at pushFront. The new node points to the old head, and then the head moves to the new node. That is why the printout starts with 1, which was added last. Notice the last line of print: the arrow is written only when there is another node after the current one, so the output has no trailing arrow.

The destructor runs when list ends at the end of main. Its message freed 3 nodes appears after the size line, because main has finished its last statement before the object is destroyed.

Worked example 5: the destructor order for a list of objects

Objects in an array or a vector are destroyed in a fixed order too. An array is destroyed from the last element back to the first. A vector also destroys its elements from the last to the first when the vector ends. Here is a short program that makes this visible.

C++ · editable

Expected output:

make 1
make 2
make 3
vector full
drop 1
drop 2
drop 3

The three objects are made in order, and the vector stores them in that order. When main returns, the vector is destroyed. It drops the elements from first to last, which the output shows: drop 1, then drop 2, then drop 3. This is a different order from a list of local variables, which goes last-in first-out. The lesson for an interview answer: the order of destruction for a vector's elements is the order they sit in the vector, while local variables in one scope are destroyed in reverse order of creation.

This looks like it breaks the reverse-order rule, but the rule is about a different thing. Local variables and class members are always destroyed in reverse order. The elements of a vector are destroyed from first to last on this compiler. The C++ language does not promise that order for every container, so do not depend on it in a real program. If the order matters, check the documentation of the container you use.

Notice also that emplace_back(1) builds the object in place, from the arguments. It is the same as writing push_back(Token(1)) but without making a separate temporary. Both lead to a constructor call with the argument 1.

The line tokens.reserve(3); matters for the output. A vector stores its elements in one block of memory, and when that block is full the vector asks for a bigger one and moves the elements across. Each move destroys an old object. Without reserve, the same program prints extra lines during the growth, and the output on g++ 15 is:

make 1
make 2
drop 1
make 3
drop 1
drop 2
vector full
drop 1
drop 2
drop 3

That is a real cost of growing a vector of objects, and it is the reason experienced programmers call reserve when they know the final size.

Common mistakes with real compiler messages

Mistake 1: no default constructor for an array. Once you write a constructor with parameters, you lose the automatic default one. Making an array of such objects then fails, because each element needs a default.

class Book {
public:
    Book(string t, int p) {}
};
int main() {
    Book shelf[3];
}
error: no matching function for call to 'Book::Book()'

The message says that the compiler looked for a constructor with no arguments and found none. The fix is to write a default constructor, or to use a vector<Book> and call emplace_back with the arguments.

Mistake 2: a return type on a constructor. A constructor has no return type. If you put void in front, the name is no longer a constructor, and the compiler reports the problem in the declaration.

class Book {
public:
    void Book(int pages) {}
};
int main() {
    Book b;
}
error: return type specification for constructor invalid
error: no matching function for call to 'Book::Book()'

The first message points at the real cause, the void. The second is a consequence: because the constructor was not recognised, Book b; has no constructor to call. Fix the first error and the second goes away.

Mistake 3: the same constructor written twice. Two constructors with the same parameter list are not allowed, even if their bodies differ. The compiler refuses the second one.

error: 'Point::Point(int)' cannot be overloaded with 'Point::Point(int)'

Overloading needs different parameter lists. Use one constructor and give the other behaviour through a parameter, or make them differ in parameter types.

Mistake 4: an implicit conversion you did not want. A constructor that takes one argument can be used to convert a value to the class type without you asking. Sometimes that is handy, and sometimes it hides a bug. Mark such constructors explicit to stop it.

class Meters {
public:
    explicit Meters(int m) {}
};
void show(Meters m) {}
int main() {
    show(5);
}
error: could not convert '5' from 'int' to 'Meters'

Without explicit, the call show(5) would silently build a Meters from 5. With explicit, the compiler asks you to write show(Meters(5)), which states your intent.

Mistake 5: forgetting delete[] or deleting twice. These errors compile without any message, because the compiler cannot see your intent. They show up at run time. The most famous case is a class that owns memory but has no copy constructor. Copying it makes two objects point at one block, and the destructor frees that block twice. Lesson 5 covers this in detail, but here is the crash, so you know what it looks like.

class IntArray {
    int* data;
public:
    IntArray(int n) : data(new int[n]()) {}
    ~IntArray() { delete[] data; }
    void set(int i, int v) { data[i] = v; }
};
int main() {
    IntArray a(3);
    IntArray b = a;      // copies the pointer, not the data
}

Compiled with g++ 15 and run, the program stops with:

free(): double free detected in tcache 2

The program aborts with exit code 134. Notice that nothing printed. The cause is in the destructors, which run after main ends. Because the abort happens before the output buffer is flushed, a program that crashes while it cleans up can lose its last few lines. The fix is in lesson 5.

Debugging walkthrough: a crash at the end of main

Suppose a program prints the first line and then crashes with a message about a double free. The crash happens after main returns, so the line that causes it is not in main. Here is how to find it.

  1. Read the message. "Double free" means some block was freed twice. The message does not say where.
  2. Ask which objects own memory. In this program, a and b are both IntArray objects, and both own a block.
  3. Ask whether the two objects share a block. IntArray b = a; calls the default copy, which copies the pointer. So a and b share one block.
  4. Ask what the destructors do. Each one calls delete[] on the same pointer. The first call frees the block, and the second call frees it again.
  5. Fix the design. Give the class a copy constructor that allocates a new block and copies the values, as lesson 5 shows.

A useful tool is the debugger, which can stop at the crash and show the call stack. Compile with -g to keep the names, and run under gdb or the address sanitiser with -fsanitize=address. The sanitiser prints the line of the first free and the line of the second free, which makes the bug easy to spot.

For smaller problems, add print statements to the constructor and destructor. Printing this shows which object is which, because each object has a different address. Seeing two destructors print the same address is a strong hint that a copy was made without a copy constructor.

Variations and edge cases

Delegating constructors. Since C++11, one constructor can call another in its initialiser list. This removes repeated code. For example, Book() : Book("untitled", 0) {} calls the two-parameter constructor with the default values. Only one delegation per constructor is allowed, and you cannot also initialise members in the same list.

Default values in the parameter list. Instead of two constructors, you can write one constructor with default arguments: Book(string t = "untitled", int p = 0). This makes the class callable with zero, one or two arguments. Keep it simple: defaults make the interface harder to read when there are many parameters.

Value initialisation with braces. Book b{}; value-initialises the object. For a class with a default constructor, that calls the constructor. For a plain int member with no constructor, it sets the value to zero. This is a safe habit when you want a known state.

= default and = delete. You can ask the compiler for the default constructor with Book() = default;. You can forbid copying with Book(const Book&) = delete;. Both are useful in interviews, because they show that you understand what the compiler generates for you.

Exceptions in constructors. If a constructor throws an error, the object is never finished, so its destructor does not run. Members that were already built are destroyed, though. This is why a class that owns memory should allocate inside a member that cleans itself up, such as a vector, rather than in a raw new in the constructor body.

Interview questions and how to answer them

What runs first, the constructor body or the member constructors? The member constructors run first, in declaration order, and then the body runs. The destructor runs in the reverse order: the body first, then the members.

Why does a class with a pointer member need a destructor? The pointer usually owns memory from new. Without a destructor, the memory is never freed and the program leaks it.

What is the this pointer? A hidden pointer that every non-static method receives. It points to the object the method was called on. Use *this to return the object itself.

Why must a const member be set in the initialiser list? A const member cannot be assigned after it is created, so it must be given its value at creation, and only the initialiser list does that.

What is RAII? Acquire a resource in a constructor and release it in the destructor, so the release always happens when the object leaves scope. It is the main way C++ avoids leaks.

Real-world uses

Constructors and destructors are everywhere in real code. A file class opens the file in its constructor and closes it in its destructor, so you can never forget to close it, even when an error stops the function early. A lock class takes a lock in its constructor and releases it in its destructor, so other threads are never blocked by a forgotten unlock. A database connection pool hands out connections whose destructors return them to the pool. In each case the object's lifetime controls a real resource, and the language guarantees the clean-up.

In a linked list, a tree or a graph, the nodes are allocated one at a time with new. A list class with a destructor is the simplest correct design. Later, a smart pointer such as unique_ptr takes over the job of delete, and the destructor of the owner frees the node automatically.

Practice

Try each exercise before you open the solution. Use Run to check your code.

  1. Add a constructor to Account from lesson 1 that takes an opening balance. Make it refuse a negative opening balance by setting the balance to 0. Hint: use the initialiser list for balance.
Solution
class Account {
    int balance;
public:
    Account(int opening) : balance(opening < 0 ? 0 : opening) {}
    int getBalance() const { return balance; }
};

The conditional operator picks 0 for a negative opening amount and the given amount otherwise. Because balance is set in the initialiser list, it never holds garbage.

  1. Create two objects of a class that prints a message in its constructor and its destructor. Predict the order of messages when the objects are declared in a block, and then check your prediction. Expected for A a; B b; in a block: build A, build B, drop B, drop A.
Solution
#include <iostream>
using namespace std;
 
class A {
public:
    A() { cout << "build A\n"; }
    ~A() { cout << "drop A\n"; }
};
 
class B {
public:
    B() { cout << "build B\n"; }
    ~B() { cout << "drop B\n"; }
};
 
int main() {
    {
        A a;
        B b;
    }
    return 0;
}

Objects in one scope are destroyed in the reverse order of creation, so drop B comes before drop A.

  1. Make Point from worked example 3 print its coordinates when it is destroyed. Predict the output for Point p(3, 4); followed by cout << "end\n"; in main.
Solution
#include <iostream>
using namespace std;
 
class Point {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {}
    ~Point() { cout << "destroyed " << x << "," << y << "\n"; }
};
 
int main() {
    Point p(3, 4);
    cout << "end\n";
    return 0;
}

The output is end and then destroyed 3,4. The destructor runs when main returns, after the last line.

  1. The class below does not compile. Explain why in one sentence, and fix it by writing a constructor that sets count from a parameter.
class Counter {
    const int limit;
public:
    Counter(int n) { limit = n; }
};
Solution

limit is const, so it must be set in the initialiser list, not assigned in the body. The fix is Counter(int n) : limit(n) {}.

  1. Write a class Tally with a constructor that takes a starting value and a method add that returns *this, so that t.add(2).add(5) works. Start at 10 and print the result. Expected output: 17.
Solution
#include <iostream>
using namespace std;
 
class Tally {
    int total;
public:
    Tally(int start) : total(start) {}
    Tally& add(int x) {
        total += x;
        return *this;
    }
    int value() const { return total; }
};
 
int main() {
    Tally t(10);
    cout << t.add(2).add(5).value() << "\n";
    return 0;
}

Each call returns the same object, so the chain adds 2 and then 5 to the total.

  1. A class holds int* data = new int[100]; in its constructor and has no destructor. Describe what goes wrong, and write the destructor.
Solution

Every object leaks its 100 integers, because nothing calls delete[]. A long-running program that creates many objects eventually runs out of memory. The destructor is:

~Holder() { delete[] data; }
  1. Write a class Logger whose constructor prints open <name> and whose destructor prints close <name>. Create one logger inside a block, and print working inside the block. Predict the three lines.
Solution
#include <iostream>
#include <string>
using namespace std;
 
class Logger {
    string name;
public:
    Logger(string n) : name(n) { cout << "open " << name << "\n"; }
    ~Logger() { cout << "close " << name << "\n"; }
};
 
int main() {
    {
        Logger log("file.txt");
        cout << "working\n";
    }
    return 0;
}

The output is open file.txt, then working, then close file.txt. The close message appears when the block ends, which is the RAII pattern.

  1. Using the IntList class from this lesson, write a main that pushes 5, 6 and 7 and then prints the list. Predict the printout before you run it. Expected: 7 -> 6 -> 5.
Solution
#include <iostream>
using namespace std;
 
class IntList {
    struct Node {
        int value;
        Node* next;
    };
 
    Node* head = nullptr;
    int count = 0;
 
public:
    ~IntList() {
        Node* cur = head;
        while (cur != nullptr) {
            Node* next = cur->next;
            delete cur;
            cur = next;
        }
    }
 
    void pushFront(int v) {
        head = new Node{v, head};
        count++;
    }
 
    void print() const {
        for (Node* p = head; p != nullptr; p = p->next) {
            cout << p->value << (p->next != nullptr ? " -> " : "\n");
        }
    }
};
 
int main() {
    IntList list;
    list.pushFront(5);
    list.pushFront(6);
    list.pushFront(7);
    list.print();
    return 0;
}

The last value pushed sits at the front, so the printout starts with 7. The destructor then prints freed 3 nodes.

  1. Write a class Pair with two int members set by the initialiser list, and a copy-free method swapped() that returns a new Pair with the values in reverse order. Hint: a method can return an object it creates locally, with return Pair(b, a);.
Solution
#include <iostream>
using namespace std;
 
class Pair {
    int a, b;
public:
    Pair(int a, int b) : a(a), b(b) {}
    Pair swapped() const { return Pair(b, a); }
    void show() const { cout << a << " " << b << "\n"; }
};
 
int main() {
    Pair p(1, 2);
    p.swapped().show();
    return 0;
}

The output is 2 1. The original p is unchanged, because swapped builds a new object and returns it.

  1. Explain in two sentences why a destructor should not read members after it has freed the memory they point to.
Solution

After delete[], the pointer still holds the old address, but the memory there no longer belongs to the object. Reading through it is undefined behaviour and may return garbage or crash.

What you now know

  • A constructor runs when an object is created and sets every member to a valid value. It has the class name and no return type.
  • The initialiser list sets members before the body runs. const members and reference members must use it.
  • Members are built in declaration order and destroyed in reverse order, after the destructor body runs.
  • this points to the object a method was called on. Return *this to allow chaining.
  • A destructor releases what the object owns. RAII ties that release to the object's lifetime.
  • A linked list class must free every node in its destructor, walking the chain and saving the next pointer first.
  • Common errors are a missing default constructor for arrays, a void return type on a constructor, and a shallow copy that causes a double free.
  • Next, lesson 3 builds a derived class from a base class, and shows the order of constructors across the inheritance chain.
Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Classes, objects and encapsulation