OOP Search

Week 05 / 14 Object-Oriented Programming

Lab 5 - Copy semantics and the Rule of Three

We copy an object that owns heap memory, watch the compiler-generated copy share that memory, and then write the destructor, copy constructor and operator= that make copies independent.

Objectives
  • Explain what the compiler-generated copy constructor and operator= do
  • Tell a shallow copy from a deep copy and draw both
  • Write a copy constructor that performs a deep copy
  • Write operator= with a self-assignment check that returns *this
  • Say when the copy constructor runs and when operator= runs
  • State the Rule of Three and apply it to a class with pointer members

Plan (90 min)

TimeActivity
0-5Recap through questions: who owns this memory?
5-15Predict: what happens after Student s2 = s1;?
15-30Demo: the experiment, addresses in the debugger, the memory diagram
30-60Individual work: destructor, copy constructor, operator=
60-75Requirement change: addGrade
75-85Individual checks and the mandatory questions
85-90Three things you know now

Starting program

This class owns two heap blocks: the name and the grades. It has a destructor, but no copy constructor and no operator=. Read it, then run it.

#include <iostream>
#include <cstring>
using namespace std;

class Student {
    char* name;
    int* grades;
    int noGrades;
public:
    Student(const char* n, const int* g, int count) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
        noGrades = count;
        grades = new int[noGrades];
        for (int i = 0; i < noGrades; i++) grades[i] = g[i];
    }
    ~Student() {
        cout << "~Student frees name at " << (void*)name << endl;
        delete[] name;
        delete[] grades;
    }
    void setGrade(int i, int value) { grades[i] = value; }
    void print() const {
        cout << name << ":";
        for (int i = 0; i < noGrades; i++) cout << " " << grades[i];
        cout << "   name at " << (void*)name << ", grades at " << grades << endl;
    }
};

int main() {
    int g[] = {8, 9, 7};
    Student s1("Ana", g, 3);
    Student s2 = s1;   // no copy constructor yet: the compiler generates one
    s1.print();
    s2.print();
    s2.setGrade(0, 4);
    s1.print();        // did s1 change?
}                      // s2, then s1 are destroyed here

The experiment

Where do the copies point?

Run the program and compare the addresses printed for s1 and s2. Then put a breakpoint on s2.setGrade(0, 4); and look at s1.grades and s2.grades in the Locals window.

  1. Are the addresses the same or different?
  2. After setGrade, why does s1 show the grade 4?
  3. How many times does ~Student run, and which address does it free each time?
HintThe generated copy constructor copies each member as it is. What is the value of a char* member?
HintThe value of a pointer is an address. Copying it gives you a second pointer to the same block, not a second block.
Solution

The addresses are identical: s2.name == s1.name and s2.grades == s1.grades. This is a shallow copy. Writing through s2.grades writes into the only block that exists, so s1 sees it. At the end of main, s2 is destroyed first and frees both blocks. Then s1’s destructor frees the same addresses again: a double delete.

SHALLOW COPY                 DEEP COPY
s1 ----+                     s1 --------> block A
       +----> block          s2 --------> block B
s2 ----+

Predict, then check. This version has no destructor, so it leaks, but it never deletes anything twice:

#include <iostream>
using namespace std;
class Student {
public:
    int* grades;
    int noGrades;
    Student(int n) : grades(new int[n]), noGrades(n) {
        for (int i = 0; i < n; i++) grades[i] = 5;
    }
    // no destructor here: this version leaks, but it never deletes twice
};
int main() {
    Student s1(2);
    Student s2 = s1;
    s2.grades[0] = 10;
    cout << s1.grades[0] << " " << (s1.grades == s2.grades);
}

STACK
s1.name: ●
s1.grades: ●
HEAP
Ana
8 9 7

Student s1(...); - s1 owns two heap blocks.

Step 1/3

Implement the Rule of Three

Copy constructor

Write Student(const Student& s). It must allocate new blocks for name and grades and copy the contents, not the addresses. Run the starting program again: the addresses of s1 and s2 must now differ.

HintCopy what the normal constructor does, but read from s.name, s.grades and s.noGrades.
HintWhy must the parameter be a reference? Think about what passing a Student by value would call.
Solution
Student(const Student& s) {
    name = new char[strlen(s.name) + 1];
    strcpy(name, s.name);
    noGrades = s.noGrades;
    grades = new int[noGrades];
    for (int i = 0; i < noGrades; i++) grades[i] = s.grades[i];
}

If the parameter were Student s, passing the argument would itself need the copy constructor, which would call itself forever. The compiler rejects it.

Assignment operator

Write Student& operator=(const Student& s). The object already has its own blocks, so free them before copying. Test it with s3 = s1; and with s3 = s3;.

HintFour steps: check this == &s, free the old blocks, copy like the copy constructor, return *this;.
HintWithout the check, s3 = s3; frees its own name and then copies from the freed memory.
Solution
Student& operator=(const Student& s) {
    if (this == &s) return *this;
    delete[] name;
    delete[] grades;
    name = new char[strlen(s.name) + 1];
    strcpy(name, s.name);
    noGrades = s.noGrades;
    grades = new int[noGrades];
    for (int i = 0; i < noGrades; i++) grades[i] = s.grades[i];
    return *this;
}

Returning *this by reference lets you chain assignments, like a = b = c;, the same way it works for int.

Predict, then check. Which line prints copy and which prints assign?

#include <iostream>
using namespace std;
class Student {
public:
    Student() {}
    Student(const Student&) { cout << "copy" << endl; }
    Student& operator=(const Student&) { cout << "assign" << endl; return *this; }
};
int main() {
    Student s1;
    Student s2 = s1;
    Student s3;
    s3 = s1;
    Student s4(s1);
}

Mandatory questions

  1. What is a shallow copy?
  2. What is a deep copy?
  3. When is the copy constructor called?
  4. What does the compiler-generated copy constructor do?
  5. What is the difference between Student s2 = s1; and s2 = s1;?
  6. Why does operator= usually return *this?

Minimum task

A Student that copies correctly

Complete the class below: copy constructor and operator=. Run it before you start, so you see the bug, and again when you finish. When you are done, s1 must keep its grades after s2 and s3 change.

#include <iostream>
#include <cstring>
using namespace std;

class Student {
  char* name;
  int* grades;
  int noGrades;
public:
  Student(const char* n, const int* g, int count) {
      name = new char[strlen(n) + 1];
      strcpy(name, n);
      noGrades = count;
      grades = new int[noGrades];
      for (int i = 0; i < noGrades; i++) grades[i] = g[i];
  }
  // TODO: copy constructor (deep copy)
  // TODO: operator= (self-assignment check, free, copy, return *this)
  ~Student() {
      delete[] name;
      delete[] grades;
  }
  void setGrade(int i, int value) { grades[i] = value; }
  void print() const {
      cout << name << ":";
      for (int i = 0; i < noGrades; i++) cout << " " << grades[i];
      cout << endl;
  }
};

int main() {
  int g[] = {8, 9, 7};
  Student s1("Ana", g, 3);
  Student s2 = s1;
  s2.setGrade(0, 4);
  int h[] = {10};
  Student s3("Dan", h, 1);
  s3 = s1;
  s3.setGrade(1, 5);
  s3 = s3;
  s1.print();   // expected: Ana: 8 9 7
  s2.print();   // expected: Ana: 4 9 7
  s3.print();   // expected: Ana: 8 5 7
}
Input (stdin)

Open in Godbolt

Requirement change

New requirement: add void addGrade(int value), which grows the grades array by one element. Then copy a student and add a grade to the copy. Does the original change? What would go wrong if addGrade forgot to delete[] the old array, and what if it forgot to update noGrades?

Code in repository: L05

Romanian version