Chapter 1 · Lectures

Programming basics, arrays, and vectors

1.1 Arrays and Vectors (general concept)

The motivation: one name, many values

A regular variable holds one value. That is fine when you have a handful of distinct things to track (a wage and a salary), but it falls apart the moment you need a list: the four quarter scores of a basketball game, 365 daily temperature readings, every bid in an auction. Nobody wants to declare score1, score2, score3, score4 — and declaring temperature_001 through temperature_365 is absurd.

A useful analogy: a normal variable is a truck with one cargo bay; an array is a train whose cars are all coupled together and numbered, but which you still call by one name.

Indexing

The essential feature of an array is direct access: given an index, you can read or write the element at that position in constant time, without scanning from the front. You write this with bracket notation in most languages:

myArray[2]       // element at index 2

Zero-based indexing

In C, C++, Java, Python, and most languages you will meet, the first element is at index 0, not 1. An array with 4 elements has valid indices 0, 1, 2, 3not 1, 2, 3, 4.

This is not a pointless quirk. The index is the offset from the start of the array. Element 0 is 0 slots from the start; element 3 is 3 slots from the start. The math in the box above works out cleanly because of it.

Arrays vs. vectors

Some languages distinguish between arrays (fixed size, set at creation) and vectors (resizable, can grow or shrink). Coral uses the word array loosely; C++ gives you both:

1.2 Vectors (in C++)

This is where the course drops the Coral training wheels and starts writing real C++. A std::vector is the C++ workhorse container — the one you reach for by default when you need an ordered list — and essentially every CS 300 project uses it.

Declaring a vector

Two things have to happen before you can use a vector in a source file:

#include <vector>     // bring in the vector type
std::vector<int> gameScores(4);   // declare a vector of 4 ints

The piece in angle brackets is the element type — the kind of value every element will hold. The number in parentheses is the initial size.

Accessing elements

You have two ways to read or write an element:

v.at(i)   // bounds-checked: throws std::out_of_range if i is invalid
v[i]      // not bounds-checked: undefined behaviour if i is invalid

Indices start at 0, same as arrays. A vector with 4 elements has valid indices 0, 1, 2, 3. The index can be any non-negative integer expression — a literal, a variable, the result of arithmetic. It cannot be a floating-point value (v.at(1.0) will not compile).

Size, looping, and size_t

Every vector knows its current length via v.size(). The classic indexed loop over every element:

for (std::size_t i = 0; i < v.size(); ++i) {
    std::cout << v.at(i);
}

For read-only traversals, the modern and much cleaner form is the range-based for loop:

for (int value : v) {
    std::cout << value;
}

No index, no bounds to worry about. Use this whenever you do not actually need i.

Initialisation

Four common patterns:

std::vector<int> a(5);               // 5 elements, each 0
std::vector<int> b(5, -1);           // 5 elements, each -1
std::vector<int> c = {3, 1, 4};      // 3 elements, listed explicitly
std::vector<int> d;                  // empty; push_back later

Pattern d is by far the most common in the projects — you start empty and push_back each bid as you read the CSV.

Common errors

  • Missing #include <vector> — the compiler does not know what vector is, and the error message is spectacularly unhelpful (“ISO C++ forbids declaration of vector with no type”).

  • Missing std:: or using namespace std; — same error, same fix.

  • Out-of-range access with [] — no crash, no diagnostic, wrong data. If you cannot reproduce a bug, suspect this first.

  • Signed/unsigned comparison in the loop — warning, not error, but occasionally causes an infinite loop if i goes negative.

1.3 Array / Vector Iteration Drill

The textbook presents this section as three interactive drills with no textual content:

  1. find the maximum value,

  2. count the negatives,

  3. bubble the largest value to the end.

Each drill is a specific instance of a general pattern. Writing those patterns out once makes the drills easier — and more importantly, every iterative algorithm later in the course is a remix of them.

Pattern 1 — Linear scan with a running “best”

Find the maximum.

int maxVal = v.at(0);                // seed with the first element
for (std::size_t i = 1; i < v.size(); ++i) {
    if (v.at(i) > maxVal) {
        maxVal = v.at(i);
    }
}

Find the minimum.

Same pattern, flip the comparison: if (v.at(i) < minVal).

Find the index of the maximum.

When you need the position (e.g., for sorting), track i instead of the value:

std::size_t maxIdx = 0;
for (std::size_t i = 1; i < v.size(); ++i) {
    if (v.at(i) > v.at(maxIdx)) {
        maxIdx = i;
    }
}

Pattern 2 — Linear scan with a running counter

Count the negatives.

int negCount = 0;
for (std::size_t i = 0; i < v.size(); ++i) {
    if (v.at(i) < 0) {
        ++negCount;
    }
}

The same shape solves “how many even numbers,” “how many bids above $100,” “how many strings start with A.” The condition changes; the structure does not.

Running sum / running product.

A tiny variation: instead of incrementing by 1, accumulate the element itself. sum += v.at(i) gives a total; product *= v.at(i) gives a product (seed with 1, not 0).

Pattern 3 — One selection-sort pass

The third drill asks you to move the largest value to the last position of the array. This is exactly one pass of selection sort, and it combines Patterns 1 (find the max index) with a swap:

// After this loop, the largest value sits at v.back().
std::size_t maxIdx = 0;
for (std::size_t i = 1; i < v.size(); ++i) {
    if (v.at(i) > v.at(maxIdx)) {
        maxIdx = i;
    }
}
std::swap(v.at(maxIdx), v.at(v.size() - 1));

Range-based form

When you do not need the index, the same three patterns read much more cleanly:

int maxVal = v.at(0);
for (int x : v) { if (x > maxVal) maxVal = x; }

int negCount = 0;
for (int x : v) { if (x < 0) ++negCount; }

Use the indexed form when you need i (e.g., for the swap in Pattern 3) and the range-based form otherwise.

1.4 Iterating Through Vectors

Section 1.3 covered the patterns (running best, counter, scan-plus-swap). This section is about the loop idiom those patterns are built on, and about the standard-library tools that often replace the loop entirely.

Anatomy of the canonical loop

The loop shape you will type thousands of times in C++:

for (std::size_t i = 0; i < v.size(); ++i) {
    // work with v.at(i) or v[i]
}

Three pieces, each doing one job:

  • Init: std::size_t i = 0.
    Declares the loop counter inside the for header so it does not leak into the surrounding scope. size_t is the type v.size() returns; matching it silences the “signed/unsigned comparison” warning. Start at 0 because indices are zero-based.

  • Condition: i < v.size().
    Continue while i is a valid index. Valid indices for a vector of length nn are 0,1,,n10, 1, \ldots, n{-}1. The last valid index is v.size() - 1, not v.size(), which is why the condition is strict less-than.

  • Update: ++i.
    Advance to the next index. Prefer ++i to i++; see the aside below.

Empty loops and infinite loops

Two classic mistakes with the for header:

  • Missing conditionfor (i = 0; ; ++i). With no condition the loop never exits on its own. It will eventually walk off the end of the vector and crash.

  • Condition that never becomes falsefor (i = 0; i < 99; ++i) over a vector of 365 elements visits only the first 99; a condition referring to something that is not the vector’s size is nearly always a bug. Use v.size() so the loop adapts to whatever the vector actually is.

Common errors

  • Out-of-range access with .at() throws std::out_of_range (program aborts unless you catch it). With [] it is silent undefined behaviour. Bounds-check the index whenever it came from the user.

  • Seeding maxVal or minVal with 0. Covered in 1.3. Seed with v.at(0) instead.

  • Mixing signed and unsigned in the comparison. int i vs. v.size() produces warnings and can surprise you if i ever goes negative. Use size_t.

The standard-library alternative

Most of the loops in 1.3 and this section have a one-liner equivalent in <algorithm> and <numeric>. Knowing these saves a surprising amount of noise in your code:

#include <algorithm>
#include <numeric>

// Sum of all elements
int total = std::accumulate(v.begin(), v.end(), 0);

// Maximum element (returns an iterator; dereference with *)
int maxVal = *std::max_element(v.begin(), v.end());

// Count elements matching a condition
int negCount = std::count_if(v.begin(), v.end(),
                             [](int x) { return x < 0; });

1.5 Multiple Vectors

The parallel-vectors technique

Sometimes the thing you need to store has more than one field per “row.” A standard example pairs country names with the average minutes of TV watched per day in each country:

std::vector<std::string> ctryNames(5);
std::vector<int>         ctryMins(5);

ctryNames.at(0) = "China";   ctryMins.at(0) = 155;
ctryNames.at(1) = "Sweden";  ctryMins.at(1) = 154;
ctryNames.at(2) = "Russia";  ctryMins.at(2) = 246;
ctryNames.at(3) = "UK";      ctryMins.at(3) = 216;
ctryNames.at(4) = "USA";     ctryMins.at(4) = 274;

The convention: same index, same row. Index 3 means “UK, 216 minutes,” no matter which vector you look in. To search, you scan one vector for the key and read the matching index in the other:

for (std::size_t i = 0; i < ctryNames.size(); ++i) {
    if (ctryNames.at(i) == userCountry) {
        std::cout << ctryMins.at(i) << '\n';
        break;              // early exit once we find it
    }
}

The textbook uses a flag variable to stop the loop early:

bool foundCountry = false;
for (i = 0; (i < ctryNames.size()) && (!foundCountry); ++i) {
    if (ctryNames.at(i) == userCountry) {
        foundCountry = true;
        /* ... */
    }
}

This works, but it is not the cleanest form. Two better options:

  • break out of the loop directly — no flag, one less variable to reason about.

  • std::find from <algorithm> expresses “scan for a value” in one line:

    auto it = std::find(ctryNames.begin(), ctryNames.end(), userCountry);
    if (it != ctryNames.end()) {
        std::size_t i = it - ctryNames.begin();  // index from iterator
        std::cout << ctryMins.at(i) << '\n';
    }

Why parallel vectors are a smell

Parallel vectors are a reasonable starting technique, but they are fragile. Two specific ways they go wrong:

  • They drift out of sync. Insert into one vector and forget the other; now index 3 means “UK, 246 minutes” instead of “UK, 216.” The compiler will not warn you.

  • They do not scale past two or three fields. When a “row” has ten fields you end up with ten vectors you have to keep in lockstep — and every operation (sort, delete, filter) has to touch all of them.

When parallel vectors are actually okay

Two niches:

  • Temporary scratch data — e.g., during sorting you sometimes need a parallel “order of original indices” vector.

  • Performance-critical numerical code — storing each field contiguously (“struct of arrays” layout) can make SIMD and cache behaviour better. This is a microoptimisation you will not need in CS 300.

1.6 Vector Resize

Declaring without a size

The size given in the declaration is not mandatory:

std::vector<int> carSales;   // empty: size 0, no elements yet

At this point any access — carSales.at(0), carSales[0], carSales.front() — is invalid, because there is nothing there. You have to grow the vector first.

resize(n)

resize(n) sets the vector’s size to exactly n.

  • If n is larger than the current size, new elements are appended at the end. They are default-initialised: 0 for numeric types, empty string for std::string, default-constructed for your own structs/classes.

  • If n is smaller, the extra elements at the end are destroyed. Anything stored past index n-1 is gone — no warning, no recycle bin.

  • If n equals the current size, resize does nothing.

std::vector<int> v;
v.resize(3);     // v is now {0, 0, 0}
v.at(0) = 122;
v.at(1) = 11;
v.at(2) = 7;     // v is {122, 11, 7}

v.resize(2);     // v is {122, 11}; the 7 is gone
v.at(2);         // throws std::out_of_range

resize() vs. push_back() — which one?

Two different grow-the-vector strategies, and it is worth knowing when each fits.

Size, capacity, and reserve() (what intro texts omit)

A vector tracks two numbers, not one:

When you push_back and size is about to exceed capacity, the vector allocates a bigger internal buffer (typically 1.5×1.5\times or 2×2\times the old capacity), copies everything over, and frees the old buffer. That reallocation is O(n)O(n) — but because it happens geometrically less often as the vector grows, the amortised cost of push_back is O(1)O(1).

If you know ahead of time roughly how big the vector will get, you can skip the intermediate reallocations with reserve:

std::vector<Bid> bids;
bids.reserve(10000);         // capacity -> 10000, size still 0
for (/* each CSV row */) {
    bids.push_back(parseRow(row));  // no reallocations
}
  • v.clear() — empty the vector (size becomes 0; capacity usually untouched).

  • v.pop_back() — remove the last element; shrinks size by 1.

  • v.empty() — returns true when size is 0. Prefer v.empty() to v.size() == 0 because on some containers (not vector, but good habit) size() is not O(1)O(1).

1.7 Vector push_back, back, and pop_back

Section 1.6 already introduced push_back as a way to grow a vector one element at a time. The real value of this section is that it completes the “back of the vector” interface — push_back, back, pop_back — which, taken together, turn a plain vector into a perfectly good stack.

The three back-end operations

Vector as a stack

In fact the C++ standard library’s std::stack is a thin wrapper around exactly this pattern. You will meet it formally in Chapter 4.

Why the back is fast, the front is slow

A common question: why is there no push_front in the headline interface?

This asymmetry is the reason std::vector makes a great stack but a poor queue. A queue needs O(1)O(1) on both ends; a vector does not offer that.

Worked example: building a list by reading input

Small gotchas

  • back() on an empty vector is undefined behaviour — the reference it returns refers to nothing. Always test !v.empty() first when you are not sure.

  • pop_back() on an empty vector is also undefined behaviour.

  • A reference from back() is invalidated by a later push_back if the push triggers a reallocation. Read the value, use it, then push — do not hold a reference across a push.

1.8 Modifying, Copying, and Comparing Vectors

Three distinct topics in one source-text section. The unifying idea, and the thing to walk away with, is that a std::vector has value semantics: assignment copies the data, equality compares the data, and two vectors that were equal a moment ago can diverge without affecting each other.

Modifying elements in a loop

You read an element with v.at(i); you write one with v.at(i) = newValue. There is nothing special about writing during a loop — the same index works for both directions:

// Clamp every negative value to 0.
for (std::size_t i = 0; i < v.size(); ++i) {
    if (v.at(i) < 0) {
        v.at(i) = 0;
    }
}

The range-based form

is just as happy — but the loop variable has to be a reference or you will be modifying a copy:

for (int& x : v) {        // note the &
    if (x < 0) x = 0;
}

Shifting patterns — the direction matters

The two superficially similar left/right shifts in the textbook exercises produce very different results because of the order the writes happen:

Vector copy with =

Assigning one vector to another copies every element:

std::vector<int> origPrices = {10, 20, 30, 40};
std::vector<int> salePrices;

salePrices = origPrices;     // deep copy: salePrices is now {10, 20, 30, 40}
salePrices.at(2) = 27;       // does NOT affect origPrices

Vector comparison with ==

== compares two vectors element-by-element: true if they have the same size and every element pair is equal.

std::vector<int> x = {3, 4};
std::vector<int> y = {3, 4, 0, 7, 8};
std::vector<int> z = {3, 4, 0, 6, 8};

x == y;   // false: different sizes
y == z;   // false: same size, but index 3 differs
y == y;   // true

Worth knowing, since 1.6 introduced capacity:

  • a = b always at minimum resets a’s size. If b is larger than a’s capacity, the internal buffer is reallocated.

  • A push_back that crosses the capacity threshold reallocates — any references or iterators you were holding into the vector are now stale.

  • Capacity grows but does not shrink on its own; see the shrink_to_fit warnbox in .

1.9 Swapping Two Variables

Swapping looks trivial until you write it. The instinct x = y; y = x; overwrites x before its original value is ever used, so both variables end up holding y’s value and the old x is gone. The fix is to save it somewhere first.

Why the naïve version fails

The temp-variable pattern

C++ shortcut: std::swap

C++ gives you the pattern for free in <utility> (or <algorithm> in older standards), and you should almost always use it instead of writing the temp-variable version by hand.

Swapping list elements

Most real swaps are between two positions in a vector, not between two free-standing variables. Reversing a vector is the canonical example: swap index 0 with n-1, index 1 with n-2, and so on, stopping at the middle.

1.10 Debugging Example: Reversing a Vector

The previous section gave the finished, correct reversal loop. This section walks through the three bugs a first attempt typically hits, in the order they surface, because the debugging process is the real lesson. Loop-plus-vector code is where most early C++ bugs live, and the tracing technique used here works for everything later in the course.

The buggy starting point

Imagine writing this without thinking too hard:

Bug 1: off-by-one on the mirror index

Bug 2: assignment is not a swap

After fixing the index, the loop runs without crashing but produces the wrong output — values appear duplicated and others disappear:

The fix is the temp-variable swap (or std::swap):

int tmp = v.at(i);
v.at(i) = v.at(v.size() - 1 - i);
v.at(v.size() - 1 - i) = tmp;

Bug 3: the loop runs twice as long as it should

After fixing the swap, the output comes out identical to the input. The swap is correct this time; the loop just runs too many iterations:

The debugging methodology, not the answer

The final correct version was already given in . What matters here is the technique:

  1. Read the error. out_of_range pointed straight at an index expression. Don’t skip the stack trace.

  2. Trace by hand. Write out the vector contents after each iteration on paper or in a comment. Bugs 2 and 3 are invisible from the source but obvious after three lines of trace output.

  3. Fix one bug at a time. Each fix exposes the next bug. Changing three things at once means you will not know which change did what.

  4. Use .at() during development. Turn silent out-of-bounds into exceptions. Switch to [] later if you have measured that bounds checking is a bottleneck (it rarely is).

1.11 Arrays vs. Vectors

C++ inherits the raw C array and also provides std::vector. They look similar on the surface — indexed access, contiguous storage — but they have very different safety and feature profiles. For this course the rule is simple: use std::vector unless a concrete reason forces otherwise.

Syntax side-by-side

Why arrays are dangerous

An out-of-range write to a C array does not throw, does not warn, does not even necessarily crash. It just scribbles over whatever happens to live at that memory address.

With std::vector the same mistake using .at() throws std::out_of_range immediately, pointing at the exact line.

Feature comparison

::: center Feature C array std::vector


Size fixed at compile time^*^ runtime Can grow / shrink no yes Knows its own size no yes (.size()) Bounds checking available no yes (.at()) Copy with = no (decays to pointer) yes (deep copy) Pass to function as pointer + length as value or const& Compare with == no (pointer compare) yes (element-wise) :::

^*^C99 variable-length arrays exist but are a GCC extension in C++ and should be avoided.

When arrays still show up

You will not eliminate arrays from your C++ code — they appear in places you cannot avoid:

  • String literals are arrays: "hello" is a const char[6].

  • argv in main(int argc, char* argv[]) is a C-style array of C-style strings.

  • Legacy C APIs (POSIX, Win32, most game engines) take pointer-plus-length pairs.

  • Embedded / performance code sometimes needs the stack-only, zero-overhead guarantee of a raw array.

For the fixed-size cases where you would be tempted to use a C array, prefer std::array<T, N> — it has a compile-time size like a C array but carries .size(), .at(), iterators, and value semantics like a vector.

1.12 Two-Dimensional Arrays

A 2D array is a table: R rows ×\times C columns, for R * C elements total. The language lets you write a[r][c] and pretend you are indexing a grid, but underneath, memory is flat. Knowing how the rows are laid out explains cache performance, how to pass 2D data to functions, and why std::vector<std::vector<T>> is not actually a 2D array.

Declaring and using

Iterating: the loop order matters

The idiomatic nested loop walks the outer (row) index first, the inner (column) index second:

Passing a 2D array to a function

This is where raw 2D arrays bite. The compiler needs to know the row length to compute r * C + c, so all dimensions except the first must be spelled out in the parameter type:

The C++ alternative: std::vector of vector

For runtime-sized 2D data the common idiom is a vector of vectors.

Higher dimensions

int a[2][3][5] declares 30 elements; int a[100][100][5][3] declares 150 000. Dimensions multiply, and stack-allocated arrays can blow the stack surprisingly fast. If any dimension is large, allocate with std::vector (heap) rather than a raw array.

1.13 Char Arrays / C Strings

Before std::string existed, a string in C was just an array of chars ending with a special sentinel byte. C++ still supports this representation — it’s what you get from string literals, command-line arguments, and every legacy C API you will ever link against — so it is worth understanding even though modern C++ code should use std::string.

Declaring a C string

Why printing “just works”

std::cout << some_c_string does not print 20 characters. It prints characters one at a time until it sees ’\0’. That convention runs through every C string API — printf, strlen, strcpy, file I/O — which is why a missing or overwritten null byte turns into a memory-corruption bug instead of a tidy error.

Three common bugs

You cannot assign to a C string

C strings vs. std::string

::: center C string (char[]) std::string


Header <cstring> <string> Stores length no (scan for \0) yes (.size()) Assignable with = no yes Concat with + no yes Resizes automatically no yes Bounds-checked access no .at() :::

1.14 String Library Functions

Because C strings do not know their own length and do not support = or ==, the standard library ships a family of free functions that take raw char* pointers and walk them looking for the null terminator. They all live in <cstring> (the C++ name) or <string.h> (the C name). You will recognize most of them from any C tutorial.

Modifying strings

::: center


strcpy(dst, src) Copy src (through null) into dst. strncpy(dst, src, n) Copy up to n chars. strcat(dst, src) Append src to the end of dst. strncat(dst, src, n) Append up to n chars, then a null.


:::

Inspecting strings

::: center


strlen(s) Returns number of chars before the null (type size_t). strcmp(a, b) <0<0 if a < b, 0 if equal, >0>0 if a > b. strncmp(a, b, n) Same, limited to first n chars. strchr(s, ch) Pointer to first ch in s, or NULL. strstr(hay, needle) Pointer to first occurrence of needle, or NULL.


:::

The three strcmp traps

These come up constantly and none of them are syntax errors — the compiler will not catch any of them.

A common pattern: transform characters in place

The modern C++ translation

Every function in this section has a cleaner std::string equivalent that does not buffer-overflow and knows its own size.

::: center C C++ (std::string)


strcpy(dst, src) dst = src; strcat(dst, src) dst += src; strlen(s) s.size() strcmp(a, b) == 0 a == b strcmp(a, b) < 0 a < b strchr(s, ch) s.find(ch) strstr(h, n) h.find(n) :::

1.15 Char Library Functions: cctype

<cctype> is the C++ header for classifying and converting individual char values — “is this letter a digit?” “convert this to uppercase.” It’s tiny, and every function takes an int, returns an int, and is named issomething or tosomething. Learn the short list and you have 95% of the character-manipulation toolbox.

Classification: “is this character a …?”

All of these return an int — nonzero for true, 0 for false. You will usually use them in an if condition directly.

::: center


isalpha(c) alphabetic: az or AZ isdigit(c) decimal digit: 09 isalnum(c) isalpha or isdigit isspace(c) space, tab, newline, CR, vertical tab, form-feed isblank(c) just space or tab islower(c) / isupper(c) az / AZ isxdigit(c) hex digit: 09, af, AF ispunct(c) printable, not space, not alphanumeric isprint(c) printable (includes space) iscntrl(c) control char — the complement of isprint


:::

Conversion: toupper and tolower

Both take a character and return a (possibly different) character. If the input isn’t a letter of the relevant case, the function returns it unchanged — so you can apply it to every character in a string without branching.

A concrete use case: case-insensitive compare

strcmp is case-sensitive. "Apple" and "apple" are not equal to it. The standard trick is to lowercase both sides and compare.

The C++20 alternative: ranges + lambdas

When you are already using std::string (and you should be), the loop gets shorter and the cast disappears into the algorithm.

Parsing pattern: extract digits from a string

This pops up in every intro assignment that asks you to pull a phone number, zip code, or ID out of user input:

Summary

Chapter 1 is a whirlwind review of the C++ building blocks every later data-structures topic assumes. The threads worth carrying forward:

  • Prefer safe abstractions. std::vector over raw arrays, std::string over char[], .at() over [] while debugging. Every C-flavored alternative has a silent-failure mode that costs more to debug than the bounds-checked version costs to run.

  • Indices are contracts. size_t is unsigned; v.size() - 1 underflows catastrophically on an empty vector; a 2D index is r * C + c under the hood. Treat every index expression as something to verify, not guess.

  • Copy is not free. Vector copy is O(n); passing a big container by value does a deep copy. Use const& when you only read, & when you mutate, and move semantics when ownership transfers. Value semantics are the reason all of this feels sane compared to C.

  • Algorithms over loops. accumulate, max_element, count_if, find, reverse, swap, transform — the <algorithm> header already has the loop you were about to write. Reach for it first.

  • Debug by tracing, not by staring. When a loop produces the wrong output, print the state after every iteration. Three bugs in a ten-line reversal loop were only findable that way.

  • Vectors are the runway for the whole course. Stacks, queues, hash tables, and heaps will all be built on top of std::vector or the indexing pattern it exposes. The habits you lock in here — correct sizing, bounds checking, pass-by-reference, reserve-before-loop — will carry through every remaining chapter.

Companion materials. A two-page compact notes reference for this chapter lives at chapters/ch_1/notes.tex; twelve drill prompts you can paste into a fresh Claude session live at chapters/ch_1/practice.md. Use the notes to review, the prompts to test.

interactive mode active