C++ Primer, Chapter 3

Strings, Vectors, and Arrays

From raw character arrays to the modern library types that make C++ livable. How data lives in contiguous memory, and why that matters.

Prerequisites: Chapter 2 (Variables). That's it.
9
Chapters
5+
Simulations

Chapter 0: Why Containers?

A single int can hold one number. But almost every real program needs to work with collections: a list of names, a sequence of sensor readings, a grid of pixels. How do you hold many values at once?

C gives you one tool: the array — a fixed-size block of contiguous memory. It's fast but unforgiving: you must know the size at compile time, there's no bounds checking, and passing arrays to functions is awkward. C++ keeps arrays but adds library types that manage memory for you: string for text and vector for sequences of anything.

The core idea: Arrays give you raw performance but no safety net. string and vector wrap arrays with automatic memory management, bounds information, and a clean interface. Use the library types unless you have a specific reason not to.
Array vs Vector

Click "Push" to add elements. The array has a fixed size and overflows. The vector grows automatically.

Elements: 0
What happens when you try to add an element beyond an array's fixed size?

Chapter 1: string — Text That Manages Itself

The std::string type manages a dynamically-sized sequence of characters. Unlike a C-style char array, a string knows its own length, can grow and shrink, and handles memory allocation for you.

c++
#include <string>
using std::string;

string s1;                // default init: empty string ""
string s2 = "hello";      // copy from string literal
string s3(5, 'x');       // "xxxxx" — 5 copies of 'x'
string s4 = s2 + " world";// concatenation

Key Operations

OperationEffect
s.size()Number of characters
s.empty()True if no characters
s[i]Access character at index i (no bounds check)
s1 + s2Concatenation (returns new string)
s1 == s2Content comparison
size() returns size_type, not int. This is an unsigned type. Comparing s.size() < n when n is a negative int will do the wrong thing because the negative value gets converted to a huge unsigned number. Use decltype(s.size()) or just auto.
What is the type returned by s.size()?

Chapter 2: vector — The Workhorse Container

A std::vector is a dynamically-sized array. It stores its elements in contiguous memory, just like a C array, but it handles allocation and resizing for you. When you push_back an element and the vector is full, it allocates a new, larger block (typically 2x), copies everything over, and frees the old block.

c++
#include <vector>
using std::vector;

vector<int> v1;                 // empty vector of ints
vector<int> v2(10, 0);          // 10 elements, all 0
vector<int> v3 = {1,2,3,4,5}; // list initialization
v1.push_back(42);               // add element to end

Size vs Capacity

Size is how many elements the vector currently holds. Capacity is how many it can hold before needing to reallocate. When size exceeds capacity, the vector doubles its capacity (on most implementations), copies all elements, and frees the old memory. This is why push_back is amortized O(1) but occasionally O(n).

The growth pattern: Start with capacity 1. Push until full. Double to 2. Push until full. Double to 4. Then 8, 16, 32... Each doubling costs a full copy, but it happens exponentially less often. On average, each push costs O(1).
Vector Growth Visualizer

Click "push_back" to add elements. Watch size grow linearly while capacity doubles.

size: 0 / cap: 0
If a vector has size 4 and capacity 4, what happens on the next push_back?

Chapter 3: Iterators — Generalized Pointers

An iterator is an object that points to an element in a container. You can advance it (like incrementing a pointer), dereference it (to get the element), and compare two iterators (to check if you've reached the end). Iterators are the glue between containers and algorithms.

c++
vector<int> v = {10, 20, 30};
auto it = v.begin();   // points to first element (10)
*it;                    // 10 — dereference
++it;                   // advance to next element (20)
it == v.end();          // false — end() is one past the last

begin() and end()

begin() returns an iterator to the first element. end() returns an iterator to one past the last element. This is a half-open range [begin, end). If the container is empty, begin() == end().

Why half-open ranges? Because looping is natural: while (it != end) { process(*it); ++it; }. There's no off-by-one error, and an empty range (begin == end) means the loop body never executes. Elegant.

Iterator Invalidation

This is the trap. Operations that change a container's size can invalidate existing iterators. If a vector reallocates, all iterators are invalidated. Using an invalidated iterator is undefined behavior — the dreaded "dangling iterator."

Golden rule: Don't modify a container's size while iterating over it with iterators. If you must, use the return value of insert/erase to get a valid iterator.
What does v.end() point to?

Chapter 4: Arrays — The Low-Level Foundation

A built-in array is a fixed-size, contiguous block of memory. Unlike a vector, its size must be known at compile time (or allocated dynamically). Unlike a vector, it has no member functions, no bounds checking, and no automatic resizing.

c++
int arr[5] = {1, 2, 3, 4, 5};  // fixed size = 5
int *p = arr;                     // array name decays to pointer
arr[2];                            // 3 — subscript
*(p + 2);                          // 3 — pointer arithmetic (same thing)

Array Decay

In most contexts, the name of an array decays to a pointer to its first element. This is why you can write int *p = arr; without the address-of operator. It also means arrays cannot be copied or assigned — what looks like a copy actually just copies the pointer.

When does an array NOT decay? In three places: sizeof(arr) (returns total bytes, not pointer size), &arr (returns a pointer to the whole array), and decltype(arr) (gives the array type).
What is arr equivalent to in most expressions?

Chapter 5: Pointer Arithmetic

Pointer arithmetic is the key to understanding how arrays work under the hood. When you add an integer n to a pointer, the pointer moves forward by n * sizeof(element) bytes. The subscript operator arr[i] is literally defined as *(arr + i).

c++
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;       // points to arr[0]
*(p + 2);            // 30 — same as arr[2]
p[3];                // 40 — subscript works on pointers too
int *q = p + 5;     // one past the end (like end())
q - p;               // 5 — pointer difference = number of elements
This is what iterators generalize. A vector iterator does exactly what pointer arithmetic does for arrays: advance, dereference, and compare. The difference is that iterators work on any container, not just contiguous memory. This abstraction is what makes the STL algorithms so powerful.
Pointer Arithmetic Visualizer

Click the arrows to move the pointer through the array. The address and dereferenced value update in real time.

If p points to arr[1] and arr is {10,20,30,40,50}, what is *(p+2)?

Chapter 6: Memory Layout Explorer

Let's see how strings, vectors, and arrays actually live in memory. A vector object on the stack holds three pointers: one to the start of the data on the heap, one to the end of the used region, and one to the end of the allocated region. When it grows, the heap data moves — but the stack object stays put.

Container Memory Simulator

Push elements to a vector and watch the heap allocation grow. When capacity is exceeded, the data relocates to a new, larger block.

size: 0 / cap: 0
Notice: The vector object itself lives on the stack (3 pointers = 24 bytes on 64-bit). The actual element data lives on the heap. When the vector grows past its capacity, it allocates a new block on the heap, copies elements, and frees the old block. The stack object stays at the same address.

Chapter 7: Range For — The Modern Idiom

C++11 introduced the range-based for loop, which iterates over every element in a container without explicit iterator management:

c++
vector<int> v = {1, 2, 3, 4, 5};
for (auto x : v)        // x is a COPY of each element
    cout << x;
for (auto &x : v)       // x is a REFERENCE — can modify
    x *= 2;
for (const auto &x : v) // read-only reference — efficient, safe
    cout << x;
Best practice: Use const auto& by default. It avoids copies (efficient), prevents accidental modification (safe), and works with every type (generic). Only use auto& when you need to modify elements, and plain auto only for cheap-to-copy types like int.

Under the Hood

The compiler translates for (auto x : v) into:

c++
for (auto __begin = v.begin(), __end = v.end();
     __begin != __end; ++__begin) {
    auto x = *__begin;
    // loop body
}

This means the range-for works with any type that has begin() and end(). It also means you must not modify the container's size during iteration — the cached end() would be stale.

What does for (const auto &x : v) give you?

Chapter 8: Beyond — Connections

This chapter introduced the three fundamental ways C++ stores sequences: arrays (low-level, fixed), strings (text-specific, dynamic), and vectors (general-purpose, dynamic). The iterator abstraction unifies them all.

This ChapterBuilds Toward
vectorSequential containers (Ch 9), algorithm input (Ch 10)
IteratorsGeneric algorithms (Ch 10), iterator categories
Arrays / pointersDynamic memory (Ch 12), C interop
Range-forLambda expressions (Ch 10), structured bindings (C++17)
Memory layoutMove semantics (Ch 13), allocators
Lippman's advice: "Use vectors and strings instead of built-in arrays and C-style strings. Use iterators instead of subscripts." The library types are safer, more expressive, and no slower.

Continue Reading

Next up: Chapter 6: Functions — where we learn how to pass these containers efficiently.