Standard Library[lib]

C++ linked list: write one, then use std::list

Section
Standard Library [lib]
Standard
C++17
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
9 min read
In this answer6 clauses
  1. 1How do you write a singly linked list in C++?
  2. 2How do you insert and delete a node?
  3. 3How do you free a linked list without leaking memory?
  4. 3.1Let std::unique_ptr own the nodes
  5. 4What are std::list and std::forward_list?
  6. 5What is the time complexity of a linked list?
  7. 6Linked list or vector: which should you use?

Abstract

A C++ linked list is a chain of nodes, each holding a value and a pointer to the next, with the list keeping a pointer to the first; adding or removing where you stand is cheap, and reaching the nth node means a walk. Written by hand, it needs a destructor that deletes every node, or std::unique_ptr links that free themselves; the standard library has two ready-made ones, std::list and std::forward_list. For most jobs the C++ Core Guidelines recommend a std::vector instead.

Three boxes in a row, each split into a hatched value cell and a pointer cell with a dot. Blue arrows run from each dot to the next box; the last pointer drops to a ground mark.
Figure 1 — Nodes joined by pointers: each node holds a value and points to the next, and the last one points to nothing.

A C++ linked list is a chain of nodes: each node holds a value and a pointer, the next node’s address, and the list keeps a pointer to the first. Write one with a Node struct, or use the standard library’s std::list or std::forward_list. Adding or removing where you stand is cheap; reaching the tenth node means walking past nine.

The simplest example, three nodes holding 3, 7 and 9, in a whole program:

[Example 1:
#include <iostream>

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

int main() {
    Node* head = new Node{9, nullptr};  // 9
    head = new Node{7, head};           // 7 -> 9
    head = new Node{3, head};           // 3 -> 7 -> 9

    for (Node* n = head; n != nullptr; n = n->next) {
        std::cout << n->value << " -> ";
    }
    std::cout << "nullptr\n";           // 3 -> 7 -> 9 -> nullptr

    while (head != nullptr) {           // free every node
        Node* next = head->next;
        delete head;
        head = next;
    }
}

— end example]

It printed:

[Example 2:
3 -> 7 -> 9 -> nullptr

— end example]

new creates a node and returns its address, and nullptr, the null pointer, marks the end of the chain. Each new node points at the old first one, so the list is built back to front. Every new needs a delete, and the last loop gives each node its own.

The samples are C++17 (Since When’s C++17 table gives the first GCC, Clang and MSVC releases of its features), built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++). Every sample that runs printed the same on all four.

How do you write a singly linked list in C++?

Put the head pointer in a class: a LinkedList keeps head_, a pointer to the first node, and its member functions add a node at the front, then print and count the nodes by walking the chain. Singly linked means each node points one way only, to the next.

head points to node 3, then 7, then 9, whose next is nullptr. A new node 5 sits below: bold arrows run from 3 to 5 and from 5 to 7; the old arrow from 3 to 7 is dashed.
Figure 2 — Each node points to the next; inserting a node only redirects two pointers.
[Example 3:
struct Node {
    int value;
    Node* next;
};

class LinkedList {
public:
    // ...
    void push_front(int value) {
        head_ = new Node{value, head_};
    }

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

    std::size_t size() const {
        std::size_t count = 0;
        for (const Node* n = head_; n != nullptr; n = n->next) {
            ++count;
        }
        return count;
    }

private:
    Node* head_ = nullptr;
};

int main() {
    LinkedList numbers;
    numbers.push_front(9);
    numbers.push_front(7);
    numbers.push_front(3);
    numbers.print();                        // 3 -> 7 -> 9 -> nullptr
    std::cout << numbers.size() << '\n';    // 3
}

— end example]

All four printed 3 -> 7 -> 9 -> nullptr, then 3. push_front is one line: the new node points at the old head, then becomes the head, and no other node moves. The // ... stands for the destructor and the copy rules, shown in the section on freeing the list.

How do you insert and delete a node?

Change one or two pointers once you stand on the node before the spot. Inserting after a node sets the new node’s next and redirects the old node’s, as the drawing of 5 going in after 3 shows; deleting the node after it redirects one pointer and frees the node. Getting to that node is the walk, here find:

[Example 4:
Node* find(int value) {             // the walk
    for (Node* n = head_; n != nullptr; n = n->next) {
        if (n->value == value) {
            return n;
        }
    }
    return nullptr;
}

void insert_after(Node* position, int value) {
    position->next = new Node{value, position->next};
}

bool remove(int value) {
    if (head_ == nullptr) {
        return false;
    }
    if (head_->value == value) {    // the first node
        Node* old = head_;
        head_ = head_->next;
        delete old;
        return true;
    }
    for (Node* n = head_; n->next != nullptr; n = n->next) {
        if (n->next->value == value) {
            Node* old = n->next;
            n->next = old->next;    // step over it
            delete old;
            return true;
        }
    }
    return false;
}
// ...
numbers.insert_after(numbers.find(3), 5);
numbers.print();            // 3 -> 5 -> 7 -> 9 -> nullptr

numbers.remove(7);
numbers.print();            // 3 -> 5 -> 9 -> nullptr

— end example]

All four printed what the comments show. remove looks one node ahead because the node before is the one whose pointer changes. The sample then calls remove(3), which takes the first-node branch, moves head_ itself and printed 5 -> 9 -> nullptr; remove(42) finds nothing and returns false. insert_after assumes a real node, and find returns nullptr for a missing value, so check what it returns first.

How do you free a linked list without leaking memory?

Give the class a destructor, the member function that runs when the object is destroyed, and have it walk the list and delete each node; a node made with new and never deleted is a leak. Deleting the copy constructor and the copy assignment with = delete means a LinkedList cannot be copied, so no two lists hold the same nodes:

[Example 5:
class LinkedList {
public:
    LinkedList() = default;
    LinkedList(const LinkedList&) = delete;             // no copies
    LinkedList& operator=(const LinkedList&) = delete;

    ~LinkedList() {                     // delete every node
        while (head_ != nullptr) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }
    // ...
};

— end example]

Copy constructors and the rest are in C++ constructors: default, parameterized, copy and move. A version of these samples whose nodes count every construction and destruction printed matching totals on all four compilers, for this class and for the std::unique_ptr one below.

Let std::unique_ptr own the nodes

The other way is to make each next a std::unique_ptr, a smart pointer that owns one object and deletes it when it is destroyed itself. Each node then frees the one after it, and no delete is written:

[Example 6:
struct Node {
    int value;
    std::unique_ptr<Node> next;         // owns the next node
};

class LinkedList {
public:
    ~LinkedList() {
        // one node at a time; the default would recurse down the chain
        while (head_) {
            auto next = std::move(head_->next);  // take the rest
            head_ = std::move(next);  // the old first node is freed
        }
    }

    void push_front(int value) {
        auto node = std::make_unique<Node>();
        node->value = value;
        node->next = std::move(head_);  // the old first node
        head_ = std::move(node);        // the new node is first
    }

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

private:
    std::unique_ptr<Node> head_;
};

— end example]

It printed 3 -> 7 -> 9 -> nullptr. std::move hands the old first node to the new node’s next, and .get() reads the plain pointer for the walk. The destructor still frees the nodes in a loop, one per pass: it takes the rest of the chain out of the first node, then frees that node alone. Left to the default, each node would destroy the next one recursively, which cppreference’s std::unique_ptr page warns causes a stack overflow for a long enough list.

What are std::list and std::forward_list?

They are the standard library’s own linked lists. std::list, from <list>, can be walked both ways and is usually built as a doubly linked list, each node pointing back as well as forward:

[Example 7:
#include <algorithm>
#include <iostream>
#include <list>

void print(const std::list<int>& numbers) {
    std::cout << '{';
    for (int n : numbers) {
        std::cout << ' ' << n;
    }
    std::cout << " }\n";
}
// ...
std::list<int> numbers;
numbers.push_back(7);
numbers.push_back(9);
numbers.push_front(3);
print(numbers);                     // { 3 7 9 }

auto seven = std::find(numbers.begin(), numbers.end(), 7);
numbers.insert(seven, 5);           // before 7
print(numbers);                     // { 3 5 7 9 }

numbers.erase(seven);     // removes 7; seven is now invalid
print(numbers);                     // { 3 5 9 }

std::cout << numbers.size() << '\n';    // 3

— end example]

All four printed what the comments show. std::find returns an iterator, an object that marks one element’s position, and insert adds 5 before it. Adding or removing elements leaves iterators to the other elements valid, so seven still points at 7 after the insert. erase(seven) then removes 7 and leaves seven invalid: an erase breaks only the iterators to the erased element.

std::forward_list, in the library since C++11, is singly linked like the class above and takes less memory than a std::list. Its node knows only the next one, so it inserts after a position:

[Example 8:
#include <forward_list>
// ...
std::forward_list<int> numbers;
numbers.push_front(9);
numbers.push_front(7);
numbers.push_front(3);              // 3 7 9

numbers.insert_after(numbers.begin(), 5);   // after 3

std::cout << '{';
for (int n : numbers) {
    std::cout << ' ' << n;
}
std::cout << " }\n";                // { 3 5 7 9 }

— end example]

It printed { 3 5 7 9 }. A std::forward_list has no size(): count its elements by walking it.

Neither list has [], the index operator, so reaching an element means walking to it. On a std::list<int> named numbers, this does not compile:

[Example 9:
int second = numbers[1];    // error: std::list has no []

— end example]

Clang said:

[Example 10:
error: type 'std::list<int>' does not provide a subscript operator

— end example]

GCC reports no match for 'operator[]', and MSVC error C2676.

What is the time complexity of a linked list?

Reaching the nth element is O(n), linear: the time grows with n, because you walk past every node before it. Inserting or erasing at a position you already hold is O(1), constant: the same few pointer changes at any length. A std::vector swaps those costs, except at the end:

Operation std::list std::vector
Reach element n O(n), a walk O(1)
Insert or erase at a known position O(1) O(n), later elements move
Add at the front O(1) O(n), every element moves
Add at the end O(1) O(1) amortized

Amortized means on average over many calls. The class above keeps no pointer to its last node, so adding at its end means walking there first: O(n).

Linked list or vector: which should you use?

A std::vector, by default. The C++ Core Guidelines say so in SL.con.2, and add that even where a list gives “efficient insertion in the middle, a vector will usually still perform better for containers up to a few KB in size”. The reason it gives: a vector’s elements sit side by side in one block of memory. Use a linked list when you insert and erase in the middle often and keep iterators or pointers to elements, which a std::list leaves valid until that element is erased.

You need Use
Values to index, loop over and add to std::vector
Many inserts and erases in the middle, iterators kept std::list
The same, singly linked, less memory per node std::forward_list
A course exercise a Node class with a destructor and no copies

If you write one by hand:

  • End the chain with nullptr, and start an empty list’s head as nullptr.
  • Give every new a delete: a destructor that walks the list and frees each node.
  • Delete copying with = delete, or let std::unique_ptr own each node and free them in a loop.
  • Check what find returns before you insert after it.

The other containers are in Standard library.

Questions

Does C++ have a built-in linked list?

Not in the language, but the standard library has two: std::list, in the header <list>, which walks both ways and is usually built as a doubly linked list, and std::forward_list, in <forward_list>, which is singly linked and arrived in C++11. Neither has an index operator: you reach an element by walking to it.

Is std::list a doubly linked list?

In practice, yes. The standard requires that a std::list can be walked in both directions and allows constant-time insertion and erasure anywhere, and cppreference describes it as usually implemented as a doubly linked list. The singly linked one is std::forward_list.

How do you reverse a linked list in C++?

On a std::list, call its reverse() member: numbers.reverse() turned { 3 5 9 } into { 9 5 3 }. By hand, walk the list with three pointers: save the next node, point the current node’s next at the previous one, step forward, and make the old last node the head.

How do you find the length of a linked list?

Call size() on a std::list; since C++11 it takes constant time. By hand, count the nodes while walking from the head to nullptr, as the size() member of the class in this answer does. std::forward_list has no size(), so walk it and count.

What is the difference between a singly and a doubly linked list?

A node of a singly linked list has one pointer, to the next node; a node of a doubly linked list has two, to the next and the previous. The second pointer costs memory, but it lets you walk backwards and erase the node you are standing on, where a singly linked list needs the node before it.

Why use a linked list instead of an array?

Inserting or removing an element in the middle changes a few pointers instead of moving every element after it, and the elements never move, so iterators and references to the others stay valid. The price is no indexing: reaching the nth element means walking past the ones before it. For most programs a std::vector is still the better default.

Sources

  1. std::listcppreference.com
  2. std::forward_listcppreference.com
  3. std::unique_ptrcppreference.com
  4. std::vectorcppreference.com
  5. [list.overview] OverviewC++ standard working draft
  6. SL.con.2: Prefer using STL vector by default unless you have a reason to use a different containerC++ Core Guidelines

Read next

  1. Stack in C++: how std::stack works, with examplesStandard Library · Miriam Castellanos · written for C++17
  2. std::variant in C++: std::visit, std::get and the trapsStandard Library · Miriam Castellanos · written for C++17
  3. C++ getline: how to read a whole line of inputStandard Library · Miriam Castellanos · written for C++11