xSKILLby Infirals
Register

C++ for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Constructors, destructors, and lifetime

Classes and object lifetime13 min readLesson 2 of 4

2.1 When an object begins and ends

An object's lifetime starts when its constructor finishes and ends when its destructor is called. The constructor's job is to leave the object in a state where every member function can run safely. The destructor's job is to release anything the object acquired.

C++ calls both automatically. A local object is destroyed at the end of its scope, a member is destroyed with its containing object, and a heap object is destroyed by delete. You write the constructor and destructor once, and the compiler inserts the calls at every exit path.

cpp
#include <iostream>
#include <string>

class Book {
public:
    Book(std::string title, int pages) : title_(title), pages_(pages) {}
    void show() const { std::cout << title_ << " has " << pages_ << " pages\n"; }
private:
    std::string title_;
    int pages_;
};

int main() {
    Book book("C++ Notes", 240);
    book.show();
    return 0;
}

Output

C++ Notes has 240 pages

2.1.1 The member initializer list

The text between : and the constructor body is the member initializer list. Members are constructed there, before the body runs. A member that appears only in the body was first default-constructed and then assigned, which costs work and can be impossible.

const members and reference members must be initialized in the list because they cannot be assigned later. Members are initialized in declaration order, whatever order the list uses. Compilers warn when the two orders differ, and reading a member that has not been initialized yet is undefined behavior.

cpp
#include <iostream>
#include <string>

class Ticket {
public:
    Ticket(std::string owner, int seat) : owner_(owner), seat_(seat) {}
    void show() const { std::cout << owner_ << " seat " << seat_ << "\n"; }
private:
    std::string owner_;
    const int seat_;
};

int main() {
    Ticket ticket("Ravi", 14);
    ticket.show();
    return 0;
}

Output

Ravi seat 14

2.1.2 Default, deleted, and explicit constructors

A class with no user-declared constructor gets a generated default constructor. Declaring any constructor removes that generated version, so a class that needs both forms writes ClassName() = default; or provides its own no-argument constructor.

= delete removes a function from the overload set. Session() = delete; prevents an object from being created without arguments, and the compiler reports the attempt at the call site. The explicit keyword on a one-argument constructor blocks implicit conversions such as Session s = 7;, while still allowing Session s(7);.

cpp
#include <iostream>

class Session {
public:
    Session() = delete;
    explicit Session(int id) : id_(id) {}
    int id() const { return id_; }
private:
    int id_;
};

int main() {
    Session session(7);
    std::cout << "Session " << session.id() << "\n";
    return 0;
}

Output

Session 7

2.1.3 Destructors and scope

A destructor has the class name with a leading ~, takes no parameters, and has no return type. It runs when an object's lifetime ends. Local objects are destroyed in reverse order of construction, and a member is destroyed after the destructor body of its containing object.

A class that owns a resource releases it in the destructor. A class that owns nothing can often omit the destructor entirely, because its members clean themselves up. Declaring a destructor also affects the copy and move operations the compiler generates, which matters for classes that manage memory.

cpp
#include <iostream>
#include <string>

class Marker {
public:
    explicit Marker(std::string name) : name_(name) {
        std::cout << "open " << name_ << "\n";
    }
    ~Marker() { std::cout << "close " << name_ << "\n"; }
private:
    std::string name_;
};

int main() {
    Marker outer("outer");
    { Marker inner("inner"); }
    std::cout << "back in main\n";
    return 0;
}

Output

open outer
open inner
close inner
back in main
close outer

2.1.4 Resource acquisition is initialization

RAII ties a resource to an object's lifetime. The constructor acquires the resource, and the destructor releases it. A std::string acquires character storage in its constructor and frees it in its destructor. A file stream opens a file and closes it the same way.

The pattern works on every exit path, including an early return and an exception. That is why the standard library uses it for memory, files, and locks. A class that allocates memory with new needs a destructor that calls delete, plus decisions about copying and moving. Later lessons replace most raw new with std::unique_ptr, which applies RAII for you.

cpp
#include <cstddef>
#include <iostream>
class Buffer {
public:
    explicit Buffer(std::size_t size) : size_(size), data_(new int[size]()) {}
    ~Buffer() { delete[] data_; }
    void set(std::size_t i, int value) { if (i < size_) data_[i] = value; }
    int get(std::size_t i) const { return i < size_ ? data_[i] : -1; }
private:
    std::size_t size_;
    int* data_;
};

int main() {
    Buffer buffer(3);
    buffer.set(0, 10);
    buffer.set(2, 30);
    std::cout << buffer.get(0) << " " << buffer.get(2) << "\n";
    return 0;
}

Output

10 30

2.1.5 Temporaries and return values

A temporary object is created by an expression and destroyed at the end of the full expression that created it. print(Point(1, 2)); constructs a Point, passes it to print, and destroys it before the next statement.

Returning an object by value is safe. C++17 guarantees copy elision when a function returns a prvalue, so return Point(x, y); constructs the result directly in the caller's storage. Returning a reference to a local is still wrong, because the local dies at the return. Prefer values for new objects and references for objects that already exist.

2.1.6 Common mistakes

  • Assigning members in the constructor body when the initializer list would do.
  • Writing the initializer list in a different order from the member declarations.
  • Forgetting that declaring a constructor removes the generated default constructor.
  • Copying an object that owns a raw resource, which leads to a double delete.
  • Throwing from a destructor. A destructor that runs during unwinding must not throw another exception.
  • Expecting Session session = 7; to compile when the constructor is explicit.

2.1.7 Practice

  1. Write a Point class with an initializer-list constructor and a show() function.
  2. Add a const int id_ member and initialize it from a constructor parameter.
  3. Print a message from a constructor and from a destructor, then create an object inside a nested block.
  4. Write a class that allocates an array with new[] and frees it with delete[].
  5. Explain why Session s = 7; fails when the constructor is explicit.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01Why does a constructor use an initializer list instead of assigning in the body?

    Members are constructed before the body runs, so assigning in the body means they were first default-constructed and then overwritten. Some members, such as const members and references, cannot be assigned at all and need the list.

  • 02In what order are members initialized?

    In the order they are declared in the class, not the order they appear in the initializer list. A mismatch between the two orders can surprise you, and compilers warn about it.

  • 03When is a destructor called?

    When an object's lifetime ends: at the end of its scope for automatic objects, at delete for heap objects, and during stack unwinding when an exception leaves a scope. Members are destroyed after the destructor body runs.