Copy Constructors: Deep Copy vs Shallow Copy

Learn C++ copy constructors, deep copy vs shallow copy differences. Avoid memory leaks, prevent bugs, and create robust classes with proper copying.

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.

C++
#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:

  1. Simple class: Contains a single integer member variable
  2. Regular constructor: Creates object with specified value
  3. Copy constructor signature: Takes const Simple& parameter (reference to const object)
  4. const parameter: Prevents modification of the source object being copied
  5. Reference parameter: Must be reference (not value) to avoid infinite recursion
  6. Member initialization: Copies value from other.value to this object’s value
  7. obj2 = obj1: Copy initialization syntax that invokes copy constructor
  8. obj3(obj1): Direct initialization syntax also invoking copy constructor
  9. Independent objects: obj1, obj2, and obj3 are separate objects in memory
  10. Separate storage: Modifying obj1.value doesn’t affect obj2 or obj3
  11. Why const: Protects source object from accidental modification during copying
  12. Why reference: Passing by value would require copying, which needs copy constructor—infinite loop!

Output:

HTML
=== 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: 42

When is the Copy Constructor Called?

The copy constructor is invoked automatically in several situations:

C++
#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:

  1. Tracker class: Assigns unique ID to each object to track copying
  2. Static nextId: Shared counter for generating unique IDs
  3. Default constructor: Creates new object with next available ID
  4. Copy constructor: Creates new object copying from existing, gets new ID
  5. Destructor: Prints message when object is destroyed
  6. Scenario 1: Direct initialization with = calls copy constructor
  7. Scenario 2: Passing by value creates temporary copy of argument
  8. functionByValue parameter: Receives copy of obj1, so copy constructor called
  9. Scenario 3: Returning by value may create temporary (compilers often optimize this)
  10. RVO/NRVO: Modern compilers use Return Value Optimization to avoid copying
  11. Scenario 4: Assignment operator called, NOT copy constructor (obj4 already exists)
  12. Copy vs assignment: Copy constructor initializes new object; assignment modifies existing object
  13. Temporary objects: Copy constructor creates temporaries that are destroyed when no longer needed
  14. Optimization: Compilers may eliminate unnecessary copies (copy elision)

Output (may vary with compiler optimizations):

HTML
=== 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 destroyed

Default 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.

C++
#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:

  1. ShallowExample class: Contains pointer to dynamically allocated array
  2. Constructor allocates memory: Uses new int[size] to create array
  3. Stores pointer: data pointer holds address of allocated memory
  4. No copy constructor: Class relies on compiler-generated default
  5. Default is shallow: Compiler copies data pointer value, not array contents
  6. obj2 = obj1: Both obj1.data and obj2.data point to SAME array
  7. Shared memory: Both objects share the dynamically allocated array
  8. Modification problem: Changing obj2’s array also changes obj1’s array
  9. Both pointers equal: obj1.data == obj2.data (pointing to same memory)
  10. Destructor problem: Both destructors will try to delete same memory
  11. Double deletion: First destructor succeeds, second tries to delete freed memory
  12. Undefined behavior: Program will likely crash or exhibit strange behavior
  13. 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.

C++
#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:

  1. DeepCopyString class: Manages dynamic string memory
  2. Constructor allocates: Creates new array with new char[length + 1]
  3. Stores content: Copies string contents into allocated memory
  4. Copy constructor header: Same signature as any copy constructor
  5. Allocate new memory: data = new char[length + 1] creates separate array
  6. Copy contents: strcpy(data, other.data) copies string content
  7. Different pointers: str1.data and str2.data point to different memory locations
  8. Independent objects: Each object owns its own memory
  9. Modification safety: Changing str2 doesn’t affect str1
  10. Different addresses: Print addresses to verify separate allocations
  11. Safe destruction: Each destructor deletes its own unique memory
  12. No double deletion: Each delete[] operates on different memory
  13. Resource independence: Objects can be copied, modified, and destroyed independently

Output:

HTML
=== 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:

C++
#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:

  1. Two similar classes: Both manage dynamic integer, one shallow, one deep
  2. ShallowCopy uses default: Compiler-generated copy constructor
  3. Default copies pointer: shallow2.data = shallow1.data (same address)
  4. DeepCopy custom constructor: Explicitly allocates new memory
  5. Allocates independent memory: deep2.data points to different location than deep1.data
  6. Address comparison: Printing addresses shows shallow copies share, deep copies don’t
  7. Modification test: Changing shallow2 affects shallow1 (shared memory)
  8. Deep independence: Changing deep2 doesn’t affect deep1 (separate memory)
  9. Destruction difference: Shallow causes double-delete, deep deletes different memory
  10. Shallow danger: Both destructors operate on same address—undefined behavior
  11. Deep safety: Each destructor has its own memory to delete
  12. Visual evidence: Address output proves shallow shares, deep separates

Output (shallow copy may crash):

HTML
=== 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:

C++
#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:

  1. DynamicArray class: Manages dynamic integer array with capacity tracking
  2. Three pointers to track: elements (data), size (used), capacity (allocated)
  3. Constructor allocates: Creates initial array with specified capacity
  4. Copy constructor deep copy: Allocates new array, copies all elements
  5. Element-by-element copy: Loop copies each element to new array
  6. Copy assignment operator: Handles copying to already-existing object
  7. Self-assignment check: Prevents deleting memory we’re about to copy from
  8. Delete old memory: Frees existing array before allocating new
  9. Allocate and copy: Same deep copy process as copy constructor
  10. *Return this: Enables chained assignments (a = b = c)
  11. Destructor cleanup: Deletes dynamically allocated array
  12. add() method: Demonstrates modifying one copy doesn’t affect others
  13. Independent arrays: Each object has its own memory
  14. Safe operations: All copying and destruction operations are safe

Output:

HTML
=== 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 elements

When to Delete Copy Constructor

Sometimes you want to prevent copying entirely. Use = delete to disable the copy constructor:

C++
#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:

  1. NonCopyable class: Manages unique resource that shouldn’t be copied
  2. = delete syntax: Explicitly deletes copy constructor
  3. Compiler error: Attempting to copy generates compile-time error
  4. Also delete assignment: Prevents copying via assignment operator
  5. FileHandle example: Real-world case where copying doesn’t make sense
  6. File descriptors: Can’t have two objects owning same file handle
  7. Prevention is clear: Deletion makes intent explicit in code
  8. Pointers allowed: Can still use pointers and references to access object
  9. No accidental copies: Compiler prevents mistakes
  10. Better than private: Deleting is clearer than making copy constructor private
  11. Modern C++ practice: Use = delete instead of older private-without-implementation pattern
  12. Resource management: Essential for classes managing unique system resources

Output:

HTML
=== 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 42

Common Copy Constructor Mistakes

Let’s examine common mistakes and how to avoid them:

C++
#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:

  1. Missing const: Source object could be accidentally modified during copying
  2. Pass by value: Creates infinite recursion (copy needs copy needs copy…)
  3. Shallow instead of deep: Multiple objects share same dynamic memory
  4. Forgetting members: Some data doesn’t get copied, left uninitialized
  5. No self-assignment check: Deleting own data before copying causes crash

Best Practices Summary

Here are the key guidelines for copy constructors:

C++
#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:

  1. Use const reference parameter
  2. Use member initializer list
  3. Perform deep copy for dynamic resources
  4. Copy all data members
  5. Implement both copy constructor and copy assignment (Rule of Three)
  6. Check for self-assignment in copy assignment
  7. Consider deleting copy operations for non-copyable classes
  8. 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 = delete to 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.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Console Input and Output in C#

Learn how to handle console input and output in C#, including error handling, input validation,…

Anduril Expands with New Long Beach Defense Tech Campus

Anduril announces major Long Beach campus investment to scale advanced weapons systems production. Defense tech…

Understanding Matrices and Vectors in AI Applications

Learn how matrices and vectors power AI applications. Understand image processing, NLP, recommendation systems, and…

Java Control Flow: if, else, and switch Statements

Learn the fundamentals of Java control flow, including if-else statements, switch cases and loops. Optimize…

Visualizing Mathematical Concepts with Matplotlib

Master Matplotlib for machine learning visualization. Learn to create line plots, scatter plots, histograms, heatmaps,…

Exploring Capacitors: Types and Capacitance Values

Discover the different types of capacitors, their capacitance values, and applications. Learn how capacitors function…

Click For More
0
Would love your thoughts, please comment.x
()
x