C++: Copy Control
Last updated: 2026-08-26
In Lesson 29, we learned advanced constructors.
But if you want to copy objects, assign objects, or move objects, C++ has a set of copy control rules.
Understanding these rules is essential for writing correct classes.
1. Rule of Three/Five
(1) 1.1 What Is the Rule of Three/Five?
If your class needs a custom implementation of any of the following functions, then you must custom-implement all related functions:
| Version | Functions to Manage | Description |
|---|---|---|
| Rule of Three (C++98) | 1. Destructorbr2. Copy constructorbr3. Copy assignment operator |
Needed when there's dynamic memory |
| Rule of Five (C++11) | Rule of Three +br4. Move constructorbr5. Move assignment operator |
C++11 introduced move semantics |
(2) 1.2 Why the Rule of Three/Five?
If you need to customize one of them, it means the class has resources requiring special handling (like dynamic memory, file handles).
Example: Consequences of Not Following the Rule of Three (Difficulty ⭐⭐)
▶ Example 2: Code Example (Difficulty ⭐)
#include <iostream>
#include <cstring>
class BadString {
private:
char* data;
public:
// Constructor
BadString(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// ❌ No custom copy constructor, copy assignment operator, or destructor
// The compiler will generate defaults (shallow copy)
};
int main() {
BadString s1("Hello");
BadString s2 = s1; // ❌ Shallow copy: s1.data and s2.data point to the same memory
// When main ends, s2 is destructed first (freeing data)
// Then s1 is destructed (freeing data again, but it's already been freed) → Crash!
return 0;
}
Output:
Hello
Hello
Consequence: Program crash (double free of the same memory).
Fix: Follow the Rule of Three — custom-implement all three functions.
2. Copy Assignment Operator*
(1) 2.1 Basic Usage
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// Constructor
MyString(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// Copy constructor
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// Copy assignment operator
MyString& operator=(const MyString& other) {
if (this != &other) { // Prevent self-assignment
delete data; // Free old memory
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
// Destructor
~MyString() {
delete data;
}
};
int main() {
MyString s1("Hello");
MyString s2("World");
s2 = s1; // ✅ Calls the copy assignment operator
return 0;
}
💡 Key Point: The copy assignment operator must check for self-assignment (if (this != &other)).
3. Move Semantics (C++11)*
(1) 3.1 Why Do We Need Move?
If you have a temporary object (about to be destroyed), copying its content is wasteful — it's better to just "steal" its resources.
Example: Using Move to Avoid Unnecessary Copies (Difficulty ⭐⭐)
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// Move constructor (C++11)
MyString(MyString&& other) noexcept {
data = other.data; // Steal the resource
other.data = nullptr; // Set other to null
}
// Move assignment operator (C++11)
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete data; // Free old resource
data = other.data; // Steal the resource
other.data = nullptr; // Set other to null
}
return *this;
}
};
int main() {
MyString s1("Hello");
MyString s2 = std::move(s1); // ✅ Calls the move constructor
// Now s1.data is nullptr, s2.data points to the original memory
return 0;
}
💡 Tip: std::move() converts an lvalue to an rvalue reference, triggering the move constructor or move assignment operator.
4. = default and = delete*
(1) 4.1 Using = default to Let the Compiler Generate Default Implementations*
If your class doesn't need special copy logic, you can use = default to let the compiler generate the defaults.
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// Let the compiler generate default copy constructor, copy assignment operator, and destructor
Student(const Student& other) = default;
Student& operator=(const Student& other) = default;
~Student() = default;
};
int main() {
Student s1;
Student s2 = s1; // ✅ Calls the default copy constructor
return 0;
}
💡 Tip: If a class only has STL types (std::string, std::vector, etc.), using = default is sufficient.
(2) 4.2 Using = delete to Prohibit Copying*
If you don't want a class to be copied (e.g., singleton pattern), you can use = delete to prohibit it.
#include <iostream>
class Singleton {
private:
Singleton() {} // Private constructor
public:
// Prohibit copying
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
int main() {
Singleton& s = Singleton::getInstance();
// Singleton s2 = s; // ❌ Compile error: copy constructor is deleted
return 0;
}
5. Practice: Complete MyString Class*
▶ Example 1: Implementing a Complete MyString Class (Difficulty ⭐⭐⭐)
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
int length;
public:
// Constructor
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
}
// Copy constructor
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// Move constructor
MyString(MyString&& other) noexcept {
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
// Copy assignment operator
MyString& operator=(const MyString& other) {
if (this != &other) {
delete data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// Move assignment operator
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// Destructor
~MyString() {
delete data;
}
// Other member functions
void print() const {
if (data) std::cout << data;
}
int size() const {
return length;
}
};
int main() {
MyString s1("Hello");
MyString s2 = s1; // Copy constructor
s1.print(); // Hello
std::cout << std::endl;
s2.print(); // Hello
std::cout << std::endl;
return 0;
}
Output:
(program output)
▶ Example 3: Deep Copy Example (Difficulty ⭐)
#include <iostream>
class Buffer {
int* data;
int size;
public:
Buffer(int s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; }
// Deep copy constructor
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
for (int i = 0; i < size; i++) data[i] = other.data[i];
}
int& operator[](int i) { return data[i]; }
};
int main() {
Buffer b1(3);
b1[0] = 1; b1[1] = 2; b1[2] = 3;
Buffer b2 = b1; // Deep copy
b2[0] = 100;
std::cout << "b1[0]=" << b1[0] << ", b2[0]=" << b2[0] << std::endl;
return 0;
}
Output:
b1[0]=1, b2[0]=100
Q: When should I define a custom copy constructor? A: When the class has pointer members (pointing to dynamic memory).
If the class only has basic types (
int,double) or STL types (std::string,std::vector), no custom copy constructor is needed — the compiler-generated one works correctly (STL types implement deep copy themselves).
Q: What do =default and =delete do? A: > -
= default: lets the compiler generate the default implementation (member-by-member copy) > -= delete: prohibits a function (e.g., prohibiting copying)Using
= defaultis recommended over hand-writing copy logic (if no special logic is needed).
Q: What happens if I don't write a copy constructor? A: The compiler automatically generates a default copy constructor (member-by-member copy).
But if the class has pointer members, the default will perform a shallow copy (only copies the pointer value, not the content it points to), causing double free of memory.
❓ FAQ
📖 Summary
- Copy constructor: initializes a new object from an object of the same type
- Copy assignment operator: overloading the = operator
- Rule of Three/Five: if you customize one, you typically need all
- Shallow copy vs deep copy: pointer members must use deep copy
- Delete functions prevent copying: = delete
📝 Exercises
-
Basic (Difficulty ⭐): Define a
Pointclass with two members: x and y. -
Follow the Rule of Three — define a custom copy constructor, copy assignment operator, and destructor
-
Test copying in
main -
Intermediate (Difficulty ⭐⭐): Define a
DynamicArrayclass (dynamic array) with: -
int* data(pointing to an array on the heap) -
int size(array size)
Follow the Rule of Five — implement all special member functions.
- Challenge (Difficulty ⭐⭐⭐):
Extend the
DynamicArrayclass by adding: void push_back(int x): add an element to the end of the array (if space is insufficient, double the capacity)void pop_back(): remove the last element
6. 🚀 Next Steps*
Now that you've learned copy control and move semantics, let's move on to Inheritance (Lesson 31) — creating "parent-child" relationships between classes for code reuse!