C++: The Basics - eps1.9_VectorAndString
Hello and welcome. As my faithful companion, you last time joined me in discovering the array. We learned the concept behind it and the special case of the char array. We also know how to work with this data structure.
The array has its origin in the C programming language and you will find many source codes in which C-style arrays are used.
C++ supports arrays, but provides its own template classes based on them for storing and arranging data or for strings. Although many disadvantages disappear with it and the use is around a lot more comfortable, they are often not given the deserved attention.
I would like to change this and therefore we look in this contribution std::vector
and std::string
from the C++ Standard Library.
Vectors in C++
If you use a static array, you have to think about the size of the array. After all it should be sufficient in every situation. Unfortunately you have to accept the fixed memory size as a compromise. No matter what the level of the field is.
It would be so much easier and more efficient if an array could grow or shrink dynamically, according to the current amount of data. C does not provide dynamic arrays by itself, but with the memory management capabilities
free the mallocs! ;-)
and pointers you can create your own dynamic object.
Constantly you reach a point where dynamic behavior would be needed. And for this reason the C++ Standard Library offers several ready to use solutions. There are Container classes, which are intended for the storage of data. These are distinguished into sequential and associative containers. Sequential containers are specialized in inserting, removing and editing of hosted elements. Associative containers have their strength in searching for a specific element content.
Of course, we would like to have an improved array. That is, a dynamic data structure that holds our information in a particular order. For this purpose we find in the C++ Standard Library the sequential container std::vector
.
This template class offers you the typical functionality of a dynamic array. The really evolutionary of this data type are its characteristic properties.
- Appending more elements to the array with constant time. The time to append or discard an element at the end of the array is independent of the size of the field.
- The required time elements in the middle of the array is directly proportional to the number of elements following it.
- The number of all elements of the array is dynamic. The Vector class takes care of memory management.
The functionality and components of std::vector
are defined in the C++ Standard Library and therefore standardized. For you this means that you can use vectors in all C++ development environments that commit to the ISO Standard.
std::vector in action
So, enough of words. Now let’s finally see std::vector
in action. Let’s first declare a vector for Integer. After that we want to initialize another one for Characters. We must not forget to tell the preprocessor that we want to use the class <vector>
by using the #include
directive.
#include <iostream>
#include <vector> // including vector class
int main()
{
std::vector<int> intVector(3); // declaring a vector of integers
intVector[2] = 76; // assigning a value to an element
std::cout << intVector[2] << std::endl;
std::vector<char>charVector{'H','i', '!'}; // initialize a vector of characters
std::cout << charVector.at(0)<< std::endl;
return 0;
}
// listing1: declaring and initializing a vector
As you can see, the syntax is very similar to C-style arrays. There is not the big difference. The big advantage besides the dynamic behavior are the numerous functions this container class can handle.
std::vector doesn't check if the element you want to access exists at all, just like a conventional array. So you run the risk of retrieving data outside the bounds of the array. But
std::vector is aware of this problem and holds a safe solution. With the function
vectorName.at(elementNumber)` you can query the value of an element and get a feedback when leaving the boundaries of the field. So you have a function for more security.
Differentiation between size and capacity
What is the memory requirement of a std::vector
object? When it comes to size, you need to distinguish between size and capacity.
The function vectorName.size()
returns you the number of elements inside the field. Similar to a static array, you can multiply the number by the memory size of the data type of the elements to get the total memory requirement.
Unfortunately, this is only half the truth. Due to the dynamic behavior of std::vector
the memory requirement is often higher than the pure amount of data of the elements. The requirement may be equal or greater. But sometimes the container class allocates additional memory to respond to dynamic growth without having to reallocate memory.
But the class does not leave you in the dark. You are given the function vectorName.capacity()
. This function gives you the currently occupied memory of the vector object as the number of possible elements.
There lies the small but essential difference. With vectorName.size()
you get the number of elements in the field. vectorName.capacity()
shows you how many elements fit into the memory size of the object.
To confuse you even more, capacity does not limit the size of the field. If you append more elements to the vector object and exceed the capacity, then the container object is automatically allocated more memory.
You can’t keep appending additional elements indefinitely. At some point the end is reached. The function vectorName.max_size()
returns the theoretical limit for the size of the object.
#include <iostream>
#include <vector> // including vector class
int main()
{
std::vector<char>charVector{'H','i', '!'};
std::cout << "Size: " << charVector.size() << std::endl; // number of elements
std::cout << "Capacity: " << charVector.capacity() << std::endl; // size of charVector given in number of elements
std::cout << "Max size: " << charVector.max_size() << std::endl; // maximum size of charVector
return 0;
}
// listing2: size() and capacity()
Let’s take a closer look at other functions of std::vector
.
push_back(), insert(), pop_back()
At the beginning I claimed that this container class has a dynamic size and allows adding as well as removing elements. Of course I don’t want to let this statement stand just like that.
With the function vectorName.push_back(Value)
you can add another value to the end of the field during runtime. However, if you want to remove the last value, the vectorName.pop_back()
function will help you do that. Let’s take a look at the functions with an example in Listing3. vectorName.back()
returns the value of the last element.
#include <iostream>
#include <vector> // including vector class
int main()
{
std::vector<int> intVector{ 0, 1, 2, 3, 4 };
int sizeVec = intVector.size(); // number of elements
int valueVec = intVector.back(); // value of the last element
std::cout << "size: " << sizeVec << std::endl;
std::cout << "value at the end: " << valueVec << std::endl;
intVector.push_back(5); // adding an element to the end
sizeVec = intVector.size();
valueVec = intVector.back();
std::cout << "new size: " << sizeVec << std::endl;
std::cout << "New value at the end: " << valueVec << std::endl;
intVector.pop_back(); // removing an element from the end
sizeVec = intVector.size();
valueVec = intVector.back();
std::cout << "size after pop_back(): " << sizeVec<< std::endl;
std::cout << "value after pop_back(): " << valueVec << std::endl;
std::cout << "value after pop_back(): " << intVector.at(sizeVec-1) << std::endl;
return 0;
}
// listing3: push_back() and pop_back()
Not only elements can be attached or deleted at the end. With the two functions nameVector.insert()
and nameVector.erase()
you can add or remove elements at any position of the Vector object.
In order for nameVector.insert()
to insert a value as a new element before the specified position, the function takes two parameters. The position within the Vector object must be passed as an iterator. In short, an iterator points to a location in memory that belongs to a container object. In Listing4, the iterator used is nameVector.begin()
, which points to the beginning of the vector. In order to point to a specific element, you need to add to it the relative position in the field.
The value is to be made sure that it corresponds to the data type of the other elements.
Removing an element works the same way. Pass the function nameVector.erase()
the iterator to the element to be erased.
Since vectors are based on arrays, using nameVector.insert()
or nameVector.erase()
will reallocate the subsequent elements in memory. This not only sounds cumbersome, but is also rather inefficient. The Vector class is optimized to grow or shrink at the end. If you often need operations that change the interior of the field, then you’d better look at the other container classes in the standard library that are designed for that.
#include <iostream>
#include <vector> // including vector class
int main()
{
int position= 3;
std::vector<int> intVector{ 0, 1, 2, 3, 4 };
int sizeVec = intVector.size();
int valueVec = intVector.at(position);
std::cout << "size: " << sizeVec << std::endl;
std::cout << "value at position: " << valueVec << std::endl;
intVector.insert(intVector.begin()+position, 1, 6); // inserting an element
sizeVec = intVector.size();
valueVec = intVector.at(position);
std::cout << "new size: " << sizeVec << std::endl;
std::cout << "New value at position: " << valueVec << std::endl;
intVector.erase(intVector.begin()+position); // erasing an element
sizeVec = intVector.size();
valueVec = intVector.at(position);
std::cout << "size after erase(): " << sizeVec<< std::endl;
std::cout << "value after erase(): " << valueVec << std::endl;
return 0;
}
// listing4: insert() and erase()
All functions of std::vector
would go beyond the scope at this point. But we have covered the most important ones and can now work with this data structure. If you want to look deeper into std::vector
, just have a look at the C++ Standard Library.
Seems very one dimensional
You may have noticed that in all the examples the Vector objects have only one length and no other dimension. The Vector class is a sequential container and has only that one dimension. Just like the C-style array, a Vector encloses a sequence of memory locations.
But there is also the possibility to create a multidimensional object. You simply take a vector from vectors. Comparable with the array of arrays.
Another solution is not offered by C++ or the standard library. Alternatively you can write your own multidimensional array class or search for it in other libraries and use it.
// declaration of a multi-dimensional array of integers
int[][] mulitArray;
// declaration of a multi-dimensional vector of integers
std::vector<std::vector<int>> mulitVector;
// listing5: multi-dimensional vector
C++ and strings
In the last post on arrays we looked at the special case of the char array. Now, before you jump the gun and suggest a “Char Vector” as an alternative or equivalent, I have to put the brakes on you. Sure, you can do it that way. It’s just…
Let’s get straight to the point. The C++ Standard Library provides an extra class std::string
for processing strings. Strings are objects consisting of a sequence of characters. They are similar to the Vector class, have no fixed size and can dynamically adapt to data sets if needed. However, they are specialized to the Character data type.
Strings handle text input efficiently and safely, and provide string manipulation functions. This clearly makes them our first choice when we want to process text.
Let’s write a simple dialog between the user and the console. First, we tell the preprocessor that we want to use the class <string>
. <iostream>
provides us with the functions to take user input and for output to the console.
First we define the string greeting
and output the value. The following call to action is a string literal. Then we declare the string name
to fill it with user input. Finally, the contents of the string and its length are output.
#include <iostream>
#include <string> // including string class
int main()
{
std::string greeting ("Hello there!"); // defining a string variable
std::cout << greeting << std::endl; // showing value of string variable
std::cout << "What is your name?" << std::endl; // showing string literal
std::string name; // declaring a string variable
std::cin >> name; // assign a stream to a string variable
std::cout << "Hi "<< name << "," << std::endl;
std::cout << "your name has "<< name.length() // length of a string
<< " characters" << std::endl;
return 0;
}
// listing6: c++ string
We have already worked a lot with strings in the form of string literals. The examples in the previous posts would have been a lot more elegant and easier to solve with std::string
. Besides, in many textbooks these are among the first data types you are shown.
Why did I wait so long with the introduction?
I find it a bit clumsy to start with a more complex data type from the C++ Standard Library. This runs the risk of ambiguity and making flaky assumptions. Only after you know the primitive data types and understand the concept of data types, then complex data types are much easier to understand.
We will now use std::string
more often and get to know more and more of the functions.
That sticks
Our portfolio of data types is growing and we even work with first classes of the C++ Standard Library. std::vector` is a container class from it. It serves as a dynamic alternative to static arrays, whose roots lie in the C programming language. In addition to the ability to rearrange and resize in memory, there are features that make it safer and easier for you to work with.
If you have a sequence of variables of the same data type, std::vector
should be your first choice. If the sequence consists of data of the type character, then a special case arises. For this the C++ Standard Library contains the specialized class std::string
. With it you can process texts and user input very comfortably.
As you can see, there is a huge variety of data types that you can use in the corresponding situation. Which one is the right one can be discussed at length. In the end, it’s your personal decision. After all, all of them only serve to make your source code more targeted, safer and easier to use.
Now we are armed with a good selection of data types and can create meaningful variables to hold data with appropriate types in memory. Next, let’s start working with variables.
I wish you maximum success!
Sources
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017