1

Value, Reference, and Pointer Semantics

Why this matters

This refresher is meant to help you review concepts from CS31, CS32, and CS35L before we start CS131. We’ll begin with parameter passing: which changes inside a function affect the caller, and why?

This is optional practice. You don’t need to turn anything in. The tests and knowledge checks provide feedback; you can continue without passing them and return to a problem later.

🎯 You will learn to

  • Work out whether an assignment inside a function is visible to the caller
  • Tell the difference between changing a pointer and changing what it points to
  • Write the reference and pointer versions of a function that modifies its argument

The Point class

Everything on this step uses the following Point class, which makes all of its members public for simplicity:

class Point {
public:
    Point(int x = 0, int y = 0) { x_ = x; y_ = y; }
    void display() const { cout << "(" << x_ << ", " << y_ << ")" << endl; }
    int x_, y_;
};

Here’s how each version below gets used:

Point pt(10, 10);
modify(pt);       // or modify(&pt) for the pointer versions
pt.display();

Each version sets x_ to 50 on line A. Line B either assigns a new Point(20, 30) to an object or reassigns the local pointer. Compare both the parameter declaration and the left side of line B:

// Version A - pass by value
void modify(Point p) {
    p.x_ = 50;          // line A
    p = Point(20, 30);  // line B
}

// Version B - pass by reference
void modify(Point &p) {
    p.x_ = 50;          // line A
    p = Point(20, 30);  // line B
}

// Version C - pass by pointer, reassigning the pointer
void modify(Point *p) {
    p->x_ = 50;             // line A
    p = new Point(20, 30);  // line B
}

// Version D - pass by pointer, writing through the pointer
void modify(Point *p) {
    p->x_ = 50;         // line A
    *p = Point(20, 30); // line B
}

✏️ Predict before you read on

For each version, figure out what will be printed and try to explain why you get that result. Don’t just compile and run the code to get answers, but try to understand what’s really happening under the hood.

Write down all four answers before you open the reveal. Each one is:

  • (a) (10, 10) — the caller sees neither line
  • (b) (50, 10) — the caller sees line A but not line B
  • (c) (20, 30) — the caller sees both lines

Versions C and D are the interesting ones. Their parameter lists are identical and their line A is identical. Only line B is different.

Reveal — all four answers

A prints (10, 10). Why? Because when we pass by value we make a copy of the original pt variable and pass that in. Both lines modify a temporary copy, and the copy dies at the closing brace, so no changes are made to the original pt.

B prints (20, 30). Why? Because when you pass by reference, the formal parameter p is essentially an alias for the passed-in variable pt. Any change, including assigning a whole new value to p, modifies the original pt as if you'd referred to it instead of p.

C prints (50, 10). Why? Because here we're passing the address of pt into modify. Think of pt as a house that holds some stuff (the values x_ and y_), and think of p as a piece of paper where we wrote down the address of that house. Line A uses the address on the paper to find the house and change its x_ to 50, so that change sticks. But line B only scratches out the address on our paper and writes down the address of a brand new house. That does nothing to the contents of the original house, so pt.y_ is still 10. We also have a memory leak, since we never delete the memory we allocated with new.

D prints (20, 30). Why? Because *p = Point(20, 30); creates a temporary new Point at 20, 30 and copies its contents into the memory pointed to by p, which is the pt variable. Using our analogy again: we read the address on the paper, drive to that address (that's what the * means), and update the contents of the house.

C and D are the whole lesson. p = ... changes where the arrow points. *p = ... changes what it points at.

🔧 Now write two of them yourself

Use Run to compile and run the current C++17 program in the browser. The output panel shows compiler messages and anything your program prints. After editing, choose Run again to compile the new version. If compilation fails, fix the first reported error and try again.

Open param_passing.cpp. modify_by_value is already written for you as a worked example. Fill in the other two so that both leave the caller’s point at (20, 30).

  • modify_by_reference — you pick how the parameter is declared.
  • modify_by_pointer — the parameter is already Point *p. Reach the caller’s object on every assignment. Don’t point p somewhere else.

The supplied main() creates a separate Point(10, 10) for each call and displays the result. Run the starter once, then compare its output with your prediction as you complete the two functions. You can change the starting coordinates in main() to try more examples.

Check your work

Choose Test My Work to compile and call each function on separate Point(10, 10) and Point(-4, 7) objects. The automatic checks verify that:

  • modify_by_value leaves the caller’s coordinates unchanged.
  • modify_by_reference leaves the caller’s point at (20, 30).
  • modify_by_pointer, called with the point’s address, leaves it at (20, 30).

The checks inspect the actual coordinates, so changing the display format or printing the expected answers cannot change the result. They do not require particular parameter names or a particular way of assigning the coordinates. You can continue without passing these checks.

For each assignment, explain whether its left side names a local copy, the caller’s object, or the local pointer. This reflection is for your own review; the automatic checks do not grade your explanation.

Starter files
cs131/param_passing.cpp
#include <iostream>
using namespace std;

class Point {
public:
    Point(int x = 0, int y = 0) {
        x_ = x;
        y_ = y;
    }
    void display() const {
        cout << "(" << x_ << ", " << y_ << ")" << endl;
    }

    int x_, y_;
};

// Version A - by value. Finished for you.
// The caller still sees (10, 10) after this returns.
void modify_by_value(Point p) {
    p.x_ = 50;
    p = Point(20, 30);
}

// Version B - by reference.
// Goal: after this returns, the CALLER's point is (20, 30).
// You choose how the parameter is bound.
void modify_by_reference(Point p) {
}

// Version D - by pointer, written so the caller sees both lines.
// Goal: after this returns, the CALLER's point is (20, 30).
// The parameter is already a pointer. Do not make it point elsewhere.
void modify_by_pointer(Point *p) {
}

int main() {
    Point by_value(10, 10);
    modify_by_value(by_value);
    cout << "by value: ";
    by_value.display();

    Point by_reference(10, 10);
    modify_by_reference(by_reference);
    cout << "by reference: ";
    by_reference.display();

    Point by_pointer(10, 10);
    modify_by_pointer(&by_pointer);
    cout << "by pointer: ";
    by_pointer.display();
    return 0;
}
2

The Point Class in Python

Why this matters

For this step we’ll explore classes and parameter passing in Python to get a feel for how it’s similar to and different from C++. As it turns out, Python has one parameter passing scheme: object references. Compare the caller-visible behavior with the four C++ examples from Step 1.

🎯 You will learn to

  • Write a C++ class in Python 3.x syntax
  • Explain which C++ example has the same caller-visible behavior

From C++ to Python

Same class, different syntax. Three things change:

C++ Python Why
Point(int x = 0, int y = 0) { ... } def __init__(self, x=0, y=0): The constructor has a fixed name, and the object it’s working on is an explicit first parameter
x_ = x; self.x_ = x This assignment creates an instance attribute; assigning to bare x_ inside the method would create a local variable
public: / private: (nothing) In Python, everything is public by default

✏️ Predict before you run

point_lab.py already contains the Python version of the driver from Step 1:

def modify(p):
    p.x_ = 50          # line A
    p = Point(20, 30)  # line B

pt = Point(10, 10)
modify(pt)
pt.display()

Once you’ve written the class and pressed Run, what gets printed?

  • (a) (10, 10) — like C++ version A, Python copies the object
  • (b) (50, 10) — like C++ version C, line A lands but line B doesn’t
  • (c) (20, 30) — like C++ version B or D, both lines land
  • (d) A TypeError — you can’t assign to a parameter in Python

Pick a letter before you press Run, then see whether the machine agrees with you.

🔧 Your task

Open point_lab.py and write an equivalent version of the Point class in Python 3.x syntax. Fill in __init__ and display.

  • Store the coordinates as self.x_ and self.y_, keeping the C++ names so the two files are easy to compare. Preserve the default values so Point() creates a point at (0, 0).
  • display must print the point’s two coordinates, separated by a comma, and return None. Parentheses and whitespace are up to you: (50, 10) and 50, 10 are both accepted.

Run your Python code here

Finish your edits in point_lab.py, then click Run above the output panel. There is no separate compile command for these Python steps. Run executes the file, including the driver at the bottom; once your class is complete, this driver should print (50, 10).

After each edit, click Run again to see the new result. If Python reports an error, use the file name and line number in the traceback to find the problem, then fix it and rerun. Clear clears the output; it does not change your code.

Use the same workflow on the later Python steps. Test My Work runs additional checks on different inputs, so matching the driver’s output alone does not mean every case is correct. Both running your code and passing the checks are optional; Next lets you continue.

Starter files
point_lab.py
class Point:
    """Python twin of the C++ Point from Step 1."""

    def __init__(self, x=0, y=0):
        # TODO: give this instance its two coordinates, named x_ and y_
        pass

    def display(self):
        # TODO: print this point in the same format as the C++ version,
        # e.g. a point at x=50, y=10 prints as:  (50, 10)
        pass


def modify(p):
    p.x_ = 50          # line A
    p = Point(20, 30)  # line B


# The Python twin of the C++ driver from Step 1.
pt = Point(10, 10)
modify(pt)
pt.display()
3

Mutation Versus Rebinding

Why this matters

In Homework 0, the Python function changes a point’s attribute but cannot replace the caller’s variable by assigning to its parameter. Let’s try the same distinction with a swap: what changes when we swap attributes, and what changes when we swap local names?

🎯 You will learn to

  • Apply the change-it versus reassign-it rule to say which changes survive a call
  • Explain why rebinding parameters does not swap the caller’s variables

The rule

A call binds the function’s parameter names to the same objects the caller passed in. Two very different things can happen next:

  • You change the object. p.x_ = 50, items.append(3), d["k"] = 1. You reach through the name to the object itself. The caller is holding that same object, so the caller sees the change.
  • You reassign the name. p = Point(20, 30), items = []. You point the function’s own name at a different object. The caller’s name never moved.

🔧 Your task

Open references_lab.py and write two functions.

  1. swap_coords(p) — swap one point’s own x_ and y_ in place. The caller has to see the change without reassigning anything.
  2. swap_points(a, b) — write the naive version on purpose: the one you’d write in a language where parameters are aliases for the caller’s variables. Reassign a and b to each other and nothing else. Don’t touch their attributes.

Writing the broken one deliberately is the point of this step. The checker verifies that swap_coords works, that swap_points really does attempt the swap, and that the caller’s variables come back unswapped.

✏️ Predict before you run

After swap_coords(first) and then swap_points(first, second), what do the three display() calls print?

Reveal — after you have run it

(2, 1), then (2, 1) and (3, 4).

swap_coords changes the object, so the change sticks. swap_points reassigns two local names that disappear at the return, so first and second in the caller never move. During the call, a and b really were pointing at the caller's objects. Python just gives you no way to reach back through a parameter to the caller's variable.

To swap the caller's names, write first, second = second, first in the caller, or return a pair and assign that result there.

Starter files
references_lab.py
class Point:
    def __init__(self, x=0, y=0):
        self.x_ = x
        self.y_ = y

    def display(self):
        print(f"({self.x_}, {self.y_})")


def swap_coords(p):
    """Swap this point's own x_ and y_, in place.

    The caller must see the swap without reassigning anything.
    """
    # TODO
    pass


def swap_points(a, b):
    """Deliberately naive: rebind a and b to each other.

    Write the version you would write in a language where parameters
    are aliases for the caller's variables. Do not touch .x_ or .y_ —
    watching this one fail is the point of the exercise.
    """
    # TODO
    pass


first = Point(1, 2)
second = Point(3, 4)

swap_coords(first)
first.display()

swap_points(first, second)
first.display()
second.display()
4

Recursion With No Loops Allowed

Why this matters

In this step we’ll review basic recursion. Recursive definitions will be important in the functional programming part of CS131, so we’ll practice solving a smaller problem and using its answer without writing a loop.

🎯 You will learn to

  • Write a recursive function whose base case covers every permitted input
  • Break an array traversal into “the first item” plus “the rest”

Recursion on the Rest of an Array

An int arr[] function parameter is treated as a pointer, so these functions also receive an element count. arr + 1 points one element later in the same array. This is the pointer arithmetic from CS31, and it’s how you say “the rest of the array” without copying anything:

Concept C++ Python
the first item arr[0] values[0]
everything but the first arr + 1, n - 1 values[1:]
is there only one item left? n == 1 len(values) == 1

Here’s one way to organize both functions:

  1. Base case. What’s the answer when there’s exactly one item left? Both functions here are guaranteed at least one item, so n == 1 is a safe place to stop.
  2. Recursive case. Ask the same function for the answer to arr + 1, n - 1, then combine that answer with arr[0].

✏️ Predict before you write

For index_of_biggest, the recursive call answers a question about the sub-array starting at arr + 1. If that call returns 2, what is that 2 an index into?

  • (a) The original array, so use it as is
  • (b) The sub-array, so the original index is one higher
  • (c) The sub-array, so the original index is one lower

This is the classic off-by-one in this exact problem, so it’s worth deciding before you type.

Reveal

(b). The recursive call knows nothing about the element you sliced off. Index 2 of arr + 1 is index 3 of arr. So every index that comes back from recursion needs + 1 before you can compare it against arr[0] or return it. Compare arr[rest] with arr[0], where rest is the adjusted index.

That's why find_biggest is easier than index_of_biggest. Values mean the same thing no matter which sub-array they came from. Indices don't.

🔧 Your task

Open recursion.cpp and write both functions. Given an array that is guaranteed to have at least one item, find_biggest returns the largest value in the array, and index_of_biggest returns the index of the largest value, where the first item is at index zero. Both functions receive at least one item. For index_of_biggest, assume there are no duplicate numbers. No looping allowed! Recursive helpers are welcome.

int arr[] = {-1, 10, 3};
find_biggest(arr, 3);      // 10
index_of_biggest(arr, 3);  // 1

Choose Run to compile and run recursion.cpp as C++17 in the browser. The supplied main() prints the largest value and its index for {-1, 10, 3}. Replace the placeholder returns as you implement each function, and edit the array in main() to try the cases below. Update the element count in both calls when you change the array’s length.

This step uses manual review: compare the output with the expected answers and trace your recursive calls. Step 5 has automatic checks for the Python versions; those checks do not grade this C++ implementation.

Check your work (manual review)

For each input, write the returned value and the returned index:

Array find_biggest index_of_biggest
{5} 5 0
{-5, -2, -9} -2 1
{9, 2, 3} 9 0
{1, 2, 9} 9 2

Check that every recursive call receives a smaller problem, that the base case is checked before recursing, and that an index is measured from the right starting address. For the array {7, 7, 2}, find_biggest should return 7; the no-duplicates guarantee applies only to index_of_biggest.

Starter files
cs131/recursion.cpp
#include <iostream>

// Return the largest value in arr[0..n-1].
// Guaranteed: n >= 1. No loops allowed.
int find_biggest(int arr[], int n) {
    // TODO: base case, then combine arr[0] with the answer for the rest
    return 0; // Placeholder: replace this with your recursive implementation.
}

// Return the INDEX (into arr) of the largest value in arr[0..n-1].
// Guaranteed: n >= 1, no duplicate values. No loops allowed.
int index_of_biggest(int arr[], int n) {
    // TODO: remember that an index from the sub-array needs adjusting
    return 0; // Placeholder: replace this with your recursive implementation.
}

int main() {
    int arr[] = {-1, 10, 3};
    std::cout << "largest value: " << find_biggest(arr, 3) << "\n";
    std::cout << "index of largest: " << index_of_biggest(arr, 3) << "\n";
    return 0;
}
5

The Same Recursion in Python

Why this matters

Now write the same two algorithms in Python and try them on different inputs. If a result differs from your prediction, trace the base case and the value returned by each recursive call. Compare that reasoning with your C++ version.

🎯 You will learn to

  • Turn pointer-and-count recursion into slice-based recursion
  • Check a recursive algorithm against inputs chosen to break it

Slices replace pointer arithmetic

Here are some hints to help you if you haven’t worked with lists in Python before. Python lists know their own length, so the count parameter goes away:

C++ Python
find_biggest(arr + 1, n - 1) find_biggest(values[1:])
n == 1 len(values) == 1
arr[0] values[0]

Given a list x, to obtain everything but the first item (that is, the tail of the list) you write x[1:]. To get the length of a list, use len(). One real difference from C++: values[1:] builds a new list every call, where arr + 1 was just an address. That makes the translation easy to read, but adds copying work. An index-based recursive helper can avoid those repeated slices.

🔧 Your task

Open recursion_lab.py and implement both functions using recursion. Each receives a non-empty list of numbers. find_biggest returns the largest value, including when values repeat. index_of_biggest returns its zero-based index; for that function, assume there are no duplicates.

No loops or comprehensions. You may write recursive helpers, use a slice or an index to describe the remaining input, and compare two candidates with max. The traversal must be recursive; a call to max(values) or sorted(values) alone does not meet the task.

find_biggest([-1, 10, 3])      # 10
index_of_biggest([-1, 10, 3])  # 1

The tests run your functions on all-negative lists, single-element lists, and lists whose largest value sits at the front, the middle, and the end. Before you test, explain why [1, 2, 3] alone would miss a solution that starts its largest value at zero, or always returns the last element.

Starter files
recursion_lab.py
def find_biggest(values):
    """Return the largest value in values. Assumes at least one item.

    Use recursion, directly or through a helper; no loops or comprehensions.
    """
    # TODO
    pass


def index_of_biggest(values):
    """Return the index of the largest value in values.

    Assumes at least one item and no duplicates.
    No loops, no comprehensions.
    """
    # TODO
    pass


print(find_biggest([-1, 10, 3]))      # expect 10
print(index_of_biggest([-1, 10, 3]))  # expect 1
6

Building New Lists Recursively

Why this matters

This problem asks you to transform a list without changing the original. The recursive solution is similar to one we’ll use in functional programming: decide what to keep, then build the result from those values.

🎯 You will learn to

  • Build a new list recursively instead of changing an existing one
  • Pick a base case for a function that can legitimately be handed an empty list

A New List From the Remaining Values

One approach is to ask whether the first item belongs in the output, then combine that decision with the result for the rest. These operations can help you build a separate list:

x = [10, 20, 30]
x[1:]        # [20, 30]          the rest of the list
[5] + x      # [5, 10, 20, 30]   a NEW list, x is untouched
len(x)       # 3

Note the difference between [5] + x and x.append(5). The first builds a new list and leaves x alone. The second changes the list the caller is holding. You may change a new output list while building it; the restriction is that the input list must stay unchanged.

🔧 Your task

Open del_item_lab.py and write del_item(values, item), which takes a list of values as its first parameter and an item to delete as its second. Without changing the values list, and without using loops of any kind, your function must return an output list that removes all of the occurrences equal to item from the input list. Preserve the order of the remaining values, and return a new list even when the input is empty or nothing matches. Recursive helpers are welcome:

x = [1, 2, 3, 1, 4]
y = del_item(x, 1)
print(y)   # prints [2, 3, 4]
print(x)   # still prints [1, 2, 3, 1, 4]

No comprehensions either. Three cases decide the whole function: the list is empty, the first item matches, and the first item doesn’t.

✏️ Predict before you run

Suppose the only base case is if len(values) == 1: return values. What happens for del_item([], 7) and del_item([7], 7)?

Reveal

The empty input misses this base case and then fails if the function tries to read values[0]. The singleton input incorrectly returns [7] instead of removing the match.

Checking the empty list first handles both cases naturally. A solution can also special-case a singleton, but it still needs to handle empty input and decide whether that last item should be kept.

Starter files
del_item_lab.py
def del_item(values, item):
    """Return a NEW list with every occurrence of item removed.

    values must come back unchanged.
    No loops or comprehensions. Helpers are allowed; do not change values.
    """
    # TODO: three cases - empty, first item matches, first item doesn't
    pass


x = [1, 2, 3, 1, 4]
y = del_item(x, 1)
print(y)  # expect [2, 3, 4]
print(x)  # expect [1, 2, 3, 1, 4]
7

Virtual Dispatch and Object Slicing

Why this matters

In this step we’ll explore inheritance and polymorphism using C++. The question is which function body runs when a base-class reference refers to a derived object. Keep the parameter passing rules from Step 1 in mind as you work through the calls.

🎯 You will learn to

  • Trace which version of a function runs when a base-class reference names a derived object
  • Evaluate how a parameter’s declared form changes that choice
  • Add a new subclass to an existing hierarchy

The comedy club

Consider the following classes, which represent regular people and people who giggle (a subclass of people) when they attend a comedy club:

class Person {
public:
  Person(const std::string& name) { name_ = name; }
  void listen_to_joke() { laugh(); }
  virtual void laugh()  { cout << "haha!\n"; }
  virtual void heckle() { cout << "that's dumb!\n"; }
private:
  std::string name_;
};

class GigglyPerson : public Person {
public:
  GigglyPerson(const std::string& name) : Person(name) { }
  virtual void laugh()  { cout << "giggle giggle!\n"; }
  virtual void heckle() { cout << "that's... giggle... dumb... giggle!\n"; }
};

Now consider the following code that uses our classes:

void comedy_club(Person& p) {
  p.listen_to_joke();
  p.heckle();
}

int main() {
  Person p("Leia");
  comedy_club(p);

  GigglyPerson g("Sanjay");
  comedy_club(g);
}

Notice what listen_to_joke is: a non-virtual function, defined only on Person, that calls laugh(). Nobody overrides it. Keep that in mind for the prediction.

✏️ Predict before you read on

What will this program print? Why? Don’t just compile and run this code. Instead, try to reason out what is printed and why that’s the case. If you get a different answer, trace the call that surprised you. Four lines come out, so write all four down before you open the reveal.

  • (a) haha!, that's dumb!, haha!, that's... giggle... dumb... giggle!
  • (b) haha!, that's dumb!, giggle giggle!, that's... giggle... dumb... giggle!
  • (c) haha!, that's dumb!, haha!, that's dumb!
Reveal

(b).

haha!
that's dumb!
giggle giggle!
that's... giggle... dumb... giggle!

Line 3 is the interesting one. comedy_club(g) binds the reference p to the GigglyPerson object g. There's no copy and nothing gets sliced off, because p is a reference, which is version B from Step 1. p.listen_to_joke() runs Person's only copy of that function, and inside it the bare call laugh() means this->laugh(). this points at a GigglyPerson and laugh is virtual, so the overridden version in GigglyPerson is the one that runs.

The non-virtual function didn't need to be overridden. It only needed to call something virtual. This dynamic method dispatch is a key feature of object-oriented programming.

🔧 Your task

Open comedy_club.cpp and add a third kind of audience member: StonePerson, who is unimpressed by everything.

  • Inherit publicly from Person.
  • Forward the name to the Person constructor in an initializer list. Person has no default constructor, so this isn’t optional.
  • Override both laugh and heckle. The joke gets ...silence..., and the heckle gets something equally deadpan. The exact strings are up to you.
  • In main, send a StonePerson through comedy_club alongside the other two.

Predict your program’s six output lines, then choose Run to compile and run comedy_club.cpp as C++17 in the browser. Compare the output with your prediction. This step uses the manual review below; it has no automatic code tests.

Check your work (manual review)

The first four lines should match the original program. The final two should come from your StonePerson overrides, even though comedy_club still takes Person&. Check that the constructor and both overrides are public, and that the override signatures match void laugh() and void heckle(). Adding override after each signature lets the compiler catch a mismatch when you choose Run.

Now consider replacing Person& p with Person p. Passing g then copies only its Person base part into a new Person object. This is called object slicing. The original g stays a GigglyPerson, but the local copy prints haha! and that's dumb!. A reference or pointer can refer to the original derived object without making that copy.

Starter files
cs131/comedy_club.cpp
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
  Person(const std::string& name) { name_ = name; }
  void listen_to_joke() { laugh(); }
  virtual void laugh()  { cout << "haha!\n"; }
  virtual void heckle() { cout << "that's dumb!\n"; }
private:
  std::string name_;
};

class GigglyPerson : public Person {
public:
  GigglyPerson(const std::string& name) : Person(name) { }
  virtual void laugh()  { cout << "giggle giggle!\n"; }
  virtual void heckle() { cout << "that's... giggle... dumb... giggle!\n"; }
};

// TODO: add StonePerson here.
// Inherit publicly from Person, forward the name to Person's
// constructor, and override both laugh() and heckle().

void comedy_club(Person& p) {
  p.listen_to_joke();
  p.heckle();
}

int main() {
  Person p("Leia");
  comedy_club(p);

  GigglyPerson g("Sanjay");
  comedy_club(g);

  // TODO: build a StonePerson and send it to comedy_club too.
}
8

Linked Lists Without a Destructor

Why this matters

Consider the linked list class below. Translating it to Python brings together classes, object references, and traversal. The chain has the same structure in both languages, but Python manages node memory automatically.

🎯 You will learn to

  • Write a singly linked list in Python with a nested node class
  • Explain why this Python linked list needs no manual node cleanup

The C++ original

class List {
private:
  struct Node {
    Node(int v) { val = v; next = nullptr; }
    int val;
    Node *next;
  };

  Node *head_;
public:
  List() { head_ = nullptr; }
  ~List() {
    while (head_ != nullptr) {
      Node *temp = head_->next;
      delete head_;
      head_ = temp;
    }
  }
  void add_to_front(int val) {
    Node *new_node = new Node(val);
    new_node->next = head_;
    head_ = new_node;
  }
  void print() {
    Node *p = head_;
    while (p) {
      cout << p->val << endl;
      p = p->next;
    }
  }
};

The constructor, insertion, and traversal have Python equivalents. The explicit node cleanup is unnecessary: Python can reclaim nodes when they are no longer reachable. The timing depends on the Python implementation, so we do not rely on immediate collection.

C++ Python
struct Node { ... }; nested and private a class Node: nested inside List
Node *head_; set to nullptr self.head = None
new Node(val) self.Node(val)
~List() walking the chain calling delete nothing at all

🔧 Your task

Open linked_list_lab.py and finish the class:

  • Node stays nested inside List, with attributes val and next, mirroring the C++ layout. A new node stores its supplied value and starts with next = None.
  • List.__init__ starts with self.head = None.
  • add_to_front(val) links a new node in at the front. It returns nothing.
  • values() returns a plain Python list of the values, front to back, by walking the chain.
  • print_items() prints one value per line, front to back.

Both values() and print_items() must leave the nodes and their links unchanged so the list can be read again.

Loops are allowed again here. The no-loops requirement applies to the recursion problems. This interactive version adds values() so you can inspect the chain and names the printing method print_items(); it does the same job as print() in Homework 0.

✏️ Predict before you run

You add 1, then 2, then 3, each to the front. What does values() return, and which end of the chain is head?

Reveal

[3, 2, 1]. Every insertion goes in front of whatever was already there, so the newest item is always head and the list comes out in reverse insertion order. That's why add_to_front is so cheap, and why a stack is the natural thing to build out of one of these.

A Later Review

After a break, try three small variations without looking at the reveals: explain a pointer reassignment, trace an index returned by recursion, and insert a node into a two-node chain. Use any gap in your explanation to choose the step to revisit. That’s all for this refresher!

Starter files
linked_list_lab.py
class List:
    """Python port of the C++ singly linked list."""

    class Node:
        def __init__(self, v):
            # TODO: a node holds a value and a link to the next node
            pass

    def __init__(self):
        # TODO: an empty list points at nothing
        pass

    def add_to_front(self, val):
        # TODO: build a node, link it ahead of the current front,
        # then make it the new front
        pass

    def values(self):
        # TODO: walk the chain and collect the values, front to back
        pass

    def print_items(self):
        # TODO: print one value per line, front to back
        pass

    # No destructor needed - see the quiz below for why.


lst = List()
lst.add_to_front(1)
lst.add_to_front(2)
lst.add_to_front(3)
print(lst.values())  # expect [3, 2, 1]
lst.print_items()