Skip to content
C++ Better Explained
Go back
C++ Copy Constructor: Deep Copy vs Shallow Copy Explained
Edit page

C++ Copy Constructor: Deep Copy vs Shallow Copy Explained

When you write MyClass b = a; in C++, what exactly happens? The copy constructor is called — it’s a special constructor whose job is to create a new object that’s a copy of an existing one. Most of the time the compiler generates one for you, but when your class manages raw memory, you need to write your own or risk some very subtle bugs.


The Default Copy Constructor

If you don’t define a copy constructor, the compiler generates one that does a member-wise copy: it copies each data member one by one.

#include <iostream>
#include <string>

class Book {
public:
    std::string title;
    int pages;

    Book(std::string t, int p) : title(t), pages(p) {}
};

int main() {
    Book original("Clean Code", 431);
    Book copy = original;  // compiler-generated copy constructor

    std::cout << copy.title << " (" << copy.pages << " pages)\n";
    return 0;
}

This works perfectly because std::string and int both copy correctly. The problem appears when your class holds raw pointers.


The Shallow Copy Problem

Here’s a class that stores a name on the heap using a raw pointer:

#include <iostream>
#include <cstring>

class Player {
public:
    char* name;

    Player(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    ~Player() {
        delete[] name;
    }
};

int main() {
    Player p1("Alice");
    Player p2 = p1;  // shallow copy — both point to SAME memory!

    // When main() ends, ~Player() is called for both p1 and p2
    // Both try to delete[] the same memory — double free crash!
    return 0;
}

The compiler copies the name pointer — the address — not the data it points to. Now p1.name and p2.name point to the same block of heap memory. When either object is destroyed, the memory is freed; when the second destructor runs, it tries to free already-freed memory. This is undefined behaviour and often causes a crash.


Writing a Deep Copy Constructor

A deep copy allocates new memory and copies the contents:

#include <iostream>
#include <cstring>

class Player {
public:
    char* name;

    // Regular constructor
    Player(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    // Copy constructor — deep copy
    Player(const Player& other) {
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
        std::cout << "Copy constructor called\n";
    }

    // Destructor
    ~Player() {
        delete[] name;
    }

    void printName() const {
        std::cout << "Name: " << name << "\n";
    }
};

int main() {
    Player p1("Alice");
    Player p2 = p1;  // calls our copy constructor

    p1.printName();  // Alice
    p2.printName();  // Alice — independent copy

    // Safely modify p2 without affecting p1
    strcpy(p2.name, "Bob");
    p1.printName();  // Still Alice
    p2.printName();  // Bob

    return 0;
}

Now each Player owns its own allocation. The two objects are completely independent.

If you're looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you're a complete beginner or looking to solidify your understanding. Just $19.

The Rule of Three

If your class needs a custom copy constructor, it almost certainly also needs a custom destructor and copy assignment operator. This is known as the Rule of Three:

class Player {
public:
    char* name;

    // 1. Constructor
    Player(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    // 2. Copy constructor (deep copy)
    Player(const Player& other) {
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
    }

    // 3. Copy assignment operator (deep copy)
    Player& operator=(const Player& other) {
        if (this == &other) return *this;  // handle self-assignment
        delete[] name;
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
        return *this;
    }

    // 4. Destructor
    ~Player() {
        delete[] name;
    }
};

The copy assignment operator handles p1 = p2; (when both already exist), while the copy constructor handles Player p2 = p1; (creation from copy).


The Modern Solution: Use std::string or std::vector

In practice, you can avoid writing copy constructors entirely by using standard library types that already handle memory correctly:

#include <iostream>
#include <string>

class Player {
public:
    std::string name;  // std::string manages its own memory

    Player(std::string n) : name(std::move(n)) {}
};

int main() {
    Player p1("Alice");
    Player p2 = p1;  // compiler-generated copy just works!

    std::cout << p1.name << "\n";  // Alice
    std::cout << p2.name << "\n";  // Alice — independent copy
    return 0;
}

std::string has its own correct copy constructor, so the compiler-generated copy constructor for Player does the right thing automatically.



Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19

📋

Free Download: The 10 Mistakes Every C++ Beginner Makes

A free 1-page checklist that shows the exact traps that slow down every C++ beginner — so you can avoid them from day one.

🔒 No spam. Unsubscribe anytime.


Edit page
Share this post on:

Previous Post
C++ 2D Arrays: How to Declare, Initialize, and Iterate
Next Post
C++ Constructor Initializer Lists Explained for Beginners