One responsibility of implementing a container class is defining a destructor to clean up resources when the object is destroyed. However, we also need to consider what happens when we copy an object of that class. The default copy behavior for objects is to perform a shallow copy, which means that it copies the values of the member variables directly.
PlayerList original(3); // Create a PlayerList with space for 3 players
original.setPlayerName(0, "Alice");
original.setPlayerName(1, "Bob");
original.setPlayerName(2, "Carlos");
PlayerList copy(original); // Copy the original PlayerList
This is a shallow copy. There is really only one array and it is shared by original and copy. This means changing one will change the other. It also means that we will have an error as they go out of scope: First one will be destroyed and it will delete the array. Then the other PlayerList will be destroyed and it will also try to delete the array.
This program demonstrates the issue. After copying pList, it changes the name of the second player in the copy. But when we then print out pList, it has been modified as well. AddressSanitizer alerts us to a heap-use-after-free in ~PlayerList(). That is the second destructor call trying to delete the array that has already been deleted.
Things would be even worse if one of the two PlayerLists was scoped to last longer than the other. Whichever one was destroyed first would delete the array and the other would be left with a dangling pointer.
To avoid these problems, we need to implement a deep copy. A deep copy means that we create a new array and copy the values from the original array into the new array. This way, each PlayerList object has its own separate copy of the data. Something like this:
To do this, we need to implement a copy constructor. A copy constructor is a special constructor that is called when an object is initialized with another object of the same class. It allows us to define how the copying should be done, including performing a deep copy of any dynamically allocated memory. The prototype for a copy constructor always looks like:
export class PlayerList {
private:
string* m_players; // pointer to the array of player names
int m_size; // number of players in the list
public:
...
PlayerList(const PlayerList& other); // Copy constructor
...
};
PlayerList::PlayerList(const PlayerList& other) {
// Copy non-pointer members
m_size = other.m_size;
// Allocate own dynamic storage
m_players = new string[m_size];
// Copy the contents from the other PlayerList
for (int i = 0; i < m_size; ++i) {
m_players[i] = other.m_players[i];
}
}
...
Now, when we run the same main program, we should see that changing the copy does not change the array used by the original. There also will not be any memory errors:
We want to add a copy constructor to the NumberList class. It manages a dynamic array of integers. Examine the members and decide what needs to be deleted. Then build the function as it would appear when defined outside the class.