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, 3 — not 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 whatvectoris, and the error message is spectacularly unhelpful (“ISO C++ forbids declaration of vector with no type”). -
Missing
std::orusing 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
igoes negative.
1.3 Array / Vector Iteration Drill
The textbook presents this section as three interactive drills with no textual content:
-
find the maximum value,
-
count the negatives,
-
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 theforheader so it does not leak into the surrounding scope.size_tis the typev.size()returns; matching it silences the “signed/unsigned comparison” warning. Start at0because indices are zero-based. -
Condition:
i < v.size().
Continue whileiis a valid index. Valid indices for a vector of length are . The last valid index isv.size() - 1, notv.size(), which is why the condition is strict less-than. -
Update:
++i.
Advance to the next index. Prefer++itoi++; see the aside below.
Empty loops and infinite loops
Two classic mistakes with the for header:
-
Missing condition —
for (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 false —
for (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. Usev.size()so the loop adapts to whatever the vector actually is.
Common errors
-
Out-of-range access with
.at()throwsstd::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
maxValorminValwith 0. Covered in 1.3. Seed withv.at(0)instead. -
Mixing signed and unsigned in the comparison.
int ivs.v.size()produces warnings and can surprise you ifiever goes negative. Usesize_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
}
} Short-circuiting the search
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:
-
breakout of the loop directly — no flag, one less variable to reason about. -
std::findfrom<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
nis larger than the current size, new elements are appended at the end. They are default-initialised:0for numeric types, empty string forstd::string, default-constructed for your own structs/classes. -
If
nis smaller, the extra elements at the end are destroyed. Anything stored past indexn-1is gone — no warning, no recycle bin. -
If
nequals the current size,resizedoes 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 or the old capacity), copies everything over, and frees the old buffer. That reallocation is — but because it happens geometrically less often as the vector grows, the amortised cost of push_back is .
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
} Related operations
-
v.clear()— empty the vector (size becomes 0; capacity usually untouched). -
v.pop_back()— remove the last element; shrinks size by 1. -
v.empty()— returnstruewhen size is 0. Preferv.empty()tov.size() == 0because on some containers (notvector, but good habit)size()is not .
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 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 laterpush_backif 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 Related: does the vector’s backing memory move?
Worth knowing, since 1.6 introduced capacity:
-
a = balways at minimum resetsa’s size. Ifbis larger thana’s capacity, the internal buffer is reallocated. -
A
push_backthat 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_fitwarnbox 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:
-
Read the error.
out_of_rangepointed straight at an index expression. Don’t skip the stack trace. -
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.
-
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.
-
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 aconst char[6]. -
argvinmain(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 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) if a < b, 0 if equal, 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: a—z or A—Z
isdigit(c) decimal digit: 0—9
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) a—z / A—Z
isxdigit(c) hex digit: 0—9, a—f, A—F
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::vectorover raw arrays,std::stringoverchar[],.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_tis unsigned;v.size() - 1underflows catastrophically on an empty vector; a 2D index isr * C + cunder 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::vectoror 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.