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.
- 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)
| Time | Activity |
|---|---|
| 0-5 | Recap through questions: who owns this memory? |
| 5-15 | Predict: what happens after Student s2 = s1;? |
| 15-30 | Demo: the experiment, addresses in the debugger, the memory diagram |
| 30-60 | Individual work: destructor, copy constructor, operator= |
| 60-75 | Requirement change: addGrade |
| 75-85 | Individual checks and the mandatory questions |
| 85-90 | Three 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.
- Are the addresses the same or different?
- After
setGrade, why doess1show the grade 4? - How many times does
~Studentrun, and which address does it free each time?
Hint
The generated copy constructor copies each member as it is. What is the value of achar* member?Hint
The 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);
} 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.
Hint
Copy what the normal constructor does, but read froms.name, s.grades and s.noGrades.Hint
Why must the parameter be a reference? Think about what passing aStudent 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;.
Hint
Four steps: checkthis == &s, free the old blocks, copy like the copy constructor, return *this;.Hint
Without 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
- What is a shallow copy?
- What is a deep copy?
- When is the copy constructor called?
- What does the compiler-generated copy constructor do?
- What is the difference between
Student s2 = s1;ands2 = s1;? - 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)
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?