C++ Vectors Explained: Dynamic Arrays, Functions, Capacity & Operations

4
0

Dynamic Arrays in C++

A dynamic array stores an ordered sequence of same-type values while allowing the sequence to grow or shrink during program execution. In C++, the standard dynamic-array container is vector. It provides familiar indexed access like a built-in array, together with functions for managing its size and contents.

Creating the First Vector

The <vector> header must be included before using vector. The element type is written inside angle brackets, followed by the variable name. For example, vector<int> creates a vector that stores integers.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values = {7, 8, 9};
cout << values[0] << '\n';
cout << values[1] << '\n';
cout << values.size() << '\n';
return 0;
}

Indexing begins at 0, exactly as it does for a built-in array. A vector containing n elements has valid indexes from 0 through n - 1. The size() member function reports the current number of elements.

Why Fixed Arrays Are Not Always Enough

A built-in array has a fixed size. Once int values[5] has been created, it cannot become an array of six elements or shrink to four elements. This is sufficient when the required size is known and never changes, but many programs discover or modify their storage requirements while running.

A vector solves this limitation:

  • It stores values of one declared element type.

  • It preserves the order of its elements.

  • It supports zero-based indexing.

  • Its elements are stored contiguously, like those of a built-in array.

  • Its size can grow or shrink at runtime.

  • It manages allocation and deallocation of its storage automatically.

Contiguous storage means neighboring elements occupy neighboring memory locations. If an int occupies four bytes on a particular system, the addresses of adjacent vector elements commonly differ by four bytes.

How Vector Storage Is Managed

A vector must preserve contiguous storage even when it grows. Suppose three elements occupy one continuous memory region and a fourth element is appended. If the next memory location is unavailable, the vector obtains a larger continuous region, moves or copies its existing elements there, adds the new element, and releases the old storage.

This relocation is called reallocation. It happens automatically, so ordinary vector use does not require manual memory management. However, reallocation can invalidate pointers, iterators, and references to existing elements because those elements may now live at different addresses.

Different Ways to Create Vectors

Vectors support several construction forms:

  • vector<int> first; creates an empty vector with size 0.

  • vector<int> second(5); creates five integers, each initialized to 0.

  • vector<int> third(5, -1); creates five integers, each initialized to -1.

  • vector<int> fourth{7, 8, 9}; creates three elements with the listed values.

Parentheses and Braces Are Different

The expressions vector<int> a(5) and vector<int> b{5} do not mean the same thing. Parentheses select the size constructor, so a contains five zeroes. Braces provide an initializer list, so b contains one element whose value is 5.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> emptyVector;
vector<int> zeroes(5);
vector<int> negativeOnes(5, -1);
vector<int> listed{7, 8, 9};
vector<int> oneValue{5};
cout << emptyVector.size() << '\n';
cout << zeroes.size() << ' ' << zeroes[0] << '\n';
cout << negativeOnes[4] << '\n';
cout << listed.size() << '\n';
cout << oneValue.size() << ' ' << oneValue[0] << '\n';
return 0;
}

Size, Empty State, and Valid Indexes

The size() function returns the number of elements currently present. The empty() function returns true when the size is zero and false otherwise. An empty vector has no valid element index.

Boolean values are printed as 1 and 0 by default, so printing emptyVector.empty() normally produces 1.

Accessing Vector Elements

Unchecked and Checked Access

The subscript operator, such as values[1], performs unchecked access. The index must already be valid. Using an out-of-range index causes undefined behavior; the program may print an arbitrary value, corrupt data, crash, or appear to work.

The at() function performs checked access. If the index is invalid, it throws an out_of_range exception instead of silently accessing unrelated memory.

First and Last Elements

The front() function accesses the first element, while back() accesses the last. Both require a non-empty vector. Calling either function on an empty vector causes undefined behavior, so empty() should be checked first when emptiness is possible.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values{8, 6, 4};
cout << values[1] << '\n';
cout << values.at(1) << '\n';
if (!values.empty()) {
cout << values.front() << '\n';
cout << values.back() << '\n';
} else {
cout << "Vector is empty\n";
}
return 0;
}

Growing and Shrinking a Vector

The push_back(value) function appends a value and increases the size by one. The emplace_back(value) function also appends an element; for simple types such as int, their visible effect is the same, while emplace_back can construct more complex objects directly in the vector.

The pop_back() function removes the last element and reduces the size by one. It does not return the removed element. The vector must be non-empty before pop_back() is called; calling it on an empty vector causes undefined behavior.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values;
cout << values.size() << '\n';
values.push_back(8);
values.push_back(3);
values.emplace_back(11);
cout << values.size() << '\n';
if (!values.empty()) {
values.pop_back();
}
cout << values.size() << '\n';
return 0;
}

Traversing and Modifying Elements

An indexed loop can traverse from index 0 while i < values.size(). A range-based loop provides a shorter form when the index itself is unnecessary.

  • for (int value : values) gives the loop a copy of each element. Changing value does not affect the vector.

  • for (int &value : values) gives the loop a reference to each original element. Changes are reflected in the vector.

  • for (const int &value : values) avoids a copy while preventing modification.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values{7, 6, 9, 17};
for (int i = 0; i < static_cast<int>(values.size()); i++) {
cout << values[i] << ' ';
}
cout << '\n';
for (int &value : values) {
value *= 2;
}
for (int value : values) {
cout << value << ' ';
}
return 0;
}

Passing and Returning Vectors

Function parameters determine whether a vector is copied, modified, or treated as read-only:

  • vector<int> values passes by value and creates a copy. Changes remain local to the function.

  • vector<int>& values passes by reference. Changes affect the caller's vector.

  • const vector<int>& values passes by constant reference. It avoids copying and prevents modification.

Unlike a built-in array, a vector can be returned by value safely. The returned vector manages its own lifetime and storage, and modern C++ efficiently moves or constructs the result for the caller.

Size, Capacity, Reserve, and Resize

A vector's size is the number of elements that currently exist. Its capacity is the number of elements that can fit in the currently allocated storage before another allocation becomes necessary.

Using reserve()

reserve(100) requests storage for at least one hundred elements but creates no elements. The size remains unchanged. Future appends can use the reserved storage without reallocating until the size exceeds the available capacity.

Using resize()

resize(100) changes the number of elements to one hundred. When an integer vector grows this way, newly created elements are initialized to zero. Resizing to a smaller value removes elements from the end.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> reserved;
reserved.reserve(100);
cout << reserved.size() << ' ' << reserved.capacity() << '\n';
reserved.push_back(10);
cout << reserved.size() << ' ' << reserved.capacity() << '\n';
vector<int> resized;
resized.resize(100);
cout << resized.size() << ' ' << resized[0] << '\n';
resized.push_back(10);
cout << resized.size() << '\n';
return 0;
}

After reserve(100), the size is still 0, while capacity is at least 100. The standard guarantees a minimum requested capacity, not necessarily an exact value. After resize(100), the size is exactly 100. Appending afterward creates element number 101.

Reallocation and Invalid References

If an append exceeds the current capacity, reallocation may move every element. A reference such as int& first = values[0] may then become invalid after push_back(). It is unsafe to keep using such a reference unless it is known that no reallocation occurred. Reserving sufficient capacity in advance can reduce reallocations, but code should still respect vector invalidation rules.

Built-In Arrays and Vectors

  • Built-in array: Fixed size, no member function for obtaining its logical length, and dimensions generally known at compile time.

  • Vector: Runtime size, automatic memory management, size(), and operations for growing or shrinking.

  • Dynamic matrix: A vector whose elements are themselves vectors, allowing rows and columns to be created at runtime.

Creating a Dynamic Matrix

A two-dimensional vector is declared as vector<vector<int>>. To create three rows and four columns initialized to -1, construct three copies of a four-element integer vector.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<vector<int>> matrix(3, vector<int>(4, -1));
matrix[0].push_back(7);
matrix.push_back(vector<int>(10, 9));
for (int i = 0; i < static_cast<int>(matrix.size()); i++) {
for (int j = 0; j < static_cast<int>(matrix[i].size()); j++) {
cout << matrix[i][j] << ' ';
}
cout << '\n';
}
return 0;
}

matrix.size() returns the number of rows, while matrix[i].size() returns the number of elements in row i. Appending 7 to row 0 makes that row longer than the others. Appending vector<int>(10, 9) adds a new row containing ten copies of 9. A vector of vectors can therefore be rectangular or ragged.

Problem 1: Double Every Element

Given {2, 5, 8}, modify the same vector so that it becomes {4, 10, 16}. Because the original vector must change, the function receives it by non-const reference. A range-based loop also uses element references.

#include <iostream>
#include <vector>
using namespace std;
void doubleElements(vector<int>& values) {
for (int &value : values) {
value *= 2;
}
}
int main() {
vector<int> values{2, 5, 8};
doubleElements(values);
for (int value : values) {
cout << value << ' ';
}
return 0;
}

The function runs in O(n) time and uses O(1) auxiliary space.

Problem 2: Keep Only Even Numbers

Given {1, 2, 3, 4, 6}, return a new vector containing the even values in their original order: {2, 4, 6}. Start with an empty result vector, test every value with value % 2 == 0, and append each match.

#include <iostream>
#include <vector>
using namespace std;
vector<int> keepEven(const vector<int>& values) {
vector<int> result;
for (int value : values) {
if (value % 2 == 0) {
result.push_back(value);
}
}
return result;
}
int main() {
vector<int> values{1, 2, 3, 4, 6};
vector<int> result = keepEven(values);
for (int value : result) {
cout << value << ' ';
}
return 0;
}

The traversal takes O(n) time. The result may contain up to n elements, so the auxiliary space is O(n).

Problem 3: Combine Two Vectors

Given {1, 2, 3} and {4, 5}, return {1, 2, 3, 4, 5}. Append every element of the first vector, followed by every element of the second.

#include <iostream>
#include <vector>
using namespace std;
vector<int> combine(
const vector<int>& first,
const vector<int>& second
) {
vector<int> result;
result.reserve(first.size() + second.size());
for (int value : first) {
result.push_back(value);
}
for (int value : second) {
result.push_back(value);
}
return result;
}
int main() {
vector<int> first{1, 2, 3};
vector<int> second{4, 5};
vector<int> result = combine(first, second);
for (int value : result) {
cout << value << ' ';
}
return 0;
}

If the vectors contain n and m elements, the time and result-space complexities are O(n + m). Reserving the final size is optional but avoids repeated reallocations while the result grows.

Problem 4: Remove the Last K Elements

Given {10, 20, 30, 40, 50} and k = 2, remove the final two elements so that the vector becomes {10, 20, 30}. The problem guarantees that k is non-negative and no greater than the vector's size.

#include <iostream>
#include <vector>
using namespace std;
void removeLastK(vector<int>& values, int k) {
while (k > 0) {
values.pop_back();
k--;
}
}
int main() {
vector<int> values{10, 20, 30, 40, 50};
removeLastK(values, 2);
for (int value : values) {
cout << value << ' ';
}
return 0;
}

The loop runs exactly k times, giving O(k) time and O(1) auxiliary space. The size guarantee is essential because pop_back() must never be called on an empty vector.

Problem 5: Row Sums in a Dynamic Matrix

Given {{1, 2, 3}, {4, 5, 6}}, return {6, 15}. This revisits the matrix row-sum problem, but a vector can now be returned safely from the function.

#include <iostream>
#include <vector>
using namespace std;
vector<int> rowSums(const vector<vector<int>>& matrix) {
vector<int> result;
result.reserve(matrix.size());
for (int i = 0; i < static_cast<int>(matrix.size()); i++) {
int sum = 0;
for (int j = 0; j < static_cast<int>(matrix[i].size()); j++) {
sum += matrix[i][j];
}
result.push_back(sum);
}
return result;
}
int main() {
vector<vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6}
};
vector<int> result = rowSums(matrix);
for (int sum : result) {
cout << sum << ' ';
}
return 0;
}

The function works for rectangular and ragged dynamic matrices because each row's own size() is used. If the matrix contains a total of E elements, the time complexity is O(E). The returned result contains one value per row, so its space complexity is O(rows).

Working with Online Judges

Online judges commonly provide vectors and ask only for a function implementation. Their hidden driver code handles input, calls the function, and checks the modified or returned vector.

  • Use a non-const reference when the original vector must be modified.

  • Use a const reference when the function only reads the vector.

  • Return a vector when the problem requires a new sequence.

  • Do not add input or output when the platform asks only for the function.

Summary

  • vector is C++'s standard dynamic-array container.

  • It stores ordered, same-type elements contiguously and manages memory automatically.

  • push_back() and pop_back() grow and shrink the vector from the end.

  • size() counts existing elements, while capacity() describes currently allocated storage.

  • reserve() changes storage capacity without creating elements; resize() changes the element count.

  • Reallocation may invalidate pointers, iterators, and references to vector elements.

  • Passing by reference modifies the original vector, while a const reference provides efficient read-only access.

  • A vector can be returned safely and can contain other vectors to form a dynamic matrix.

C++

Read Similar Blogs

Comments0