C / ARRAYS
Fixed-size arrays and zero-based indexing
Declare fixed-size C arrays, read and write elements by index, and work out the valid index range and element count from the array's own type.
What you will learn
- Declare a fixed-size array with a compile-time constant length, as in int temps[4];
- Read the index as an offset: temps[0] is the first slot, temps[3] the last of four.
- Get the count with sizeof temps / sizeof temps[0] instead of repeating the literal.
- Know that the last valid index is always length - 1, so a 10-slot array ends at 9.
Understanding Fixed-size arrays and zero-based indexing
The declaration int temps[4]; reserves one block of storage big enough for four ints, laid out back to back with no gaps or bookkeeping between them. It is a single object, not four separate variables that happen to share a name, and the 4 has to be a value the compiler already knows, so the block can never grow or shrink later. Right after the declaration the storage exists but the bytes in it are whatever was left in that memory, so you write into the slots before you read them.
An index in C is not an ordinal position, it is a count of elements from the start of the block. temps[0] means zero elements past the beginning, which is the first slot; the compiler translates temps[i] into the address of the block plus i multiplied by the size of one element. Starting at 0 is what makes that a single multiply and add with no correction term, and it is why the highest usable index of a four-element array is 3, one less than the count.
The length is part of the array's type: temps has type int[4], and that fact lives in the compiler, not in memory next to the data. That is why sizeof temps gives the size of the whole block while sizeof temps[0] gives one element, and dividing one by the other recovers the count. Nothing at run time stores the number 4, so the mental model is a numbered row of boxes with the count written on the outside label, not a list object that can report its own length.
<stdio.h>
N
int main(void)
{
int temps[N];
temps[0] = 17;
temps[1] = 21;
temps[2] = 19;
temps[3] = 24;
printf("whole array: %zu bytes\n", sizeof temps);
printf("one element: %zu bytes\n", sizeof temps[0]);
printf("elements: %zu\n", sizeof temps / sizeof temps[0]);
for (int i = 0; i < N; i++) {
printf("temps[%d] = %2d offset %td bytes from temps[0]\n",
i, temps[i], (char *)&temps[i] - (char *)&temps[0]);
}
printf("first index 0, last index %d\n", N - 1);
return 0;
}
An array index is a count of elements from the start of a fixed block, which is why the first element sits at 0 and the last at length minus one.
Worked examples
The subscript is pointer arithmetic
Shows that v[i] is defined as *(v + i), so the index scales by the element size and stops at N - 1.
<stdio.h>
N
int main(void)
{
double weights[N];
for (int i = 0; i < N; i++) {
weights[i] = 0.5 * (i + 1);
}
printf("index 0 -> %.1f\n", weights[0]);
printf("index %d -> %.1f\n", N - 1, weights[N - 1]);
printf("weights[2] == *(weights + 2): %d\n",
weights[2] == *(weights + 2));
printf("step between slots: %td bytes\n",
(char *)&weights[1] - (char *)&weights[0]);
return 0;
}
Example explained
Line 1weights[i] = 0.5 * (i + 1); writes through indices 0 to 4, which are exactly the five slots the declaration reserved.
Line 2The last element is read with N - 1 because index N would name a sixth slot that was never allocated.
Line 3The comparison prints 1 because a subscript is literally defined as that pointer addition, not as a separate lookup rule.
Line 4The step is 8 bytes rather than 1 because adding 1 to a double pointer advances by one whole double.
Same count, different byte sizes
Shows that the element count lives in the type while the byte size depends on the element type.
<stdio.h>
enum { SLOTS = 3 };
int main(void)
{
short small[SLOTS];
double big[SLOTS];
small[0] = 1; small[1] = 2; small[2] = 3;
big[0] = 1.5; big[1] = 2.5; big[2] = 3.5;
printf("small: %zu elements, %zu bytes\n",
sizeof small / sizeof small[0], sizeof small);
printf("big: %zu elements, %zu bytes\n",
sizeof big / sizeof big[0], sizeof big);
printf("last small: %d, last big: %.1f\n",
small[SLOTS - 1], big[SLOTS - 1]);
return 0;
}
Example explained
Line 1enum { SLOTS = 3 }; supplies a compile-time constant, so both arrays get a length fixed inside their type.
Line 2sizeof small is 6 and sizeof big is 24: the same three slots, scaled by a 2-byte short and an 8-byte double.
Line 3Dividing by sizeof arr[0] cancels the element size, which is why both divisions report 3.
Line 4SLOTS - 1 is the last index for both arrays, since numbering starts at 0 whatever the element type is.
Important notes
The 4-byte and 8-byte offsets above come from int and double on a typical desktop; the rule is always index * sizeof(element), so the numbers change with the element type, not with the index.
sizeof arr / sizeof arr[0] only tells the truth while the declared array type is in scope; once all you hold is a pointer to the first element, that division measures the pointer instead.
Common mistakes
Counting from 1 and writing temps[4] on an int temps[4] because it looks like the fourth slot; that index names memory one element past the block and the write silently corrupts whatever is next.
Using sizeof temps as the number of elements; for int temps[4] that is 16, so any loop or check driven by it is off by a factor of sizeof(int).
Writing int n = 4; int a[n]; and believing it is a fixed-size array; it is a variable-length array whose size is worked out at run time, and it will not compile at all under C89 or a C11 implementation that defines __STDC_NO_VLA__.
Try it yourself
Change, predict, then run
Declare int gaps[6];, store the square of each index into its own slot, then print gaps[0], the last element located with sizeof arithmetic instead of a literal 5, and the byte offset of gaps[5] from gaps[0].
Open the C workspaceCheck your understanding
For double v[6]; on a machine where double is 8 bytes, which statement is true?
- &v[3] is 24 bytes past &v[0], and the last valid index is 5.
- &v[3] is 3 bytes past &v[0], and the last valid index is 5.
- sizeof v is 6, and the last valid index is 5.
- &v[1] is the start of the array, because v[0] is an unused header slot.
Show answer
An index is an element count, so the compiler scales it by the element size: 3 * 8 gives a 24-byte offset, and six slots numbered from 0 end at 5. Option 3 is tempting because the array really does hold six elements, but sizeof reports bytes, so sizeof v is 48; you only get 6 after dividing by sizeof v[0].