Deep Copy and Shallow Copy in C++

1. Core Concept: What is Copying?

In C++, when we initialize one object with another object of the same type, or pass an object as a parameter to a function (by value), or return an object from a function (by value), a “copying” behavior occurs.

This copying behavior is controlled by the copy constructor and the copy assignment operator.

class MyClass {public:    MyClass(); // Default constructor    MyClass(const MyClass& other); // Copy constructor    MyClass& operator=(const MyClass& other); // Copy assignment operator};
MyClass obj1;MyClass obj2 = obj1; // Calls copy constructorMyClass obj3;obj3 = obj1; // Calls copy assignment operator

The core of the problem is: how do these default-generated copy functions work? This leads us to the distinction between shallow copy and deep copy.

2. Shallow Copy

Definition: A shallow copy only copies the data members of the object itself (i.e., “bitwise copy”). If the object contains pointer members, it copies the value of the pointer (the memory address), not the data pointed to by the pointer.

How it Occurs: If you do not explicitly define a copy constructor and copy assignment operator for a class, the compiler will automatically generate a set for you. These compiler-generated versions perform shallow copy.

Example and Issues:

#include <iostream>
class ShallowClass {public:    int* data;    int size;
    // Constructor    ShallowClass(int size) : size(size) {        data = new int[size];        for (int i = 0; i < size; ++i) {            data[i] = i;        }    }
    // Destructor    ~ShallowClass() {        delete[] data;        std::cout << "Destructor called. Memory freed." << std::endl;    }
    // Note: No custom copy constructor and copy assignment operator provided    // Compiler will provide the default shallow copy version};
int main() {    ShallowClass obj1(5);    ShallowClass obj2 = obj1; // Compiler-generated shallow copy constructor is called
    // Print the data pointer addresses of both objects, they point to the same memory!    std::cout << "obj1.data: " << obj1.data << std::endl;    std::cout << "obj2.data: " << obj2.data << std::endl;
    // Modifying obj2's data will also affect obj1!    obj2.data[0] = 100;    std::cout << "obj1.data[0]: " << obj1.data[0] << std::endl; // Outputs 100
    return 0;} // End of main function, destructors for obj2 and obj1 are called

Running the above code will lead to<span>double free</span> or program crash!

Cause Analysis:

  1. 1.

    <span>obj1</span> and <span>obj2</span>‘s <span>data</span> pointers point to the same heap memory.

  2. 2.

    When the <span>main</span> function ends, <span>obj2</span> and <span>obj1</span> will be destroyed in order, calling their respective destructors.

  3. 3.

    <span>obj2</span>‘s destructor executes first, <span>delete[] data;</span> frees the shared memory.

  4. 4.

    Immediately after, <span>obj1</span>‘s destructor executes, trying to free the same memory that has already been freed by obj2. Freeing already freed memory is undefined behavior, which usually leads to a program crash.

This is the biggest problem with shallow copy:multiple objects share resources, leading to double freeing, dangling pointers, and unintended data modification.

3. Deep Copy

Definition: A deep copy not only copies the data members of the object itself but also creates new copies of the resources held by the object (such as heap memory, file handles, etc.). The new object and the original object are completely independent and do not affect each other.

How to Implement: You must explicitly define your own copy constructor and copy assignment operator for the class, manually allocating new resources and copying content in these functions.

Example and Solution:

#include &lt;iostream&gt;
class DeepClass {public:    int* data;    int size;
    // Constructor    DeepClass(int size) : size(size) {        data = new int[size];        for (int i = 0; i &lt; size; ++i) {            data[i] = i;        }    }
    // 1. Custom copy constructor (deep copy)    DeepClass(const DeepClass&amp; other) : size(other.size) {        data = new int[size]; // 1. Allocate new memory block        for (int i = 0; i &lt; size; ++i) {            data[i] = other.data[i]; // 2. Copy data content        }        std::cout &lt;&lt; "Custom Copy Constructor called (Deep Copy)." &lt;&lt; std::endl;    }
    // 2. Custom copy assignment operator (deep copy)    DeepClass&amp; operator=(const DeepClass&amp; other) {        if (this == &amp;other) { // Self-assignment check is crucial!            return *this;        }        // Release original resources        delete[] data;
        // Allocate new resources and copy content        size = other.size;        data = new int[size];        for (int i = 0; i &lt; size; ++i) {            data[i] = other.data[i];        }        std::cout &lt;&lt; "Custom Copy Assignment Operator called (Deep Copy)." &lt;&lt; std::endl;        return *this;    }
    // Destructor    ~DeepClass() {        delete[] data;        std::cout &lt;&lt; "Destructor called. Memory freed." &lt;&lt; std::endl;    }};
int main() {    DeepClass obj1(5);    DeepClass obj2 = obj1; // Calls custom deep copy copy constructor
    // Print the data pointer addresses of both objects, they now point to different memory!    std::cout &lt;&lt; "obj1.data: " &lt;&lt; obj1.data &lt;&lt; std::endl;    std::cout &lt;&lt; "obj2.data: " &lt;&lt; obj2.data &lt;&lt; std::endl;
    // Modifying obj2's data will not affect obj1!    obj2.data[0] = 100;    std::cout &lt;&lt; "obj1.data[0]: " &lt;&lt; obj1.data[0] &lt;&lt; std::endl; // Outputs 0    std::cout &lt;&lt; "obj2.data[0]: " &lt;&lt; obj2.data[0] &lt;&lt; std::endl; // Outputs 100
    return 0;} // Safe exit, both objects release their own memory

Deep copy solves all problems:

  1. 1.

    Independence: <span>obj1</span> and <span>obj2</span> have their own copies of data, modifying one will not affect the other.

  2. 2.

    Safe Release: During destruction, each releases its own memory, preventing <span>double free</span>.

4. Rule of Three and Rule of Five

The implementation of deep copy introduces an important guideline in C++:

  • Rule of Three: If a class needs to explicitly define any of the destructor, copy constructor, or copy assignment operator, it likely needs all three.

    • Logic: Needing a custom destructor to release resources (like memory) usually means that the default shallow copy behavior is unsafe, thus requiring custom copy functions to implement deep copy.

  • Rule of Five (C++11 and later): Based on the Rule of Three, it adds move constructor and move assignment operator. If you define copy control functions, you should also consider defining move operations to improve performance and avoid unnecessary deep copies.

// Adding move semantics in DeepClass (Rule of Five)
DeepClass(DeepClass&amp;&amp; other) noexcept // Move constructor    : data(other.data), size(other.size) {    other.data = nullptr; // After stealing resources, set the source object to destructible state    other.size = 0;}
DeepClass&amp; operator=(DeepClass&amp;&amp; other) noexcept { // Move assignment operator    if (this == &amp;other) return *this;    delete[] data; // Release original resources    data = other.data; // Steal resources    size = other.size;    other.data = nullptr;    other.size = 0;    return *this;}

5. How to Choose? Best Practices in Modern C++

  1. 1.

    Avoid Manual Management: The simplest choice is to avoid using raw pointers and manual resource management. Use smart pointers from the C++ standard library (<span>std::unique_ptr</span>, <span>std::shared_ptr</span>) and containers (<span>std::vector</span>, <span>std::string</span>). They automatically manage resources, and the default copy behavior is safe (deep copy or reference counting).

#include &lt;memory&gt;#include &lt;vector&gt;
class SafeClass {public:    // std::vector and std::unique_ptr both implement safe copy/move semantics    std::vector&lt;int&gt; data;     std::unique_ptr&lt;int[]&gt; smartData; 
    SafeClass(int size) {        smartData = std::make_unique&lt;int[]&gt;(size);        data.resize(size);        for (int i = 0; i &lt; size; ++i) {            data[i] = i;            smartData[i] = i;        }    }    // No need to explicitly define destructor, copy constructor, copy assignment!    // The compiler-generated default versions work correctly.};
  1. 2.

    Follow the Rule of Five When Manual Management is Needed: If you must perform low-level resource management, be sure to explicitly define all necessary special member functions (destructor, copy constructor, copy assignment, move constructor, move assignment), or use <span>= delete</span> to explicitly prohibit copying or moving.

  2. 3.

    Explicitly Prohibit Copying: If copying an object does not make sense (for example, an object representing a unique resource), you can use <span>= delete</span> to delete copy functions, or use <span>std::unique_ptr</span> and other non-copyable members to implicitly prohibit it.

class NonCopyable {public:    NonCopyable() = default;    // Prohibit copying    NonCopyable(const NonCopyable&amp;) = delete;    NonCopyable&amp; operator=(const NonCopyable&amp;) = delete;    // Allow moving (optional)    NonCopyable(NonCopyable&amp;&amp;) = default;    NonCopyable&amp; operator=(NonCopyable&amp;&amp;) = default;};

Summary Comparison

Feature

Shallow Copy

Deep Copy

Behavior

Copies the value (address) of pointers

Copies the actual data pointed to by pointers

Implementation

Compiler-generated default

Requires explicit implementation by the programmer

Resource Ownership

Multiple objects share resources

Each object owns its own copy of resources

Advantages

Simple, fast

Safe, independent objects

Disadvantages

Dangling pointers, double freeing, data interference

Performance overhead (allocating memory, copying data)

Applicable Scenarios

Simple classes without pointers (POD)

Classes managing dynamic resources (heap memory, files, etc.)

C++ Mechanism

Default copy constructor, default copy assignment operator

Custom copy constructor, custom copy assignment operator

Core Idea: When your class holds resources that require “ownership”, you must be responsible for its copying behavior. The default shallow copy is dangerous in most cases, and you should choose to implement deep copy or explicitly prohibit copying. In modern C++, prefer using standard library containers and smart pointers to automatically and safely handle these issues.

Leave a Comment