Dynamic arrays
In this lesson we'll learn how to use dynamic arrays in C++ using std::vector
.
More about vector
You might be wondering why the type that represents a dynamic array data structure in C++ uses the name "vector." It can be confusing for those who associate the term with the mathematical concept of vectors. Interestingly, this name was chosen by the original author of the STL (Standard Template Library), Alexander Stepanov. However, in retrospect, Stepanov admitted that using "vector" as the name of the data structure was a mistake.
vector
is one of the many containers in the standard library. You'll learn about other containers
gradually as you progress through the course. In this lesson I will often use the term array and vector interchangeably.
Once you finish this lesson you can look at the documentation if you want to learn more about vector
s.
Be aware that the documentation is not a tutorial, but rather a reference and it may be a bit overwhelming at first.
Creating a vector
Let's go back and consider the code we've shown in the Motivation section of the previous lesson.
There is a great candidate to be turned into an array. The following variables are of the same type (std::string
)
and differ only in number at the end of their name.
std::string player_name1;
std::string player_name2;
std::string player_name3;
Instead of creating three separate variables, we can create one array that contains three elements.
To use std::vector
we first have to include its header file:
#include <vector>
As we already know from the Introduction, to create a vector
we have to know the type of the elements
that will be stored inside it. All elements in a vector are of the same type.
#include <iostream>
#include <string> // do not forget about string
#include <vector>
int main()
{
std::vector<std::string> player_names(3);
// ...
}
The above example shows how to create a vector that stores std::string
s.
Before we continue, let's put an appropriate using
statement
at the beginning of the main
function block to skip the std::
prefix:
int main() {
using std::vector, std::string;
vector<string> player_names(3);
}
Later we'll use more elements from the standard library, like std::cout
and std::cin
.
You can add these to the using
statement too.
”
vector is a template, which means that it can be used with different types.
We tell it which type to use by putting it in angle brackets right after vector
. Like this:
vector< int > array_of_ints;
vector< float > array_of_floats;
vector< char > array_of_chars;
vector< string > array_of_strings;
vector< /*other type*/ > array_of_xyz;
This code defines the vector player_names
that stores text elements (std::strings
):
vector<string> player_names(3);
Writing (3) after player_names makes it so that when it is created it will have room for three elements
of type string
right from the beginning.
player_names(3)
Note that this is a vector-specific thing and not a general rule. If you want to create a vector that is initially empty, just don't write the parentheses at all:
vector<string> player_names;
A mistake that is often made by beginners is to write empty parentheses when creating a vector (or actually any other type):
vector< string > player_names();
Later in the course you'll learn about functions, and the syntax that declares them:
// prism-push-types:AnyType
AnyType function_name();
This is why the empty parentheses turns this into a function declaration, which is not what we want.
Accessing elements
There are two ways of accessing an element of a vector, that we're interested in:
- using the
[]
operator - calling the
at()
method
Both ways are very similar, with the only difference being that the []
operator does not check if the index is out of bounds.
We'll show you what this means in a moment. Now let's see how we can use them - we'll assign the following names to the players:
Player index | Name |
---|---|
0 | HappyBanana |
1 | AngryCrab |
2 | SadWolf |
Here is how we can set them in the code:
#include <iostream>
#include <string>
#include <vector>
int main()
{
using std::vector, std::string, std::cout;
vector<string> player_names(3);
// Setting names of the players
player_names[0] = "HappyBanana";
player_names[1] = "AngryCrab";
player_names[2] = "SadWolf";
// Printing the name of the first player:
cout << "Name of the first player: " << player_names[0];
}
Name of the first player: HappyBanana
To access an element of an array, we put its index in square brackets right after the array name:
arrayName[ index ]
A non-empty array with a number of elements equal to N
always has indexes ranging from 0
to N-1
inclusive.
The three-element array player_names
has indexes 0
, 1
and 2
.
Out of bounds access
A very common mistake is to try to access an element with an index that is out of bounds.
If we had tried to rename the player at index 3
the program would most likely crash and burn.
player_names[3] = "NewPlayer"; // !
This code will compile correctly (we might get a warning) but when the program is executed it will try to access a memory that is not allocated for the program. This is called a buffer overflow and it is a very serious problem.
An alternative, safer way of accessing an element is to use the at()
method:
player_names.at( 3 ) = "NewPlayer";
The difference is that the at()
method checks if the index is out of bounds and throws an exception if it is.
Be aware that it won't make your program valid by itself. The key benefit is that it will show you a useful
error message and won't allow the program to perform a potentially dangerous operation.
We'll learn more about the syntax we used to call the at()
method in a moment.
Example
This is how we can ask a user to enter the name of a certain player:
cout << "Enter the name of the second player: ";
cin >> player_names[1];
Now let's output the number of characters in the provided name:
cout << "The name of the second player has " << player_names[1].size() << " characters.\n";
Enter the name of the second player: FuriousFlamingo
The name of the second player has 15 characters.
Providing initial values
There is also a way to provide initial values for the elements of the vector
right from the beginning:
vector<string> player_names = {
"HappyBanana",
"AngryCrab",
"SadWolf",
};
Alternative syntax
In the case of initializing a vector
you can also omit the =
sign:
vector<string> player_names { // note the lack of '='
"HappyBanana",
"AngryCrab",
"SadWolf",
};
The result is the same.
We will use this method of initialization in the current lesson from now on.
Adding elements at the end
To add an item to the end of an array (in this case player_names
), we need to use push_back
like this:
player_names.push_back("WickedWitch");
// Printing the name of player with index 3
cout << "Name of the player with index 3: " << player_names[3];
Name of the player with index 3: WickedWitch
From the moment .push_back(...)
instruction was executed, the player_names
array already has four elements.
We say that we are calling the push_back(...)
method. We'll talk more about calls and methods in the future.
For now, just remember that:
- we put a dot after the name of the array
- we write the name of the method, which is in this case
push_back
- then in parentheses we enter what we want to add (e.g. a value or a variable)
Note that after we added the new element, the array has four elements, with indices 0
, 1
, 2
and 3
:
Before:
Index | Name |
---|---|
0 | HappyBanana |
1 | AngryCrab |
2 | SadWolf |
After:
Index | Name |
---|---|
0 | HappyBanana |
1 | AngryCrab |
2 | SadWolf |
3 | WickedWitch |
To let the user add a new player, we can ask them to enter their name, and then add it to the array:
std::cout << "Enter the name of the new player: ";
std::string new_player_name;
std::cin >> new_player_name;
player_names.push_back(new_player_name);
Inserting elements at a specific position
At this point you will have to trust me a little. I won't go into details because it's too complicated for now.
To insert before index n
to a vector (in this case player_names
) we use the following notation:
player_names.insert(player_names.begin() + n, elementToInsert);
Simply put, insert
adds an element just before a
specified position
begin()
and add the index to it.
Knowing this, we will now insert a new player just before the AngryCrab
(index 1
):
player_names.insert(player_names.begin() + 1, "BadPenguin");
This is how the array changes after the insertion:
Before:
Index | Name |
---|---|
0 | HappyBanana |
1 | AngryCrab |
2 | SadWolf |
3 | WickedWitch |
After:
Index | Name |
---|---|
0 | HappyBanana |
1 | BadPenguin |
2 | AngryCrab |
3 | SadWolf |
4 | WickedWitch |
Removing elements
This is a similar case to inserting elements. We need to specify a position again.
So to remove the n
-th element from a vector (e.g. from player_names
) we will use the following notation:
player_names.erase( player_names.begin() + n );
We'll now erase the first player from the array:
player_names.erase( player_names.begin() + 0 );
Note that in this case + 0
actually does nothing, but I put it there to make it clear: player_names.begin()
is the same as player_names.begin() + 0
.
The contents of the array before and after the removal:
Before:
Index | Name |
---|---|
0 | HappyBanana <-- gets deleted |
1 | BadPenguin |
1 | AngryCrab |
2 | SadWolf |
3 | WickedWitch |
After:
Index | Name |
---|---|
0 | BadPenguin |
1 | AngryCrab |
2 | SadWolf |
3 | WickedWitch |
As you can see the first element was removed and the rest of the elements shifted by one index.
This is because the array is a contiguous block of memory and vector
ensures that there is no
empty space left after an element is removed.
Before erasing an item from an array, make sure it exists (that is, it's in scope [0, N)
).
int index;
cin >> index; // don't forget to add using std::cin;
if (index >= 0 && index < player_names.size())
{
player_names.erase( player_names.begin() + index );
}
else
cout << "Index " << index << " does not exist!";
Clearing the contents
To clear the contents of a vector, we can use the clear()
method:
player_names.clear();
After the call, the array will be empty.
Reading the size
Because vector
can change size whenever you want it to, you may sometimes need to read the number of elements
it currently contains. The current size can be read using the size()
method:
vector<string> player_names = {
"HappyBanana",
"AngryCrab",
"SadWolf",
};
cout << "The array contains " << player_names.size() << " elements\n";
// Adding a new player:
player_names.push_back("WickedWitch");
cout << "Added new player.\n";
cout << "The array contains " << player_names.size() << " elements\n";
The array contains 3 elements
Added new player.
The array contains 4 elements
Just like we previously did with the method push_back
, we write the name after the dot.
Then we put the parentheses, and in the case of .size
we leave them empty.
Checking if vector is empty
To check if a vector is empty, we can use the empty()
method like this:
if (player_names.empty())
cout << "The array is empty!\n";
else
cout << "The array is not empty!\n";
}
Displaying elements
If we want to display all the elements of an array, we'll have to use a loop. We will tell more about loops in the future. They allow you to execute the same piece of code multiple times.
for (string name : player_names)
{
cout << "Player name: " << name << '\n';
}
Player name: HappyBanana
Player name: AngryCrab
Player name: SadWolf
Player name: WickedWitch
To understand this, let me show you how to "read" it:
For (
for
) each player name (name
) which is of a typestd::string
in an arrayplayer_names
execute the following block of code...
There is only one statement inside this block of code:
cout << "Player name: " << name << '\n';
The loop will write the nicknames of the players one by one
into the variable name
, and execute the display instruction (cout
) for each of them.
Summary
We learned how to do basic operations on std::vector
type. Make sure to check out the sublessons because there is still
a lot to practice. You can find them in the sidebar if you expand the "Dynamic arrays" item.