Katkat

C++ Code Reading: Bug in a Linked-List insertAfter and Predicting Polymorphic Output

AMDSoftware Engineer · Onsite

Last updated

Two C++ code-reading exercises: find the bug in a singly linked list insertAfter function that is missing a pointer update and predict what the program prints, then predict and explain the output of code mixing virtual overriding, name hiding, constructors, default arguments and pass-by-value. It tests precise tracing of pointers and dispatch rules.

This round handed out short C++ programs and asked you to read them: find a bug, predict the exact output, and explain why. The exact snippets used in the interview were not shared. The two programs below are representative reconstructions: the first has the defect described in the round (a singly linked list insertAfter function missing its key pointer update), and the second exercises the polymorphism and overriding concepts the round covered.

Clarifying Questions

  • Should the predicted output be exact, including spacing and line breaks?
  • May I assume a standard-conforming C++17 compiler with default settings?
  • Besides the output, should I fix the code and point out other problems, such as memory leaks?

Part 1 — The buggy insertAfter

insertAfter(prev, value) is supposed to insert a new node holding value immediately after prev. The comments in main show the list each call is meant to produce. Find the bug, predict exactly what the program prints, and fix it.

#include <iostream>

struct Node {
 int value;
 Node* next;
 Node(int v) : value(v), next(nullptr) {}
};

void insertAfter(Node* prev, int value) {
 if (prev == nullptr) return;
 Node* node = new Node(value);
 prev->next = node;
}

void print(const Node* head) {
 for (const Node* cur = head; cur != nullptr; cur = cur->next) {
 std::cout << cur->value << (cur->next ? " -> " : "\n");
 }
}

int main() {
 Node* head = new Node(1);
 insertAfter(head, 3); // intended list: 1 -> 3
 insertAfter(head, 2); // intended list: 1 -> 2 -> 3
 insertAfter(head->next, 4); // intended list: 1 -> 2 -> 4 -> 3
 print(head);
}

What This Part Should Cover

  • The exact output, backed by a step-by-step trace
  • The missing assignment and its consequences for the list and for memory
  • A corrected function, including why the order of the two assignments matters
  • Edge cases: a null prev, inserting after the tail, and cleanup of the list

Part 2 — Predict the polymorphic output

Predict exactly what this program prints. For every line of output, explain why it is printed.

#include <iostream>

class Base {
public:
 Base() { hello(); }
 virtual ~Base() = default;
 virtual void hello() const { std::cout << "Base::hello\n"; }
 void greet() const { std::cout << "Base::greet\n"; }
 virtual void show(int x = 1) const { std::cout << "Base::show " << x << "\n"; }
};

class Derived : public Base {
public:
 Derived() { hello(); }
 void hello() const override { std::cout << "Derived::hello\n"; }
 void greet() const { std::cout << "Derived::greet\n"; }
 void show(int x = 2) const override { std::cout << "Derived::show " << x << "\n"; }
};

void byValue(Base b) { b.hello(); }

int main() {
 Derived d;
 Base* p = &d;
 p->hello();
 p->greet();
 p->show();
 d.show();
 byValue(d);
}

What This Part Should Cover

  • The exact output, line by line
  • Virtual dispatch versus non-virtual name hiding
  • The rules for virtual calls during construction, default arguments on virtual functions, and passing a derived object by value
  • How language features such as override, final and references help avoid these surprises

Hints

Hint 1

Draw every pointer: Draw the list after each call, including where every node's `next` points, and compare it with the intended list in the comments. Ask what happens to the node that used to follow `prev`.

Hint 2

Which type decides: For each call, note the static type of the expression, the dynamic type of the object at that moment, and whether the function is virtual.

Hint 3

Special rules: Constructors, default arguments and pass-by-value each follow their own rule about which type is used. Check each of those calls separately.

Follow-up questions

  • In Part 1, what would happen if you wrote the two pointer assignments in the opposite order?
  • In Part 2, what happens if `Base::hello` is made pure virtual and is still called from the `Base` constructor?
  • If `Derived::greet` were declared with `override`, what would the compiler do?
  • How would you change `byValue` so that it calls `Derived::hello`?