Lesson 3: The Lifecycle of an Object (Constructors & Destructors)

Lesson Overview

This lesson explores how objects are initialized, copied, assigned, and safely deallocated from memory in C++. You will learn how to automate setup and teardown tasks, manage dynamic heap memory, and prevent memory leaks and double-free crashes using the Rule of Three.


πŸš€ 1. The β€œWhy” & Concept Breakdown

Every object in C++ undergoes a strict lifecycle: it is created (born), it performs actions (lives), and it is destroyed (dies).

graph TD
    A["[Object Birth]<br>Implicitly invokes CONSTRUCTOR"] --> B["[Object Life]<br>Member functions execute; state modified"]
    B --> C["[Object Death]<br>Implicitly invokes DESTRUCTOR<br>(Frees heap memory/resources)"]
    
    style A fill:#2d3748,stroke:#48bb78,stroke-width:2px,color:#fff
    style B fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#fff
    style C fill:#2d3748,stroke:#f56565,stroke-width:2px,color:#fff

Key Properties of Constructors

  1. Name Match: Must have the exact same name as the enclosing class.
  2. Automatic Invocation: Called implicitly by the compiler the moment an object is instantiated {allocated in memory at birth}.
  3. No Return Type: Has no return typeβ€”not even void. Specifying any return type turns it into a normal member function.
  4. Visibility: Normally placed in the public section so that external functions (like main()) can create instances.

Key Properties of Destructors

  1. Name Match with Tilde (~): Same name as the class, preceded by a tilde symbol (~).
  2. Automatic Invocation: Called implicitly when an object goes out of scope {reaches the closing curly brace }} or is explicitly deallocated via delete.
  3. No Parameters & No Overloading: A destructor never takes arguments and cannot be overloaded. Consequently, there is only one destructor per class.
  4. No Return Type: Does not return any value.

The Rule of Three (Memory Safety Principle)

The Rule of Three

If a class manages dynamic heap resources via raw pointers, relying on compiler-generated copy actions causes severe crashes (double-free errors {trying to free already-freed RAM} and memory leaks {orphaned heap RAM that was never released}). If you define any one of these three, you must explicitly define all three:

  1. Destructor: To release allocated heap memory (delete[]).
  2. Copy Constructor: To perform a Deep Copy {allocating a brand new independent memory block} when a new object is initialized from an existing one (ClassName b = a;).
  3. Copy Assignment Operator (operator=): To perform a Deep Copy when an already existing object is assigned to another (b = a;).

⚑ Lifecycles & Memory: Python vs. C++

  • Constructors: Overloadable setup functions (def __init__ equivalent). Support Member Initializer Lists (: var(val)).
  • Destructors (~Class): No garbage collector in C++. Stack memory frees automatically at }; heap memory allocated with new must be freed in destructors (delete[]).
  • The Assignment Trap: b = a creates pointer aliases in Python. In C++, default b = a shallow-copies pointers, causing double-free crashes. Deep copying requires custom Copy Constructor + operator=.

πŸ” 2. Keyword & Syntax Dictionary

Keyword / SymbolMeaningExample
ClassName::ClassName()Default Constructor: Accepts no arguments; sets safe default values.Book::Book() : price(0.0) {}
ClassName::ClassName(...)Parameterized Constructor: Accepts arguments to initialize custom state upon creation.Book::Book(double p) : price(p) {}
ClassName::ClassName(const ClassName &obj)Copy Constructor: Creates a new object as a deep copy of an existing object. Parameter must be passed by reference.Book::Book(const Book &b) { price = b.price; }
ClassName& operator=(const ClassName &rhs)Copy Assignment Operator: Handles deep copying when assigning one existing object to another.Book& operator=(const Book& rhs);
ClassName::~ClassName()Destructor: Performs cleanup and frees dynamically allocated memory.Book::~Book() { delete[] data; }
: (Initializer List)Constructor Initializer List: Initializes member variables directly before constructor body executes. High performance.Book(double p) : price(p) {}
new / delete[]Dynamic Operators: new allocates memory on the Heap; delete[] deallocates heap arrays.int *arr = new int[5]; delete[] arr;

πŸ’» 3. Step-by-Step Code Evolution

Step 3.1: Building a Dynamic Array Class with the Rule of Three

Let’s build an exam-grade dynamic array management class (DynamicArray) implementing all components of the Rule of Three.

#include <iostream>
using namespace std;
 
class DynamicArray {
private:
    int* data;
    int size;
 
public:
    // 1. Default Constructor
    DynamicArray() : data(nullptr), size(0) {
        cout << "[Default Constructor] Empty array created." << endl;
    }
 
    // 2. Parameterized Constructor
    DynamicArray(int s, int defaultVal = 0) {
        size = s;
        data = new int[size]; // Allocating on the heap
        for (int i = 0; i < size; i++) {
            data[i] = defaultVal;
        }
        cout << "[Parameterized Constructor] Created array of size " << size << "." << endl;
    }
 
    // 3. Copy Constructor (Deep Copy for Initialization)
    DynamicArray(const DynamicArray& source) {
        size = source.size;
        if (source.data != nullptr) {
            data = new int[size]; // Allocate distinct memory block
            for (int i = 0; i < size; i++) {
                data[i] = source.data[i]; // Copy contents independently
            }
        } else {
            data = nullptr;
        }
        cout << "[Copy Constructor] Deep Copied array of size " << size << "." << endl;
    }
 
    // 4. Copy Assignment Operator (Deep Copy for Assignment)
    DynamicArray& operator=(const DynamicArray& rhs) {
        cout << "[Copy Assignment] Copying state independently." << endl;
        if (this != &rhs) { // Guard against self-assignment (a = a)
            delete[] data;  // 1. Free existing heap memory (Prevents Memory Leak!)
            size = rhs.size;
            if (rhs.data != nullptr) {
                data = new int[size]; // 2. Allocate fresh independent memory
                for (int i = 0; i < size; i++) {
                    data[i] = rhs.data[i]; // 3. Copy values
                }
            } else {
                data = nullptr;
            }
        }
        return *this; // Return reference to allow chaining (a = b = c)
    }
 
    // 5. Destructor (Prevents Memory Leaks)
    ~DynamicArray() {
        delete[] data;
        cout << "[Destructor] Deallocated array memory." << endl;
    }
 
    void setValue(int index, int val) {
        if (index >= 0 && index < size) data[index] = val;
    }
 
    void display() const {
        if (size == 0) { cout << "Empty." << endl; return; }
        for (int i = 0; i < size; i++) cout << data[i] << " ";
        cout << endl;
    }
};

πŸ“Š 4. Memory Visualizations & Execution Order Trace

Trace: Scoped Object Lifecycle (MyString Example)

Understanding constructor and destructor execution order is a staple of lab quizzes:

#include <iostream>
#include <cstring>
using namespace std;
 
class MyString {
private:
    char* name;
public:
    MyString(const char* s) {
        name = new char[strlen(s) + 1];
        strcpy(name, s);
        cout << "[+] Constructor: " << name << endl;
    }
    ~MyString() {
        cout << "[-] Destructor: " << name << endl;
        delete[] name;
    }
};
 
int main() {
    cout << "--- Outer Block Start ---" << endl;
    MyString s1("Global_Alice");
    {
        cout << "\n   --> Entering Inner Block" << endl;
        MyString s2("Local_Bob");
        cout << "   <-- Exiting Inner Block" << endl;
    } // s2 destroyed here!
    
    MyString s3("Global_Charlie");
    cout << "\n--- Outer Block End ---" << endl;
    return 0;
} // s3 destroyed, then s1 destroyed (Reverse order of creation!)

Output Trace:

--- Outer Block Start ---
[+] Constructor: Global_Alice
 
   --> Entering Inner Block
[+] Constructor: Local_Bob
   <-- Exiting Inner Block
[-] Destructor: Local_Bob
 
[+] Constructor: Global_Charlie
 
--- Outer Block End ---
[-] Destructor: Global_Charlie
[-] Destructor: Global_Alice

Destruction Order Principle

Local stack objects are destroyed in strictly reverse order of their creation (LIFO: Last-In, First-Out).


Shallow Copy (Bug) vs. Deep Copy (Fix)

SHALLOW COPY (Compiler Default - Double-Free Bug!):
STACK MEMORY                             HEAP MEMORY
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ obj1.data ───┼────────────────────────>β”‚ [ 10 | 20 | 30 ]      β”‚<───┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                      β”‚ Direct pointer copy!
β”‚ obj2.data β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DEEP COPY (Custom Copy Constructor - Isolated & Safe):
STACK MEMORY                             HEAP MEMORY
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ obj1.data ───┼────────────────────────>β”‚ [ 10 | 20 | 30 ]      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ obj2.data ───┼────────────────────────>β”‚ [ 10 | 20 | 30 ]      β”‚ <-- Independent Heap Block!
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚠️ 5. The Examiner’s Trap (Debugging & Quizzes)

Trap 1: Passing by Value to Copy Constructor

Point(Point p) { ... } // ❌ FATAL ERROR
  • The Cause: Passing by value requires making a copy of the argument, which calls the copy constructor… recursively triggering infinite calls until stack overflow.
  • The Fix: Always pass by reference: Point(const Point& p).

Trap 2: Destructor with Parameters

~Box(int x) { } // ❌ ERROR: Destructors cannot have parameters
  • The Cause: Destructors are called automatically by runtime; arguments cannot be supplied.

Trap 3: Constructor/Destructor with Return Types

void Account() { balance = 0.0; } // ❌ ERROR
  • The Cause: Specifying any return type (even void) turns it into a standard function that is never automatically called on instantiation.

πŸ“ 6. Practice Quiz

Q1. What is the return type of a destructor?

  • A) void
  • B) int
  • C) Same as class name
  • D) No return type

Q2. When an object is initialized from another object of the same class at declaration (A b = a;), which function is called?

  • A) Default Constructor
  • B) Parameterized Constructor
  • C) Copy Constructor
  • D) Copy Assignment Operator

Q3. If a class contains a dynamic heap pointer and you omit a custom copy constructor, what happens upon copying?

  • A) Compiler generates a deep copy automatically.
  • B) Compiler generates a shallow copy, risking double-free crashes.
  • C) Compilation fails.
  • D) The pointer is set to null.

Q4. Can a destructor be overloaded in C++?

  • A) Yes, by varying parameter counts.
  • B) No, a class can have only one destructor.
  • C) Yes, by changing access specifiers.
  • D) Only if marked virtual.

Quiz Answers

  1. D β€” Destructors have no return type.
  2. C β€” Initialization at declaration invokes the Copy Constructor (operator= is invoked only when assigning to already existing objects).
  3. B β€” Default compiler action is bit-by-bit shallow copy.
  4. B β€” Destructors cannot accept arguments, making overloading impossible.

πŸ’¬ 7. Viva Quick-Prep

Q1: What is the Rule of Three in C++ memory management?

Answer: It states that if a class requires a custom Destructor, Copy Constructor, or Copy Assignment Operator, it almost certainly requires all three to safely manage dynamic heap resources and avoid memory corruption.

Q2: Why must the copy assignment operator check for self-assignment (if (this != &rhs))?

Answer: In self-assignment (a = a), without this check, the function will deallocate a’s heap memory (delete[] data;) before reading from it, leading to reading corrupted/deleted memory.

Q3: What is the difference between delete and delete[]?

Answer: delete deallocates a single dynamically allocated object, whereas delete[] deallocates an entire dynamically allocated array, ensuring all individual element destructors are properly invoked.