Arrays and strings · about 45 min

Arrays: contiguous memory

Why this matters

So far every value has lived in its own named variable. Real programs handle many values of the same kind: the samples of an audio clip, the pixels of an image, the lines of a file, the scores of a class. An array stores many values of one type under one name, side by side in memory, and lets you pick any of them by number. Arrays are the most basic data structure there is, and in C they are also the closest to the hardware: an array is exactly a block of consecutive bytes. Understanding that layout is the foundation for strings (this module) and pointers (the next).

Declaring and indexing

int scores[5];

declares an array named scores holding 5 ints. The number in brackets is the length; it must be a positive integer constant here. Each element is reached by indexing with scores[i], where the index i runs from 0 to 4:

#include <stdio.h>

int main(void)
{
    int scores[5] = {90, 72, 85, 64, 99};
    int sum = 0;

    for (int i = 0; i < 5; i++) {
        sum += scores[i];
    }
    printf("first: %d, last: %d\n", scores[0], scores[4]);
    printf("sum: %d, average: %.1f\n", sum, sum / 5.0);
    printf("sizeof scores = %zu, sizeof scores[0] = %zu\n", sizeof scores, sizeof scores[0]);
    printf("element count = %zu\n", sizeof scores / sizeof scores[0]);
    return 0;
}
first: 90, last: 99
sum: 410, average: 82.0
sizeof scores = 20, sizeof scores[0] = 4
element count = 5

scores[i] behaves exactly like an int variable: you can read it, assign to it, increment it, and take its address with &scores[i] (which is why scanf("%d", &scores[i]) works).

The loop for (int i = 0; i < N; i++) is the standard way to visit every element. Note <, not <=: the last valid index is N - 1.

What it looks like in memory

The elements of an array are contiguous: they sit next to each other with no gaps. With 4-byte ints, scores occupies 20 consecutive bytes:

  address (example)   0x1000   0x1004   0x1008   0x100c   0x1010   0x1014
                     +--------+--------+--------+--------+--------+
  scores             |   90   |   72   |   85   |   64   |   99   |
                     +--------+--------+--------+--------+--------+
  index                  0        1        2        3        4

(Real addresses vary from run to run; these are made up to keep the arithmetic easy.)

This is why indexes start at 0. To find element i, the machine computes

  address of scores[i] = address of scores + i * sizeof(int)

scores[0] is zero steps from the start; scores[3] is at 0x1000 + 3 * 4 = 0x100c. One multiply and one add, no matter how big the array is. Reaching any element takes the same constant time, which is the defining strength of arrays.

sizeof reports the whole block: sizeof scores is 5 * 4 = 20 bytes. Dividing by the size of one element recovers the length. You will often see this as a macro:

#define COUNT(a) (sizeof (a) / sizeof (a)[0])

Initializing

An uninitialized local array contains whatever bytes happened to be in that memory, just like an uninitialized int. Initializers fix that:

#include <stdio.h>

int main(void)
{
    int a[6] = {1, 2};          /* the rest become 0 */
    int b[] = {7, 8, 9};        /* size taken from the initializer: 3 */
    int c[6] = {[4] = 40, [1] = 10};

    for (int i = 0; i < 6; i++) printf("%d ", a[i]);
    printf("\n");
    printf("b has %zu elements\n", sizeof b / sizeof b[0]);
    for (int i = 0; i < 6; i++) printf("%d ", c[i]);
    printf("\n");
    return 0;
}
1 2 0 0 0 0 
b has 3 elements
0 10 0 0 40 0 

Rules worth remembering:

A worked example: counting with an array

Arrays indexed by a value make excellent counters. This program reads digits and counts how often each occurs, using the digit itself as the index:

#include <stdio.h>

int main(void)
{
    int counts[10] = {0};
    int digit;
    while (scanf("%d", &digit) == 1) {
        if (digit >= 0 && digit <= 9) {
            counts[digit]++;
        }
    }
    for (int d = 0; d < 10; d++) {
        if (counts[d] > 0) printf("%d: %d\n", d, counts[d]);
    }
    return 0;
}
$ echo 3 1 4 1 5 9 2 6 5 3 5 | ./prog
1: 2
2: 1
3: 2
4: 1
5: 3
6: 1
9: 1

The check digit >= 0 && digit <= 9 is not decoration. Without it, an input of 12 would increment counts[12], memory that does not belong to the array.

Arrays are not values you can assign

You cannot copy an array with = or compare two arrays with ==:

int a[3] = {1, 2, 3};
int b[3];
b = a;
main.c:7:7: error: assignment to expression with array type
    7 |     b = a;
      |       ^

To copy, loop over the elements (or use memcpy, which you will meet with strings). a == b compiles, but it does not compare contents; what it does compare will make sense in the pointers module.

No bounds checking

C does not check indexes. This program has an off-by-one error (<= instead of <):

#include <stdio.h>

int main(void)
{
    int nums[4] = {10, 20, 30, 40};
    for (int i = 0; i <= 4; i++) {
        printf("nums[%d] = %d\n", i, nums[i]);
    }
    return 0;
}

It compiles without a single warning, and in the sandbox it printed:

nums[0] = 10
nums[1] = 20
nums[2] = 30
nums[3] = 40
nums[4] = 0

nums[4] read the 4 bytes just past the array, which held some other data that happened to be 0. On another run, another compiler or another machine it could print anything, crash, or (when writing out of bounds) silently overwrite another variable. This is undefined behavior, and out-of-bounds writes are the root of the famous buffer overflow security bugs.

Tools can catch it at run time. Building with -fsanitize=address adds checks to every memory access:

$ gcc -g -fsanitize=address -o prog main.c && ./prog
nums[0] = 10
...
==34==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f7586900030 ...
READ of size 4 at 0x7f7586900030 thread T0
    #0 0x5593afc95368 in main /work/main.c:7
...
    [32, 48) 'nums' (line 5) <== Memory access at offset 48 overflows this variable

It names the line and the array. Use it whenever an array program behaves strangely.

Common mistakes

Recap

Going further: take the off-by-one example and change the loop to write nums[i] = 0 for i <= 4, with another int variable declared next to nums. Print that variable afterwards, then rebuild with -fsanitize=address and read the report.

Exercises

Exercise 1 · quiz

Valid indexes

Given int data[8];, which indexes may you legally read and write?

Answer
  • 0 to 7

An array of N elements has indexes 0 to N-1. The index is an offset from the start: data[0] is zero elements past the beginning, data[7] is seven past it, the last one. data[8] would be one element past the end, which is outside the array. C does not check this: reading data[8] is undefined behavior, not an error message.

Exercise 2 · quiz

sizeof an array

On the sandbox (where double is 8 bytes), what does this print?

double samples[10] = {1.5, 2.5};
printf("%zu %zu\n", sizeof samples, sizeof samples / sizeof samples[0]);
Answer
  • 80 10

sizeof on an array gives the size of the whole array in bytes: 10 elements times 8 bytes = 80. How many elements were given initial values does not matter; the other 8 are set to 0.0. Dividing by the size of one element gives the element count, 10. This idiom only works where the array itself is in scope, as the next lesson shows.

Exercise 3 · write code

Print in reverse

Read exactly 5 integers from standard input into an array, then print them in reverse order on one line, separated by single spaces, followed by a newline.

Input 1 2 3 4 5 must produce:

5 4 3 2 1

There is no space after the last number.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int values[5];

    /* read 5 integers into values[0] .. values[4] */

    /* print them from values[4] down to values[0] */

    return 0;
}

Example

input

1 2 3 4 5

expected output

5 4 3 2 1

Example

input

-3
10
0
7
-100

expected output

-100 7 0 10 -3
Hint 1

scanf("%d", &values[i]); reads into element i. The & is needed just as for a plain int variable.

Hint 2

Count down: for (int i = 4; i >= 0; i--).

Hint 3

Print a space before every number except the first one printed (or after every number except the last).

Reference solution
#include <stdio.h>

int main(void)
{
    int values[5];

    for (int i = 0; i < 5; i++) {
        scanf("%d", &values[i]);
    }
    for (int i = 4; i >= 0; i--) {
        printf("%d", values[i]);
        if (i > 0) {
            printf(" ");
        }
    }
    printf("\n");
    return 0;
}

Exercise 4 · write code

How many are above average?

Read integers from standard input until end of input (there will be at most 100 of them, and at least 1). Then print, on one line:

<count> values, average <avg>, <above> above average

where <avg> has exactly 2 decimals and <above> is how many values are strictly greater than the average. For input 4 8 15 16 23 42 the output is:

6 values, average 18.00, 2 above average

You cannot know which values are above the average until you have read all of them, which is why you need to keep them in an array.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int values[100];
    int n = 0;
    int x;

    while (scanf("%d", &x) == 1) {
        /* store x */
    }

    printf("%d values\n", n);
    return 0;
}

Example

input

4 8 15 16 23 42

expected output

6 values, average 18.00, 2 above average

Example

input

5 5 5

expected output

3 values, average 5.00, 0 above average
Hint 1

Store with values[n] = x; n++; and stop when n reaches 100 so you never write past the end.

Hint 2

First loop: sum the n stored values. Compute the average as a double: (double)sum / n.

Hint 3

Second loop over the same n values: count those with values[i] > avg.

Reference solution
#include <stdio.h>

int main(void)
{
    int values[100];
    int n = 0;
    int x;

    while (n < 100 && scanf("%d", &x) == 1) {
        values[n] = x;
        n++;
    }

    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += values[i];
    }
    double avg = (double)sum / n;

    int above = 0;
    for (int i = 0; i < n; i++) {
        if (values[i] > avg) {
            above++;
        }
    }
    printf("%d values, average %.2f, %d above average\n", n, avg, above);
    return 0;
}