One Name, Many Values

Every variable so far has held exactly one value, so a program needing five numbers declared five variables and then repeated itself five times over. An array ends that. It is a fixed number of objects of the same type, laid out one after another, sharing a single name. int scores[5]; declares one thing and reserves room for five ints in a row, and the number in the brackets is the element count, always a plain literal the compiler can see. You reach an individual element with those same brackets, now holding an index, and C counts from 0, so the five elements are scores[0] through scores[4]. There is no scores[5], which is the fact the last section of this lesson is built on.

An initializer list in braces gives starting values without five separate assignments, and it comes with a rule that pays for itself immediately: a list shorter than the array zero-initializes every remaining element.

#include <stdio.h>

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

    counts[2] = 7;

    for (int i = 0; i < 5; ++i)
    {
        printf("scores[%d] is %d and counts[%d] is %d\n", i, scores[i], i, counts[i]);
    }

    return 0;
}
scores[0] is 90 and counts[0] is 0
scores[1] is 72 and counts[1] is 0
scores[2] is 85 and counts[2] is 7
scores[3] is 0 and counts[3] is 0
scores[4] is 0 and counts[4] is 0

scores was given three values and its other two came out as 0 rather than as whatever the memory happened to hold, and out of that rule falls the idiom worth memorizing: int counts[5] = {0}; zeroes the entire array, at any size, because the single 0 initializes element 0 and the shorter-list rule does the rest. Prefer it to a loop that stores 0 into each element, since it is one line, it cannot get its bounds wrong, and it is finished before the first statement runs. Leave the initializer off altogether and chapter 1's rule applies element by element: an array with automatic storage duration and no initializer holds indeterminate values in every element, and reading one before you write it is undefined behaviour, so int scores[5]; obliges you to fill all five before anything reads them. Notice also that the brackets do two different jobs above, so read [5] in a declaration as how many and [2] in an expression as which one. That indices stop one short of the count is not a quirk to memorize either, because an index is a distance from the start and scores[0] is the element zero places along. Chapter 2's for (int i = 0; i < 5; ++i) was built for exactly this, producing every index an array of 5 has and no others. Count from 0 and use < was a rule about fenceposts there; here it is a rule about which memory you are allowed to touch.

Contiguity Is Guaranteed, Location Is Not

"Laid out one after another" is a claim you can check, and chapter 3's %p with a (void *) cast is how you check it. While the array is in front of us, sizeof can answer a second question too.

#include <stdio.h>

int main(void)
{
    int scores[5] = {90, 72, 85, 61, 78};

    for (int i = 0; i < 5; ++i)
    {
        printf("&scores[%d] is %p\n", i, (void *)&scores[i]);
    }

    printf("sizeof(scores) is %zu\n", sizeof(scores));
    printf("sizeof(scores[0]) is %zu\n", sizeof(scores[0]));
    printf("the array holds %zu elements\n", sizeof(scores) / sizeof(scores[0]));

    return 0;
}
&scores[0] is 0xfbff865f0020
&scores[1] is 0xfbff865f0024
&scores[2] is 0xfbff865f0028
&scores[3] is 0xfbff865f002c
&scores[4] is 0xfbff865f0030
sizeof(scores) is 20
sizeof(scores[0]) is 4
the array holds 5 elements

Each address is exactly 4 past the one before it, and one element is 4 bytes, so element 1 begins where element 0 ends. Be precise about what that shows, because three chapters have insisted that layout is the implementation's business. The contiguity is a language guarantee: the elements occupy consecutive storage in index order with no gaps, and &scores[i + 1] is always sizeof(scores[0]) bytes past &scores[i]. What is not guaranteed is a single one of the actual numbers above. Where the array sits is as unpromised as a frame's address was, address space layout randomization moves it on every run, and your numbers will differ from these and from one run of your own to the next. The spacing is the guarantee; the addresses are one run's accident. That same contiguity is why the last two lines work. The whole array is 20 bytes, one element is 4, and so sizeof(scores) / sizeof(scores[0]) is the standard idiom for "how many elements". It beats writing 5 by hand because it stays right when you change the declaration, and it works here because scores is a real array named in the scope that declared it. Hand an array to a function and that stops being true, in a way that catches nearly everyone exactly once; that is the next lesson.

The Array Does Not Know Its Length

Here is the fact that shapes every array program you will write. At run time an array is a block of elements and nothing else. It carries no length, no bounds, and no marker saying where the filled part stops. The idiom above answered the question at compile time, from the declaration, and there is no run-time equivalent to ask. So how many elements you have actually filled is a fact only you know, and the only place to keep it is a variable of your own sitting beside the array.

#include <stdio.h>

int main(void)
{
    int values[5] = {0};
    int count = 0;
    int value = 0;

    while (count < 5 && scanf("%d", &value) == 1)
    {
        values[count] = value;
        ++count;
    }

    int total = 0;

    for (int i = 0; i < count; ++i)
    {
        total += values[i];
    }

    printf("read %d numbers, total %d\n", count, total);

    return 0;
}

Given 12 5 30 stop that prints read 3 numbers, total 47. Two of chapter 2's idioms are working together in that condition, and short-circuit evaluation keeps them in the right order: count < 5 is tested first, so once the array is full the scanf is never even attempted and the rest of the input is left unread. Given 4 8 15 16 23 42 the program prints read 5 numbers, total 66, having never looked at the 42. After the loop it is count, and not 5, that bounds everything which follows, because the elements past count were never filled and still hold the zeroes the initializer put there. A largest-so-far loop is the same shape with one extra care: it has to start from values[0] rather than from 0, so that every input being negative does not silently produce an answer of 0, and that in turn means handling the count == 0 case before reading values[0] at all.

Where the Fencepost Bug Lands

The next program is wrong, and its mistake is a single character in a loop condition.

#include <stdio.h>

int main(void)
{
    int scores[5] = {0};

    for (int i = 0; i <= 5; ++i)
    {
        scores[i] = i * 10;
    }

    printf("the last element is %d\n", scores[4]);

    return 0;
}

Chapter 2 measured i <= 5 in passes: six instead of five. Measure it in memory now. The sixth pass assigns to scores[5], and scores has no element 5, so those four bytes belong to some other variable or to nothing at all. Reading or writing an array outside indices 0 through the count minus one is undefined behaviour, and that includes reading one element past the end even though nothing is written there. It is not "you get a wrong number" and it is not "it crashes"; the standard promises nothing at all about what the program does next. Here gcc happens to see it coming, because the bound is a constant it can reason about.

fill.c: In function 'main':
fill.c:9:15: warning: array subscript 5 is above array bounds of 'int[5]' [-Warray-bounds=]
    9 |         scores[i] = i * 10;
      |         ~~~~~~^~~
fill.c:5:9: note: while referencing 'scores'

Treat that as luck rather than as protection. Give the loop a bound that arrives from input, as the scanf fill above does, and there is nothing left for the compiler to reason about and it says nothing whatsoever. What does not depend on luck is AddressSanitizer, which this platform builds into every program and which stops this one at the offending write.

==12==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xfbff95ff0034
WRITE of size 4 at 0xfbff95ff0034 thread T0
    #0 0x000000400a0c in main /tmp/fill.c:9
Address 0xfbff95ff0034 is located in stack of thread T0 at offset 52 in frame
    #0 0x00000040086c in main /tmp/fill.c:4
  This frame has 1 object(s):
    [32, 52) 'scores' (line 5) <== Memory access at offset 52 overflows this variable

That last annotation is what makes the report readable, because it names the variable: scores is the 20 bytes at offsets 32 to 52 of main's frame, and the write went to offset 52, the very first byte past the end. stack-buffer-overflow joins SEGV on unknown address, stack-overflow and stack-use-after-scope in your collection, and like all of them it aborts with a nonzero exit status rather than letting a plausible wrong answer through. Change i <= 5 to i < 5 and the program prints the last element is 40 and exits cleanly. One character separated a correct program from undefined behaviour, which is why chapter 2 gave the fencepost a whole section of its own.

Key Takeaways

  • int scores[5]; reserves five ints under one name and the valid indices are 0 through 4, because an index is a distance from the start rather than a position number.
  • An initializer list shorter than the array zero-initializes the rest, so int counts[5] = {0}; zeroes the whole array; write that instead of a zeroing loop. With no initializer at all, an automatic array's elements are indeterminate and reading one is undefined behaviour.
  • The elements are contiguous in index order, and that is a language guarantee you can see with %p, consecutive elements sitting sizeof(scores[0]) bytes apart. Where the array lives is not guaranteed and moves every run.
  • sizeof(scores) / sizeof(scores[0]) gives the element count wherever scores is a real array in the scope that declared it. What happens when an array meets a function parameter is the next lesson.
  • An array does not carry its length at run time. Keep a count variable beside it, guard the fill with count < 5 && scanf(...) == 1, and bound every later loop with i < count.
  • Indexing outside the array is undefined behaviour, for reads as well as writes, one past the end included. gcc catches only what it can see constants for; AddressSanitizer reports the rest as stack-buffer-overflow and aborts.