C++: Vectors

We've talked a fair bit about arrays during this C++ course, but there are some limitations to using the default array functionality. The most obvious limitation that you may have already ran into when creating applications which make use of arrays, is that arrays (at least without some workarounds) have a fixed size. This becomes problematic as some situations require arrays which change size, or at the very least require some sort of storage form or "container" in which the length is not initially known. For these situations we can use things called vectors.

A vector is a container in the C++ Standard Library (a bunch of stuff which sort of comes "bundled" with C++) which is essentially just an array that can grow and shrink in size. These things have been highly optimized and tried and tested for several years, and as such are generally considered a standard when creating C++ applications. It's worth remembering that all the containers which we're going to cover in the next few tutorials use the std:: namespace.

To sum up the main advantages and disadvantages of vectors in a sentence: They're array-based, dynamic, and have some very good error-checking built in, however can be slower than arrays. Generally speaking, you should use arrays where possible (when you don't need dynamic sizing or want to optimize completely for speed), however if you need a variable sized container (and want to write minimal code to make it possible), vectors are a great solution. As vectors aren't as primitive as arrays and are part of the standard library, they also have a bunch of nice member functions which make some tasks a lot easier -- we'll talk about these in a minute.

To work with vectors, we have to firstly #include <vector>, and then we can use the vector name; syntax to declare a vector (as usual, any data-type can be used -- int, string, your custom classes or structs, whatever!). We can optionally pass one or two parameters on creating a vector, the number of elements (to start out with), and the value to initialize these elements to. All three possible declaration formats can be seen in the example below.

1
2
3
vector<int> vectorOne; //vectorOne: {}
vector<int> vectorTwo(5); //vectorTwo: {0, 0, 0, 0, 0}
vector<int> vectorThree(5, 3); //vectorThree: {3, 3, 3, 3, 3}

Once a vector has been created, we can access elements by either using square brackets (like an array), or more safely by using the at member function and providing the element index as a parameter. Using the square brackets would work fine, however if an index is specified outside of the vector range, you'll just get some rubbish from memory, whereas .at has some error handling so an exception will be thrown when the index is outside of the vector's range. Just to give an example of this basic functionality, I have provided the simple code snippet as follows:

1
2
3
4
5
6
vector<int> vectorOne(10, 1);

for(int i = 0; i < 10; i++)
{
	cout << "Element "<< I << ": "<< vectorOne.at(i) << endl;
}

For comparison between the square brackets and at member function, try using both and changing the 'for' loop condition to "i < 15". It's always good practice to know how to break things and what to expect when things do go wrong, and as such you should always be testing all of the rules and boundaries that I present to you.

Most of the further vector functionality simply comes with knowing the other member functions which vector objects have. With a vector declared, elements can easily be added and removed to/from the end of the container by using the push_back and pop_back member functions respectively, take for example the following:

1
2
3
4
vector<int> vectorOne(4, 1); //vectorOne is now {1, 1, 1, 1}
vectorOne.push_back(2); //vectorOne is now {1, 1, 1, 1, 2}
vectorOne.push_back(3) //vectorOne is now {1, 1, 1, 1, 2, 3}
vectorOne.pop_back();    //vectorOne is now {1, 1, 1, 1, 2}

The last element in the vector can be handily grabbed using the .back member function, and the first element with the .front member function, vectors can be resized using the resize member function, the size of a vector can be retrieved using the size member function... the list goes on. If you want to know more of the member functions that can be used on vectors, I suggest the relevant cplusplus.com reference page.

Elements can even be inserted into the vector at any point in the vector by using the insert member function with two parameters. Note, however, that the insert member function is pretty damn inefficient memory-wise, and if you're going to be regularly inserting data into the vector, you may want to look for another container. Vectors are good at defining and accessing elements through the whole container, but they're especially good at working with the elements at the end of the vector (pushing, popping, and so forth). This is why a good knowledge of the different containers is useful, as some containers are more fit for certain jobs than others (and hence, in future tutorials we will be covering some different containers).

If you want a bit of an insight into how vectors work (since they use arrays in the back-end), you can see that they actually just change array size at certain thresholds. The current size of the actual array behind the scenes (not the size of the vector) can be seen by using the capacity member function - and as such you can see the points at which the vector switches to a bigger array by using a simple loop:

1
2
3
4
5
6
7
vector<int> newVector;

for(int i = 0; i < 32; i++)
{
	newVector.push_back(i);
	cout << i << ": Size "<< newVector.size()<< ", Capacity "<< newVector.capacity()<< endl;
}

The above loop shows the array size (vector "capacity") expanding as the array is filled. This should explain the basic system behind vectors, and should hopefully make comparison to other containers, which we'll learn about in the future, much easier. Iterating through arrays using 'for' loops like this (as we have previously - using an iterator variable), however, isn't generally the best method of iteration. When we #include <vector>, we also get these great little things called vector iterators bundled along.

Iterators are essentially slightly less intelligent pointers, and the idea is that you can set the iterator to point to the first element of your vector (or other component) and then use pointer arithmetic to iterate through the vector, having the iterator point to the current object at each iteration. Vector iterators are defined similarly to their normal vector counterparts, however with ::iterator stuck on the end -- for example:

1
vector<int>::iterator it;

From here functionality is much as previously described, with the .begin() and .end() vector methods returning references to the first and last elements respectively:

1
2
3
4
5
6
7
8
9
10
vector<int> vec(10, 3); //New integer vector
vector<int>::iterator it; //New integer vector iterator

//Set the iterator to a reference to the first element of the vector.
//Loop until the current element we're pointing at is the last.
//Increment the iterator at each loop iteration.
for(it = vec.begin(); it != vec.end(); it++)
{
		cout << *it << endl;
}

Using these standard iterators has a whole bunch of advantages (convenience, safety, and so on), but one of the big immediately notable advantages in the case of vectors is that it allows you to use some of the vector member functions that require an iterator parameter. .erase is a useful example of this which allows for the removal of an element in the vector via an iterator parameter of the element you want to remove.