Skip to content

Clones and Copying

Copying with Dynamic Memory

We are used to the fact that when we copy an integer, we get a perfect copy — that is, the copy takes the same value represented by the original.

1
2
3
4
5
int main() {
  int a = 6;
  int b;
  b = a;  // 'a' is copied; 'b' takes the same value as 'a'
}

We can also do this with objects (as we did with string). Since an object is defined by the values of its data members, copying means copying those values. In a simple case, this happens automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class A {
  int a;
public:
  A(int a = 6) : a(a) {}

  int get_a() const { return a; }
};

int main() {
  A a(4);
  A b;
  cout << b.get_a() << " -> ";
  b = a;
  cout << b.get_a() << endl;
}

In the example above, since we didn’t define how copying should occur, the default copy ran, which copied each member’s value. Therefore, the output is:

6 -> 4

However, this is not correct when the object contains dynamically allocated memory. The dynamically allocated memory is not part of the object itself, only the pointer that refers to it. So when we copy the object, the pointer value (address) is copied too — both objects point to the same memory area.
This means both objects use the same memory — modifying one will affect the other.

The correct approach is to allocate new memory and copy each value individually, so that the two objects refer to different memory regions.

Copy Constructor

To properly handle copying with dynamic memory, we must explicitly define how copying should happen. One way is by implementing a copy constructor. When using a copy constructor, we create a new object based on another existing object.

Because the copy constructor is still a constructor, its name matches the class name, and its parameter is a reference to another object of the same type.
The parameter must be a constant reference (const &), because the source object will not be modified.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;

class CopyCon {
  int value;
  int *heap_int;
public:
  CopyCon(int value = 1, int heap_value = 2) : value(value), heap_int(new int(heap_value)) {}

  // Copy constructor
  CopyCon(const CopyCon& cc) : value(cc.value), heap_int(new int(*cc.heap_int)) {}

  int get_heap_int() const { return *heap_int; }
  void set_heap_int(int a) { *heap_int = a; }
};

int main() {
  CopyCon a(3);
  a.set_heap_int(8);

  CopyCon b(a);
  cout << b.get_heap_int() << endl;
}

In this example, the copy constructor receives an object and copies its values. For dynamically allocated elements, it allocates new memory and copies the stored value(s).
If we had used new[] to allocate an array, we would need to copy each element, e.g., in a loop.

Assignment Operator

The copy constructor allows us to create a new object based on another.
However, we may also want to assign one existing object’s values to another existing object.
For this, we overload the operator=.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class AssignOp {
  int *ip;
public:
  AssignOp(int n) : ip(new int) { *ip = n; }
  AssignOp(const AssignOp& ao) : ip(new int) { *ip = *(ao.ip); }

  // Assignment operator
  AssignOp& operator=(const AssignOp& ao) {
    delete ip;
    ip = new int;
    *ip = *(ao.ip);
    return *this;
  }
};

Here, we free previously allocated memory, allocate new space, and copy the value.
However, this version still has a flaw: if we assign an object to itself (a = a;), it first deletes its own memory and then tries to copy from it — which is already freed!

To avoid this, we must check for self-assignment:

1
2
3
4
5
6
7
8
9
AssignOp& operator=(const AssignOp& ao) {
  if (this == &ao)  // check for self-assignment
    return *this;

  delete array;
  array = new int;
  *array = *(ao.array);
  return *this;
}

Because the copy constructor and assignment operator are closely related, they should always be implemented together (or both explicitly deleted using = delete).

Postfix ++ Operator

The difference between prefix and postfix ++ operators is in the value returned.
The postfix version (x++) must return the old value, so it must create a copy of the object.
Therefore, with dynamic memory, it relies on the copy constructor and assignment operator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PostFixOp {
  int* p;
public:
  PostFixOp& operator++() { return *this; }

  PostFixOp operator++(int) {
    PostFixOp copied_to_return = *this; // copy constructor
    operator++(); // increment the object
    return copied_to_return; // return old state
  }
};

Object Cloning

In all previous examples, we knew exactly what type we were copying.
But what if we want to perform copying polymorphically?
Constructors cannot be virtual (since the object doesn’t yet exist when they would be called).
The solution is to define a clone() method that performs the copying — and can be virtual.

Following Java’s convention, this method can be named clone, which creates and returns a copy of the current object.

Example with the University and FinanceDepartment classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Employee {
public:
  const string& name;
  Employee(const std::string& name) : name(name) {}
  virtual ~Employee() {}
  Employee(const Employee& e) : name(e.name) {}

  virtual void attendMeeting() const {
    cout << "Employee went to a meeting." << endl;
  }

  virtual Employee* clone() const {
    return new Employee(*this);
  }
};

class Researcher : public Employee {
  unsigned projects;
  int* workData = nullptr;
public:
  void attendMeeting() const override {
    std::cout << "Research work requires a lot of discussions with other researchers." << std::endl;
  }

  Researcher(const std::string& name, unsigned projects)
    : Employee(name), projects(projects), workData(new int[projects]) {}

  Researcher(const Researcher& r) : Employee(r.name) {
    if (r.workData != nullptr) {
      projects = r.projects;
      workData = new int[r.projects];
      for (int i = 0; i < projects; i++) {
        workData[i] = r.workData[i];
      }
    }
  }

  virtual Researcher* clone() const override {
    return new Researcher(*this);
  }

  ~Researcher() { delete[] workData; }
};

class University;
class FinanceDepartment {
  vector<Employee*> employees;
public:
  void retrieveEmployees(const University& university);
  void meeting() const {
    for (const auto& emp : employees)
      emp->attendMeeting();
  }
};

class University {
  vector<Employee*> employees;
public:
  University() = default;

  ~University() {
    for (int i = 0; i < employees.size(); i++)
      delete employees[i];
  }

  University& operator+(Employee* employee) {
    employees.push_back(employee);
    return *this;
  }

  void meeting() const {
    for (const auto& emp : employees)
      emp->attendMeeting();
  }

  friend void FinanceDepartment::retrieveEmployees(const University& university);
};

void FinanceDepartment::retrieveEmployees(const University& university) {
  for (const auto& e : university.employees) {
    employees.push_back(e->clone());
  }
}

int main() {
  University u;
  Employee* e1 = new Employee("Bela");
  Researcher* r = new Researcher("Scientist", 10);
  u + r + e1;

  FinanceDepartment fd;
  fd.retrieveEmployees(u);
  fd.meeting();
}

Last update: 2025-11-27
Created: 2025-11-27