Skip to content
Home C / C++ Copy Constructor in C++: Deep Dive with Real-World Examples

Copy Constructor in C++: Deep Dive with Real-World Examples

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C++ Basics → Topic 17 of 19
Master C++ copy constructors: Understand the Rule of Three, resolve shallow vs.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
Master C++ copy constructors: Understand the Rule of Three, resolve shallow vs.
  • The compiler-generated copy constructor performs a shallow (member-wise) copy — safe for value types, silently dangerous for any class that owns raw heap memory via a pointer.
  • A deep copy constructor allocates fresh memory and copies the data across; after the copy both objects are fully independent and can be safely destroyed in any order.
  • The Rule of Three: if your class needs any one of a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three — missing one is a common source of double-free crashes and memory leaks.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

Imagine you have a detailed blueprint for a house. When your friend wants to build the same house, they could either trace your blueprint (sharing the same paper) or make a completely fresh photocopy they can mark up independently. A copy constructor is C++'s way of making that fresh photocopy of an object — so the new one starts life as an identical twin but is completely independent. Without it, C++ might just hand your friend the original blueprint, and any changes they make would mess up your copy too.

Every serious C++ program eventually needs to duplicate objects — passing one to a function, returning one from a method, or storing one in a container. At that moment, C++ must answer a critical question: what does 'make a copy' actually mean for this specific class? For a simple integer that answer is obvious, but the moment your class owns a raw pointer, opens a file handle, or manages a network socket, a naive byte-for-byte duplicate can silently corrupt your program. That's the problem the copy constructor was designed to solve.

The copy constructor is a special member function that C++ calls whenever an object is initialised from another object of the same type. It lets you define exactly what 'copy' means for your class — whether that's a shallow mirror image, a fully independent deep clone, or something in between. Without a thoughtful copy constructor, two seemingly separate objects can end up sharing the same underlying memory, leading to double-free crashes, dangling pointers, and data corruption that only shows up at the worst possible moment.

By the end of this article you'll understand why the compiler-generated copy constructor is sometimes a ticking time bomb, how to write a correct deep-copying version, when to delete it entirely, and how to talk confidently about it in a technical interview. We'll build a realistic DocumentBuffer class step by step so every concept has immediate, tangible context.

What the Compiler Generates — and Why That's Sometimes Dangerous

If you don't write a copy constructor, the compiler generates one for you. This default version performs a member-wise copy: it copies each data member from the source object into the new object one by one. For value types like int, double, or std::string, that works perfectly because those types already know how to copy themselves correctly.

The danger arrives with raw pointers. If your class holds a char or int pointing to heap memory, a member-wise copy duplicates the pointer value — the memory address — not the data it points to. Both the original object and the new copy now point at the exact same block of heap memory. Neither object knows the other exists.

When the first one is destroyed, its destructor frees that memory. When the second one is later destroyed, its destructor tries to free memory that's already gone. That's undefined behaviour, and on most platforms it's an immediate crash.

This is called a shallow copy, and understanding it deeply is the single most important prerequisite for writing correct C++ classes that manage resources.

ShallowCopyDanger.cpp · CPP
123456789101112131415161718192021222324252627282930313233343536373839404142
#include <iostream>
#include <cstring>

namespace io::thecodeforge::examples {

// A naive buffer class that does NOT define a copy constructor.
// The compiler will generate a shallow copy — watch what happens.
class NaiveBuffer {
public:
    char* data;   
    int   size;

    NaiveBuffer(const char* text) {
        size = static_cast<int>(std::strlen(text)) + 1;
        data = new char[size];           
        std::strcpy(data, text);         
        std::cout << "[Construct] Allocated buffer at address "
                  << static_cast<void*>(data) << "\n";
    }

    ~NaiveBuffer() {
        std::cout << "[Destroy]   Freeing buffer at address "
                  << static_cast<void*>(data) << "\n";
        delete[] data;                   
    }
};

}

int main() {
    using namespace io::thecodeforge::examples;
    NaiveBuffer original("Hello, CodeForge");

    // The compiler's shallow copy constructor runs here.
    NaiveBuffer copyOfBuffer = original;  

    std::cout << "Original address:  " << static_cast<void*>(original.data) << "\n";
    std::cout << "Copy address:      " << static_cast<void*>(copyOfBuffer.data) << "\n";

    // When main() exits, the double-free crash occurs here.
    return 0;
}
▶ Output
[Construct] Allocated buffer at address 0x55a3c2e6aeb0
Original address: 0x55a3c2e6aeb0
Copy address: 0x55a3c2e6aeb0

[Destroy] Freeing buffer at address 0x55a3c2e6aeb0
[Destroy] Freeing buffer at address 0x55a3c2e6aeb0
free(): double free detected in tcache 2
Aborted (core dumped)
⚠ Watch Out: The Rule of Three
If your class needs a custom destructor (because it owns a raw resource), it almost certainly needs a custom copy constructor AND a custom copy assignment operator too. Defining only one of the three is a very common source of memory bugs. This pattern is so important it has a name: the Rule of Three.

Writing a Deep Copy Constructor That Actually Works

A deep copy constructor solves the shared-pointer problem by allocating brand-new memory for the copy and then copying the data across — not just the address. The result is two completely independent objects that happen to hold identical data at that moment in time.

The signature of a copy constructor is always the same pattern: ClassName(const ClassName& source). The parameter is a const reference to the same type. It must be a reference — if it were passed by value, C++ would need to copy it, which would call the copy constructor again, causing infinite recursion. The const is there because copying shouldn't modify the source.

Let's fix our NaiveBuffer by renaming it DocumentBuffer and adding a proper copy constructor, copy assignment operator, and destructor — the full Rule of Three in action.

DocumentBuffer.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
#include <iostream>
#include <cstring>
#include <algorithm>

namespace io::thecodeforge::core {

class DocumentBuffer {
public:
    char* content;   
    int   capacity;  

    explicit DocumentBuffer(const char* text) {
        capacity = static_cast<int>(std::strlen(text)) + 1;
        content  = new char[capacity];
        std::strcpy(content, text);
        std::cout << "[Constructor] Created at " << (void*)content << "\n";
    }

    // --- The Deep Copy Constructor ---
    DocumentBuffer(const DocumentBuffer& source) {
        capacity = source.capacity;
        content  = new char[capacity];          // Allocate FRESH memory
        std::strcpy(content, source.content);   // Copy actual DATA
        std::cout << "[Copy Constructor] Cloned to " << (void*)content << "\n";
    }

    // --- The Deep Copy Assignment Operator ---
    DocumentBuffer& operator=(const DocumentBuffer& source) {
        if (this == &source) return *this;      // Self-assignment guard

        delete[] content;                       // Cleanup existing resource
        capacity = source.capacity;
        content  = new char[capacity];
        std::strcpy(content, source.content);
        std::cout << "[Copy Assignment] Re-assigned at " << (void*)content << "\n";
        return *this;
    }

    ~DocumentBuffer() {
        std::cout << "[Destructor] Freeing " << (void*)content << "\n";
        delete[] content;  
    }
};

}

int main() {
    using namespace io::thecodeforge::core;
    DocumentBuffer draft("TheCodeForge");
    DocumentBuffer backup = draft; // Copy Constructor
    return 0;
}
▶ Output
[Constructor] Created at 0x55f1a3c02eb0
[Copy Constructor] Cloned to 0x55f1a3c02ed0
[Destructor] Freeing 0x55f1a3c02ed0
[Destructor] Freeing 0x55f1a3c02eb0
💡Pro Tip: Use std::string and Smart Pointers to Sidestep This Entirely
In modern C++ (C++11 and beyond), wrapping your resource in a std::string, std::vector, or std::unique_ptr means the compiler-generated copy constructor does the right thing automatically — because those types already implement deep copying. Prefer this to managing raw pointers yourself unless you're writing low-level infrastructure code.

When to Delete the Copy Constructor — and the Move Constructor Alternative

Sometimes copying an object makes no logical sense. A database connection, a file handle, or a mutex represent unique real-world resources that can't meaningfully be duplicated. If you copy a database connection object, should both copies now own the same connection? The safest answer is to make copying impossible by deleting the copy constructor.

C++11 introduced the move constructor as a complementary tool. Where a copy says 'make me an identical twin', a move says 'transfer ownership to me — the original gives up its resource'. Moves are typically $O(1)$ operations because they just swap pointers, whereas deep copies are $O(N)$.

DatabaseConnection.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243
#include <iostream>
#include <string>
#include <utility>

namespace io::thecodeforge::db {

class DatabaseConnection {
private:
    std::string* conn_string;

public:
    explicit DatabaseConnection(const std::string& str) {
        conn_string = new std::string(str);
        std::cout << "[DB] Connection opened.\n";
    }

    // DISABLE COPYING
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;

    // ENABLE MOVING (Ownership Transfer)
    DatabaseConnection(DatabaseConnection&& other) noexcept : conn_string(other.conn_string) {
        other.conn_string = nullptr; // Leave donor in valid but empty state
        std::cout << "[DB] Connection moved.\n";
    }

    ~DatabaseConnection() {
        if (conn_string) {
            delete conn_string;
            std::cout << "[DB] Connection closed.\n";
        }
    }
};

}

int main() {
    using namespace io::thecodeforge::db;
    DatabaseConnection conn("sql://prod:5432");
    // DatabaseConnection copy = conn; // This would cause COMPILE ERROR
    DatabaseConnection moved_conn = std::move(conn); // This works!
    return 0;
}
▶ Output
[DB] Connection opened.
[DB] Connection moved.
[DB] Connection closed.
🔥Interview Gold: Copy vs Move Semantics
Interviewers love asking the difference between a copy constructor and a move constructor. The crisp answer: a copy constructor creates an independent duplicate (both objects remain valid); a move constructor transfers ownership (the source is left in a valid-but-empty state). Moves are typically O(1); copies are typically O(n) where n is the size of the data.
AspectShallow Copy (Compiler Default)Deep Copy (Custom Constructor)
What gets copiedPointer address only (4 or 8 bytes)Pointer address + all data it points to
Memory ownershipBoth objects share the same heap blockEach object owns its own independent heap block
Destructor safetyDouble-free crash when second object is destroyedEach destructor frees its own memory safely
Mutation independenceChanging data via one object affects the otherObjects are fully independent after copying
Performance costO(1) — just copies a few bytesO(n) — proportional to data size
When it's correctOnly when class has no pointer/resource membersAny class that owns heap memory or external resources
Rule of Three required?No (but dangerous if resources exist)Yes — always pair with destructor and copy assignment

🎯 Key Takeaways

  • The compiler-generated copy constructor performs a shallow (member-wise) copy — safe for value types, silently dangerous for any class that owns raw heap memory via a pointer.
  • A deep copy constructor allocates fresh memory and copies the data across; after the copy both objects are fully independent and can be safely destroyed in any order.
  • The Rule of Three: if your class needs any one of a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three — missing one is a common source of double-free crashes and memory leaks.
  • Use = delete to make a class non-copyable when copying makes no logical sense (file handles, sockets, threads). Pair it with a move constructor to still allow efficient ownership transfer.

⚠ Common Mistakes to Avoid

    Forgetting the copy assignment operator after writing a copy constructor — The class looks safe because the copy constructor does a deep copy, but `buffer2 = buffer1;` still performs a shallow copy via the compiler-generated assignment operator, causing a double-free when both go out of scope. Fix: whenever you write a copy constructor, immediately write the copy assignment operator too (and vice versa). The Rule of Three exists for exactly this reason.
    Fix

    whenever you write a copy constructor, immediately write the copy assignment operator too (and vice versa). The Rule of Three exists for exactly this reason.

    Passing a copy constructor parameter by value instead of by reference — Writing `DocumentBuffer(DocumentBuffer source)` instead of `DocumentBuffer(const DocumentBuffer& source)` causes infinite recursion: to pass `source` by value, C++ must copy it, which calls the copy constructor, which tries to copy `source` again, and so on until the stack overflows. Fix: the parameter must always be `const ClassName&`. The compiler will actually error on this in many cases, but understanding why the rule exists matters.
    Fix

    the parameter must always be const ClassName&. The compiler will actually error on this in many cases, but understanding why the rule exists matters.

    Not handling self-assignment in the copy assignment operator — Writing `archive = archive;` is legal C++ and must work correctly. Without a self-assignment guard (`if (this == &source) return *this;`), the operator frees the old memory with `delete[] content` first, then tries to copy from `source.content` — which is now dangling memory that was just freed. Fix: always put the `if (this == &source) return *this;` guard at the very top of your copy assignment operator before touching any memory.
    Fix

    always put the if (this == &source) return *this; guard at the very top of your copy assignment operator before touching any memory.

Interview Questions on This Topic

  • QExplain the 'Copy-and-Swap' idiom. How does it simplify the implementation of the copy assignment operator while providing strong exception safety?
  • QWhat is the difference between a Copy Constructor and a Copy Assignment Operator? Can you call the copy constructor from within the copy assignment operator?
  • QWhat is the Rule of Five? Why did the introduction of C++11 expand the Rule of Three, and what happens if you define a destructor but not the move constructors?
  • QWhy must the copy constructor's parameter be passed by reference? Walk through the step-by-step memory/stack events if a compiler allowed passing it by value.
  • QExplain 'Copy Elision' and 'Return Value Optimization' (RVO). Why might your copy constructor not fire even when you are returning a large object by value?

Frequently Asked Questions

What happens if I write a Copy Constructor but forget the Copy Assignment Operator?

This violates the Rule of Three. While creating a new object from an old one will be safe (Deep Copy), assigning an old object to another (e.g., obj1 = obj2;) will trigger the default compiler-generated assignment operator, which performs a Shallow Copy. This leads to double-free crashes when the objects are destroyed.

When is the copy constructor called automatically in C++?

C++ calls the copy constructor in three situations: when you initialise a new object from an existing one (Buffer b = a;), when you pass an object to a function by value, and when a function returns an object by value. Note that copy assignment (a = b; where a already exists) calls the copy assignment operator, not the copy constructor.

What is the difference between a copy constructor and a copy assignment operator?

The copy constructor creates a brand-new object initialised from an existing one — it runs during object construction. The copy assignment operator runs on an already-existing object to replace its current state with a copy of another object's state. Because the object already exists during assignment, you must free its old resources before copying the new ones in — that's why the copy assignment operator needs a self-assignment guard and a delete[] call that the copy constructor doesn't.

Why must the copy constructor take its parameter by reference and not by value?

If the copy constructor took its parameter by value, calling it would require copying the argument, which would call the copy constructor again to make that copy, which would need to copy its argument, and so on infinitely. Taking the parameter by const reference breaks this infinite recursion because passing by reference doesn't invoke a copy. This is enforced by the C++ standard — the compiler will reject a copy constructor defined with a by-value parameter.

What is the 'Rule of Zero' in Modern C++?

The Rule of Zero suggests that classes should be designed so that they don't need to manage resources directly. By using smart pointers (std::unique_ptr) and standard containers (std::vector, std::string), the compiler-generated defaults work perfectly. This means you don't have to write any of the 'Rule of Three' functions, leading to cleaner, safer code.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousVirtual Functions in C++Next →Static Members in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged