C++ Arrays using std::vector

An introduction to how we can store collections of objects into a single container, using the std::vector class
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

3D art showing a character in a fantasy environment
Ryan McCombe
Ryan McCombe
Posted

Inevitably, we will want to store a group of related objects. That might be a collection of characters in a party; a collection of buttons on a UI, or just a simple collection of numbers.

Programmers have invented a huge range of options for storing collections - these containers are typically called data structures. Here, we’ll introduce the most common choice - the dynamic array.

Lets imagine we have these characters:

class Character {};
Character Frodo;
Character Gandalf;
Character Gimli;
Character Legolas;
Character Aragorn;

We may want to group these characters together, to form a party. This is where arrays come in

Under the hood, arrays are a contiguous block of memory, big enough to store all our objects.

Dynamic Arrays vs Static Arrays

Arrays broadly fall into two categories - static arrays, and dynamic arrays.

With static arrays, we need to know at compile time how much space we need. For example, if we create a static array for holding 5 characters, and we need to add a 6, we’re out of luck.

Dynamic arrays have the ability to resize themselves at run time. If we try to add a 6th character to a dynamic array that can only hold 5, the dynamic array will do a load of work and memory management to make itself bigger.

That all happens behind the scenes - from our perspective, it just works.

In this lesson, we're working with dynamic, resizable arrays. The intermediate course introduces static arrays.

There are hundreds of implementations of arrays in C++ that we can use, or we can even create our own once we learn more advanced topics. We're using the version in the standard library here just because it is easy to include in our project, and is a good introduction to the key concepts.

Creating an Array using std::vector

The standard library has an implementation of dynamic arrays that we can use. Confusingly, the C++ standard library calls dynamic arrays “vectors”. This has little in common with the vectors we’ve been working with in earlier lessons. It’s just an unfortunate name.

It’s also generally not the name used for these collections, even among C++ developers. Typically, we call them “dynamic arrays”, “resizable arrays”, or simply “arrays”.

To use std::vector, we need to #include <vector>. We can then declare a vector by giving it a name, and specifying the type of objects it will contain. The following example shows how we can declare an array that stores integers:

#include <vector>

std::vector<int> MyVector;

We can initialize the values at the same time:

std::vector<int> MyVector { 1, 2, 3, 4, 5 };

When we are initializing the values at the same time we declare the vector, we can optionally remove the type. This lets the compiler infer it.

To do this, the compiler is using Class Template Argument Deduction (CTAD), which we’ll cover more in the intermediate course:

std::vector MyVector { 1, 2, 3, 4, 5 };

Accessing Items in Arrays

We can access the members of our array using the Vector[x] notation, where x is the index of the element we want to access. The index of an element within a vector is just its position.

However, we start counting from 0. This means the first element of the vector is at index 0, the second element is at index 1, and so on.

For example, to access the elements of our vector, we would do this:

std::vector MyVector { 1, 2, 3, 4, 5 };

int FirstElement { MyVector[0] };
int SecondElement { MyVector[1] };
int LastElement { MyVector[4] };

Note that because we start counting from 0, this also means the last element of a vector is at an index of 1 less than its size. For a vector of size 5, the last element is at index 4.

Array Size

The “size” of an array refers to the number of elements it currently contains. This is also commonly called the “length” of the array.

As with all values, the index can be derived from any expression that results in an integer:

using std::cout, std::vector;

vector MyVector { "First", "Second", "Third" };

// Log out the element at index 0
cout << MyVector[3 - 3] << "\n";

// Log out the element at index 1
int Index { 1 };
cout << MyVector[index] << "\n";

// Log out the element at index 2
int CalculateIndex() {
    return 1 + 1;
}
cout << MyVector[CalculateIndex()] << "\n";

This code logs out elements at indices 0, 1, and 2 in order:

First
Second
Third

Adding Items to Arrays

Once our vector is created, we’ll often want to add more items to it. Typically, we want to add items to the end of arrays. Adding items to the end of arrays has performance benefits over adding them to the start. We’ll cover the performance characteristics of containers in more detail in the intermediate chapter.

For now, what we need to know is that vectors have two methods for adding items to their end: push_back and emplace_back

push_back

If an object has already been constructed, and we want to add it to the back of our vector, we can use push_back:

class Character {
public:
  std::string Name;
}

int main() {
  Character Legolas { "Legolas" };
  std::vector<Character> MyVector;
  MyVector.push_back(Legolas);
}

emplace_back

If the object we want to add does not already exist, we can construct it right inside of the vector, using emplace_back.

The arguments we call emplace_back with will be passed to the constructor of the object type the vector stores:

class Character {
public:
  std::string Name;
}

int main() {
  std::vector<Character> MyVector;
  MyVector.emplace_back("Legolas");
}

Constructing an object in place has performance benefits over creating the object and then moving or copying it into the vector. So, where possible, prefer emplace_back over push_back.

Storing Complex Types in Arrays

Our above example uses vectors with simple integers, but we can store any type in our array.

class Character {};

// A party of characters
std::vector<Character>;

// A party of character pointers
std::vector<Character*>;

// A party of const character references
std::vector<const Character&>;

Arrays can also store other arrays. This creates "multidimensional arrays". For example, a 3x3 grid could be represented as an array of 3 rows, each row being an array of 3 items

std::vector<std::vector<int>> MyGrid {{
  { 1, 2, 3 },
  { 4, 5, 6 },
  { 7, 8, 9 }
}};

int TopLeft { MyGrid[0][0] }; // 1
int BottomRight { MyGrid[2][2] }; // 9

Type Aliases

Often, C++ types can become very complex. This can make our code hard to read, or just frustrating to use if we are often repeating a huge type name in our code.

We can use a using statement to create a simpler alias for our types. Here, we alias our complex array type to the much simpler name of Grid:

using Grid = std::vector<std::vector<int>>;

Grid MyGrid {{
  { 1, 2, 3 },
  { 4, 5, 6 },
  { 7, 8, 9 }
}};

We cover type aliases in more detail in the intermediate course

We can also have pointers to arrays:

// Pointer to an array
std::vector<std::vector<int>>* MyGridPointer;

// Pointer to an array with a using statement
using Grid = std::vector<std::vector<int>>;
Grid* AliasedPointer;

We can also have references to vectors, with or without const:

using Grid = std::vector<std::vector<int>>;

void SetTopLeft(Grid& GridToChange, int Value) {
  GridToChange[0][0] = Value;
}

void LogTopLeft(const Grid& GridToLog) {
  std::cout << GridToLog[0][0];
}

Looping over Arrays using a for-loop

A common task we have when working with arrays is to loop over every element in them, in order to do something to each object.

We could do this with a for loop:

std::vector MyVector { 1, 2, 3 };

for (int i { 0 }; i < 3; ++i) {
  std::cout << MyVector[i];
}

Line 3, highlighted above, uses the i < 3 conditional, as we know our array has a size of 3. However, often, we won’t know what size the vector we’re dealing with is.

We can make our code a little smarter - std::vector has a size method that, predictably, returns the size of the vector. The following code will log out 3.

std::vector MyVector { 1, 2, 3 };
std::cout << MyVector.size(); // 3

We can update our loop to use this method:

std::vector MyVector { 1, 2, 3 };

for (int i { 0 }; i < MyVector.size(); ++i) {
  std::cout << MyVector[i];
}

Using the std::size_t type

There is an issue with using int values as the index of vectors: the size of vectors can be larger than the maximum value storable in an int.

To deal with this problem, we have the std::size_t data type. size_t is guaranteed to be large enough to match the largest possible size of the vector, as well as other objects.

Because of this, it is the recommended way of storing vector indices:

std::vector MyVector { 1, 2, 3 };

for (std::size_t i { 0 }; i < 3; ++i) {
  std::cout << MyVector[i];
}

Iterating over Arrays using a range-based for-loop

Often, we usually don’t need to work with indices at all - we just want to iterate over everything in the vector.

For those scenarios, we can just use this syntax:

std::vector MyVector { 1, 2, 3 };

for (int Number : MyVector) {
  std::cout << Number;
}

This is known as a range-based for loop. There are some things to note:

Firstly, by convention, C++ programmers tend to use auto when working with these types of loops:

std::vector MyVector { 1, 2, 3 };

for (auto Number : MyVector) {
  std::cout << Number;
}

Secondly, they use pass-by-value. This means each item in the collection is copied into the body of the loop. This is almost never what we want, so we can pass by reference instead:

std::vector MyVector { 1, 2, 3 };

for (auto& Number : MyVector) {
  std::cout << Number;
}

Finally, just like when we’re passing-by-reference into a function, if the function isn’t going to modify that reference, we should mark it as const:

std::vector MyVector { 1, 2, 3 };

for (const auto& Number : MyVector) {
  std::cout << Number;
}

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Odds and Ends
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access!

This course includes:

  • 66 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Part 2 - December 2023

Professional C++

Advance straight from the beginner course, dive deeper into C++, and learn expert workflows

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved