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
118: The Rule of Three, Five, and Zero
I want to show you a piece of code that looks perfectly fine at a glance, but it's a ticking time bomb. This is a classic scenario where a developer manages a raw resource and forgets that C++ provides default behaviors that are often too simplistic for the job.
#include <iostream>
#include <cstring>
class SimpleBuffer {
char* data;
public:
SimpleBuffer(const char* text) {
data = new char[std::strlen(text) + 1];
std::strcpy(data, text);
}
~SimpleBuffer() {
delete[] data;
}
void print() { std::cout < data < "\n"; }
};
void printDouble(SimpleBuffer buf) {
buf.print();
}
int main() {
SimpleBuffer myBuf("Hello, C++!");
printDouble(myBuf); // Pass by value
return 0;
}
If you run this, it might work once, or it might crash immediately with a double free detected error. Why? Look at printDouble. I'm passing myBuf by value. Because I didn't define a copy constructor, C++ uses the default one, which does a "shallow copy." It just copies the data pointer address. Now, myBuf and the local buf inside the function both point to the exact same memory on the heap.
The Shallow Copy Trap
Here is where the disaster happens: when printDouble finishes, the local buf object is destroyed, and its destructor calls delete[] data. The memory is gone. But myBuf in main still thinks it owns that memory. When main ends, it tries to call delete[] data on that same address again. That's a double-free, and it's a fast track to a segmentation fault or a security vulnerability.
Implementing the Rule of Three
Since we have a custom destructor, we are now bound by the "Rule of Three." This rule says that if you need a custom destructor, you almost certainly need a custom copy constructor and a custom copy assignment operator. We need to ensure that when an object is copied, it gets its own unique piece of memory—a "deep copy."
class SimpleBuffer {
char* data;
public:
SimpleBuffer(const char* text) {
data = new char[std::strlen(text) + 1];
std::strcpy(data, text);
}
// 1. Destructor
~SimpleBuffer() {
delete[] data;
}
// 2. Copy Constructor
SimpleBuffer(const SimpleBuffer& other) {
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
// 3. Copy Assignment Operator
SimpleBuffer& operator=(const SimpleBuffer& other) {
if (this == &other) return *this; // Guard against self-assignment
delete[] data; // Clean up existing resource
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
return *this;
}
void print() { std::cout < data < "\n"; }
};
Now, when printDouble is called, a brand new buffer is allocated for the copy. When the copy is destroyed, the original remains untouched. It's safer, but it's also slower because we're constantly allocating and copying memory.
Boosting Performance with the Rule of Five
In modern C++ (C++11 and later), we have "move semantics." If we're copying a temporary object that's about to be destroyed anyway, why bother allocating new memory? We can just "steal" the pointer from the temporary object. This expands the Rule of Three into the Rule of Five by adding the Move Constructor and the Move Assignment Operator.
// 4. Move Constructor SimpleBuffer(SimpleBuffer&& other) noexcept { data = other.data; // Steal the pointer other.data = nullptr; // Leave the other object in a valid, empty state } // 5. Move Assignment Operator SimpleBuffer& operator=(SimpleBuffer&& other) noexcept { if (this == &other) return *this; delete[] data; data = other.data; other.data = nullptr; return *this; }I'll be honest: writing all five of these is tedious and error-prone. You have to handle self-assignment, null pointers, and exception safety. This is why experienced engineers try to avoid this entirely.
The Rule of Zero
The Rule of Zero is the gold standard of modern C++. It states that you should design your classes so that you don't have to write any of the special member functions. How? By using components that already manage their own memory.
If we replace our raw
char*with astd::stringor astd::vector<char>, the compiler-generated copy and move operations will just call the string's own copy and move operations. Everything "just works" automatically.#include <string> class SimpleBuffer { std::string data; // The string manages its own memory! public: SimpleBuffer(const std::string& text) : data(text) {} // No destructor, copy constructor, or move operators needed. // The Rule of Zero in action. void print() { std::cout < data < "\n"; } };This is the most robust way to write C++. You eliminate the possibility of memory leaks and double-frees because you've delegated the resource management to the Standard Library, which has already been battle-tested by millions of developers.
📋 Practical Task
Fixing the Leaky StringManager
You've been handed a StringManager class that manages a dynamic character array. Currently, it only has a constructor and a destructor. It is suffering from memory leaks and crashes when objects are assigned to one another.
Your Task: Implement the Rule of Three for the StringManager class. You must provide a copy constructor and a copy assignment operator to ensure that the class performs deep copies of its internal buffer. Ensure you handle self-assignment in the assignment operator to prevent deleting the buffer before you try to copy it.
#include <iostream>
#include <cstring>
class StringManager {
private:
char* buffer;
public:
StringManager(const char* str) {
buffer = new char[std::strlen(str) + 1];
std::strcpy(buffer, str);
}
~StringManager() {
delete[] buffer;
}
// TODO: Implement Copy Constructor
// TODO: Implement Copy Assignment Operator
void display() const {
if (buffer) std::cout < buffer < "\n";
}
};
int main() {
StringManager s1("Original");
StringManager s2 = s1; // Should trigger copy constructor
StringManager s3("Temporary");
s3 = s1; // Should trigger copy assignment
s1.display();
s2.display();
s3.display();
return 0;
}
There are no comments for now.