CS U111 · Lecture 8 · Lesson — first time through

Arrays, from the beginning

One idea at a time, with the reasoning before the syntax. Allow about 45 minutes, and try each "check yourself" before you open it. The short version is on the notes page. After this lesson, write the programs on the arrays ladder.

Start here · what's actually new

Arrays are new to the whole class. School in India doesn't teach them, and neither did Labs 1–4. What you already have: loops, if, and variables. This lesson adds one idea on top of those: a number that picks which box you mean (an index). Search, sort and 2D grids are all loops walking over that index.

Scope: this page follows Lecture 8 (Arrays in C), whose reading is Hanly & Koffman ch. 7. The Lab 6 sheet hasn't reached us yet, so this page is built from the lecture. When the sheet arrives, its problems get their own worked page. Lecture 8 doesn't cover passing an array into a function. The lecturer is saving that for after pointers, so it doesn't appear here, apart from one warning in §9.

1 · The problem arrays solve

Store the marks of five students with what you have so far and you write five variables: mark1, mark2, … mark5. Now find the highest. You need a separate if for every variable, because the name of each one is fixed when you write the program. A loop can't help. There's no way to say "the i-th variable" when the variables are mark1 to mark5.

Now make it 100 students. Or as many as the user types in. The copy-and-paste approach stops working, just as it did for "print Hello n times" before loops.

The shift this lecture asks for

Until now each variable held one value. An array is one name holding many values of the same type. You choose a value with a number, and that number can be a variable. So marks[i] inside a loop means "each mark in turn", which is exactly what mark1…mark100 could never do.

2 · Declaring one: a row of boxes

// data_type  name[size];
int    marks[5];          // room for 5 ints
double temperatures[7];   // room for 7 doubles
char   grades[10];        // room for 10 chars

The number in brackets is the size: how many values C reserves space for. marks[5] holds exactly five ints. Every element has the same type, so you can't mix ints and doubles in one array.

What C does with that declaration explains everything later on this page. It sets aside five boxes next to each other in memory, back to back with no gaps:

int marks[5] = {72, 85, 91, 68, 77}; 72 85 91 68 77 marks[0] marks[1] marks[2] marks[3] marks[4] ? marks[5] not yours 5 boxes × 4 bytes = 20 bytes, no gaps
Five boxes numbered 0 to 4. The dashed box after them is memory that belongs to something else. C lets you read it anyway (§4).

Two facts come straight out of the picture:

  1. Numbering starts at 0. The first box is marks[0], the last is marks[4]. An array of size 5 has indices 0 to 4, and there is no index 5. The index says how many boxes to step past from the start: the first box is 0 steps in.
  2. Any box can be reached instantly. The boxes are evenly spaced, so C finds box i with arithmetic (start + i × 4 bytes). It doesn't search for it. That's why marks[3] is just as fast as marks[0].
Check yourself: double temperatures[7]; — what are the first and last valid indices?

0 and 6. The last valid index is always size − 1. Nearly every array bug on this page comes from forgetting that.

3 · Reading and writing one box

An element like marks[2] behaves exactly like an ordinary int variable. You can print it, use it in arithmetic, or assign to it:

printf("%d\n", marks[2]);   // 91
marks[2] = 95;              // overwrite that one box
printf("%d\n", marks[2]);   // 95

And here's the idea from §1 that makes arrays useful: the thing in the brackets can be any integer expression, including a variable:

int i = 3;
printf("%d\n", marks[i]);       // 68  — box 3
printf("%d\n", marks[i + 1]);   // 77  — box 4

Read marks[i] aloud as "marks, box i". Which box that is depends on what i holds right now. Put that inside a loop where i goes 0, 1, 2, 3, 4, and the same line of code visits every box. The rest of this lesson builds on that.

Check yourself: with marks = {72, 85, 91, 68, 77} and i = 1, what is marks[i] + marks[i*2]?

marks[1] + marks[2] = 85 + 91 = 176. Work out the index first, then look up the box.

4 · Going past the end: C won't stop you

Ask for marks[5] and you might expect an error. There isn't one. C doesn't check array bounds. It does the same arithmetic as always (start + 5 × 4 bytes) and hands you whatever is stored there. That memory belongs to another variable, or to nothing at all.

int marks[5] = {72, 85, 91, 68, 77};
printf("%d\n", marks[5]);   // compiles, and runs

With clang -Wall you get a warning, because the 5 is written out explicitly:

warning: array index 5 is past the end of the array (that has type 'int[5]') [-Warray-bounds]

On the Mac these notes were written on, it then printed 1. That number means nothing, and yours may be different. What the C standard actually says is that going past the end is undefined behaviour: the program might print junk, might seem to work, or might crash. Writing past the end is the most dangerous case, because it overwrites someone else's data. The lecture's point that there's no compiler error is exactly right. Just don't count on a quiet wrong answer either: sometimes you get a crash.

Classic trap · <= in the loop

The usual way to go past the end is for (int i = 0; i <= 5; i++) instead of i < 5. The loop takes i = 0, 1, 2, 3, 4, 5, and the last pass reads the box that isn't yours. Tested: summing that way printed sum = 394 instead of 393, with no warning at all. The compiler only warns when the bad index is a written-out number, not when it comes from a loop variable. Your eyes are the only check.

5 · Giving the boxes starting values

A new array holds garbage, like a new int does (Lab 4's uninitialised-accumulator bug). You can fill it when you declare it:

int marks[5]   = {72, 85, 91, 68, 77};   // all five given
int numbers[]  = {10, 20, 30, 40};        // size left out: C counts 4
int zeros[100] = {0};                     // all 100 are 0
int part[5]    = {1, 2};                  // 1 2 0 0 0

The rule behind the last two lines: if you give fewer values than the size, the rest become 0. So {0} means "the first is 0, and the rest default to 0", which gives all zeros. Tested: part prints 1 2 0 0 0.

You can leave the size out only when you give values, because that's the only way C can count them. int numbers[]; on its own is an error:

error: definition of variable with array type needs an explicit size or an initializer
Check yourself: what does int t[4] = {5}; hold?

5 0 0 0. Only the first value is given, and the rest default to 0. It is not four 5s. {0} gives all zeros only because the leftover boxes are zero anyway.

6 · The traversal pattern — the most important four lines

Putting §3 and Lab 4 together gives the pattern almost everything else uses:

for (int i = 0; i < size; i++) {
    // do something with array[i]
}

Check it against the loop questions from Lab 4. State: i starts at 0, the first index. Stop: keep going while i < size, so the last pass has i = size − 1, the last index. Advance: i++. The < is what makes it exact: for (i = a; i < b; i++) runs b − a times, and 5 − 0 = 5 boxes.

int marks[5] = {72, 85, 91, 68, 77};
for (int i = 0; i < 5; i++) {
    printf("%d\n", marks[i]);
}
72
85
91
68
77

Every other program on this page is this loop with a different line in the body. The lecture lists the four steps: declare, initialise, traverse, operate. The next three sections are three different "operate" lines.

7 · Sum and average

int sum = 0;                           // the total of the boxes seen so far: none yet
for (int i = 0; i < 5; i++) {
    sum += marks[i];
}
double average = (double) sum / 5;
sum = 393, average = 78.60

The accumulator is the one from Lab 4. What's new is where the numbers come from: the boxes, not i. That's why the body says marks[i], not i.

The (double) on the last line matters. sum and 5 are both ints, and int ÷ int drops the fraction. Without the cast, sum / 5 gives 78, and storing that in a double only makes it 78.00. Tested: without cast = 78.00. The cast turns sum into a double before dividing, so the division keeps the fraction.

Check yourself: why is (double)(sum / 5) still wrong?

The brackets make the division happen first, as int ÷ int = 78. Converting that 78 to a double afterwards gives 78.0. The fraction is already gone. The cast has to be applied to an operand before the division.

8 · Minimum and maximum

int minVal = marks[0];             // first guess: the first box
for (int i = 1; i < 5; i++) {      // so start comparing from box 1
    if (marks[i] < minVal) {
        minVal = marks[i];         // found a smaller one — remember it
    }
}

Think of it as walking along the row holding the smallest number seen so far and swapping it whenever you meet a smaller one. Trace it: 72 (start) → 85 no → 91 no → 68 yes → 77 no. Result: min = 68. For the maximum, flip < to >. Tested: max = 91.

Why start with marks[0]? You need a starting guess, and it has to be one of the actual values. The first box always is. The obvious-looking alternative, starting at 0, fails:

Classic trap · seeding the minimum with 0

int minVal = 0; looks harmless. But every mark is bigger than 0, so no mark ever beats it, and the program says the minimum is 0. Tested: min seeded with 0 = 0. The same thing happens to a maximum seeded with 0 when every value is negative. Seed with a[0] every time.

Check yourself: how would you find the index of the minimum instead of its value?

Keep an index instead of a value: int minIdx = 0; then if (marks[i] < marks[minIdx]) minIdx = i;. For these marks it ends at 3. Keep this version in mind: selection sort in §12 is built on it.

9 · The sizeof trick — and where it stops working

Writing 5 into every loop is fragile. Change the array to 6 marks and you have to find every 5 and fix it. C can count for you:

int size = sizeof(marks) / sizeof(marks[0]);
sizeof(marks) = 20, sizeof(marks[0]) = 4, size = 5

sizeof gives a size in bytes. The whole array takes 20 bytes, and one element (an int on this machine) takes 4, so there are 20 ÷ 4 = 5 elements. Because it divides by the element size, it works for any element type.

Unpack this step — bytes, and why an int is 4 of them

A byte is 8 bits, the smallest amount of memory with its own address (Lecture 4's bits and bytes). On the Macs and lab PCs you'll use, an int is 4 bytes and a double is 8. C doesn't guarantee those numbers everywhere, which is one more reason to let sizeof count rather than typing 4.

Classic trap · the trick fails inside a function

This only works where the array was declared. Once an array is passed into a function (coming after pointers), the function only receives the array's starting address, not the whole array. Tested on a Mac: the same line gives in main: 5 but in f: 2. clang warns: "sizeof on array function parameter will return size of 'int *' instead of 'int[]'". Functions that take arrays always take the size as a separate parameter too. You'll see why when pointers arrive. For now: use sizeof only in the function where the array is declared.

10 · Linear search: is it in there?

To find where 91 is, check the boxes one by one from the left and stop at the first match. If you reach the end without one, it isn't there.

int target = 91;
int foundIndex = -1;                 // -1 means "not found (yet)"
for (int i = 0; i < 5; i++) {
    if (marks[i] == target) {
        foundIndex = i;
        break;                        // stop at the first match
    }
}
foundIndex = 2

Why −1? The answer is an index, and every index from 0 up is a real box. −1 can never be a valid index, so it clearly means "not found". Search for 80 and nothing ever overwrites it: tested, foundIndex for 80 = -1. After the loop, check if (foundIndex == -1) before using the result.

The break is Lab 4's "stop as soon as you find it". Without it the program still gives the right answer here, but keeps checking boxes 3 and 4 for nothing. With repeated values it would also give you the last match instead of the first.

Check yourself: the lecture's activity asks you to count how many students scored exactly 90. Why isn't that a search?

A search can stop at the first 90. Counting has to look at every box, because the next one might be another 90. So you drop the break and use a counter: if (scores[i] == 90) count++;. That's the difference the activity is getting at: "is it there?" can stop early; "how many?" has to visit every box.

11 · Binary search: faster, but only on sorted data

Linear search checks every box in the worst case. If the array is sorted, you can do much better, the way you'd look up a word in a dictionary. Open it in the middle, see which half the word must be in, and ignore the other half completely. Each look halves what's left.

int left = 0, right = n - 1;
int foundIndex = -1;
while (left <= right) {
    int mid = left + (right - left) / 2;
    if (arr[mid] == target)     { foundIndex = mid; break; }
    else if (arr[mid] < target) left = mid + 1;     // target is to the right
    else                        right = mid - 1;    // target is to the left
}

left and right mark the part of the array that could still contain the target. The loop keeps going while that part has at least one box (left <= right). Here's the lecture's trace, searching the sorted marks {68, 72, 77, 85, 91} for 85 (every row below is from the compiled program):

stepleft, rightmidarr[mid]decision
10, 427777 < 85 → search right: left = 3
23, 4385match → found at index 3

The lecture doesn't show what happens when the target isn't there. Search for 70:

stepleft, rightmidarr[mid]decision
10, 427777 > 70 → search left: right = 1
20, 106868 < 70 → search right: left = 1
31, 117272 > 70 → search left: right = 0
—1, 0——left > right: nothing left to search → −1

This is how "not found" shows up: left and right cross. The + 1 and − 1 are what make them cross. Without them the range can stop shrinking and the loop never ends. It's the same guessing-game loop as Lab 4's Problem 5.

Unpack this step — why left + (right − left) / 2 is just the midpoint

Algebra: l + (r − l)/2 = (2l + r − l)/2 = (l + r)/2. Same value. The lecture prefers the first form because left + right can overflow on huge arrays before the division happens. For your arrays the two give identical answers, so write either.

Classic trap · binary search on unsorted data

It doesn't just go slower. It gives wrong answers. Tested on the unsorted {72, 85, 91, 68, 77}, searching for 68 (which is at index 3): mid lands on 91, the code decides 68 must be to the left, throws away the right half, and returns −1. Binary search is only valid when the array is sorted.

Check yourself: at most how many steps does binary search need for 1000 sorted numbers?

About 10, because halving 1000 ten times gets you to one box (210 = 1024). Linear search might need 1000. That gap is the whole reason to sort first.

12 · Counting into categories

How many students got an A? A B? Instead of five separate counter variables, use an array of counters, one box per category:

int counts[5] = {0};   // counts[0]=A, [1]=B, [2]=C, [3]=D, [4]=F — all start at 0
for (int i = 0; i < 5; i++) {
    if      (marks[i] >= 90) counts[0]++;
    else if (marks[i] >= 80) counts[1]++;
    else if (marks[i] >= 70) counts[2]++;
    else if (marks[i] >= 60) counts[3]++;
    else                     counts[4]++;
}
A=1 B=1 C=2 D=1 F=0

Two arrays are working here: marks holds the data and counts holds the tallies. Note that counts is initialised with {0} for the same reason sum = 0 was in §7. The if … else if chain is Lab 3's grade ladder, unchanged.

13 · Sorting: putting the boxes in order

The lecture covers two methods. Both are loops inside loops (Lab 4 §7), and both only ever do two things: compare two boxes, and swap two boxes.

The swap, first

int temp = marks[i];      // 1. save one value
marks[i] = marks[i+1];    // 2. overwrite it
marks[i+1] = temp;        // 3. put the saved value in the other box

It takes three lines and a spare variable, like swapping the drinks in two full glasses: you need a third glass. Try it with two lines, b[0] = b[1]; b[1] = b[0];, and the first line destroys b[0] before you've saved it. Tested on {3, 9}: you get 9 9.

Bubble sort: fix neighbours that are out of order

Go along the row comparing each pair of neighbours, and swap any pair that's the wrong way round. After one full pass the largest value has "bubbled" to the end. It gets swapped along every time it meets something smaller. So the next pass can stop one box earlier. That's what 4 - pass in the lecture's code is for.

for (int pass = 0; pass < 4; pass++) {
    for (int i = 0; i < 4 - pass; i++) {
        if (marks[i] > marks[i+1]) {
            int temp = marks[i];  marks[i] = marks[i+1];  marks[i+1] = temp;
        }
    }
}

Selection sort: find the smallest, put it at the front

Use the min-index idea from §8's check: find the minimum of the whole array and swap it into box 0. Then find the minimum of boxes 1 to 4 and swap it into box 1, and so on. Each pass settles one more box at the front.

for (int i = 0; i < 4; i++) {
    int minIdx = i;
    for (int j = i + 1; j < 5; j++) {
        if (marks[j] < marks[minIdx]) minIdx = j;
    }
    int temp = marks[i];  marks[i] = marks[minIdx];  marks[minIdx] = temp;
}

Now watch both run on the lecture's marks. Step through each one and look at what gets compared:

Sort stepper. The program below works out each step by actually running the sort on {72, 85, 91, 68, 77}. Blue outline = the boxes being compared or swapped. Green = in its final place. Selection sort marks the current minimum with ▼.

What the stepper shows, and what the compiled program confirms:

after passbubble sortselection sort
1{72,85,68,77,91}{68,85,91,72,77}
2{72,68,77,85,91}{68,72,91,85,77}
3{68,72,77,85,91}{68,72,77,85,91}
4{68,72,77,85,91} (no change){68,72,77,85,91} (swap with itself)
totals10 comparisons, 5 swaps10 comparisons, 4 swap statements

The rows agree with the lecture's slides: both arrays are sorted after pass 3. Two details are worth knowing, because a trace question can ask about them:

Stopping early

If a whole pass of bubble sort makes no swaps, every neighbour pair is in order, so the array is sorted. A flag (Lab 4's pattern 5) can notice that and stop:

for (int pass = 0; pass < 4; pass++) {
    bool swapped = false;                 // #include <stdbool.h>
    for (int i = 0; i < 4 - pass; i++) {
        if (marks[i] > marks[i+1]) {
            int temp = marks[i];  marks[i] = marks[i+1];  marks[i+1] = temp;
            swapped = true;
        }
    }
    if (!swapped) break;                  // a clean pass: done
}

Be honest about what this buys. On the lecture's marks it still takes 4 passes (tested), because pass 3 made a swap, so it takes pass 4 to see a clean pass. Where it pays off is nearly sorted input. Given an array that's already sorted, it stops after 1 pass and 4 comparisons instead of 10 (tested). Labs often ask for this version.

Check yourself: after one pass of bubble sort on {5, 1, 4, 2}, what does the array look like?

5 > 1 swap → {1,5,4,2}; 5 > 4 swap → {1,4,5,2}; 5 > 2 swap → {1,4,2,5}. The largest value has bubbled to the end, as promised. A selection-sort pass would instead give {1,5,4,2}: the minimum, 1, swapped into box 0.

14 · Two dimensions: rows and columns

A 2D array is a grid: an array of rows, where each row is itself an array. You need two indices, and the row always comes first.

int grid[3][4];   // 3 rows, 4 columns: 12 ints
col 0 col 1 col 2 col 3 row 0 row 1 row 2 grid[1][2] — row 1 first, then column 2
First index picks the row (down), second picks the column (across). Each row is a row of boxes just like §2's.

Visiting every cell is a nested loop, the multiplication-table shape from Lab 4. The outer loop picks a row, and the inner loop walks along it:

int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};   // one {…} per row
for (int r = 0; r < 2; r++) {
    for (int c = 0; c < 3; c++) {
        printf("%d ", grid[r][c]);
    }
    printf("\n");                          // end of a row
}
1 2 3 
4 5 6 

Adding two matrices, and flipping one

Addition works cell by cell. The same position in each grid goes into the same position in the answer: C[i][j] = A[i][j] + B[i][j];. Transpose turns rows into columns, so what was at row i, column j moves to row j, column i: T[j][i] = A[i][j];. The swapped indices are the whole idea. A 2×3 matrix becomes a 3×2 one, so T must be declared T[3][2].

int A[2][3] = {{1,2,3},{4,5,6}}, B[2][3] = {{10,20,30},{40,50,60}};
int C[2][3], T[3][2];
for (int i = 0; i < 2; i++)
    for (int j = 0; j < 3; j++) {
        C[i][j] = A[i][j] + B[i][j];
        T[j][i] = A[i][j];
    }
A+B:        transpose:
11 22 33    1 4
44 55 66    2 5
            3 6
Check yourself: in the transpose above, where does the 6 go, and why?

6 is A[1][2] (row 1, column 2). The transpose rule sends it to T[2][1], which is row 2, column 1 of T: the bottom-right of the 3×2 result. The output agrees: the last row is 3 6.

Just a look: matrix multiplication

The lecture shows this only so you recognise its shape, and says it won't trace it. Each answer cell is a row of A times a column of B, multiplied pair by pair and added up. A (m×n) can only multiply B (n×p) when the middle numbers match, and the answer is m×p. The code needs three nested loops: two choose the cell, and the third adds up the products.

for (int i = 0; i < m; i++)
    for (int j = 0; j < p; j++) {
        R[i][j] = 0;                             // accumulator — reset per cell
        for (int k = 0; k < n; k++)
            R[i][j] += A[i][k] * B[k][j];
    }
Unpack this step — one cell by hand

With P = [[1,2],[3,4]] and Q = [[5,6],[7,8]], cell R[0][0] = row 0 of P · column 0 of Q = 1×5 + 2×7 = 19. The program gives 19 22 / 43 50. That's school matrix multiplication, if you've met it. If not, the lecture only asks you to recognise the three-loop shape.

More dimensions

int cube[2][3][4]; is 2 grids of 3 rows by 4 columns, 24 ints in total (tested). Each extra bracket is one more index and one more nested loop to visit everything. That's all the lecture asks you to know about it.

Asking an AI well — three prompts pinned to Lecture 8

Use it as a tutor, not a ghostwriter. The handout tests whether you can verify generated code (lectures 40–41), and lab evaluations test what your own hands can type. Prompts that help:

  1. "Give me 5 short C programs that use a 1-D int array and a for loop, like Hanly & Koffman chapter 7, and ask me to predict each output. Include one with i <= size and one where a minimum is seeded wrongly. Don't show answers until I reply."
  2. "Here is my bubble sort (or selection sort) and the array I ran it on: [paste]. Don't fix anything. Show me the array after each pass as a table, so I can compare it with my own trace."
  3. "I'm learning binary search on a sorted int array in C. Give me a sorted array of 9 numbers and two targets, one present and one absent. Ask me to fill in left, right and mid for each step, then check my table."

Anything that starts "write a program that…" skips the practice the lab is testing.

Where to go next

The rung between this page and the lab

Next, write small programs yourself. The arrays ladder is a series of one-idea programs, from traversal to 2D, with outputs to check. The predict-the-output drill generates array and function traces that mark themselves. Do the ladder first, then one drill sheet a day.