Start Learning

A list is an essential data structure used for storing elements of the same type. In C++, it differs from a vector in that its data is not stored in contiguous memory. This has some major implications for basic operations like finding or inserting elements. So if you’d like to learn about these differences while becoming more familiar with some fundamental programming concepts, let‘s dive right in!

Some background on C++ linked lists

In order to understand the features of a linked list in C++, we first have to look at a closely related data structure: the vector. A vector is a collection of elements that are stored in contiguous memory addresses. This means that accessing the data is extremely fast. For instance, computing the length of a vector is as simple as subtracting the first element’s address from the last. However, the adjacency-in-memory constraint also implies that whenever one part of the vector is manipulated in any way, other elements must be moved, too. This can be an unnecessarily costly operation. And that is where the linked list comes in.

C++ Linked Lists

So what is a C++ linked list? 

A linked list has a more complex data structure than a vector; each of its elements consists of the data itself and then one or more pointers. A pointer is a variable that contains a memory address. In the case of a singly linked list, there will be just one pointer referencing the address of the next element. In a doubly linked list, each element will have pointers to both its antecedent and the element that follows it. The default list data structure, as found in the C++ Standard Library, is implemented as a doubly linked list. Creating a list is simple:

std::list<int> {0, 1, 1, 2, 3, 5, 8} fibonacci;

Note that when defining a list, the data type of its elements must be specified in pointy brackets. All the elements in a C++ list (as in vectors and arrays) must be of the same type.

List operations

By visualizing a linked list as a chain of elements, it is easy to see how it can be manipulated in a more dynamic way than a vector. When inserting or deleting an element, the only other elements affected are the ones directly connected to it. All that needs to be done is to update the pointers to reflect the new order. In contrast, a vector is more like an egg carton. Whenever we insert (or remove) an egg, all the other eggs in the carton need to be shifted as well in order to maintain the original order.

Let’s look at how an element is inserted at the beginning of a linked list:

#include <iostream>
#include <list>
using namespace std;

int main() {
  list<int> fibo = {1, 1, 2, 3, 5, 8}; // create a list of fibonacci numbers sans zero

  fibo.push_front(0);

  for (int n : fibo) {
    cout << n << ' ';
  }
}

// output: 0 1 1 2 3 5 8

The above code uses the push_front method, shorthand for inserting an element at the beginning of a list. Similarly, to append an element to the end of a list, use push_back. 

We can easily remove elements if we know their name. In the following example, we use remove to delete elements with a value of 1 from our Fibonacci sequence:

#include <iostream>
#include <list>
using namespace std;

int main () {
  list<int> fibo = {0, 1, 1, 2, 3, 5, 8};

  fibo.remove(1);

  for (int n : fibo) {
    cout << n << ' ';
  }
}

// output: 0 2 3 5 8

This removes all elements of the same name. 

In order to target a specific element, we need to work with an iterator. It points to the address of the element that we want to erase:

#include <iostream>
#include <list>
using namespace std;

int main () {
  list<int> fibo = {0, 1, 1, 2, 3, 5, 8};
  list<int> iterator it;

  it = fibo.begin(); // we point the iterator to the beginning of the list
  ++it;              // and move it by one element
  fibo.erase(it);

  for (int n : fibo) {
    cout << n << ' ';
  }
}

// output: 0 1 2 3 5 8

erase is the method of choice when deleting an element by position. An iterator will help us put the element back into place, too:

#include <iostream>
#include <list>
using namespace std;

int main () {
  list<int> fibo = {0, 1, 2, 3, 5, 8};
  list<int> iterator it;

  it = fibo.begin();
  ++it;
  fibo.insert(it, 1);

  for (int n : fibo) {
    cout << n << ' ';
  }
}

// output: 0 1 1 2 3 5 8

ìnsert acts as a complement to erase in that it takes an argument denoting the position of the element behind which the new data should be inserted. 

Finally, how about printing our Fibonacci sequence in reverse? In a vector, each element would have to be moved, but in a doubly linked list, it’s enough simply to swap the two pointers of each element. In code, this is as easy as using reverse on our list:

#include <iostream>
#include <list>
using namespace std;

int main() {
  list<int> fibo = {0, 1, 1, 2, 3, 5, 8};

  fibo.reverse();

  for (int n : fibo) {
  cout << n << ' ';
  }
}

// output: 8, 5, 3, 2, 1, 1, 0

When not to use linked lists

When you want to do operations related to accessing elements in your collection, such as indexing, you are better off using vectors. The flexible nature of the linked list results in a longer computation time: since its elements are not stored in contiguous memory addresses, an indexing or length-measuring function will need to perform the computationally expensive action of traversing the linked list in order to access its elements.
All these differences between the two data structures result in relatively small variations in the speed of your computations. And while at this point of your C++ journey optimizing for fast performance should not be your highest priority, learning about the concept of doubly linked lists still helps you gain a better understanding of how your code works internally, which ultimately enables you to become a better programmer.

Lists or vectors?

So when to use lists and when vectors? This question comes up again and again in the C++ community, and it’s harder to answer than the theory would suggest. What about a miscellaneous task that involves both accessing and manipulating the data? This post by Bjarne Stroustrup, the creator of the C++ language, references a surprising answer to a problem that to a large extent amounts to deleting from and inserting into a collection. While these actions seem to suggest that this is a task perfectly made for linked lists, Bjarne shows that even for collections with more than half a million elements, a vector will perform better. His conclusion is that, given the complex architecture of computers, answering the question of which data structure will be faster is nontrivial—and should involve empirical evidence.

Further reading

See this in-depth post for a manual implementation of a doubly linked list. For more information on list operations in C++, read the specifications on this page. And if you want to learn more about the concept of linked lists in general, Geeksforgeeks has an entire series of articles on the topic.

Looking to learn more about C++? Sign up for our C++ Nanodegree.

Start Learning

Stephen Welch
Stephen Welch
Stephen is a Content Developer at Udacity and has built the C++ and Self-Driving Car Engineer Nanodegree programs. He started teaching and coding while completing a Ph.D. in mathematics, and has been passionate about engineering education ever since.