Skip to content
C++ Better Explained
Go back
Linked Lists in C++: Build One From Scratch (Beginner's Guide)

Linked Lists in C++: Build One From Scratch

A linked list is the first data structure where pointers stop being an abstract exercise and start doing real work. Instead of storing elements side by side like an array, a linked list stores each element in its own box and keeps a pointer to the next box.

Once you build one by hand, pointers make a lot more sense.


The Mental Model: A Chain of Boxes

Think of a treasure hunt. Each clue tells you where to find the next clue. You can’t skip to clue #5 — you have to follow the chain from the start.

That’s a linked list. Each node holds two things:

  1. The data you care about
  2. A pointer to the next node

The last node points to nullptr, which means “the chain ends here.”

struct Node {
    int data;
    Node* next;
};

That’s the entire structure. A Node contains a pointer to another Node — this is legal because the compiler only needs to know the size of a pointer, not the full size of Node, at that point.


Building a List by Hand

Before writing any functions, let’s wire three nodes together manually so you can see what’s happening:

#include <iostream>

struct Node {
    int data;
    Node* next;
};

int main() {
    // Create three nodes on the heap
    Node* first  = new Node{10, nullptr};
    Node* second = new Node{20, nullptr};
    Node* third  = new Node{30, nullptr};

    // Link them together
    first->next  = second;
    second->next = third;
    // third->next stays nullptr — end of the list

    // Walk the chain
    Node* current = first;
    while (current != nullptr) {
        std::cout << current->data << " -> ";
        current = current->next;
    }
    std::cout << "nullptr\n";

    // Clean up
    delete first;
    delete second;
    delete third;

    return 0;
}

Output:

10 -> 20 -> 30 -> nullptr

The traversal loop is the pattern you’ll use constantly: start at the head, print, move to next, stop at nullptr.

Notice we used new to put nodes on the heap. This matters — nodes created on the stack inside a function would be destroyed when that function returns, leaving your list full of dangling pointers.


Inserting at the Front

Adding to the front is the fastest operation a linked list offers. It’s three steps and it never depends on how long the list is:

void pushFront(Node*& head, int value) {
    Node* newNode = new Node{value, head};  // point new node at old head
    head = newNode;                         // head now points at new node
}

Two details worth pausing on:

This is O(1) — constant time. Compare that to inserting at the front of an array, which requires shifting every existing element one slot to the right.

If you're looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you're a complete beginner or looking to solidify your understanding. Just $19.

Inserting at the End

Appending requires walking to the last node first, which makes it O(n):

void pushBack(Node*& head, int value) {
    Node* newNode = new Node{value, nullptr};

    if (head == nullptr) {      // empty list — new node becomes the head
        head = newNode;
        return;
    }

    Node* current = head;
    while (current->next != nullptr) {  // stop at the LAST node
        current = current->next;
    }
    current->next = newNode;
}

The empty-list check is not optional. Skip it and current->next dereferences a null pointer on the very first append.

Also note the loop condition: current->next != nullptr, not current != nullptr. We want to stop on the last node so we can attach to it, not run past it.


Deleting a Node

Deletion is where beginners lose memory. You need to reconnect the chain and free the node:

void removeValue(Node*& head, int value) {
    if (head == nullptr) return;

    // Case 1: the head itself matches
    if (head->data == value) {
        Node* doomed = head;
        head = head->next;   // move head forward first
        delete doomed;       // then free the old one
        return;
    }

    // Case 2: search with a "previous" pointer
    Node* current = head;
    while (current->next != nullptr && current->next->data != value) {
        current = current->next;
    }

    if (current->next != nullptr) {
        Node* doomed = current->next;
        current->next = doomed->next;  // bypass the doomed node
        delete doomed;
    }
}

The key idea: you must hold a pointer to the node before the one you’re removing, because that’s the node whose next needs rewiring. Save the doomed node in a temporary variable before you unlink it — once you overwrite the pointer, you can’t reach it to delete it, and that’s a memory leak.


A Complete Working Program

Here’s everything together, including proper cleanup:

#include <iostream>

struct Node {
    int data;
    Node* next;
};

void pushFront(Node*& head, int value) {
    head = new Node{value, head};
}

void pushBack(Node*& head, int value) {
    Node* newNode = new Node{value, nullptr};
    if (head == nullptr) {
        head = newNode;
        return;
    }
    Node* current = head;
    while (current->next != nullptr) {
        current = current->next;
    }
    current->next = newNode;
}

void printList(Node* head) {
    Node* current = head;
    while (current != nullptr) {
        std::cout << current->data << " -> ";
        current = current->next;
    }
    std::cout << "nullptr\n";
}

bool contains(Node* head, int value) {
    for (Node* current = head; current != nullptr; current = current->next) {
        if (current->data == value) return true;
    }
    return false;
}

void destroyList(Node*& head) {
    while (head != nullptr) {
        Node* next = head->next;  // save next BEFORE deleting
        delete head;
        head = next;
    }
}

int main() {
    Node* head = nullptr;

    pushBack(head, 20);
    pushBack(head, 30);
    pushFront(head, 10);

    printList(head);
    std::cout << "Contains 30? " << (contains(head, 30) ? "yes" : "no") << "\n";
    std::cout << "Contains 99? " << (contains(head, 99) ? "yes" : "no") << "\n";

    destroyList(head);
    printList(head);

    return 0;
}

Output:

10 -> 20 -> 30 -> nullptr
Contains 30? yes
Contains 99? no
nullptr

Look closely at destroyList. It saves head->next into a local variable before calling delete head. Delete first and you’ve thrown away the only pointer to the rest of your list — a classic beginner bug that leaks every remaining node.


Linked List vs Vector: Which Should You Use?

OperationLinked Liststd::vector
Insert at frontO(1)O(n) — shifts everything
Insert at backO(n)*O(1) amortized
Access by indexO(n) — must walkO(1) — direct math
Memory per elementData + pointerData only
Cache friendlinessPoor — scatteredExcellent — contiguous

*O(1) if you also keep a tail pointer.

Here’s the honest advice: use std::vector unless you have a measured reason not to. Modern CPUs read memory in chunks, so a vector’s contiguous layout means the next element is usually already in cache. A linked list forces the CPU to chase pointers all over the heap, and that often makes it slower in practice even when the big-O looks better on paper.

Linked lists earn their keep when you’re doing many insertions and removals in the middle of a long sequence and you already hold a pointer to that spot.

If you do need one in production code, reach for std::list (doubly linked) or std::forward_list (singly linked) rather than writing your own. Building one by hand is a learning exercise — and a very good one.


Common Mistakes

Forgetting the null check while traversing. while (current->next != nullptr) and while (current != nullptr) do different things. Use the first when you need to stop on the last node, the second when you need to visit every node.

Losing the head pointer. If you write head = head->next in a traversal loop, you’ve destroyed your only reference to the start of the list. Always walk with a separate current pointer.

Leaking every node. Every new needs a matching delete. A list of 1000 nodes needs 1000 deletes — that’s what destroyList is for.

Passing Node* head when you meant Node*& head. If a function changes which node is the head, it needs a reference to the pointer. This one is silent — no crash, the list just doesn’t change.



Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19

📋

Free Download: The 10 Mistakes Every C++ Beginner Makes

A free 1-page checklist that shows the exact traps that slow down every C++ beginner — so you can avoid them from day one.

🔒 No spam. Unsubscribe anytime.


Share this post on:

Written by

Sahil Bora

Software Engineer. Author and creator of C++ Better Explained.


Previous Post
Function Pointers in C++: A Beginner's Guide with Examples
Next Post
C++ Preprocessor Directives: #include, #define, and #ifdef Explained

Keep Learning