Module 9 · Arrays
Arrays: Many Boxes, One Name
In this lesson
- Declare an array with a named size, fill it, and read or write any one box by its index.
- Explain why the indexes run from 0 to n - 1, and count the boxes with
sizeof marks / sizeof marks[0]. - Keep capacity and count apart, and recognise what GCC 12 says about a wrong size, index or read.
Amara keeps one mark for each month of the school year: twelve marks, so twelve variables, m1 to m12. The program works, but the line for the average runs off the edge of her screen. Next year she wants any month's mark by its number. This lesson gives her one name for all twelve boxes.
Twelve variables, or one array
Here is Amara's program as she first wrote it.
#include <stdio.h>
int main(void)
{
int m1 = 72;
int m2 = 65;
int m3 = 80;
int m4 = 91;
int m5 = 58;
int m6 = 77;
int m7 = 84;
int m8 = 69;
int m9 = 90;
int m10 = 73;
int m11 = 66;
int m12 = 88;
printf("average: %.2f\n", (m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + m11 + m12) / 12.0);
return 0;
}
average: 76.08
It is correct, and it cannot grow. A question like "what was the mark in month 7?" needs twelve if tests, one per name.
An array is a row of boxes of one type, side by side, under one name. Each box is an element of the array. You pick one by its index, a whole number in square brackets, also called a subscript. Here are the twelve marks as one array.
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int marks[MONTHS] = {72, 65, 80, 91, 58, 77, 84, 69, 90, 73, 66, 88};
int total = 0;
for (int i = 0; i < MONTHS; i++) {
total += marks[i];
}
printf("average: %.2f\n", (double)total / MONTHS);
return 0;
}
Trace the loop before you run it, one row per pass, as in Module 6. Each pass adds one box, marks[i], to total.
| i | Box read | Its value | total after the pass |
|---|---|---|---|
| 0 | marks[0] | 72 | 72 |
| 1 | marks[1] | 65 | 137 |
| 2 | marks[2] | 80 | 217 |
| 3 | marks[3] | 91 | 308 |
| 4 | marks[4] | 58 | 366 |
| 5 | marks[5] | 77 | 443 |
| 6 | marks[6] | 84 | 527 |
| 7 | marks[7] | 69 | 596 |
| 8 | marks[8] | 90 | 686 |
| 9 | marks[9] | 73 | 759 |
| 10 | marks[10] | 66 | 825 |
| 11 | marks[11] | 88 | 913 |
| 12 | none: 12 < 12 is false, the loop ends | 913 |
913 / 12 is 76.083..., and Module 4's (double) cast keeps the fraction.
average: 76.08
So one array and one loop replace twelve names and a long line. Month 7 is now one box, marks[6], and the next sections explain the 6.
Declaring an array with a named size
Declaring an array
#define MONTHS 12
type name[MONTHS] = {v0, v1, ...};
typeis the type of every box; an array never mixes types.nameis one name for the whole row.[MONTHS]is how many boxes to make, fixed when the program is compiled.= {v0, v1, ...}is the initialiser: the values the boxes start with.
This track writes the number of boxes as a #define constant named for what it counts, such as MONTHS or MAX_N. Module 3 promised to explain here why a const int will not do.
A const int is a variable you promised not to change, not a true constant. Inside main, const int months = 12; followed by int marks[months]; compiles with no message at any command line. But it makes a variable length array, or VLA: an array sized while the program runs.
VLAs arrived in C99, and C11 made them optional for compilers. GCC 12 supports them, with rules a fixed array does not have. Move those two lines outside every function, and GCC 12 refuses at every command line: error: variably modified 'marks' at file scope.
So this track never uses a VLA. Every size is a #define, fixed before the program starts.
Indexes run from 0 to n - 1
The first box is box 0, not box 1, as Module 6 promised. In an array of n boxes, the indexes run from 0 to n - 1. So marks[0] is January and marks[11] is December.
Read an index as a distance: how many boxes along from the first. July, the seventh month, is six boxes along, so it is marks[6]. One box works exactly like one int variable.
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int marks[MONTHS] = {72, 65, 80, 91, 58, 77, 84, 69, 90, 73, 66, 88};
printf("July: %d\n", marks[6]);
marks[6] = 90;
printf("July now: %d\n", marks[6]);
printf("January: %d, December: %d\n", marks[0], marks[MONTHS - 1]);
return 0;
}
July: 84
July now: 90
January: 72, December: 88
The assignment changed box 6 and nothing else. So the last of n boxes is marks[n - 1], here marks[MONTHS - 1]. This lesson walks forward only; walking back from marks[n - 1] to marks[0] is Exercise 1.
Initialisers: the values the boxes start with
An initialiser is the list in braces after the =. It fills the boxes left to right, from box 0. Here are four shapes of it, printed as one table.
#include <stdio.h>
#define BOXES 5
int main(void)
{
int full[BOXES] = {3, 1, 4, 1, 5};
int part[BOXES] = {9, 8};
int zeros[BOXES] = {0};
int picked[BOXES] = {[3] = 7};
printf("box full part zeros picked\n");
for (int i = 0; i < BOXES; i++) {
printf("%3d %4d %4d %5d %6d\n", i, full[i], part[i], zeros[i], picked[i]);
}
return 0;
}
Predict each column before you run it. This table is the trace.
| Array | Boxes 0 to 4 | Why |
|---|---|---|
full | 3 1 4 1 5 | five values for five boxes |
part | 9 8 0 0 0 | two values fill boxes 0 and 1; the rest get 0 |
zeros | 0 0 0 0 0 | one value, 0, fills box 0; the rest get 0 |
picked | 0 0 0 7 0 | [3] = 7 names box 3; the rest get 0 |
box full part zeros picked
0 3 9 0 0
1 1 8 0 0
2 4 0 0 0
3 1 0 0 7
4 5 0 0 0
The rule behind part is the zero-fill rule. When a list is shorter than the array, every box it misses starts at 0. The C17 standard states it in section 6.7.9, paragraph 21. So = {0} is not a special command. It is a list of one value, and the rule does the rest.
picked uses a designated initialiser, from C99: [3] = 7 names a box by its index. Module 12 fills a structure's fields the same way.
int scores[] = {120, 95, 310}; leaves the size out, and the compiler makes one box per value. Too many values still compiles: for int a[3] = {1, 2, 3, 4}; the Playground's Compile output tab shows warning: excess elements in array initializer, and the 4 is dropped.
So a list fills from box 0, and every box it leaves out starts at 0. Every array in this track has an initialiser, even if it is only = {0}.
The boxes sit side by side, and sizeof counts them
Module 2 asked sizeof about one int called marks and got 4 bytes. Ask it about an array, and it answers for the whole array.
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int marks[MONTHS] = {0};
printf("one box: %zu bytes\n", sizeof marks[0]);
printf("the array: %zu bytes\n", sizeof marks);
printf("boxes: %zu\n", sizeof marks / sizeof marks[0]);
return 0;
}
one box: 4 bytes
the array: 48 bytes
boxes: 12
Twelve boxes of 4 bytes make 48, and sizeof marks says exactly 48. So there is no gap between the boxes. They sit in one unbroken run of memory, which is called contiguous. Box i starts i x 4 bytes after box 0, the index-as-distance idea in bytes. Module 11 prints those positions for real.
Divide the whole by one box, 48 / 4, and you get 12. That is all sizeof marks / sizeof marks[0] does, and its size_t answer prints with %zu, as in Module 2. It counts every box, used or not, and lesson 05 shows where it stops working.
Capacity and count are two different numbers
The input usually says how many values are coming, but the array must exist before the first read. So you make it big enough for the largest input allowed, and use the front of it.
That gives an array two numbers. The capacity is how many boxes exist: a #define such as MAX_N. The count is how many boxes hold real data: an int n, read while the program runs. Every loop over the data runs to the count.
#include <stdio.h>
#define MAX_N 100
int main(void)
{
int marks[MAX_N] = {0};
int n = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &marks[i]);
}
printf("marks:");
for (int i = 0; i < n; i++) {
printf(" %d", marks[i]);
}
printf("\n");
printf("%d of %d boxes used, the last one is marks[%d]\n", n, MAX_N, n - 1);
return 0;
}
Trace the reading loop for the input 5 and 70 85 62 91 48.
| i | i < n? | scanf reads | Stored in |
|---|---|---|---|
| 0 | 0 < 5, yes | 70 | marks[0] |
| 1 | 1 < 5, yes | 85 | marks[1] |
| 2 | 2 < 5, yes | 62 | marks[2] |
| 3 | 3 < 5, yes | 91 | marks[3] |
| 4 | 4 < 5, yes | 48 | marks[4] |
| 5 | 5 < 5, no: the loop ends | nothing | nothing |
The printing loop makes the same five passes over the same five boxes.
marks: 70 85 62 91 48
5 of 100 boxes used, the last one is marks[4]
That output is for the input 5 and 70 85 62 91 48. Here is the array after the reads.
The dashed boxes are inside the array but outside the data.
Look again at the read, scanf("%d", &marks[i]). One box of an int array is one int, so it takes the ampersand, as Module 3 taught. Module 3's exception was the bare name, marks with no brackets, which already means where the first box is. So the rule never broke: & asks where a box is, and a bare array name already says it. Modules 10 and 11 use the bare name; with brackets, one box always takes its &.
So the capacity is the room you made, and the count is the data you have.
One step past the last box
C does not check an index. marks[100] on a 100-box array compiles and runs, reaching into memory that belongs to something else or to nothing. That is undefined behaviour (Module 4): the program is wrong, even when its output looks right.
Bob fills a 10-box array with 1 to 10, starting at box 1, then prints the ten boxes. His array has no initialiser, and SIZE is 10.
int a[SIZE];
for (int i = 1; i <= SIZE; i++) {
a[i] = i;
}
On the Playground his program ended with Success and printed 0 1 2 3 4 5 6 7 8 9. The only sign was the Compile output tab, which turned amber. GCC 12 warns at every command line, the Playground included: warning: iteration 9 invokes undefined behavior [-Waggressive-loop-optimizations].
GCC counts passes from 0, so iteration 9 is the pass with i = 10, which writes a[10]. GCC 12's -O2 listing on Compiler Explorer shows that write landing in spare bytes just after the array. Nothing lived there, so nothing visibly broke. The leading 0 is a[0], which Bob never wrote.
The warning came only because the loop's bound was a constant. When the index comes from the input, GCC 12 is silent at every command line, -Wall -Wextra included. So the last box is a[n - 1], and a loop over the data tests i < n. Lesson 06 measures what else a stray write can do.
A lookup table is an array you fill once and read by index. Months are numbered from 1, so month m lives in box m - 1.
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int month = 0;
scanf("%d", &month);
printf("month %d has %d days\n", month, days[month - 1]);
return 0;
}
month 2 has 28 days
That output is for the input 2: February is days[1]. The table ignores leap years. A month outside 1 to 12 would read outside the array, and C would not stop it.
Kenji leaves the brackets empty. The compiler counts his scores, and sizeof tells the program how many there are.
#include <stdio.h>
int main(void)
{
int scores[] = {120, 95, 310, 87, 205};
int count = (int)(sizeof scores / sizeof scores[0]);
printf("%d scores:", count);
for (int i = 0; i < count; i++) {
printf(" %d", scores[i]);
}
printf("\n");
printf("first %d, last %d\n", scores[0], scores[count - 1]);
return 0;
}
| i | Box read | The line so far |
|---|---|---|
| 0 | scores[0], 120 | 5 scores: 120 |
| 1 | scores[1], 95 | 5 scores: 120 95 |
| 2 | scores[2], 310 | 5 scores: 120 95 310 |
| 3 | scores[3], 87 | 5 scores: 120 95 310 87 |
| 4 | scores[4], 205 | 5 scores: 120 95 310 87 205 |
| 5 | none: 5 < 5 is false | the newline ends the line |
5 scores: 120 95 310 87 205
first 120, last 205
The (int) cast gives the count the same type as i (Module 4). Add a sixth score and run it again: the count and the last score change, with no other edit.
A teacher re-marks one paper. Amara reads the n marks, then the paper's number and its new mark. She prints the list before and after. Paper k lives in marks[k - 1].
#include <stdio.h>
#define MAX_N 100
int main(void)
{
int marks[MAX_N] = {0};
int n = 0;
int paper = 0;
int new_mark = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &marks[i]);
}
scanf("%d %d", &paper, &new_mark);
printf("before:");
for (int i = 0; i < n; i++) {
printf(" %d", marks[i]);
}
printf("\n");
marks[paper - 1] = new_mark;
printf("after: ");
for (int i = 0; i < n; i++) {
printf(" %d", marks[i]);
}
printf("\n");
return 0;
}
| Box | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| After the reads | 70 | 85 | 62 | 91 | 48 |
After marks[3 - 1] = 68 | 70 | 85 | 68 | 91 | 48 |
before: 70 85 62 91 48
after: 70 85 68 91 48
That output is for the input 5, then 70 85 62 91 48, then 3 68. Only box 2 changed. Each loop walks boxes 0 to 4, like the reading loop traced above.
Zara tries the edges: paper 1 changes marks[0] and paper 5 changes marks[4]. Paper 0 would write marks[-1], before the first box, and C would not stop that either. The print loop appears twice, and lesson 05 turns it into a function.
Where this is used
- Images. A 24-bit BMP file stores each row of a picture as an array of bytes. Each pixel takes three: blue, green, red.
- Sound. CD audio is 44,100 samples a second per channel, each a 16-bit number. A WAV file stores them as one long array, left and right taking turns.
- Character tests. The GNU C Library answers
isdigit(c)by reading one box of a ready-made table. The index is the character's code from Module 1's ASCII table. - Calendars. Python's
calendarmodule keeps month lengths in a list,mdays, whose first entry is a spare 0. So January ismdays[1], the job Example 1 did withmonth - 1.
Common mistakes
1. i <= n in a loop over the data.
printf("marks:");
for (int i = 0; i <= n; i++) {
printf(" %d", marks[i]);
}
No message at any command line, because n comes from the input. For the input 5 and 70 85 62 91 48, Bob's loop prints marks: 70 85 62 91 48 0. The extra 0 is marks[5], inside the capacity but outside the data. When n equals MAX_N, the loop reads marks[100], which is undefined behaviour. Write i < n; you will write <= because "up to n" sounds as if it includes n.
2. A const int size with an initialiser.
const int months = 12;
int marks[months] = {0};
An error on every command line, the Playground included: error: variable-sized object may not be initialized. months is a variable, so marks is a VLA, and a VLA may not have an initialiser. Write #define MONTHS 12 instead. You will reach for const because Module 3 called it the careful choice, and for one value it is.
3. Copying an array with =.
int marks[MONTHS] = {72, 65, 80, 91, 58, 77, 84, 69, 90, 73, 66, 88};
int backup[MONTHS] = {0};
backup = marks;
An error on every command line, the Playground included: error: assignment to expression with array type. C never copies a whole array in one step, so copy it box by box, backup[i] = marks[i] in a loop. You will try = because it copies an int, and an array looks like twelve of them.
4. A read with no ampersand.
for (int i = 0; i < n; i++) {
scanf("%d", marks[i]);
}
Silent on the Playground. A local gcc -Wall on GCC 12 says warning: format '%d' expects argument of type 'int *', but argument 2 has type 'int' [-Wformat=]; the star is notation Module 11 explains. Without the &, scanf treats the number in the box as a place to write. On the Playground, a measured program that read three marks this way ended with Runtime error and (no output).
Write &marks[i]. You will drop it because marks[i] already looks like a place.
Amara collected the marks in the order the papers came in. She wants them from the last paper back to the first. Store them in int marks[MAX_N], with MAX_N 1000, and print them in reverse.
Input. A line with n, then n integers.
Output. The n integers in reverse order, on one line, separated by single spaces.
Constraints. 1 <= n <= 1000. Each integer is between -1000000000 and 1000000000.
Sample. Input 5 and 70 85 62 91 48 gives 48 91 62 85 70.
#include <stdio.h>
#define MAX_N 1000
int main(void)
{
int marks[MAX_N] = {0};
int n = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &marks[i]);
}
/* Print the n marks from the last box back to the first,
on one line, separated by single spaces. */
return 0;
}
Graded as reverse-print. The hidden tests include n = 1 and n = 1000, and they catch a loop that starts at marks[n] or stops before marks[0].
Alice checks lists by eye, so she wants every value printed beside its box number.
Input. A line with n, then n integers.
Output. n lines. The line for box i reads a[i] = v, where v is the value in that box and i counts from 0.
Constraints. 1 <= n <= 100. Each integer is between -1000000 and 1000000.
Sample. Input 3 and 70 85 62 gives a[0] = 70, a[1] = 85 and a[2] = 62 on three lines.
#include <stdio.h>
#define MAX_N 100
int main(void)
{
int a[MAX_N] = {0};
int n = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
/* Print one line per box, in the form a[0] = 70.
Which boxes hold data, and which index is the last one? */
return 0;
}
Not graded on its own. reverse-print already grades reading n values into an array.
Zara's rain gauge sometimes logs a negative reading when its sensor glitches. Rain cannot be negative, so she sets every negative box to 0. Then she prints the cleaned log and how many readings she fixed.
Input. A line with n, then n integers.
Output. Two lines: the n values after the fix, separated by single spaces, then the number of boxes that changed.
Constraints. 1 <= n <= 100. Each integer is between -1000000 and 1000000.
Sample. Input 4 and 3 -1 5 -7 gives 3 0 5 0 and 2 on two lines.
#include <stdio.h>
#define MAX_N 100
int main(void)
{
int rain[MAX_N] = {0};
int n = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &rain[i]);
}
/* Set every negative box to 0, and count the boxes you changed.
Then print the n values on one line, and the count on the next. */
return 0;
}
Not graded on its own. A judge sees only the output. Clamping each value as it is read gives the same output with no array. Use the array anyway, to practise changing a box.
Run in CompilerCommon doubts
Why not make every array huge and never think about sizes?
That is Kenji's plan, but every box costs memory, used or not.
int marks[1000000]takes 4,000,000 bytes to hold five marks. Take the capacity from the problem's limit on n. Lesson 03 measures where a big array inside a function breaks.Can
printfprint a whole array at once?No.
printf("%d\n", marks);is silent on the Playground and prints a number that is not a mark. One run on Compiler Explorer printed -1316912256. A localgcc -Wallon GCC 12 sayswarning: format '%d' expects argument of type 'int', but argument 2 has type 'int *' [-Wformat=]. Print the boxes one at a time, with a loop.In
int marks[12];andmarks[11] = 88;, do the brackets mean the same thing?No. In a declaration the number is how many boxes to make. Everywhere else it is an index, a distance from the first box. So
marks[12]in a declaration makes twelve boxes, andmarks[12]anywhere else is one step past the last of them.Can an array grow when more values arrive?
No. Its size is fixed when the program is compiled. Memory that grows while the program runs is Module 14's topic.
Key takeaways
- An array is one name for a row of same-type boxes, sized by a
#define, never by a VLA. - Indexes run from 0 to n - 1, because an index is a distance; the last box is
a[n - 1]. - An initialiser fills from box 0, the boxes it misses start at 0, and
[3] = 7names one box. - The boxes sit side by side, so
sizeof a / sizeof a[0]counts them, used or not. - Capacity is the boxes that exist, count is the boxes with data, and loops over the data test
i < n. - C never checks an index: a write past the end is undefined behaviour, and usually silent.
Next, Zara's maximum of three negative temperatures comes out as 0, and lesson 02 finds out why.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Traversal Patterns: Sum, Maximum, Count