9 Copy Constructors and the Rule of Three
10 Copy Constructors and the Rule of Three
Week 4, Session 1. CSS 342.
10.1 Warmup
Reverse String (#344). Reverse a vector<char> in place with two pointers.
class Solution {
public:
void reverseString(vector<char>& s) {
for (int lo = 0, hi = s.size() - 1; lo < hi; ++lo, --hi) {
swap(s[lo], s[hi]);
}
}
};Two pointers moving inward — the “squeeze” pattern. You will see it again in palindrome checks, container-with-most-water problems, and string compression.
10.2 Learning objectives
- Explain what a shallow copy is and why it causes problems for classes that own heap memory.
- Implement a deep-copy copy constructor.
- Implement
operator=with a self-assignment guard that returns*this. - State the Rule of Three and identify when each of the three special functions must be user-defined.
- Apply
constcorrectly to copy constructor and assignment operator signatures.
10.3 The disaster: a shallow copy
Consider a class that owns a heap array:
class IntList {
public:
IntList(int size) : size_(size) {
data_ = new int[size];
for (int i = 0; i < size; ++i) data_[i] = 0;
}
~IntList() {
delete[] data_;
}
void set(int i, int v) { data_[i] = v; }
int get(int i) const { return data_[i]; }
private:
int* data_;
int size_;
};What does this do?
int main() {
IntList a(3);
a.set(0, 10);
a.set(1, 20);
a.set(2, 30);
IntList b = a; // <-- COPY
b.set(0, 999);
cout << a.get(0) << endl; // 999. WAIT, WHAT.
} // CRASH on second destructorWe never told the compiler how to copy IntList. So it synthesized one — the default copy constructor — which just copies each field. data_ is a pointer, so the pointer is copied, not the array it points to. Now a.data_ and b.data_ are the same address. Modifying b modifies a. And when both go out of scope, both destructors run delete[] on the same memory. Double-free. Crash.
This is the most important code in chapters 4 through 8. Let it sink in.
10.4 The fix: deep copy
A copy constructor takes a const reference to the same type:
class IntList {
public:
IntList(const IntList& other) : size_(other.size_) {
data_ = new int[size_];
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
}
// ... rest of class
};Now IntList b = a; does a real copy: new heap allocation, element-by-element copy of values. Modifying b no longer affects a. Both destructors free their own memory.
The pattern is: allocate fresh, then copy the contents. Never share the pointer.
10.5 Copy assignment — the = for already-existing objects
IntList a(3);
IntList b(5);
b = a; // <-- not a constructor — both already existThe copy constructor runs when a new object is being created from an existing one. The copy assignment operator runs when an existing object is being assigned to.
IntList& IntList::operator=(const IntList& other) {
if (this == &other) return *this; // self-assignment guard
delete[] data_; // free old memory
size_ = other.size_;
data_ = new int[size_];
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
return *this;
}Four pieces to memorize:
const IntList&— take the other by const reference.if (this == &other) return *this;— self-assignment guard.a = a;is legal and must not crash.delete[] data_;thennew— free old, allocate fresh, copy contents.return *this;— by reference, soa = b = c;works.
Skipping the self-assignment guard is the classic bug. If you forget it and someone writes a = a;, you delete[] data_; then copy from the same data_ you just deleted. Undefined behavior.
10.6 The Rule of Three
If your class needs any of (destructor, copy constructor, copy assignment), it almost certainly needs all three.
The reasoning is symmetric:
- You wrote a destructor because your class owns a resource.
- That resource needs deep copying — otherwise the synthesized copy duplicates the pointer.
- And assignment too, for the same reason.
In modern C++ (post-C++11) this is sometimes the Rule of Five (add move constructor, move assignment). We are sticking with the Rule of Three in 342 — moves are a 343 / advanced topic.
10.7 A complete, correct class
// IntList.h
#pragma once
class IntList {
public:
IntList(int size = 0); // default + parameterized
IntList(const IntList& other); // copy constructor
IntList& operator=(const IntList& other); // copy assignment
~IntList(); // destructor
int size() const { return size_; }
int get(int i) const;
void set(int i, int v);
private:
int* data_;
int size_;
};// IntList.cpp
#include "IntList.h"
#include <stdexcept>
IntList::IntList(int size) : data_(nullptr), size_(size) {
if (size > 0) data_ = new int[size](); // () zero-initializes
}
IntList::IntList(const IntList& other) : size_(other.size_) {
data_ = (size_ > 0) ? new int[size_] : nullptr;
for (int i = 0; i < size_; ++i) data_[i] = other.data_[i];
}
IntList& IntList::operator=(const IntList& other) {
if (this == &other) return *this;
delete[] data_;
size_ = other.size_;
data_ = (size_ > 0) ? new int[size_] : nullptr;
for (int i = 0; i < size_; ++i) data_[i] = other.data_[i];
return *this;
}
IntList::~IntList() {
delete[] data_;
}
int IntList::get(int i) const { return data_[i]; }
void IntList::set(int i, int v) { data_[i] = v; }Test it:
int main() {
IntList a(3);
a.set(0, 10); a.set(1, 20); a.set(2, 30);
IntList b = a; // copy ctor
IntList c(5);
c = a; // copy assignment
c = c; // self-assignment (should not crash)
b.set(0, 999);
cout << a.get(0) << endl; // 10 (a is unchanged!)
}Compile with -fsanitize=address. Zero leaks. Zero double-frees. This is the goal state.
10.8 Copy-and-swap idiom (mentioned, not required)
There is an elegant alternative for operator=:
IntList& IntList::operator=(IntList other) { // pass BY VALUE
std::swap(data_, other.data_);
std::swap(size_, other.size_);
return *this;
}By taking other by value, the copy constructor runs automatically. Then we swap our fields with the new copy’s fields. When other goes out of scope, its destructor frees the old data. Self-assignment is handled automatically. It is beautiful. It is also more advanced than 342 expects — I am mentioning it here so you recognize it if you see it elsewhere.
10.9 Try it
Take the IntList class above. Comment out the copy constructor. Run the test program with -fsanitize=address. What happens? Restore the copy constructor and run again. Now you’ve felt the bug viscerally — that is the only way it sticks.
10.10 Self-check
operator= include if (this == &other) return *this;?delete[] data_ kills the very memory you are about to copy from.
10.11 Challenges
- Implement
IntListexactly as shown. Compile with-fsanitize=address. Verify a test program with copies, self-assignments, and chained assignments runs clean. - Implement a
Stackclass wrapping a heap array. Apply the Rule of Three. - Take an existing class that already uses
newand apply the Rule of Three to it. Confirm with sanitizer.
10.12 Where this fits
Three weeks of language mechanics culminating in this chapter. Everything from here is applying this machinery — to templates, to linked lists, to trees.
| You are here | Coming up |
|---|---|
| Deep copy, self-assignment guard, Rule of Three | Chapter 8: templates — make your class generic |