A copy constructor in C++ is a special constructor that creates a new object as a copy of an existing object. The critical distinction between shallow copy (copying pointer values) and deep copy (duplicating dynamically allocated memory) determines whether copied objects share resources or have independent copies, affecting memory management, data integrity, and preventing common bugs like double deletion and dangling pointers.
Introduction: The Hidden Danger in Object Copying
When you pass objects by value, return objects from functions, or explicitly copy objects in C++, the copy constructor silently performs crucial work behind the scenes. Understanding copy constructors—and particularly the difference between shallow and deep copying—is essential for writing correct, bug-free C++ code. A single mistake in copy constructor implementation can lead to crashes, memory leaks, and mysterious bugs that only appear under specific circumstances.
Consider this scenario: you create a String class that manages dynamic memory. You copy one String object to another. Later, when one object is destroyed, the other suddenly contains garbage data and your program crashes. This isn’t a random bug—it’s the predictable result of shallow copying when deep copying was needed.
The distinction between shallow and deep copying isn’t just academic. It’s the difference between code that works and code that fails unpredictably. When objects contain pointers to dynamically allocated memory, shallow copying creates multiple objects pointing to the same memory. When one object is destroyed and frees that memory, the others are left with dangling pointers pointing to deallocated memory. Deep copying solves this by creating independent copies of the dynamically allocated memory.
This comprehensive guide will teach you everything about copy constructors: what they are, when they’re called, how C++ generates them automatically, the critical difference between shallow and deep copies, how to implement proper copy constructors, and when to disable copying altogether. You’ll learn through practical examples that demonstrate both correct and incorrect implementations, understanding not just what to do but why it matters.
What is a Copy Constructor?
A copy constructor is a special constructor that initializes a new object as a copy of an existing object. It takes a reference to an object of the same class as its parameter.
#include <iostream>
using namespace std;
class Simple {
private:
int value;
public:
// Regular constructor
Simple(int v = 0) : value(v) {
cout << "Regular constructor called, value = " << value << endl;
}
// Copy constructor
Simple(const Simple& other) : value(other.value) {
cout << "Copy constructor called, copying value = " << other.value << endl;
}
void display() const {
cout << "Value: " << value << endl;
}
void setValue(int v) {
value = v;
}
};
int main() {
cout << "=== Creating original object ===" << endl;
Simple obj1(42);
cout << "\n=== Copy construction (initialization) ===" << endl;
Simple obj2 = obj1; // Copy constructor called
cout << "\n=== Another copy construction syntax ===" << endl;
Simple obj3(obj1); // Also calls copy constructor
cout << "\n=== Displaying objects ===" << endl;
obj1.display();
obj2.display();
obj3.display();
cout << "\n=== Modifying obj1 ===" << endl;
obj1.setValue(100);
cout << "\n=== After modification ===" << endl;
obj1.display();
obj2.display(); // Not affected by change to obj1
obj3.display(); // Not affected by change to obj1
return 0;
}Step-by-step explanation:
- Simple class: Contains a single integer member variable
- Regular constructor: Creates object with specified value
- Copy constructor signature: Takes
const Simple¶meter (reference to const object) - const parameter: Prevents modification of the source object being copied
- Reference parameter: Must be reference (not value) to avoid infinite recursion
- Member initialization: Copies value from other.value to this object’s value
- obj2 = obj1: Copy initialization syntax that invokes copy constructor
- obj3(obj1): Direct initialization syntax also invoking copy constructor
- Independent objects: obj1, obj2, and obj3 are separate objects in memory
- Separate storage: Modifying obj1.value doesn’t affect obj2 or obj3
- Why const: Protects source object from accidental modification during copying
- Why reference: Passing by value would require copying, which needs copy constructor—infinite loop!
Output:
=== Creating original object ===
Regular constructor called, value = 42
=== Copy construction (initialization) ===
Copy constructor called, copying value = 42
=== Another copy construction syntax ===
Copy constructor called, copying value = 42
=== Displaying objects ===
Value: 42
Value: 42
Value: 42
=== Modifying obj1 ===
=== After modification ===
Value: 100
Value: 42
Value: 42When is the Copy Constructor Called?
The copy constructor is invoked automatically in several situations:
#include <iostream>
using namespace std;
class Tracker {
private:
int id;
static int nextId;
public:
Tracker() : id(nextId++) {
cout << "Default constructor: Object " << id << " created" << endl;
}
Tracker(const Tracker& other) : id(nextId++) {
cout << "Copy constructor: Object " << id
<< " copied from Object " << other.id << endl;
}
~Tracker() {
cout << "Destructor: Object " << id << " destroyed" << endl;
}
int getId() const { return id; }
};
int Tracker::nextId = 1;
// Function that takes parameter by value
void functionByValue(Tracker obj) {
cout << "Inside functionByValue, object id = " << obj.getId() << endl;
}
// Function that returns by value
Tracker functionReturnByValue() {
Tracker temp;
cout << "About to return from functionReturnByValue" << endl;
return temp; // Copy constructor called (may be optimized away)
}
int main() {
cout << "=== Scenario 1: Direct initialization ===" << endl;
Tracker obj1;
Tracker obj2 = obj1; // Copy constructor
cout << "\n=== Scenario 2: Passing by value ===" << endl;
functionByValue(obj1); // Copy constructor called
cout << "\n=== Scenario 3: Returning by value ===" << endl;
Tracker obj3 = functionReturnByValue(); // Copy constructor may be called
cout << "\n=== Scenario 4: Object assignment (NOT copy constructor) ===" << endl;
Tracker obj4;
obj4 = obj1; // This is assignment, NOT copy construction
cout << "\n=== End of main ===" << endl;
return 0;
}Step-by-step explanation:
- Tracker class: Assigns unique ID to each object to track copying
- Static nextId: Shared counter for generating unique IDs
- Default constructor: Creates new object with next available ID
- Copy constructor: Creates new object copying from existing, gets new ID
- Destructor: Prints message when object is destroyed
- Scenario 1: Direct initialization with
=calls copy constructor - Scenario 2: Passing by value creates temporary copy of argument
- functionByValue parameter: Receives copy of obj1, so copy constructor called
- Scenario 3: Returning by value may create temporary (compilers often optimize this)
- RVO/NRVO: Modern compilers use Return Value Optimization to avoid copying
- Scenario 4: Assignment operator called, NOT copy constructor (obj4 already exists)
- Copy vs assignment: Copy constructor initializes new object; assignment modifies existing object
- Temporary objects: Copy constructor creates temporaries that are destroyed when no longer needed
- Optimization: Compilers may eliminate unnecessary copies (copy elision)
Output (may vary with compiler optimizations):
=== Scenario 1: Direct initialization ===
Default constructor: Object 1 created
Copy constructor: Object 2 copied from Object 1
=== Scenario 2: Passing by value ===
Copy constructor: Object 3 copied from Object 1
Inside functionByValue, object id = 3
Destructor: Object 3 destroyed
=== Scenario 3: Returning by value ===
Default constructor: Object 4 created
About to return from functionReturnByValue
Copy constructor: Object 5 copied from Object 4
Destructor: Object 4 destroyed
=== Scenario 4: Object assignment (NOT copy constructor) ===
Default constructor: Object 6 created
=== End of main ===
Destructor: Object 6 destroyed
Destructor: Object 5 destroyed
Destructor: Object 2 destroyed
Destructor: Object 1 destroyedDefault Copy Constructor: Shallow Copy
When you don’t provide a copy constructor, C++ generates one automatically. This default copy constructor performs a shallow copy—it copies each member variable as-is.
#include <iostream>
using namespace std;
class ShallowExample {
private:
int* data;
int size;
public:
ShallowExample(int s) : size(s) {
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = i + 1;
}
cout << "Constructor: Allocated array at " << data << endl;
}
// No copy constructor defined - compiler generates default (shallow copy)
~Destructor() {
cout << "Destructor: Deleting array at " << data << endl;
delete[] data;
}
void display() const {
cout << "Data at " << data << ": ";
for (int i = 0; i < size; i++) {
cout << data[i] << " ";
}
cout << endl;
}
void modifyData(int index, int value) {
if (index >= 0 && index < size) {
data[index] = value;
}
}
};
int main() {
cout << "=== Creating original object ===" << endl;
ShallowExample obj1(5);
obj1.display();
cout << "\n=== Creating copy (shallow copy) ===" << endl;
ShallowExample obj2 = obj1; // Default copy constructor (shallow)
obj2.display();
cout << "\n=== Modifying obj2 ===" << endl;
obj2.modifyData(0, 999);
cout << "\n=== After modification ===" << endl;
cout << "obj1: ";
obj1.display(); // PROBLEM: obj1 is also modified!
cout << "obj2: ";
obj2.display();
cout << "\n=== Destruction begins (CRASH LIKELY) ===" << endl;
// When obj2 is destroyed, it deletes the array
// When obj1 is destroyed, it tries to delete the same array - CRASH!
return 0;
}Step-by-step explanation:
- ShallowExample class: Contains pointer to dynamically allocated array
- Constructor allocates memory: Uses
new int[size]to create array - Stores pointer: data pointer holds address of allocated memory
- No copy constructor: Class relies on compiler-generated default
- Default is shallow: Compiler copies data pointer value, not array contents
- obj2 = obj1: Both obj1.data and obj2.data point to SAME array
- Shared memory: Both objects share the dynamically allocated array
- Modification problem: Changing obj2’s array also changes obj1’s array
- Both pointers equal: obj1.data == obj2.data (pointing to same memory)
- Destructor problem: Both destructors will try to delete same memory
- Double deletion: First destructor succeeds, second tries to delete freed memory
- Undefined behavior: Program will likely crash or exhibit strange behavior
- Memory corruption: Attempting to use already-freed memory causes corruption
This code demonstrates the problem with shallow copy – DO NOT USE IN PRODUCTION!
Deep Copy: The Correct Solution
A deep copy creates an independent copy of dynamically allocated memory, so each object owns its own resources.
#include <iostream>
#include <cstring>
using namespace std;
class DeepCopyString {
private:
char* data;
int length;
public:
// Constructor
DeepCopyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
cout << "Constructor: Created string \"" << data
<< "\" at " << static_cast<void*>(data) << endl;
}
// Copy constructor - DEEP COPY
DeepCopyString(const DeepCopyString& other) {
length = other.length;
// Allocate NEW memory for this object
data = new char[length + 1];
// Copy the contents, not just the pointer
strcpy(data, other.data);
cout << "Copy constructor: Deep copied \"" << data
<< "\" from " << static_cast<void*>(other.data)
<< " to " << static_cast<void*>(data) << endl;
}
// Destructor
~DeepCopyString() {
cout << "Destructor: Deleting \"" << data
<< "\" at " << static_cast<void*>(data) << endl;
delete[] data;
}
void display() const {
cout << "String at " << static_cast<void*>(data)
<< ": \"" << data << "\"" << endl;
}
void append(const char* str) {
int newLength = length + strlen(str);
char* newData = new char[newLength + 1];
strcpy(newData, data);
strcat(newData, str);
delete[] data;
data = newData;
length = newLength;
}
};
int main() {
cout << "=== Creating original string ===" << endl;
DeepCopyString str1("Hello");
str1.display();
cout << "\n=== Creating copy (deep copy) ===" << endl;
DeepCopyString str2 = str1;
str2.display();
cout << "\n=== Modifying str2 ===" << endl;
str2.append(" World");
cout << "\n=== After modification ===" << endl;
cout << "str1: ";
str1.display(); // Unchanged - independent copy
cout << "str2: ";
str2.display(); // Modified
cout << "\n=== Creating another copy ===" << endl;
DeepCopyString str3(str2);
str3.display();
cout << "\n=== Destruction begins (SAFE) ===" << endl;
// Each object has its own memory, destruction is safe
return 0;
}Step-by-step explanation:
- DeepCopyString class: Manages dynamic string memory
- Constructor allocates: Creates new array with
new char[length + 1] - Stores content: Copies string contents into allocated memory
- Copy constructor header: Same signature as any copy constructor
- Allocate new memory:
data = new char[length + 1]creates separate array - Copy contents:
strcpy(data, other.data)copies string content - Different pointers: str1.data and str2.data point to different memory locations
- Independent objects: Each object owns its own memory
- Modification safety: Changing str2 doesn’t affect str1
- Different addresses: Print addresses to verify separate allocations
- Safe destruction: Each destructor deletes its own unique memory
- No double deletion: Each
delete[]operates on different memory - Resource independence: Objects can be copied, modified, and destroyed independently
Output:
=== Creating original string ===
Constructor: Created string "Hello" at 0x...
=== Creating copy (deep copy) ===
Copy constructor: Deep copied "Hello" from 0x... to 0x...
=== Modifying str2 ===
=== After modification ===
str1: String at 0x...: "Hello"
str2: String at 0x...: "Hello World"
=== Creating another copy ===
Copy constructor: Deep copied "Hello World" from 0x... to 0x...
=== Destruction begins (SAFE) ===
Destructor: Deleting "Hello World" at 0x...
Destructor: Deleting "Hello World" at 0x...
Destructor: Deleting "Hello" at 0x...Shallow Copy vs Deep Copy Comparison
Let’s directly compare shallow and deep copy with a comprehensive example:
#include <iostream>
using namespace std;
// Class with SHALLOW copy (default)
class ShallowCopy {
private:
int* data;
public:
ShallowCopy(int value) {
data = new int(value);
cout << "[SHALLOW] Created: value=" << *data
<< " at address " << data << endl;
}
// Using default copy constructor (shallow)
// ShallowCopy(const ShallowCopy& other) = default;
~ShallowCopy() {
cout << "[SHALLOW] Destroying: value=" << *data
<< " at address " << data << endl;
delete data;
}
void setValue(int value) { *data = value; }
int getValue() const { return *data; }
int* getAddress() const { return data; }
};
// Class with DEEP copy (custom implementation)
class DeepCopy {
private:
int* data;
public:
DeepCopy(int value) {
data = new int(value);
cout << "[DEEP] Created: value=" << *data
<< " at address " << data << endl;
}
// Custom copy constructor - DEEP COPY
DeepCopy(const DeepCopy& other) {
data = new int(*other.data); // Allocate new memory
cout << "[DEEP] Copied: value=" << *data
<< " from " << other.data
<< " to new address " << data << endl;
}
~DeepCopy() {
cout << "[DEEP] Destroying: value=" << *data
<< " at address " << data << endl;
delete data;
}
void setValue(int value) { *data = value; }
int getValue() const { return *data; }
int* getAddress() const { return data; }
};
int main() {
cout << "=== SHALLOW COPY EXAMPLE ===" << endl;
{
ShallowCopy shallow1(100);
ShallowCopy shallow2 = shallow1;
cout << "\nAfter copying:" << endl;
cout << "shallow1: value=" << shallow1.getValue()
<< " at " << shallow1.getAddress() << endl;
cout << "shallow2: value=" << shallow2.getValue()
<< " at " << shallow2.getAddress() << endl;
cout << "Same address? " << (shallow1.getAddress() == shallow2.getAddress() ? "YES" : "NO") << endl;
cout << "\nModifying shallow2..." << endl;
shallow2.setValue(200);
cout << "shallow1: value=" << shallow1.getValue() << " (AFFECTED!)" << endl;
cout << "shallow2: value=" << shallow2.getValue() << endl;
cout << "\nLeaving scope (potential crash)..." << endl;
} // Both destructors try to delete same memory - PROBLEM!
cout << "\n=== DEEP COPY EXAMPLE ===" << endl;
{
DeepCopy deep1(100);
DeepCopy deep2 = deep1;
cout << "\nAfter copying:" << endl;
cout << "deep1: value=" << deep1.getValue()
<< " at " << deep1.getAddress() << endl;
cout << "deep2: value=" << deep2.getValue()
<< " at " << deep2.getAddress() << endl;
cout << "Same address? " << (deep1.getAddress() == deep2.getAddress() ? "YES" : "NO") << endl;
cout << "\nModifying deep2..." << endl;
deep2.setValue(200);
cout << "deep1: value=" << deep1.getValue() << " (NOT affected)" << endl;
cout << "deep2: value=" << deep2.getValue() << endl;
cout << "\nLeaving scope (safe)..." << endl;
} // Each destructor deletes different memory - SAFE!
return 0;
}Step-by-step explanation:
- Two similar classes: Both manage dynamic integer, one shallow, one deep
- ShallowCopy uses default: Compiler-generated copy constructor
- Default copies pointer: shallow2.data = shallow1.data (same address)
- DeepCopy custom constructor: Explicitly allocates new memory
- Allocates independent memory: deep2.data points to different location than deep1.data
- Address comparison: Printing addresses shows shallow copies share, deep copies don’t
- Modification test: Changing shallow2 affects shallow1 (shared memory)
- Deep independence: Changing deep2 doesn’t affect deep1 (separate memory)
- Destruction difference: Shallow causes double-delete, deep deletes different memory
- Shallow danger: Both destructors operate on same address—undefined behavior
- Deep safety: Each destructor has its own memory to delete
- Visual evidence: Address output proves shallow shares, deep separates
Output (shallow copy may crash):
=== SHALLOW COPY EXAMPLE ===
[SHALLOW] Created: value=100 at address 0x...
After copying:
shallow1: value=100 at 0x...
shallow2: value=100 at 0x... (SAME ADDRESS!)
Same address? YES
Modifying shallow2...
shallow1: value=200 (AFFECTED!)
shallow2: value=200
Leaving scope (potential crash)...
[SHALLOW] Destroying: value=200 at address 0x...
[SHALLOW] Destroying: value=200 at address 0x... (CRASH - double delete!)
=== DEEP COPY EXAMPLE ===
[DEEP] Created: value=100 at address 0x...
[DEEP] Copied: value=100 from 0x... to new address 0x...
After copying:
deep1: value=100 at 0x...
deep2: value=100 at 0x... (DIFFERENT ADDRESS!)
Same address? NO
Modifying deep2...
deep1: value=100 (NOT affected)
deep2: value=200
Leaving scope (safe)...
[DEEP] Destroying: value=200 at address 0x...
[DEEP] Destroying: value=100 at address 0x...Implementing a Complete Deep Copy Class
Here’s a comprehensive example showing proper implementation with all necessary components:
#include <iostream>
#include <cstring>
using namespace std;
class DynamicArray {
private:
int* elements;
int size;
int capacity;
void resize(int newCapacity) {
int* newElements = new int[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
delete[] elements;
elements = newElements;
capacity = newCapacity;
}
public:
// Constructor
DynamicArray(int initialCapacity = 10)
: size(0), capacity(initialCapacity) {
elements = new int[capacity];
cout << "Constructor: Created array with capacity " << capacity << endl;
}
// Copy constructor - DEEP COPY
DynamicArray(const DynamicArray& other)
: size(other.size), capacity(other.capacity) {
// Allocate new memory
elements = new int[capacity];
// Copy all elements
for (int i = 0; i < size; i++) {
elements[i] = other.elements[i];
}
cout << "Copy constructor: Deep copied " << size
<< " elements" << endl;
}
// Copy assignment operator (also needs deep copy)
DynamicArray& operator=(const DynamicArray& other) {
cout << "Copy assignment operator called" << endl;
// Check for self-assignment
if (this == &other) {
cout << "Self-assignment detected" << endl;
return *this;
}
// Delete old data
delete[] elements;
// Copy size and capacity
size = other.size;
capacity = other.capacity;
// Allocate new memory and copy elements
elements = new int[capacity];
for (int i = 0; i < size; i++) {
elements[i] = other.elements[i];
}
return *this;
}
// Destructor
~DynamicArray() {
cout << "Destructor: Deleting array with " << size << " elements" << endl;
delete[] elements;
}
void add(int value) {
if (size >= capacity) {
resize(capacity * 2);
}
elements[size++] = value;
}
int get(int index) const {
if (index >= 0 && index < size) {
return elements[index];
}
return -1;
}
int getSize() const { return size; }
void display() const {
cout << "Array (" << size << "/" << capacity << "): [";
for (int i = 0; i < size; i++) {
cout << elements[i];
if (i < size - 1) cout << ", ";
}
cout << "]" << endl;
}
};
int main() {
cout << "=== Creating original array ===" << endl;
DynamicArray arr1;
arr1.add(10);
arr1.add(20);
arr1.add(30);
arr1.display();
cout << "\n=== Copy construction ===" << endl;
DynamicArray arr2 = arr1; // Copy constructor
arr2.display();
cout << "\n=== Modifying arr2 ===" << endl;
arr2.add(40);
arr2.add(50);
cout << "\nAfter modification:" << endl;
cout << "arr1: ";
arr1.display(); // Original unchanged
cout << "arr2: ";
arr2.display(); // Modified copy
cout << "\n=== Copy assignment ===" << endl;
DynamicArray arr3;
arr3.add(100);
arr3.display();
arr3 = arr1; // Copy assignment operator
arr3.display();
cout << "\n=== Self-assignment test ===" << endl;
arr3 = arr3; // Should handle gracefully
arr3.display();
cout << "\n=== End of main (destructors will be called) ===" << endl;
return 0;
}Step-by-step explanation:
- DynamicArray class: Manages dynamic integer array with capacity tracking
- Three pointers to track: elements (data), size (used), capacity (allocated)
- Constructor allocates: Creates initial array with specified capacity
- Copy constructor deep copy: Allocates new array, copies all elements
- Element-by-element copy: Loop copies each element to new array
- Copy assignment operator: Handles copying to already-existing object
- Self-assignment check: Prevents deleting memory we’re about to copy from
- Delete old memory: Frees existing array before allocating new
- Allocate and copy: Same deep copy process as copy constructor
- *Return this: Enables chained assignments (a = b = c)
- Destructor cleanup: Deletes dynamically allocated array
- add() method: Demonstrates modifying one copy doesn’t affect others
- Independent arrays: Each object has its own memory
- Safe operations: All copying and destruction operations are safe
Output:
=== Creating original array ===
Constructor: Created array with capacity 10
Array (3/10): [10, 20, 30]
=== Copy construction ===
Copy constructor: Deep copied 3 elements
Array (3/10): [10, 20, 30]
=== Modifying arr2 ===
After modification:
arr1: Array (3/10): [10, 20, 30]
arr2: Array (5/10): [10, 20, 30, 40, 50]
=== Copy assignment ===
Constructor: Created array with capacity 10
Array (1/10): [100]
Copy assignment operator called
Array (3/10): [10, 20, 30]
=== Self-assignment test ===
Copy assignment operator called
Self-assignment detected
Array (3/10): [10, 20, 30]
=== End of main (destructors will be called) ===
Destructor: Deleting array with 3 elements
Destructor: Deleting array with 5 elements
Destructor: Deleting array with 3 elementsWhen to Delete Copy Constructor
Sometimes you want to prevent copying entirely. Use = delete to disable the copy constructor:
#include <iostream>
using namespace std;
class NonCopyable {
private:
int* uniqueResource;
public:
NonCopyable(int value) {
uniqueResource = new int(value);
cout << "NonCopyable created with value " << *uniqueResource << endl;
}
// Delete copy constructor - prevents copying
NonCopyable(const NonCopyable&) = delete;
// Delete copy assignment operator too
NonCopyable& operator=(const NonCopyable&) = delete;
~NonCopyable() {
cout << "NonCopyable destroyed, value was " << *uniqueResource << endl;
delete uniqueResource;
}
int getValue() const { return *uniqueResource; }
};
class FileHandle {
private:
int fileDescriptor;
string filename;
public:
FileHandle(const string& fname, int fd)
: filename(fname), fileDescriptor(fd) {
cout << "FileHandle opened: " << filename << endl;
}
// Copying file handles doesn't make sense - delete copy operations
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
~FileHandle() {
cout << "FileHandle closed: " << filename << endl;
// In real code, close file descriptor here
}
string getFilename() const { return filename; }
};
int main() {
cout << "=== Creating non-copyable object ===" << endl;
NonCopyable obj1(42);
cout << "Value: " << obj1.getValue() << endl;
// NonCopyable obj2 = obj1; // ERROR: copy constructor deleted
// NonCopyable obj3(obj1); // ERROR: copy constructor deleted
cout << "\n=== Creating file handle ===" << endl;
FileHandle file("data.txt", 3);
// FileHandle file2 = file; // ERROR: copy constructor deleted
cout << "\n=== Objects can only be moved or referenced ===" << endl;
NonCopyable* ptr = &obj1; // Pointer is OK
cout << "Value via pointer: " << ptr->getValue() << endl;
FileHandle& ref = file; // Reference is OK
cout << "Filename via reference: " << ref.getFilename() << endl;
cout << "\n=== End of main ===" << endl;
return 0;
}Step-by-step explanation:
- NonCopyable class: Manages unique resource that shouldn’t be copied
- = delete syntax: Explicitly deletes copy constructor
- Compiler error: Attempting to copy generates compile-time error
- Also delete assignment: Prevents copying via assignment operator
- FileHandle example: Real-world case where copying doesn’t make sense
- File descriptors: Can’t have two objects owning same file handle
- Prevention is clear: Deletion makes intent explicit in code
- Pointers allowed: Can still use pointers and references to access object
- No accidental copies: Compiler prevents mistakes
- Better than private: Deleting is clearer than making copy constructor private
- Modern C++ practice: Use
= deleteinstead of older private-without-implementation pattern - Resource management: Essential for classes managing unique system resources
Output:
=== Creating non-copyable object ===
NonCopyable created with value 42
Value: 42
=== Creating file handle ===
FileHandle opened: data.txt
=== Objects can only be moved or referenced ===
Value via pointer: 42
Filename via reference: data.txt
=== End of main ===
FileHandle closed: data.txt
NonCopyable destroyed, value was 42Common Copy Constructor Mistakes
Let’s examine common mistakes and how to avoid them:
#include <iostream>
using namespace std;
// MISTAKE 1: Forgetting const in parameter
class Mistake1 {
private:
int value;
public:
Mistake1(int v) : value(v) {}
// WRONG: Missing const allows modification of source
// Mistake1(Mistake1& other) : value(other.value) {
// other.value = 0; // Accidentally modifying source!
// }
// CORRECT: const prevents modification
Mistake1(const Mistake1& other) : value(other.value) {
// other.value = 0; // ERROR: can't modify const
}
};
// MISTAKE 2: Passing by value instead of reference
class Mistake2 {
private:
int value;
public:
Mistake2(int v) : value(v) {}
// WRONG: This would cause infinite recursion!
// Mistake2(Mistake2 other) : value(other.value) {}
// Passing by value requires copying, which calls this constructor, which...
// CORRECT: Pass by reference
Mistake2(const Mistake2& other) : value(other.value) {}
};
// MISTAKE 3: Shallow copy when deep copy needed
class Mistake3Shallow {
private:
int* data;
public:
Mistake3Shallow(int value) {
data = new int(value);
}
// WRONG: Default shallow copy
// Mistake3Shallow(const Mistake3Shallow& other) : data(other.data) {}
// CORRECT: Deep copy
Mistake3Shallow(const Mistake3Shallow& other) {
data = new int(*other.data);
}
~Mistake3Shallow() {
delete data;
}
};
// MISTAKE 4: Forgetting to copy all members
class Mistake4 {
private:
int* array;
int size;
string name;
public:
Mistake4(int s, string n) : size(s), name(n) {
array = new int[size];
}
// WRONG: Forgot to copy 'name'
// Mistake4(const Mistake4& other) : size(other.size) {
// array = new int[size];
// for (int i = 0; i < size; i++) {
// array[i] = other.array[i];
// }
// // name not initialized!
// }
// CORRECT: Copy all members
Mistake4(const Mistake4& other) : size(other.size), name(other.name) {
array = new int[size];
for (int i = 0; i < size; i++) {
array[i] = other.array[i];
}
}
~Mistake4() {
delete[] array;
}
};
// MISTAKE 5: Not handling self-assignment in copy assignment
class Mistake5 {
private:
int* data;
public:
Mistake5(int value) {
data = new int(value);
}
// WRONG: No self-assignment check
// Mistake5& operator=(const Mistake5& other) {
// delete data; // Delete own data
// data = new int(*other.data); // If other == this, accessing deleted memory!
// return *this;
// }
// CORRECT: Check for self-assignment
Mistake5& operator=(const Mistake5& other) {
if (this != &other) { // Check for self-assignment
delete data;
data = new int(*other.data);
}
return *this;
}
~Mistake5() {
delete data;
}
};
int main() {
cout << "These examples show what NOT to do!" << endl;
cout << "All mistakes are commented out to prevent compilation errors." << endl;
cout << "The correct implementations are used instead." << endl;
Mistake1 m1(10);
Mistake1 m1_copy(m1); // Uses correct version
return 0;
}Common mistakes explained:
- Missing const: Source object could be accidentally modified during copying
- Pass by value: Creates infinite recursion (copy needs copy needs copy…)
- Shallow instead of deep: Multiple objects share same dynamic memory
- Forgetting members: Some data doesn’t get copied, left uninitialized
- No self-assignment check: Deleting own data before copying causes crash
Best Practices Summary
Here are the key guidelines for copy constructors:
#include <iostream>
using namespace std;
class BestPractices {
private:
int* dynamicData;
int value;
string name;
public:
// 1. Always use const reference parameter
BestPractices(const BestPractices& other)
: value(other.value), name(other.name) { // 2. Use initializer list
// 3. Perform deep copy for dynamic memory
if (other.dynamicData != nullptr) {
dynamicData = new int(*other.dynamicData);
} else {
dynamicData = nullptr;
}
// 4. Copy ALL data members
// value and name already copied in initializer list
}
// 5. Implement copy assignment too (Rule of Three)
BestPractices& operator=(const BestPractices& other) {
if (this != &other) { // 6. Check for self-assignment
delete dynamicData; // Clean up old resources
value = other.value;
name = other.name;
if (other.dynamicData != nullptr) {
dynamicData = new int(*other.dynamicData);
} else {
dynamicData = nullptr;
}
}
return *this;
}
// Regular constructor
BestPractices(int v = 0, string n = "")
: value(v), name(n), dynamicData(nullptr) {}
// Destructor
~BestPractices() {
delete dynamicData;
}
};Best practices:
- Use const reference parameter
- Use member initializer list
- Perform deep copy for dynamic resources
- Copy all data members
- Implement both copy constructor and copy assignment (Rule of Three)
- Check for self-assignment in copy assignment
- Consider deleting copy operations for non-copyable classes
- Test your copy constructor thoroughly
Conclusion: Mastering Object Copying
Copy constructors are fundamental to C++ programming, and understanding the difference between shallow and deep copying is crucial for writing correct, bug-free code. When your classes manage dynamic memory or other resources, you must implement proper deep copy semantics to ensure each object has independent ownership of its resources.
Key takeaways:
- Copy constructors create new objects as copies of existing objects
- Default copy constructor performs shallow copy (memberwise copy)
- Shallow copy is dangerous when classes manage dynamic memory
- Deep copy allocates new memory and copies contents, not just pointers
- Always use const reference parameter:
const ClassName& - Implement deep copy for any class with dynamically allocated members
- Follow the Rule of Three: destructor, copy constructor, copy assignment
- Use
= deleteto prevent copying when it doesn’t make sense - Test copy operations to ensure objects are truly independent
When you implement a copy constructor, you’re defining what it means to duplicate an object of your class. This isn’t just a technical detail—it’s a fundamental part of your class’s contract with users. Get it right with deep copying, and your classes will behave predictably and safely. Get it wrong with shallow copying of dynamic resources, and you’ll face crashes, memory corruption, and bugs that only appear under specific circumstances.
Remember: if your class manages dynamic memory, implement deep copy. If copying doesn’t make sense for your class, delete the copy constructor. But never leave copying to chance with classes that manage resources—the default shallow copy will cause problems. Master copy constructors, and you’ll write C++ code that’s robust, safe, and professional.








