Lab 11: Classes and Pointers

Object Oriented Programming with Dynamic Memory

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:

Let’s implement some fixes for these issues.

§2 Copying Objects with Dynamic Memory #

Shallow Copy vs Deep Copy

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;
  }
...
};

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;
}

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:

§6 Questions #

  1. 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?
  1. What is the difference between a shallow copy and a deep copy of a class?
  1. What two components must be overridden for a class to support deep copying of dynamically allocated memory?
  1. Why do we need to overload the = operator in classes with dynamically allocated memory? Specifically why is the default assignment operator insufficient?
  1. In the provided BankAccount class, what does the copy constructor do to ensure a deep copy?
  1. What are the steps to take in the assignment (=) operator when implementing deep copying for a class with dynamic memory?
  1. In the example BankAccount class, what happens if you don’t delete the existing owner in the assignment operator?
  1. 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?
  1. Why is the condition if (this != &other) necessary in the overloaded assignment operator when performing a deep copy?