Skip to content

Handout: Lecture 9 In-Class Exercises

Part A is pen-and-paper: trace the pointer surgery of a push_front and a delete on the board, and spot two classic linked-list bugs. Part B is at the keyboard: build the core linked-list operations one at a time - traverse, insert at the head, delete a node, and free the whole list. None of these repeats an earlier exercise; they drill today's moves - the self-referential node, the walk-to-NULL loop, and the pointer surgery behind insert, delete, and free.

Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile with warnings on:

clang -Wall -Wextra -std=c17 myprog.c -o myprog

Reminders that will keep you out of trouble today:

  • A node is a self-referential struct: struct node { int data; struct node *next; };. It must keep its tag - node_t does not exist yet inside its own definition.
  • The list is named by a head pointer; NULL marks the end; an empty list is head == NULL. Traverse with for (node_t *cur = head; cur != NULL; cur = cur->next).
  • Insert and delete return the new head, because both can change which node is first. In the caller, write head = push_front(head, x);.
  • To delete, unlink first (prev->next = cur->next;) then free(cur) - never the other way round.
  • To free the list, save cur->next before you free(cur), or you read freed memory. One free per malloc, node by node.

Set up

mkdir -p ~/cmsc14300/lec09
cd ~/cmsc14300/lec09

Start every keyboard exercise from this node type and includes:

#include <stdio.h>
#include <stdlib.h>

struct node { int data; struct node *next; };
typedef struct node node_t;

Part A - Tracing pointers (pen and paper)

Exercise A1 - Draw the surgery

Start from this three-node list:

head -> [10 | -] -> [20 | -] -> [30 | NULL]
  1. Draw the list after head = push_front(head, 5);. Which node is the new head, and what does its next point at? How many existing next pointers changed?
  2. Starting again from the original three-node list, draw the list after head = delete_value(head, 20);. Which single next pointer changed, and which node gets freed?
  3. In one sentence each: why does inserting at the head touch no existing node, while deleting 20 requires a handle on the node before it?

Exercise A2 - Spot the bug

Each function below has one bug that today's rules warn about. Name the bug and fix it.

/* (a) free every node in the list */
void free_list(node_t *head) {
    for (node_t *cur = head; cur != NULL; cur = cur->next) {
        free(cur);
    }
}
/* (b) insert value at the front; caller writes: push_front(head, x); */
void push_front(node_t *head, int value) {
    node_t *n = malloc(sizeof(node_t));
    n->data = value;
    n->next = head;
    head = n;
}
  1. In (a): the loop advances with cur = cur->next right after free(cur). What is that called, and how do you fix it? (Hint: you need a temporary.)
  2. In (b): the caller's list never changes and one node leaks on every call. Give both reasons and rewrite the signature and the call so it works.

Part B - At the keyboard

Exercise B1 - Traverse: print and count

Build the three-node list 10 -> 20 -> 30 by hand (three mallocs, link them, terminate with NULL), then write the two traversal functions.

void print_list(node_t *head);      /* prints: 10 -> 20 -> 30 -> NULL */
int  length(node_t *head);          /* returns the number of nodes */
10 -> 20 -> 30 -> NULL
length = 3
  • print_list walks cur = head to NULL, printing cur->data -> each step, then NULL\n at the end. On an empty list (head == NULL) it prints just NULL.
  • length is the same walk, counting nodes instead of printing. Test it on your three-node list and on the empty list (NULL), which should give 0.
  • Free your nodes at the end (you will write free_list in B4; until then, free the three by hand).

Exercise B2 - push_front: insert at the head

Write push_front, then use it to build a list from numbers you read in a loop.

node_t *push_front(node_t *head, int value);
enter numbers (0 to stop): 10 20 30 0
list: 30 -> 20 -> 10 -> NULL
  • In push_front: malloc a node, check it against NULL, set data, point its next at the old head, and return the new node as the new head.
  • In main: start with node_t *head = NULL;, read integers with scanf until you read 0, and for each do head = push_front(head, x);. Print with print_list from B1. Notice the list comes out reversed - each new value goes on the front.
  • Forgetting head = in the caller leaks the new node and leaves the list unchanged
  • exactly the bug from A2(b).

Exercise B3 - delete_value: remove a node

Write delete_value, which removes the first node whose data equals a given value and returns the (possibly new) head.

node_t *delete_value(node_t *head, int value);
before: 10 -> 20 -> 30 -> NULL
delete 20: 10 -> 30 -> NULL
delete 10: 30 -> NULL
delete 99: 30 -> NULL   (unchanged)
  • Walk with two pointers, prev (starting NULL) and cur (starting head), stopping when cur->data == value or cur == NULL.
  • If cur == NULL, the value is not present - return head unchanged. If prev == NULL, you are deleting the head - the new head is cur->next. Otherwise splice: prev->next = cur->next;. In all found cases, free(cur) after unlinking.
  • Test all four cases: delete the head, a middle node, the last node, and a value that is not in the list.

Exercise B4 - free_list: free every node

Write free_list and use it to clean up. Confirm it is leak-free under valgrind.

void free_list(node_t *head);
valgrind ./myprog
# ... All heap blocks were freed -- no leaks are possible
  • Walk the list, but save cur->next into a local before free(cur), then advance to the saved value. Freeing first and then reading cur->next is a use-after-free (the bug from A2(a)).
  • In main, after you are done with the list, call free_list(head) and set head = NULL. Build a list with B2, print it, delete a node or two with B3, then free it - and run the whole thing under valgrind.

Stretch - take it further

Append at the tail

Add push_back, which inserts a new node at the end of the list (the opposite end from push_front), and returns the head.

node_t *push_back(node_t *head, int value);
push_back 10, 20, 30 -> 10 -> 20 -> 30 -> NULL
  • Make the new node with next = NULL (it will be the last one). If the list is empty (head == NULL), the new node is the head - return it. Otherwise walk to the last node (the one whose next == NULL) and point its next at the new node, then return the unchanged head.
  • Notice the asymmetry: push_front is O(1), but push_back must walk the whole list to find the end - O(n) - because a plain singly linked list only holds a head. (Keeping a tail pointer too would make it O(1); that is a design we will revisit.)