Vectors and Arrays
Sequence containers store elements in a linear order. C++ provides several: C-style arrays (low-level), std::array (fixed-size, stack-allocated), and std::vector (dynamic, heap-allocated). std::vector is the most used container in C++ — it is the default choice unless you have a specific reason for something else.
C-Style Arrays
#include <iostream>
using namespace std;
int main() {
// Fixed size — must be known at compile time (or use VLAs, which are non-standard)
int arr[5] = {10, 20, 30, 40, 50};
// Access
cout << arr[0] << "\n"; // 10
arr[2] = 99;
// No bounds checking — arr[10] is undefined behaviour!
int len = sizeof(arr) / sizeof(arr[0]); // 5 — element count trick
cout << "Length: " << len << "\n";
// C arrays decay to pointer when passed to functions
// — size information is lost!
return 0;
}Use C arrays only when interfacing with C code or embedded systems. In all other cases, prefer std::array or std::vector.
std::array — Fixed-Size, Stack-Allocated
#include <iostream>
#include <array>
#include <algorithm>
using namespace std;
int main() {
array<int, 5> a = {3, 1, 4, 1, 5};
// Size known at compile time — no runtime overhead
cout << a.size() << "\n"; // 5
cout << a.front() << "\n"; // 3
cout << a.back() << "\n"; // 5
cout << a.at(2) << "\n"; // 4 (bounds-checked)
// Ranged for
for (int n : a) cout << n << " ";
cout << "\n"; // 3 1 4 1 5
// Sort
sort(a.begin(), a.end());
for (int n : a) cout << n << " ";
cout << "\n"; // 1 1 3 4 5
// Fill
a.fill(0);
return 0;
}std::vector — Dynamic Array
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Construction
vector<int> v1; // Empty
vector<int> v2(5, 0); // 5 zeros: {0,0,0,0,0}
vector<int> v3 = {1, 2, 3, 4, 5}; // Initializer list
vector<int> v4(v3); // Copy of v3
// Size and capacity
cout << v3.size() << "\n"; // 5 — current element count
cout << v3.capacity() << "\n"; // >= 5 — allocated space
cout << v3.empty() << "\n"; // 0 (false)
// Element access
cout << v3[0] << "\n"; // 1 (no bounds check)
cout << v3.at(4) << "\n"; // 5 (throws if out of range)
cout << v3.front() << "\n"; // 1
cout << v3.back() << "\n"; // 5
return 0;
}Adding and Removing Elements
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
// Add to back
v.push_back(10);
v.push_back(20);
v.push_back(30);
// v = {10, 20, 30}
// emplace_back — constructs in-place (avoids extra copy)
v.emplace_back(40);
// Remove from back
v.pop_back();
// v = {10, 20, 30}
// Insert at position
auto it = v.begin() + 1;
v.insert(it, 15);
// v = {10, 15, 20, 30}
// Erase at position
v.erase(v.begin() + 2);
// v = {10, 15, 30}
// Erase range
v.erase(v.begin(), v.begin() + 2);
// v = {30}
// Clear all elements
v.clear();
cout << v.size() << "\n"; // 0
return 0;
}Capacity Management
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
// reserve — pre-allocate space (avoids reallocations)
v.reserve(1000);
cout << v.capacity() << "\n"; // 1000
cout << v.size() << "\n"; // 0
for (int i = 0; i < 1000; i++) v.push_back(i);
cout << v.size() << "\n"; // 1000
cout << v.capacity() << "\n"; // 1000 — no reallocation happened!
// shrink_to_fit — release unused capacity
v.shrink_to_fit();
// Doubling growth strategy (typical implementation)
vector<int> w;
for (int i = 0; i < 10; i++) {
w.push_back(i);
cout << "size=" << w.size() << " cap=" << w.capacity() << "\n";
}
// Capacity: 1, 2, 4, 4, 8, 8, 8, 8, 16, 16...
return 0;
}Note: When you know the number of elements in advance, always call reserve() before the insertion loop. This prevents O(n) reallocation copies.
2D Vectors
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 3×4 matrix initialised to 0
int rows = 3, cols = 4;
vector<vector<int>> matrix(rows, vector<int>(cols, 0));
// Fill with values
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
matrix[r][c] = r * cols + c + 1;
// Print
for (const auto& row : matrix) {
for (int val : row) cout << val << "\t";
cout << "\n";
}
// 1 2 3 4
// 5 6 7 8
// 9 10 11 12
// Add a row
matrix.push_back({13, 14, 15, 16});
cout << "Rows: " << matrix.size() << "\n"; // 4
return 0;
}Searching and Modifying
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {5, 2, 8, 1, 9, 3, 7, 4, 6};
// Find
auto it = find(v.begin(), v.end(), 9);
if (it != v.end()) {
cout << "Found 9 at index: " << distance(v.begin(), it) << "\n"; // 4
}
// Sort
sort(v.begin(), v.end());
// v = {1, 2, 3, 4, 5, 6, 7, 8, 9}
// Binary search (requires sorted vector)
bool found = binary_search(v.begin(), v.end(), 7);
cout << "Contains 7: " << found << "\n"; // 1
// Remove elements matching predicate
v.erase(remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }), // Remove evens
v.end());
for (int n : v) cout << n << " ";
cout << "\n"; // 1 3 5 7 9
return 0;
}Vector of Structs vs Struct of Vectors
#include <vector>
#include <string>
using namespace std;
// Array of Structures (AoS) — cache-friendly for accessing all fields of one record
struct Particle_AoS {
float x, y, z;
float vx, vy, vz;
float mass;
};
vector<Particle_AoS> particles_aos;
// Structure of Arrays (SoA) — cache-friendly for accessing one field across all records
struct Particles_SoA {
vector<float> x, y, z;
vector<float> vx, vy, vz;
vector<float> mass;
};
// SoA is faster when processing one field at a time (e.g., physics update loops)Practical Example: Histogram
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;
void printHistogram(const vector<int>& data, int bins = 10) {
int minVal = *min_element(data.begin(), data.end());
int maxVal = *max_element(data.begin(), data.end());
int range = maxVal - minVal + 1;
int binW = max(1, (range + bins - 1) / bins);
vector<int> counts(bins, 0);
for (int val : data) {
int bin = min((val - minVal) / binW, bins - 1);
counts[bin]++;
}
int maxCount = *max_element(counts.begin(), counts.end());
for (int i = 0; i < bins; i++) {
int lo = minVal + i * binW;
int hi = lo + binW - 1;
int bars = counts[i] * 50 / maxCount;
cout << setw(4) << lo << "-" << setw(4) << hi << " | "
<< string(bars, '#')
<< " " << counts[i] << "\n";
}
}
int main() {
// Generate sample data (exam scores)
vector<int> scores;
for (int score : {72,85,91,68,78,95,82,61,77,88,
90,74,83,56,92,79,87,65,71,98}) {
scores.push_back(score);
}
cout << "Score Distribution:\n";
printHistogram(scores, 5);
// 56- 63 | ######### 2
// 64- 71 | ############### 3
// 72- 79 | ############################### 6
// 80- 87 | ####################### 5
// 88- 95 | ####################### 4 (approx)
return 0;
}Summary
- C arrays — fixed size, no bounds checking, decay to pointers — avoid in modern C++
std::array<T,N>— fixed size, stack-allocated, full STL interface — prefer over C arraysstd::vector<T>— dynamic size, heap-allocated, amortised O(1) push_back — the default container- Call
reserve(n)before bulk insertions to avoid repeated reallocations - Use
emplace_backinstead ofpush_backto construct objects in-place erase(remove_if(...), end())is the idiomatic way to remove matching elements from a vector- 2D vectors are
vector<vector<T>>— each inner vector can have a different size (jagged)
Next, we'll explore maps and sets — associative containers for key-value pairs and unique element collections.