Copying a C++ object should be as safe and straightforward as copying an int. However, when your class manages dynamic memory, the compiler’s default copy behavior becomes a silent hazard. This lab will explain why the default copy is shallow and the problems it causes, and demonstrate how to implement the necessary copy constructor and copy assignment operator to ensure safe object copying.
§1 Dynamic Memory in Objects #
When a class contains pointers to dynamically allocated memory, special care must be taken to manage memory correctly. Without careful memory management, issues like shallow copying, memory leaks, or double deletion can arise.
An example of a class that utilizes dynamic memory:
#include <iostream>
#include <string>
// Struct representing the account owner
struct Owner {
std::string name;
std::string address;
};
// BankAccount class with dynamic reference to an Owner
class BankAccount {
public:
int accountNumber;
double balance;
Owner* owner; // Dynamic reference to an owner
// Constructor with dynamic owner
BankAccount(int accNum, double bal, std::string name, std::string address)
: accountNumber(accNum), balance(bal) {
owner = new Owner;
owner->name = name;
owner->address = address;
}
// Destructor to clean up dynamic memory
~BankAccount() {
delete owner;
}
// Display account information
void displayAccountInfo() const {
std::cout << "Account Number: " << accountNumber << std::endl;
std::cout << "Balance: $" << balance << std::endl;
std::cout << "Owner Name: " << owner->name << std::endl;
std::cout << "Owner Address: " << owner->address << std::endl;
}
};
int main() {
// Create a bank account with the owner
BankAccount account1(101, 1000.50, "John doe", "123 example st.");
// Copy the account to a new object
BankAccount account2 = account1;
// update the original account name
account1.owner->name = "john deere";
// Display account1 information
account1.displayAccountInfo();
// Display account2 information
std::cout << "\nCopied account information:\n";
account2.displayAccountInfo();
}
$ ./test
Account Number: 101
Balance: $1000.5
Owner Name: john deere
Owner Address: 123 example st.
Copied account information:
Account Number: 101
Balance: $1000.5
Owner Name: john deere // this matches account1 since both objects point to the same memory
Owner Address: 123 example st.
test(52045,0x2039fb240) malloc: Double free of object 0x14b605fc0
zsh: abort ./test
Since we perform a simplistic copy of the object, both BankAccount instances share the same dynamic memory for the owner member variable. This leads to multiple issues:
- both objects have the same owner name
- when destructed, we encounter double free errors due to the shared dynamic memory.
Let’s implement some fixes for these issues.
§2 Copying Objects with Dynamic Memory #
Shallow Copy vs Deep Copy
- Shallow Copy: When copying an object with a pointer, a shallow copy means copying the pointer itself, not the data it points to. This can cause issues if another object modifies or deletes the memory, affecting the copied object as they both share the same memory. The example in the previous section used a shallow copy.
- Deep Copy: A deep copy duplicates both the pointer and the data it points to. Each object has its own copy of the data, preventing unexpected behavior when one object modifies or deletes its data.
To implement a deep copying functionality, a few components of the class need to be modified:
Copy Constructor
A copy constructor creates a new object as a copy of an existing object. By default, C++ provides a shallow copy constructor, but we can override this to implement deep copying.
operator= Overloading
Overloading the = operator allows us to define how objects are assigned to one another, ensuring deep copying if the object contains dynamically allocated memory.
Example: Implementing Deep Copy (snippet of full BankAccount class)
// Struct representing the account owner
struct Owner {
std::string name;
std::string address;
};
class BankAccount {
...
Owner* owner; // Dynamic reference to an owner
// Copy constructor for deep copy
BankAccount(const BankAccount& other)
: accountNumber(other.accountNumber), balance(other.balance) {
owner = new Owner; // Deep copy constructor
// create new instance and manually copy pointers
owner->address = other.owner->address;
owner->name = other.owner->name;
}
// Assignment operator for deep copy
BankAccount& operator=(const BankAccount& other) {
if (this != &other) { // avoid deep copy of self `obj = obj`
accountNumber = other.accountNumber;
balance = other.balance;
delete owner; // Clean up existing owner pointer, since it is being overwritten
owner = new Owner; // Deep copy of new owner
// create new instance and manually copy pointers
owner->address = other.owner->address;
owner->name = other.owner->name;
}
return *this;
}
...
};
- The copy constructor ensures deep copying by allocating new memory for the copied object and explicitly duplicating the existing data.
- The assignment= operator handles cases where one object is assigned to another (e.g.,
account2 = account1). We first delete the old memory in the destination object, then allocate new memory and copy the data.
By adding a deep copy constructor and an overloaded assignment operator to our class, copying objects now works as expected, without the unintended side effects caused by shared memory.
§3 Pointers to Objects #
Pointers can also be used to manage instances of classes dynamically. when working with classes stored in pointers, access works a bit differently.
Example: Pointer to an object
#include <iostream>
class Point {
public:
int x, y;
// Constructor
Point(int xVal, int yVal) : x(xVal), y(yVal) {}
// Method to display the point
void display() const {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
// Dynamically allocate a Point object
Point* p = new Point(10, 20);
// Access class members using the pointer (uses arrow notation)
p->display(); // Outputs: (10, 20)
// Deallocate the memory
delete p;
}
p->display()is the syntax to call a method on an object referenced by a pointer. It’s equivalent to(*p).display()but more readable.
This technique is useful when managing objects that are created dynamically (on the heap) and can be passed around by reference.
§4 Object Pointer Chain (Linked List) #
the quintessential use of pointers in objects is the linked list.
The Linked list is one of the available containers in the standard library, the official implementation is a “doubly-linked” list, meaning that each “node” points to the next and previous node in the list, allowing traversal in both directions.
We will explore a simplified variant: the singly-linked list, refer to the diagram below to visualize the structure of the singly-linked list.

Lets implement this singly-linked list as a practical example of complex memory management in objects.
We first need to represent a Node, this is a simple structure that simply holds our data, and a reference to the next Node in the list.
// Node structure that holds an integer value and a pointer to the next node in the list
struct Node {
int data; // Data part of the node
Node* next; // Pointer to the next node
};
Next, we need to define our SinglyLinkedList class, this class will be responsible for creating, managing, and cleaning up the dynamic linked list nodes.
// Singly Linked List class
class SinglyLinkedList {
private:
Node* head; // Pointer to the head (first node) of the linked list
public:
// Constructor to initialize the linked list with an empty head
SinglyLinkedList() {
head = nullptr; // When the list is created, the head is set to nullptr (empty list)
}
};
This class’s constructor will create the initial pointer to our Node objects, but currently does not have a way to add any nodes (or data) to the list, lets implement a method to add new nodes.
// Singly Linked List class
class SinglyLinkedList {
...
// Function to add a new node with the given value at the end of the list
void append(int value) {
// Create a new node with the given data and set its `next` pointer to nullptr
Node* newNode = new Node();
newNode->data = value;
newNode->next = nullptr;
// If the list is empty (head is nullptr), make the new node the head of the list
if (head == nullptr) {
head = newNode;
} else {
// If the list is not empty, traverse to the end of the list
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
// Attach the new node at the end of the list
temp->next = newNode;
}
}
};
With the append() function, we can now add new data to the list, this works by instantiating a new Node object, then adding the new node to the SinglyLinkedList. If the list is empty, we only need to update the head pointer, if the list has elements, we must “walk” the list of nodes until we find the end of the list, then append the new node to the end.
Next, lets implement deleting nodes from the list:
// Singly Linked List class
class SinglyLinkedList {
...
// Function to delete a node with a given value from the list
void deleteNode(int value) {
// If the list is empty, print a message
if (head == nullptr) {
cout << "List is empty, cannot delete." << endl;
return;
}
// If the node to be deleted is the head (first) node
if (head->data == value) {
Node* temp = head; // Store the current head node
head = head->next; // Move the head pointer to the next node
delete temp; // Delete the old head node
return;
}
// If the node to be deleted is somewhere else in the list
Node* temp = head; // Start from the head
Node* prev = nullptr; // Keep track of the previous node
// Traverse the list to find the node with the matching value
while (temp != nullptr && temp->data != value) {
prev = temp; // Move the previous pointer
temp = temp->next; // Move to the next node
}
// If the value is not found in the list, print a message
if (temp == nullptr) {
cout << "Value not found in the list." << endl;
return;
}
// If the node is found, update the previous node's next pointer to skip the current node
prev->next = temp->next;
delete temp; // Delete the node containing the value
}
};
The deleteNode() method will traverse a list, looking for a Node that contains the requested value, then deleted it by updating the previous node pointer to point to the next node in the list, before deleting the requested node from memory.
Lets now add a method to print the contents of the linked list:
// Singly Linked List class
class SinglyLinkedList {
...
// Function to display the entire linked list
void display() {
// If the list is empty, print a message
if (head == nullptr) {
cout << "List is empty." << endl;
return;
}
// Traverse the list and print each node's data
Node* temp = head;
while (temp != nullptr) {
cout << temp->data << " -> "; // Print data with an arrow indicating the next node
temp = temp->next; // Move to the next node
}
cout << "NULL" << endl; // Indicate the end of the list with "NULL"
}
// prints list contents: e.g. 10 -> 20 -> 30 -> 40 -> NULL
};
This display() function simply “walks” through the list, printing out the value of each node until it reaches the end of the list.
Since this class works with dynamic memory, we need to ensure that the dynamic memory is properly cleaned up when the object is destroyed, lets implement a destructor:
// Singly Linked List class
class SinglyLinkedList {
...
// Destructor to delete all nodes and free memory when the list object is destroyed
~SinglyLinkedList() {
Node* temp = head;
// Traverse the list and delete each node one by one
while (temp != nullptr) {
Node* nextNode = temp->next; // Store the pointer to the next node
delete temp; // Delete the current node
temp = nextNode; // Move to the next node
}
}
};
This destructor “walks” through the list and deletes each node’s memory until the entire list is deleted.
Finally, lets implement a main() function to test our singlyLinkedList class.
// Main function to demonstrate the functionality of the SinglyLinkedList class
int main() {
SinglyLinkedList list;
// Append values to the list
list.append(10);
list.append(20);
list.append(30);
list.append(40);
// Display the list contents
cout << "list contents: " << endl;
list.display(); // prints 10 -> 20 -> 30 -> 40 -> NULL
// Append another value to the list and display the updated list
list.append(50);
cout << "\nAdding 50 to the list: " << endl;
list.display(); // prints 10 -> 20 -> 30 -> 40 -> 50 -> NULL
// Delete a node with value 20 and display the updated list
cout << "\nDeleting 20 from the list." << endl;
list.deleteNode(20);
list.display(); // prints 10 -> 30 -> 40 -> 50 -> NULL
// Delete the head node (value 10) and display the updated list
cout << "\nDeleting 10 from the list." << endl;
list.deleteNode(10);
list.display(); // prints 30 -> 40 -> 50 -> NULL
}
The full implementation can be found in linked-list.cpp in the assignment
§5 Further Reading #
Further reading on subjects in this lab can be found in the textbook:
- Chapter 11.5 Copy Constructors
- Chapter 11.6 Operator Overloading
- Section 18 Linked lists
§6 Questions #
- What is the main issue with copying objects that contain dynamically allocated memory using the default copy mechanism? When you copy objects using a default copy constructor, what happens when you modify dynamic memory values across both objects?
- What is the difference between a shallow copy and a deep copy of a class?
- What two components must be overridden for a class to support deep copying of dynamically allocated memory?
- Why do we need to overload the
=operator in classes with dynamically allocated memory? Specifically why is the default assignment operator insufficient?
- In the provided BankAccount class, what does the copy constructor do to ensure a deep copy?
- What are the steps to take in the assignment (=) operator when implementing deep copying for a class with dynamic memory?
- In the example
BankAccountclass, what happens if you don’t delete the existing owner in the assignment operator?
- What is the syntax to call a method on an object that is stored in a pointer? e.g.
BankAccount *myAccount = new BankAccount;
// how do I run the addFunds() method?
- Why is the condition
if (this != &other)necessary in the overloaded assignment operator when performing a deep copy?