Because linked lists are responsible for managing dynamic memory (using new to create nodes), they also are responsible for properly deleting that memory and doing deep copies of that memory when needed. This means we should be implementing a destructor, copy constructor, and copy assignment operator (the Rule of Three).
If the linked list itself is destroyed, say by going out of scope, we need to make sure that all the nodes in the list are deleted to avoid memory leaks. This of course is the responsibility of the destructor.
We might also want to provide a method to clear the list explicitly, which would delete all the nodes and leave the list empty. If so, the destructor can call this method to avoid duplicating code.
There is no easy way to delete all the individual nodes. If we do delete head;, it would only delete the first node in the list. (And the rest of the nodes would then be unreachable and cause a memory leak.) Instead, we need to remove the nodes one by one until the list is empty.
This loop will keep calling removeStart() until the head pointer is nullptr, indicating the list is empty. Each call to removeStart() will delete the first node in the list, so by the time the loop finishes, all nodes will have been deleted.
It is worth stopping to think about the efficiency of this approach. In particular, we should make sure that removeStart() doesnβt represent a costly operation that would make this approach inefficient. Although we have not analyzed the efficiency of removeStart() yet, it is not hard to see that it runs in constant timeβit has no loops or recursive calls. So calling it is no more expensive than doing the equivalent work directly in clear.
You can click and drag on the animation area to pan around. Use the Ctrl + mouse wheel (or touchpad scroll gesture) while over the animation area to zoom in and out.
When copying a linked list, either using a copy constructor or an assignment operator, we need to make sure to perform a deep copy of the nodes. We canβt just do something like newList.head = oldList.head; because we would end up with a shallow copy where both lists share the same nodes, leading to potential issues like double deletion.
To perform a deep copy, we can iterate through the nodes of the original list and create new nodes for the new list with the same data values. Each new node will need to be added to the end of the list of copied nodes we are building. Currently, this will be challenging since we donβt have an easy way to add nodes to the end of the list.
We could make an efficient implementation by tracking the location of what node we are copying in the other list (sourceCur) and the last node in the new list (copyCur).
Figure27.7.3.Making a deep copy using current pointers in both the source and destination lists. When sourceCur advances to the next node, we will make a new node and copyCur will be updated to point.
if (other.head == nullptr) {
head = nullptr // If the original list is empty, the new list should also be empty
return
}
ListNode* sourceCur = other.head // Pointer to traverse original list
head = new Node(other.head->data); // Create the first node of new list
ListNode* copyCur = head; // Pointer to track last node in new list
while (sourceCur->next != nullptr) {
sourceCur = sourceCur->next; // advance to the next node in the source
copyCur->next = new Node(sourceCur->data);
copyCur = copyCur->next; // advance copyCur to the new last node
}
However, we will soon improve our linked list to simplify inserting at the end of a list. That will allow us to simplify this logic significantly. In this simple linked list, we will just disable copying by deleting the copy constructor and assignment operator. That way we wonβt accidentally create shallow copies of our list and cause memory management issues. To do so, we simply set each of them to delete in the class definition: