Copying objects: shallow and deep copies

Introduction

When you copy an object (Buffer b = a;, passing it by value, returning it), C++ copies it member by member. That is fine for numbers and strings. It is a trap for a pointer member: the copy gets the same address, so both objects share one block of memory.

Copies happen all the time in C++: when you initialise one object from another, pass by value, or return a value. For most classes the automatic copy is right. For a class that owns heap memory, it is a bug waiting to happen.

How it works

The shallow-copy bug

#include <iostream>
using namespace std;

class Buffer {
public:
    int* data;
    int size;
    Buffer(int n) : size(n) {
        data = new int[n];
        for (int i = 0; i < n; i++) data[i] = 0;
    }
    ~Buffer() {
        delete[] data;
    }
};

int main() {
    Buffer a(3);
    Buffer b = a;   // copies the pointer, not the numbers
    b.data[0] = 99;
    cout << a.data[0] << endl;
    return 0;
}
Open in compiler

Buffer b = a; copies data, the pointer: watch both arrows point to the same heap block. Writing b.data[0] = 99 changes a's numbers too. Worse, at the end of main both destructors run delete[] on the same block. StepVioz stops at the second delete[]; compiled C++ may crash, or quietly corrupt memory.

Syntax

class Buffer {
public:
    Buffer(const Buffer& other);              // copy constructor
    Buffer& operator=(const Buffer& other);   // copy assignment
    ~Buffer();                                // destructor
};

Example

#include <iostream>
using namespace std;

class Buffer {
public:
    int* data;
    int size;
    Buffer(int n) : size(n) {
        data = new int[n];
        for (int i = 0; i < n; i++) data[i] = 0;
    }
    Buffer(const Buffer& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; i++) data[i] = other.data[i];
    }
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            for (int i = 0; i < size; i++) data[i] = other.data[i];
        }
        return *this;
    }
    ~Buffer() {
        delete[] data;
    }
};

int main() {
    Buffer a(3);
    Buffer b = a;
    b.data[0] = 99;
    cout << a.data[0] << " " << b.data[0] << endl;
    Buffer c(1);
    c = a;
    cout << c.size << endl;
    return 0;
}
Open in compiler

The fix is to say what copying means:

  • The copy constructor, Buffer(const Buffer& other), runs for Buffer b = a;. It allocates a new block and copies the numbers into it: a deep copy.
  • The copy assignment operator, operator=, runs for c = a; between two objects that already exist. It must release its old block first, and must survive a = a; (the this != &other test).
  • The destructor frees the block.

The rule of three: a class that needs one of these three almost always needs all three. Now each object owns its own block, the arrows point to different places, and every block is deleted exactly once.

return *this; returns the object itself (by reference), so assignments can be chained: a = b = c;.

Better still: use members that copy themselves correctly, such as vector<int> instead of int *. Then the compiler's member-by-member copy is already deep, and you need none of the three. This is called the rule of zero.

Line by line

  1. The Buffer(int n) constructor allocates n ints with new and sets them to 0.
  2. The copy constructor allocates a new block of the same size and copies every number into it.
  3. operator= checks this != &other, frees its own block, allocates a new one and copies the numbers. It returns *this.
  4. The destructor frees the block.
  5. Buffer b = a; runs the copy constructor, so changing b.data[0] to 99 leaves a.data[0] at 0.
  6. c = a; runs the copy assignment, so c.size becomes 3. At the end, each of the three blocks is deleted exactly once.

Expected result

0 99
3

In the real world

The standard library's containers, such as vector and string, are deep-copying classes written with exactly these member functions. That is why copying a vector gives a truly separate list.

Common mistakes

  • Writing a copy constructor but forgetting the copy assignment operator, so c = a; is still shallow.
  • Freeing the old block in operator= before checking for self-assignment.
  • Returning void from operator=, which stops chains such as a = b = c;.

Best practices

  • Prefer members that copy themselves, and follow the rule of zero.
  • If a class owns a raw resource, write all three or delete copying with = delete.
  • Test copying, assignment and self-assignment for every class that owns memory.

Summary

Copying a class copies its members one by one, which shares pointer members and leads to double deletes. A copy constructor, a copy assignment operator and a destructor together make deep copies, and members such as vector avoid the problem entirely.

Quick check

1. A class has an int * member and no copy constructor. After B b = a;, b's pointer:
2. Which runs for c = a; when c already exists?
3. Why does operator= check this != &other?

Key takeaways

  • The default copy copies pointer values, not what they point to.
  • A deep copy gives each object its own block.
  • operator= must release the old block and survive a = a.
  • Members that copy themselves, like vector, need none of the three.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.