C++ for C Developers | Lesson 6: Constructors & Destructors: Object Lifecycle Management

Goal: Learn about constructors (special functions for initializing objects) and destructors (special functions for cleaning up when objects are destroyed). Understand their role in ensuring objects are created in a valid state and resources are released correctly (linking back to RAII).

1. The Problem: Initialization in C vs. C++

In C, after declaring a struct variable, you often initialize its members manually or rely on aggregate initialization {}. There’s no built-in mechanism to guarantee initialization or perform setup actions automatically when a variable is created. Similarly, cleanup requires explicit function calls.

C
// C Example (from Lesson 5)
PointC p1; // Members p1.x, p1.y have indeterminate values here!
p1.x = 3.0; // Manual initialization
p1.y = 4.0;
// ... use p1 ...
// No automatic cleanup

C++ aims to ensure objects are always created in a well-defined, usable state and are properly cleaned up afterward. This is achieved through constructors and destructors.

2. Constructors: Initializing Objects

A constructor is a special member function that is automatically called whenever an object of a class is created. Its primary job is to initialize the object’s data members and perform any necessary setup.

flowchart TD
    subgraph C [C Style]
        C1["Declare struct (e.g., PointC p1)"]
        C2["Members uninitialized"]
        C3["Manual init: p1.x = 3.0;"]
        C4["Manual cleanup if needed"]
    end

    subgraph CPP [C++ Style]
        CPP1["Declare object (e.g., Message msg)"]
        CPP2["Constructor called automatically"]
        CPP3["Members initialized"]
        CPP4["Destructor called automatically"]
    end

    C1 --> C2 --> C3 --> C4
    CPP1 --> CPP2 --> CPP3 --> CPP4

Rules for Constructors:

  • They have the exact same name as the class.
  • They have no return type (not even void).
  • They can be overloaded (like regular functions – Lesson 3), allowing for different ways to create objects.
flowchart TD
    subgraph Constructor Flow
        Start["Object Created"]
        Start --> InitCheck["Is constructor defined?"]
        InitCheck -->|Yes| CallCtor["Call Constructor"]
        InitCheck -->|No| DefaultInit["Compiler Default Initialization"]
        CallCtor --> Setup["Member init / setup logic"]
        Setup --> Done["Object Ready"]
        DefaultInit --> Done
    end

Types of Constructors:

  • Default Constructor: A constructor that takes no arguments. If you don’t define any constructors yourself, the compiler will try to generate a public default constructor for you (it performs default initialization for members, which might be zero-initialization for built-in types or calling default constructors for member objects). Important: If you define any constructor (e.g., a parameterized one), the compiler will not generate the default constructor automatically. You’ll need to define it yourself if you want one.
  • Parameterized Constructor: A constructor that accepts arguments to initialize data members.

Example:

C++
#include <iostream>
#include <string>

class Message {
private:
    std::string m_text;
    int m_priority;
    bool m_sent;

public:
    // 1. Default Constructor (User-Defined)
    // If we didn't define ANY constructors, the compiler would make one.
    // But since we define others below, we MUST define this if we want
    // to be able to create a Message object like: Message msg1;
    Message() {
        std::cout << "Default Constructor called!" << std::endl;
        m_text = "[No Text]";
        m_priority = 0; // Default priority
        m_sent = false;
    }

    // 2. Parameterized Constructor
    Message(const std::string& text, int priority) {
        std::cout << "Parameterized Constructor called!" << std::endl;
        m_text = text; // Assigning value inside constructor body
        m_priority = priority;
        m_sent = false; // Still initialize all members
    }

    // Member function to display message info
    void display() const {
        std::cout << "Message: '" << m_text << "', Priority: " << m_priority
                  << ", Sent: " << (m_sent ? "Yes" : "No") << std::endl;
    }

    void send() {
        m_sent = true;
        std::cout << "Sending message: '" << m_text << "'" << std::endl;
        // ... actual sending logic ...
    }
}; // End of class Message

int main() {
    std::cout << "Creating msg1 (uses Default Constructor):" << std::endl;
    Message msg1; // Calls Message()
    msg1.display();

    std::cout << "\nCreating msg2 (uses Parameterized Constructor):" << std::endl;
    Message msg2("Meeting reminder", 5); // Calls Message(const std::string&, int)
    msg2.display();
    msg2.send();
    msg2.display();

    // Message msg3(); // Pitfall: This DECLARES A FUNCTION named msg3, it doesn't create an object!
    // To default construct, use: Message msg3;

    return 0; // msg1 and msg2 go out of scope here - destructors will be called
}

flowchart LR
    A["Constructor"]
    A --> B1["Default Constructor"]
    A --> B2["Parameterized Constructor"]
    A --> B3["Copy Constructor (Lesson 7)"]
    B1 --> C1["No arguments"]
    B2 --> C2["Takes arguments"]
    C1 --> D1["Created automatically if no other constructors"]
    C2 --> D2["Custom initialization"]

3. Constructor Initialization Lists

While assigning values inside the constructor body works (like m_text = text;), the preferred C++ way to initialize members is using member initialization lists.

Syntax: Add a colon : after the constructor’s parameter list, followed by a comma-separated list of member_name(initial_value).

flowchart TB
    A["Initialization Style"] --> B1["Member Init List"]
    A --> B2["Assignment in Constructor Body"]

    B1 --> C1["Direct Initialization"]
    C1 --> D1["Preferred for const & refs"]
    C1 --> D2["Efficient"]

    B2 --> C2["Default Init First"]
    C2 --> D3["Then Assignment"]
    D3 --> D4["Less efficient"]

Why use initialization lists?

  • True Initialization: It initializes members directly; assignment in the body first default-initializes then assigns. For complex objects, initialization is often more efficient.
  • Required for const and Reference Members: const members and reference members cannot be assigned to; they must be initialized in the initialization list.
  • Required for Base Class Constructors: When using inheritance (Lesson 8), you use the initialization list to call the base class constructor.
  • Initializes Members in Declaration Order: Members are initialized in the order they are declared in the class, regardless of their order in the initialization list (good practice to match declaration order in the list for clarity).

Example with Initialization List:

C++
#include <iostream>
#include <string>

class Rectangle {
private:
    const double m_length; // const member - MUST use init list
    const double m_width;  // const member - MUST use init list
    std::string m_label;

public:
    // Parameterized Constructor using Initialization List
    Rectangle(double length, double width, const std::string& label)
        : m_length(length), // Initialize m_length with length
          m_width(width),   // Initialize m_width with width
          m_label(label)    // Initialize m_label with label
    { // Constructor body can be empty or contain other setup code
        std::cout << "Rectangle Constructor called for '" << m_label << "'" << std::endl;
        // No assignments needed here, members are already initialized!
        // Could add validation logic here if needed.
        if (m_length <= 0 || m_width <= 0) {
            std::cerr << "Warning: Rectangle '" << m_label << "' has non-positive dimensions!" << std::endl;
         }
    }

    // Default Constructor
    // Rectangle() : m_length(1.0), m_width(1.0), m_label("Default") {} // Possible default

    double area() const {
        return m_length * m_width;
    }

    void display() const {
         std::cout << "Rectangle '" << m_label << "': "
                   << m_length << " x " << m_width << ", Area: " << area() << std::endl;
    }
};

int main() {
    Rectangle r1(10.0, 5.0, "Box");
    r1.display();

    Rectangle r2(2.5, 4.0, "Panel");
    r2.display();

    return 0; // r1 and r2 go out of scope here
}

4. Destructors: Cleaning Up Objects

A destructor is a special member function that is automatically called just before an object is destroyed. This happens when:

  • A stack-allocated object goes out of scope.
  • A heap-allocated object is explicitly deleted.
flowchart TD
    subgraph Object Destruction
        Create["Object Created"]
        Create --> Use["Used in Scope"]
        Use --> EndScope["Scope Ends"]
        EndScope --> CallDtor["Destructor Called Automatically"]
        CallDtor --> Cleanup["Resources Released"]
    end

Its primary job is to release any resources the object acquired during its lifetime (e.g., dynamic memory, file handles, network connections). This is the heart of RAII (Lesson 4).

Rules for Destructors:

  • They have the exact same name as the class, preceded by a tilde (~).
  • They have no return type (not even void).
  • They take no parameters.
  • A class can only have one destructor.
  • If you don’t provide one, the compiler generates a default one (which does nothing unless members have their own destructors).

Example Demonstrating RAII with Constructor/Destructor:

C++
#include <iostream>

class DynamicArray {
private:
    int* m_data;
    size_t m_size;

public:
    // Constructor: Acquires resource (dynamic memory)
    DynamicArray(size_t size) : m_size(size) {
        m_data = new int[m_size]; // Allocate memory
        std::cout << "DynamicArray Constructor: Allocated memory for "
                  << m_size << " integers at " << m_data << std::endl;
        // Initialize array (optional)
        for (size_t i = 0; i < m_size; ++i) {
            m_data[i] = 0;
        }
    }

    // Destructor: Releases resource (dynamic memory)
    ~DynamicArray() {
        std::cout << "DynamicArray Destructor: Deallocating memory for "
                  << m_size << " integers at " << m_data << std::endl;
        delete[] m_data; // CRITICAL: Use delete[] because we used new[]
        m_data = nullptr; // Good practice
        m_size = 0;
    }

    // Prevent copying (rule of three/five/zero - simplified for now)
    DynamicArray(const DynamicArray&) = delete;
    DynamicArray& operator=(const DynamicArray&) = delete;

    // Example method
    void print() const {
        std::cout << "Array contents (" << m_size << " elements): ";
        for (size_t i = 0; i < m_size; ++i) {
             std::cout << m_data[i] << " ";
        }
        std::cout << std::endl;
    }
};

void testScope() {
    std::cout << "--- Entering testScope ---" << std::endl;
    DynamicArray arr(5); // Constructor called
    arr.print();
    // arr manages its own memory via RAII
    std::cout << "--- Exiting testScope ---" << std::endl;
    // arr goes out of scope HERE - Destructor is automatically called!
}

int main() {
    std::cout << "*** Before testScope ***" << std::endl;
    testScope();
    std::cout << "*** After testScope ***" << std::endl;

    std::cout << "\n*** Using new/delete ***" << std::endl;
    DynamicArray* heap_arr = new DynamicArray(3); // Constructor called
    heap_arr->print();
    // Manual deletion required for heap objects
    delete heap_arr; // Destructor called HERE
    heap_arr = nullptr;
    std::cout << "*** After delete ***" << std::endl;

    // Note: This manual new/delete is exactly what smart pointers (Lesson 4) help avoid!

    return 0;
}

Summary of Lesson 6:

  • Constructors initialize objects automatically when they are created. They share the class name, have no return type, and can be overloaded. Use them to ensure objects start in a valid state.
  • Initialization Lists (: member(value)) are the preferred way to initialize members in a constructor, especially for const members, reference members, and efficiency.
  • Destructors clean up objects automatically just before they are destroyed (~ClassName). They have no return type and take no parameters.
  • Destructors are essential for RAII, ensuring resources acquired by an object (like memory allocated with new) are released correctly (e.g., by calling delete or delete[]).
  • Constructors/Destructors manage the object lifecycle, making C++ code safer and more robust compared to manual C-style initialization and cleanup.

Next Lesson: We’ll explore one of the most powerful features of C++: the Standard Template Library (STL), starting with std::vector, a dynamic array that manages its own memory, making code like our DynamicArray example much simpler.

More Sources:

cppreference – Constructors: https://en.cppreference.com/w/cpp/language/constructor

cppreference – Destructors: https://en.cppreference.com/w/cpp/language/destructor

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top