Skip to Content
Course content

26: Copy Constructors and the Copy Assignment Operator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, we've mostly dealt with types that C++ knows how to copy automatically. But the moment you start managing your own resources—like raw memory, file handles, or network sockets—the default copy behavior becomes a liability. If you aren't careful, you'll end up with two objects thinking they own the same piece of memory, and your program will crash the second one of them tries to delete it.

Managing a raw buffer

I want to build a simple DynamicBuffer class. Its only job is to hold a sequence of characters on the heap. It's a basic wrapper, but it's the perfect way to illustrate why the "Rule of Three" exists. Here is the starting point:

class DynamicBuffer {
    char* data;
    size_t size;

public:
    DynamicBuffer(const char* input) {
        size = strlen(input);
        data = new char[size + 1];
        strcpy(data, input);
    }

    ~DynamicBuffer() {
        delete[] data;
    }

    void print() { std::cout << data << std::endl; }
};

On the surface, this looks fine. I've got a constructor to allocate memory and a destructor to clean it up. But we have a massive problem waiting to happen.

The "Double-Free" disaster

If I write DynamicBuffer buf1("Hello"); and then DynamicBuffer buf2 = buf1;, C++ uses the default copy constructor. The default behavior is a "shallow copy"—it just copies the pointer address. Now, buf1.data and buf2.data point to the exact same memory location.

When the function ends, the destructors run. buf2 deletes the memory. Then buf1 tries to delete the same memory. This is a double-free error, and it's one of the most common ways to crash a C++ application. To fix this, we need a copy constructor that performs a "deep copy."

// Adding this to our class
DynamicBuffer(const DynamicBuffer& other) {
    size = other.size;
    data = new char[size + 1]; // Allocate our own separate memory
    strcpy(data, other.data);  // Copy the actual content
}

Now, when we copy a buffer, we aren't just copying a pointer; we're creating a completely independent duplicate of the data.

Fixing the memory leak in the assignment operator

Now we have to deal with the Copy Assignment Operator. This is different from the copy constructor because the object already exists and likely already owns some memory. I'll try to implement it quickly:

DynamicBuffer& operator=(const DynamicBuffer& other) {
    size = other.size;
    data = new char[size + 1]; 
    strcpy(data, other.data);
    return *this;
}

Wait. I just made a classic mistake. I allocated new memory for data, but I never deleted the memory that data was already pointing to. I've just created a memory leak. Every time I assign one buffer to another, the old buffer's memory is lost forever in the heap.

I also need to consider "self-assignment." If I accidentally write buf1 = buf1;, I might delete my own data before I try to copy it. Here is the corrected version:

DynamicBuffer& operator=(const DynamicBuffer& other) {
    // 1. Guard against self-assignment
    if (this == &other) {
        return *this;
    }

    // 2. Clean up existing resource
    delete[] data;

    // 3. Perform the deep copy
    size = other.size;
    data = new char[size + 1];
    strcpy(data, other.data);

    return *this;
}

By checking this == &other, I ensure that I don't destroy the object I'm trying to copy from. Then I wipe the slate clean with delete[] before allocating the new memory. This completes the triad: the destructor, the copy constructor, and the copy assignment operator. If you need one, you almost always need all three.




📋 Practical Task

Implement a Deep-Copying ImageBuffer Class

You are tasked with creating a class called ImageBuffer that manages a raw array of integers (representing pixel data). The class should avoid memory leaks and crashes when objects are copied or assigned.

Requirements:

  • Create a constructor ImageBuffer(int size) that allocates an array of integers of that size.
  • Implement a destructor to free the memory.
  • Implement a Copy Constructor to ensure that copying an ImageBuffer creates a new array with the same values (deep copy).
  • Implement a Copy Assignment Operator that handles self-assignment and prevents memory leaks by deleting the old array before allocating the new one.
  • Include a method void setPixel(int index, int value) and int getPixel(int index) to verify that changing one buffer does not affect its copy.

Test Case: Create buf1, set a pixel, copy it to buf2, change the pixel in buf1, and verify that buf2 still holds the original value.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.