Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

Constructors and Destructors in C++

Recommended resources

Introduction

Hello! In our last lesson, we saw that creating an object and then calling multiple setter methods to initialize it feels a bit clunky. We also saw that this approach can lead to objects existing temporarily in an invalid state, for example, if we forget to call a setter.

This lesson introduces a more robust and elegant solution: constructors. Constructors are special member functions that are automatically called the moment an object is created. Their primary job is to ensure that the object is initialized into a valid and usable state from the very beginning.

We will cover the three main types of constructors you'll encounter:

  • Default Constructor: Creates an object with a default state.
  • Parameterized Constructor: Creates an object with a specific state provided as arguments.
  • Copy Constructor: Creates a new object as a copy of an existing one.

Furthermore, we'll explore what happens when an object's life ends. For that, we have destructors, special functions that handle cleanup. This is especially critical for classes that manage resources like dynamic memory, which is a central theme in C++ and highly relevant to your goal of understanding how systems work "under the hood".


1. Why Constructors? The Need for Initialization

Before we write a constructor, let's see why they are necessary. When you create an object of a class, the memory for its member variables is allocated, but C++ makes no guarantee about what's in that memory for primitive types like int or float. It's often just leftover "garbage" data.

This video provides an excellent demonstration of this problem and introduces constructors as the solution.

Constructors in C++

Watch this segment from The Cherno's "Constructors in C++" video. It clearly shows the problem of uninitialized member variables and how a simple default constructor solves it.

Watch from the beginning to 03:33. Pay attention to: The random values printed from the uninitialized Entity object. The temporary, but clumsy, solution of an init() method. How a default constructor provides a clean, automatic way to initialize the object's state. The important note that, unlike Java, C++ does not automatically initialize primitive types to zero.


2. Parameterized Constructors and Initializer Lists

A default constructor is great for creating an object in a known default state. But what if we want to create an object with specific initial values, like we tried to do with setters? For that, we use a parameterized constructor. As the name suggests, it accepts parameters to initialize the object's members.

A class can have multiple constructors (this is a form of function overloading), allowing for flexible object creation.

class Song {
private:
    std::string m_title;
    int m_year;

public:
    // 1. Default constructor
    Song() {
        m_title = "Untitled";
        m_year = 0;
        std::cout << "Default constructor called\n";
    }

    // 2. Parameterized constructor
    Song(std::string title, int year) {
        m_title = title;
        m_year = year;
        std::cout << "Parameterized constructor called\n";
    }
};

int main() {
    Song song1; // Calls default constructor
    Song song2("Bohemian Rhapsody", 1975); // Calls parameterized constructor
    return 0;
}

A Better Way: Member Initializer Lists

The implementation above works, but there's a more idiomatic and efficient way to initialize members in C++: member initializer lists. Notice the syntax difference:

// Assignment inside constructor body
Song(std::string title, int year) {
    m_title = title; // Assignment
    m_year = year;   // Assignment
}

// Initialization using a member initializer list
Song(std::string title, int year) : m_title(title), m_year(year) {
    // Body can be empty or used for other logic
}

Why is the second way better?

  • Efficiency: It performs direct initialization. The first method default-constructs the members first and then assigns new values to them, which can be less efficient, especially for complex objects.
  • Necessity: Some members, like const variables or references, must be initialized in the initializer list because they cannot be assigned to later.

Given your interest in how things work at a lower level, understanding the distinction between initialization and assignment is key.

Understanding C++ Constructors, Destructors and Some key concepts

This article provides a superb explanation of why member initializer lists are important, especially when dealing with const members.

Read the section titled "4. Constructor Initializer Lists". Focus on: Why assignment fails for const members. The step-by-step flow of object creation. How the initializer list fits into this flow to perform initialization before the constructor body runs.


3. Resource Management: Destructors and the Copy Problem

Now let's move to a more complex scenario. Imagine a class that manages a resource, like a dynamically allocated block of memory. This is common in C++ for things like custom strings, image buffers, or any data whose size isn't known at compile time.

The Destructor (~)

If a constructor allocates a resource (e.g., using new), something must be responsible for releasing it to prevent memory leaks. This is the job of the destructor. It's a special member function that is automatically called when an object is destroyed (e.g., when it goes out of scope or is explicitly deleted).

A destructor has the same name as the class, prefixed with a tilde (~), and takes no arguments.

class MyResourceHolder {
private:
    int* m_data;
public:
    MyResourceHolder() {
        m_data = new int[100]; // Allocate resource
        std::cout << "Resource allocated.\n";
    }

    ~MyResourceHolder() {
        delete[] m_data; // Release resource
        std::cout << "Resource released.\n";
    }
};

This pattern is fundamental to C++ and is called RAII (Resource Acquisition Is Initialization).

The Copying Problem: Shallow vs. Deep Copy

Now, what happens if we copy an object of our MyResourceHolder class?

MyResourceHolder obj1;
MyResourceHolder obj2 = obj1; // What happens here?

By default, C++ performs a shallow copy, meaning it copies the values of the member variables directly. In our case, it copies the m_data pointer. Now both obj1 and obj2 point to the same block of memory. When obj1 is destroyed, its destructor frees the memory. When obj2 is destroyed later, its destructor tries to free the same memory again, leading to a crash (a "double free" error).

The solution is to perform a deep copy: when an object is copied, we must allocate new memory for the copy and then copy the contents of the original memory block. To do this, we must define our own copy constructor.

This video is one of the best explanations of this entire process. It builds a String class from scratch, demonstrating the destructor, the shallow copy problem, and the deep copy solution using a copy constructor.

Copying and Copy Constructors in C++

This video, "Copying and Copy Constructors in C++" by The Cherno, is a masterclass in C++ resource management. It will walk you through building a class that manages dynamic memory and show you precisely why a custom copy constructor is essential.

This is the core of the lesson. Please watch carefully from 03:51 to 16:13. (03:51 - 09:04) Watch how the String class is built. Note the use of new char[...] in the constructor and delete[] in the destructor. (09:04 - 13:06) This is the critical part. See how the default copy leads to a crash and how modifying the copy also modifies the original. This is the shallow copy problem in action. (13:06 - 16:13) Finally, observe how a custom copy constructor is implemented to perform a deep copy, fixing all the issues.

The canonical form of a copy constructor is ClassName(const ClassName& other). The const is there so you can copy from constant objects, and the reference (&) is there to prevent an infinite loop of copy constructor calls.


4. Hands-On Exercise: The MusicNote Class

Let's put all this together. Given your graduation project on a music notation app, let's create a simple MusicNote class that manages its pitch name using dynamic memory.

Instructions:

  1. Create a class named MusicNote.
  2. Give it a single private member: char* m_pitch;.
  3. Default Constructor: MusicNote(). Use a member initializer list to set m_pitch to nullptr.
  4. Parameterized Constructor: MusicNote(const char* pitch). This constructor should:
    • Calculate the length of the input pitch string (use strlen from <cstring>).
    • Allocate a new char array of the correct size (length + 1 for the null terminator).
    • Copy the input pitch string into your new m_pitch buffer (use strcpy from <cstring>).
  5. Destructor: ~MusicNote(). It should print a message (e.g., "Destroying note...") and then check if m_pitch is not nullptr before calling delete[] m_pitch;. This prevents trying to delete a null pointer.
  6. Copy Constructor: MusicNote(const MusicNote& other). This should perform a deep copy:
    • Check if other.m_pitch is nullptr. If so, set this->m_pitch to nullptr and return.
    • Otherwise, allocate a new buffer for this->m_pitch and copy the contents of other.m_pitch into it.
  7. Add a public method void display() const that prints the pitch.

Use the following main function to test your implementation. If it runs without crashing and produces the correct output, you've succeeded!




#include <iostream>
#include <cstring> // For strlen and strcpy

// --- Your MusicNote class definition and implementation goes here ---

class MusicNote {
private:
    char* m_pitch;

public:
    // Default Constructor
    MusicNote() : m_pitch(nullptr) {
        std::cout << "Default constructor: Note created (empty).\n";
    }

    // Parameterized Constructor
    MusicNote(const char* pitch) {
        std::cout << "Parameterized constructor: Creating note " << pitch << ".\n";
        if (pitch) {
            size_t len = strlen(pitch);
            m_pitch = new char[len + 1];
            strcpy(m_pitch, pitch);
        } else {
            m_pitch = nullptr;
        }
    }

    // Copy Constructor (Deep Copy)
    MusicNote(const MusicNote& other) {
        std::cout << "Copy constructor: Copying note " << (other.m_pitch ? other.m_pitch : "empty") << ".\n";
        if (other.m_pitch) {
            size_t len = strlen(other.m_pitch);
            m_pitch = new char[len + 1];
            strcpy(m_pitch, other.m_pitch);
        } else {
            m_pitch = nullptr;
        }
    }

    // Destructor
    ~MusicNote() {
        std::cout << "Destructor: Destroying note " << (m_pitch ? m_pitch : "empty") << ".\n";
        delete[] m_pitch;
    }

    void display() const {
        if (m_pitch) {
            std::cout << "Note Pitch: " << m_pitch << std::endl;
        } else {
            std::cout << "Note Pitch: (empty)" << std::endl;
        }
    }
};


int main() {
    std::cout << "--- Creating original note ---\n";
    MusicNote note1("C#4");
    note1.display();

    std::cout << "\n--- Creating a copy of the note ---\n";
    MusicNote note2 = note1; // Calls the copy constructor
    note2.display();

    std::cout << "\n--- Program scope ending, objects will be destroyed ---\n";
    // Watch the destructor messages. They should be called for both note2 and note1.
    // If your program crashes here, you likely have a shallow copy issue!
    return 0;
}

Conclusion

You've just tackled some of the most important concepts in C++! Mastering constructors and destructors is the key to writing safe and robust object-oriented code, especially when dealing with system resources.

Key Takeaways:

  • Constructors initialize objects. They ensure an object starts its life in a valid state.
    • Default: No arguments.
    • Parameterized: Takes arguments for custom initialization.
    • Copy: Takes another object of the same type to create a copy.
  • Member Initializer Lists are the preferred C++ way to initialize member variables for both efficiency and necessity.
  • Destructors clean up resources when an object is destroyed. This is the foundation of the RAII principle.
  • The default compiler-generated copy constructor performs a shallow copy, which is dangerous for classes managing resources.
  • You must implement a custom copy constructor to perform a deep copy for classes that manage dynamic memory or other resources.

Next Up

In this lesson, you may have noticed the use of this->member in some examples, or m_ prefixes to distinguish member variables. Inside any member function, C++ provides a special, hidden pointer called this, which points to the specific object instance the function was called on.

In our next lesson, we will explore the this pointer in detail. We will also learn how to overload operators (like + or <<) for our custom classes and dive deeper into the const keyword to make our classes even more robust.