Arrays: Contiguous Memory
Declare and loop over arrays, keep the length alongside the data, and see why reading or writing past the end is undefined behaviour that AddressSanitizer catches at run time.
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 fiveints 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 sittingsizeof(scores[0])bytes apart. Where the array lives is not guaranteed and moves every run. sizeof(scores) / sizeof(scores[0])gives the element count whereverscoresis 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
countvariable beside it, guard the fill withcount < 5 && scanf(...) == 1, and bound every later loop withi < 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-overflowand aborts.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Arrays: Contiguous Memory - Quiz
Test your understanding of the lesson.
Practice Exercises
Reading Numbers Into an Array of Five
Chapter 2 summed a stream of numbers as they arrived, keeping nothing but a running total. This time keep the numbers themselves, in an array of exactly 5 ints, and report on them afterwards. The array and the two other variables are already declared, and so are the three printf lines at the bottom, so the whole exercise is the two loops between them. The first loop fills. Read numbers with scanf and store each one at values[count], then increment count so the next number lands in the next element, and stop for either of two reasons: the input stopped converting, or the array is full. Both reasons live in one condition, count < 5 && scanf("%d", &value) == 1, and the order of the two halves is not a matter of taste. && evaluates its left side first and skips the right side entirely when the left is false, so writing the capacity check first means that once count reaches 5 the scanf is never attempted at all. Write it the other way round and the sixth number is read into value and then stored at values[5], which is a write past the end of the array, undefined behaviour, and an AddressSanitizer stack-buffer-overflow that ends your program before it prints a thing. The second loop reports. It runs from 0 up to but not including count, and count rather than 5 is the bound because the elements past count were never filled and merely hold the zeroes the initializer put there; summing all five would quietly add numbers that were never in the input. Add each element to total, and compare each element against largest, replacing largest when the element is bigger. Notice that largest is already initialized to values[0] rather than to 0, which is what makes the negative test case come out right, and that reading values[0] is only safe because the count == 0 case returned before reaching it. Do not use a helper function: an array handed to a function is next lesson's subject and nothing about it is taught yet, so keep all of this in main. Every path returns 0, the no numbers read path included, because the checker reads a nonzero exit status as a failure however right the output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!