C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
26: Copy Constructors and the Copy Assignment Operator
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
ImageBuffercreates 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)andint 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.
There are no comments for now.