Vectors and Strings
Vectors and Strings
Raw arrays and char buffers work, and you will meet them in your data structures paper. But in everyday C++ you should reach for std::vector and std::string first, because both of them do something a raw array cannot: they know their own size and they manage their own memory.
The problem with raw arrays
A C-style array has three properties that cause trouble.
- Its size is fixed at compile time.
int marks[50];holds fifty ints, forever. If the class has 51 students, you rewrite the code. - It does not know its own length. Once you pass it to a function it decays to a pointer, and the function has no way to ask how many elements there are. You must pass the count separately and hope the two never drift apart.
- Nothing checks the bounds.
marks[80]on a 50-element array compiles cleanly and reads whatever memory happens to be there. Sometimes it crashes, sometimes it quietly gives wrong answers, and the second is worse.
std::vector solves all three. It grows on demand, it tracks its own size, and it can check bounds when you ask it to.
Creating and using a vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> marks = {78, 65, 92, 41, 88};
std::cout << "Count: " << marks.size() << "\n";
std::cout << "First: " << marks[0] << "\n";
std::cout << "Last: " << marks[marks.size() - 1] << "\n";
std::cout << "Also last: " << marks.back() << "\n";
marks.push_back(55);
marks.push_back(70);
std::cout << "After adding two: " << marks.size() << "\n";
int total = 0;
for (int m : marks) {
total += m;
}
std::cout << "Average: " << (total / (double)marks.size()) << "\n";
std::vector<double> temps(5, 30.0);
std::cout << "temps has " << temps.size() << " values, each " << temps[0] << "\n";
std::vector<std::string> subjects;
subjects.push_back("Maths");
subjects.push_back("Physics");
for (const std::string& s : subjects) {
std::cout << "- " << s << "\n";
}
return 0;
}
The angle brackets in std::vector<int> say what the vector holds. That is a template argument, and it is checked at compile time, so a vector of ints will never accidentally contain a string.
The operations you will use constantly:
| Operation | Meaning |
|---|---|
v.size() |
Number of elements currently held |
v.empty() |
True when size is zero |
v[i] |
Element at index i, no bounds check |
v.at(i) |
Element at index i, throws if out of range |
v.push_back(x) |
Append x at the end, growing if needed |
v.pop_back() |
Remove the last element |
v.front() / v.back() |
First and last element |
v.clear() |
Remove everything |
v[10] on a five-element vector is undefined behaviour, exactly like a raw array. The subscript operator is fast precisely because it skips the check. v.at(10) does check and throws std::out_of_range instead, turning a silent corruption into a clear error. While you are learning, at() is worth the small cost.
How a vector grows
push_back on a full vector allocates a larger block, typically double the size, copies the existing elements over, and frees the old block. That copying sounds expensive, and a single push_back occasionally is. But because the size doubles rather than growing by one, the cost averaged over many pushes is constant. This is called amortised constant time, and it is why appending to a vector in a loop is a perfectly normal thing to do.
If you already know roughly how many elements you will add, v.reserve(1000) allocates once up front and avoids the intermediate copies.
Strings
std::string is, in effect, a vector of characters with text-specific operations bolted on. It replaces the char array plus strcpy, strlen and strcat from C, and it removes the entire category of bugs where you forgot to leave room for the terminating null byte.
#include <iostream>
#include <string>
#include <cctype>
int main() {
std::string first = "Priya";
std::string last = "Sharma";
std::string full = first + " " + last;
std::cout << "Full name: " << full << "\n";
std::cout << "Length: " << full.length() << "\n";
std::cout << "First character: " << full[0] << "\n";
std::cout << "Uppercase: ";
for (char c : full) {
std::cout << (char)toupper(c);
}
std::cout << "\n";
std::string course = "Computer Science and Engineering";
std::cout << "Substring: " << course.substr(0, 8) << "\n";
size_t pos = course.find("Science");
if (pos != std::string::npos) {
std::cout << "Found 'Science' at index " << pos << "\n";
} else {
std::cout << "Not found\n";
}
if (first == "Priya") {
std::cout << "Comparison with == works directly\n";
}
std::string padded = " trim me ";
std::cout << "[" << padded << "] has length " << padded.size() << "\n";
return 0;
}
Two things in that program are impossible with C-style strings. full = first + " " + last concatenates by writing a plus sign, with the memory handled for you. And first == "Priya" compares the contents; in C, == on two char* compares addresses and is almost always false, which is why strcmp exists.
The common string operations:
| Operation | Meaning |
|---|---|
s.size() / s.length() |
Number of characters, identical functions |
s[i] |
Character at index i |
s + t |
Concatenation |
s.substr(pos, len) |
A copy of len characters starting at pos |
s.find(t) |
Index of the first occurrence, or std::string::npos |
s.empty() |
True when the string has no characters |
s.push_back(c) |
Append a single character |
std::string::npos is the sentinel returned when find fails. Test against it explicitly. Because it is a very large unsigned value, writing if (pos >= 0) is always true and never catches the failure.
Vectors of vectors
A two-dimensional grid is a vector whose elements are themselves vectors.
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (const std::vector<int>& row : grid) {
for (int cell : row) {
std::cout << cell << " ";
}
std::cout << "\n";
}
int diagonal = 0;
for (size_t i = 0; i < grid.size(); i++) {
diagonal += grid[i][i];
}
std::cout << "Diagonal sum: " << diagonal << "\n";
std::vector<std::vector<int>> table(3, std::vector<int>(4, 0));
table[1][2] = 99;
std::cout << "table[1][2] = " << table[1][2] << "\n";
std::cout << "Rows: " << table.size() << ", Columns: " << table[0].size() << "\n";
return 0;
}
Unlike a fixed 2D array, the rows here need not all be the same length, and the dimensions can be decided while the program runs.
Common mistakes
- Forgetting the include.
<vector>for vectors and<string>for strings. Sometimes it compiles anyway because another header pulled it in, and then breaks on a different compiler. Include what you use. - Indexing past the end.
v[v.size()]is one past the last valid index. The last element isv[v.size() - 1]orv.back(). - Calling
back()orfront()on an empty container. Undefined behaviour. Checkempty()first. - Comparing a
size()result against a negative number.size()returns an unsigned type, sov.size() - 1on an empty vector wraps around to a huge number instead of going negative. - Passing a vector by value to a function. That copies every element. Use
const std::vector<int>&for read-only parameters. - Using
if (s.find(t) >= 0). Always true. Compare againststd::string::npos.
Vectors and strings will carry most of your day-to-day C++ work. Rewrite an array exercise using a vector and notice how much bookkeeping disappears, then try the container problems on the Practice page.