Constructors and Destructors in C++: Object Lifetime with Runnable Examples

Trace constructor selection, copying, scope exit, reverse destruction, and RAII through two complete C++17 programs with exact output and memory diagrams.

KnowledgeGate Team

Exam prep & CS education

Updated 29 Aug 20266 min read

A learner can often write a class yet still be unsure which constructor runs, when a copy is made, or why a destructor appears to run "by itself". Tracing object values from creation to scope exit resolves that confusion. A Student trace exposes copy construction and reverse scope cleanup; a ScoreBuffer trace shows RAII releasing heap memory.

What Constructors and Destructors Actually Do

A constructor is the special member function that establishes a usable state for a new object. It has the class name and no return type. A destructor is the special member function named ~ClassName() that runs when the object's lifetime ends and performs cleanup.

Keep storage, initialisation, and lifetime separate. Storage may exist before construction completes, but the object becomes usable only after its members are initialised. For Student{17, 86}, construction produces an object whose roll_ is 17 and score_ is 86. Destruction ends that object's lifetime before its storage is reclaimed.

Property

Constructor

Destructor

Name

Same as the class

~ClassName()

Parameters

May take parameters

Takes no parameters

Overloading

Allowed

Not allowed

Return type

None

None

Order for local objects

Creation order

Reverse creation order

To learn this inside the wider language sequence, use the C++ Programming Course - Concepts, MCQs & Coding.

Constructor Types and Why Initialiser Lists Matter

The three beginner-level constructor forms are Student(), Student(int roll, int score), and Student(const Student& other). They are the default, parameterised, and copy constructors. Move construction is another form for later study, especially when an object owns a transferable resource.

Prefer direct member initialisation:

Student(int roll, int score) : roll_(roll), score_(score) {}

This initialises the members instead of default-initialising them and assigning values later. Members are always initialised in their declaration order, not the visual order in the initialiser list. A const member or reference member must be initialised this way.

Overload selection follows the expression used. Student a; selects the default constructor. Student b(17, 86); selects the two-argument constructor. Student c = b; creates c with the copy constructor, while c = b; after both objects already exist would call copy assignment.

Worked Example: Trace Three Students from Construction to Scope Exit

Before reading the output, predict the first destructor line. Will destroy: 17 91 or destroy: 17 86 appear first, and which closing brace decides it?

#include <iostream>

class Student {
    int roll_;
    int score_;

public:
    Student() : roll_(0), score_(0) {
        std::cout << "default: " << roll_ << ' ' << score_ << '\n';
    }

    Student(int roll, int score) : roll_(roll), score_(score) {
        std::cout << "parameterised: " << roll_ << ' ' << score_ << '\n';
    }

    Student(const Student& other)
        : roll_(other.roll_), score_(other.score_) {
        std::cout << "copy: " << roll_ << ' ' << score_ << '\n';
    }

    void setScore(int score) { score_ = score; }

    ~Student() {
        std::cout << "destroy: " << roll_ << ' ' << score_ << '\n';
    }
};

int main() {
    Student a;
    Student b(17, 86);
    {
        Student c = b;
        c.setScore(91);
    }
}

The exact output is:

default: 0 0
parameterised: 17 86
copy: 17 86
destroy: 17 91
destroy: 17 86
destroy: 0 0

Line 1 constructs a with values 0 and 0. Line 2 constructs b with 17 and 86. Line 3 copy-constructs c, so it starts with its own copy of those values. c.setScore(91) changes only c, not b.

The inner closing brace ends c's lifetime, producing line 4 with 17 and 91. At the end of main, reverse construction order produces line 5 for b and line 6 for a. Therefore destroy: 17 91 comes first.

Object-lifetime timeline of the Student run, tracing a, b, and copied c from construction to reverse-order destruction.

Destructors, Ownership, and RAII with a Three-Element Buffer

RAII ties a resource to an object's lifetime. Construction acquires the resource, and destruction releases it even when a scope exits early. Production C++ should normally prefer std::vector or smart pointers. A raw array exposes the constructor's allocation and the destructor's deallocation directly.

#include <cstddef>
#include <iostream>

class ScoreBuffer {
    std::size_t size_;
    int* data_;

public:
    explicit ScoreBuffer(std::size_t n)
        : size_(n), data_(new int[n]) {
        for (std::size_t i = 0; i < size_; ++i) {
            data_[i] = static_cast<int>((i + 1) * 10);
        }
        std::cout << "allocate " << size_ << " scores\n";
    }

    ScoreBuffer(const ScoreBuffer&) = delete;
    ScoreBuffer& operator=(const ScoreBuffer&) = delete;

    int at(std::size_t index) const { return data_[index]; }

    ~ScoreBuffer() {
        std::cout << "release " << size_ << " scores\n";
        delete[] data_;
    }
};

int main() {
    ScoreBuffer scores(3);
    std::cout << "middle value: " << scores.at(1) << '\n';
}

For indices 0, 1, and 2, (i + 1) * 10 gives 10, 20, and 30. The observable sequence is:

allocate 3 scores
middle value: 20
release 3 scores

Construction stores size_ = 3, allocates three integers, and fills them. at(1) reads the second cell, which is 20. When scores leaves main, its destructor prints the release line and calls delete[] data_. Automatic cleanup also happens during a normal early return or stack unwinding after an exception.

Memory diagram of ScoreBuffer scores(3): a stack object points to a heap array of 10, 20, 30 that delete[] frees at scope exit.

When Destruction Happens and in What Order

An automatic local object is destroyed at scope exit. A dynamically allocated object is destroyed when the matching delete runs. Member objects are destroyed when their containing object is destroyed. If a program leaks a new allocation and loses its pointer, it cannot reach that object's destructor through the lost pointer.

For an order exercise, construct Logger first("A") and then Logger second("B") in one scope. Leaving that scope prints destroy B and then destroy A. If a class declares Engine engine_; before Battery battery_;, its destructor body runs first, then the members are destroyed as battery_ followed by engine_.

A base-class destructor should be virtual when an object may be deleted through a base pointer. With Base* p = new Derived; delete p;, behaviour is undefined if Base lacks a virtual destructor, so Derived cleanup is not guaranteed.

Common Constructor and Destructor Errors

Mistake

Consequence

Fix

Giving a constructor a return type

It becomes an ordinary function or fails to declare the intended constructor

Use the class name with no return type

Leaving primitive members uninitialised

The object starts with indeterminate values

Initialise every member, preferably in the initialiser list

Treating copy construction as copy assignment

You reason about the wrong special member function

Check whether the destination object already exists

Calling obj.~Student() explicitly

Normal scope exit may destroy the same object again

Let automatic lifetime rules call the destructor

Allocating in a constructor without safe ownership

Copies can share one raw pointer and double-delete it

Use a standard container, smart pointer, or correctly designed copy policy

The raw-pointer trap is exact: if two objects shallow-copy data_ = 0xA0, both destructors try delete[] 0xA0. ScoreBuffer prevents that by deleting copy construction and copy assignment. If custom copying is truly required, study the Rule of Three/Five and implement ownership deliberately.

Changing the order of initialiser-list entries does not change declaration-order initialisation. Virtual calls during construction or destruction also do not dispatch as though the most-derived object were fully alive.

How Tests and Interviews Probe Object Lifetime

Three common question shapes are selecting which overloaded constructor runs, tracing exact output across nested scopes, and spotting a shallow-copy or non-virtual-destructor bug. Reuse the worked values: Student b(17, 86) selects the parameterised constructor, Student c = b copies 17 and 86, and changing c to 91 does not alter b.

Self-check:

  1. Can a destructor be overloaded? No.

  2. Does Student c = b; call assignment? No, it copy-constructs.

  3. Which dies first, b or inner-scope c? c.

  4. What does at(1) return for [10, 20, 30]? 20.

  5. Why delete copying in ScoreBuffer? To prevent two owners from deleting the same allocation.

KnowledgeGate currently offers 40+ constructor questions in its practice bank. For broader coding practice aimed at placement problems, continue with Coding for Placements - C, C++, Java, Python. For the next concept in reasoning about program cost, read Time Complexity: Big-O, Theta, Omega & Master Theorem.

Short Version and the Next Step

Constructors establish valid state. Initialiser lists initialise members directly. Destructors release owned resources. Scope controls cleanup in reverse construction order.

If you want a structured path through programming fundamentals, browse Coding & Skill Development Courses. Once object lifetime and basic classes feel comfortable, the next worked tutorial is Sorting Algorithms: Complexity, Stability, n log n Bound.