From raw character arrays to the modern library types that make C++ livable. How data lives in contiguous memory, and why that matters.
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.
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.Click "Push" to add elements. The array has a fixed size and overflows. The vector grows automatically.
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
| Operation | Effect |
|---|---|
| s.size() | Number of characters |
| s.empty() | True if no characters |
| s[i] | Access character at index i (no bounds check) |
| s1 + s2 | Concatenation (returns new string) |
| s1 == s2 | Content comparison |
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.s.size()?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 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).
Click "push_back" to add elements. Watch size grow linearly while capacity doubles.
push_back?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() 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().
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.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."
insert/erase to get a valid iterator.v.end() point to?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)
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.
sizeof(arr) (returns total bytes, not pointer size), &arr (returns a pointer to the whole array), and decltype(arr) (gives the array type).arr equivalent to in most expressions?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
Click the arrows to move the pointer through the array. The address and dereferenced value update in real time.
p points to arr[1] and arr is {10,20,30,40,50}, what is *(p+2)?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.
Push elements to a vector and watch the heap allocation grow. When capacity is exceeded, the data relocates to a new, larger block.
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;
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.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.
for (const auto &x : v) give you?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 Chapter | Builds Toward |
|---|---|
| vector | Sequential containers (Ch 9), algorithm input (Ch 10) |
| Iterators | Generic algorithms (Ch 10), iterator categories |
| Arrays / pointers | Dynamic memory (Ch 12), C interop |
| Range-for | Lambda expressions (Ch 10), structured bindings (C++17) |
| Memory layout | Move semantics (Ch 13), allocators |
Next up: Chapter 6: Functions — where we learn how to pass these containers efficiently.