C Part I · Module 6 · Arrays
Declaring and Initializing Arrays
Every array in the previous chapter was declared empty and then filled one assignment at a time. That is bearable for four elements and absurd for forty, and it also leaves a window in which the array exists and holds nothing you chose. C lets you supply values in the declaration itself, in several forms, and one of those forms behaves in a way that is worth knowing exactly rather than approximately: when you supply fewer values than the array has elements, what happens to the rest is a specific guarantee and not an accident. It is also the one place in this part of the curriculum where leaving something out gives you a defined value instead of the indeterminate contents Chapter 2.3 warned about — which makes the case where you leave out the initializer entirely more dangerous than it looks.
Foundation16 min readCPart IFundamentalsArrays
Part I · Module 6 · Chapter 6.2 · Declaring and Initializing Arrays
1. Supplying the Values in the Declaration
int readings[4] = {120, 118, 125, 119};The braced list is an initializer. Its values are used, in order, to initialise the elements: readings[0] becomes 120, readings[1] becomes 118, and so on.
This is one declaration doing what four assignments did in Chapter 6.1, and it is better for a reason beyond brevity. Between int readings[4]; and the last assignment, the array existed with some elements set and others not. With an initializer there is no such window — the object is fully formed at the point it comes into existence, which is Chapter 2.3's argument applied to four objects at once.
2. Letting the Compiler Count
If the initializer lists every element, you do not have to state the size:
int deduced[] = {10, 20, 30};The empty brackets mean as many elements as the initializer supplies — here, three. sizeof deduced / sizeof deduced[0] gives 3, exactly as if you had written int deduced[3].
This is worth preferring whenever you are writing out a fixed list, because the count cannot disagree with the contents. Adding a fourth value to the braces changes the array's size automatically; with int deduced[3] = {10, 20, 30, 40} you would have a mismatch, and §5 shows what the compiler does about that.
3. Partial Initializers: the Rule
Now the case the chapter exists for. What if you supply fewer values than the array has elements?
#include <stdio.h>
int main(void)
{
int full[4] = {10, 20, 30, 40};
int partial[4] = {10, 20};
int zeroed[4] = {0};
for (int i = 0; i < 4; i++) { printf("full[%d] = %d\n", i, full[i]); }
for (int i = 0; i < 4; i++) { printf("partial[%d] = %d\n", i, partial[i]); }
for (int i = 0; i < 4; i++) { printf("zeroed[%d] = %d\n", i, zeroed[i]); }
return 0;
}Predict the twelve lines before reading on.
Output:
full[0] = 10
full[1] = 20
full[2] = 30
full[3] = 40
partial[0] = 10
partial[1] = 20
partial[2] = 0
partial[3] = 0
zeroed[0] = 0
zeroed[1] = 0
zeroed[2] = 0
zeroed[3] = 0If an array has an initializer, every element not supplied by it is initialised to zero. This is a guarantee in C, not a convenience the compiler happens to provide.
partial[2] and partial[3] hold 0 because the rule says so. They do not hold leftovers, and they are not indeterminate.
That rule is also what makes the third form work. int zeroed[4] = {0}; supplies one value — zero for element 0 — and the rule fills the remaining three with zero as well. This is the standard idiom for a zeroed array, and it scales: int buffer[4096] = {0}; initialises four thousand elements with five characters of initializer.
4. The Case That Is Not Covered
Here is the distinction that matters most in the chapter, and it is one character of difference in effect:
int zeroed[4] = {0}; /* all four elements are 0 */
int declared[4]; /* all four are INDETERMINATE */The zero rule applies to elements an initializer did not reach. An array with no initializer at all is not covered by it. Its elements are indeterminate in exactly the sense Chapter 2.3 described for an ordinary local variable, and reading one before assigning to it is a bug, not a way to get zero.
What makes this worse than the scalar case is what the compiler does about it. Compare:
int scalar;
printf("%d\n", scalar);warning: variable 'scalar' is uninitialized when used here [-Wuninitialized]
note: initialize the variable 'scalar' to silence this warningwith:
int declared[4];
printf("%d\n", declared[0]);(no diagnostics)Nothing. Not at -Wall -Wextra -Wpedantic, and not with -Wconditional-uninitialized either. Tracking whether a particular element of an array has been written is much harder than tracking a single variable, and in general the compiler does not try.
5. Supplying Too Many
int a[3] = {1, 2, 3, 4};warning: excess elements in array initializer [-Wexcess-initializers]A warning, not an error — the program builds, and the extra value is discarded. That is worth knowing, because it is natural to assume the compiler rejects an initializer that cannot fit.
In practice this usually means you edited the list and forgot the size, which is the argument for §2's int a[] = {...} whenever the initializer is the definition of the contents.
6. Initialising Particular Elements
C also lets you say which element a value is for:
#include <stdio.h>
int main(void)
{
int sparse[8] = { [0] = 5, [7] = 9 };
for (int i = 0; i < 8; i++) {
printf("%d ", sparse[i]);
}
printf("\n");
return 0;
}Output:
5 0 0 0 0 0 0 9[0] = 5 initialises element 0 and [7] = 9 initialises element 7. Everything else falls under §3's rule and is zero.
These are called designated initializers, and they have been standard C since C99. They are worth reaching for when most of an array should be zero and a few known positions should not — a table of defaults, say — because the alternative is counting commas and hoping.
The index must be a constant expression, for the same reason Chapter 4.3's case labels must be: the compiler is building the initial contents while compiling, so it has to know the position then.
7. The Size Must Be Known at Compile Time
Every array so far has had a size the compiler could work out: a literal, or deduced from the initializer. That is the form to use.
You may be tempted to write this:
int count = 4;
int flexible[count];It compiles, with no diagnostic at -Wall -Wextra -Wpedantic. What you have created is a variable-length array — an array whose size is determined when the declaration is reached rather than while compiling. It is a real C feature, it was made optional for implementations in C11, and it brings a set of consequences this curriculum does not cover.
cc -std=c17 -Wall -Wextra -Wpedantic -Wvla sizes.c -o sizeswarning: variable length array used [-Wvla]Two practical points. Use a constant size, so the array's size is part of its type in the ordinary way and the sizeof idiom from Chapter 6.1 keeps working. And add -Wvla if you want to be told when you have accidentally created one — several embedded and safety-oriented coding standards forbid them outright, for reasons about stack usage that Chapter 5.7's frame model already makes easy to guess.
When the number of elements genuinely is not known until the program runs, the answer is not a variable-length array; it is Module 9's dynamic allocation.
8. The Forms, Side by Side
| Form | Elements | Not-supplied elements |
|---|---|---|
int a[4]; | 4 | indeterminate — and no warning |
int a[4] = {1,2,3,4}; | 4 | none left |
int a[4] = {1,2}; | 4 | zero, guaranteed |
int a[4] = {0}; | 4 | zero, guaranteed — the idiom |
int a[4] = {}; | 4 | zero — C23; a C17 extension |
int a[] = {1,2,3}; | 3, deduced | none left |
int a[8] = {[0]=5,[7]=9}; | 8 | zero, guaranteed |
int a[3] = {1,2,3,4}; | 3 | warning; extra value discarded |
One line summarises the whole table:
An initializer, however short, zeroes the rest. No initializer zeroes nothing.
9. A Realistic Declaration
#include <stdio.h>
int main(void)
{
/* Microvolts per step for each of four gain settings. */
int step_microvolts[] = {806, 403, 201, 100};
/* One counter per fault bit; all start at zero. */
int fault_counts[8] = {0};
int gain_setting = 2;
int reading_steps = 1500;
fault_counts[3] = fault_counts[3] + 1;
printf("gain %d: %d steps is %d microvolts\n",
gain_setting, reading_steps, reading_steps * step_microvolts[gain_setting]);
printf("fault bit 3 seen %d time(s), fault bit 5 seen %d time(s)\n",
fault_counts[3], fault_counts[5]);
printf("gain settings available: %zu\n",
sizeof step_microvolts / sizeof step_microvolts[0]);
return 0;
}Output:
gain 2: 1500 steps is 301500 microvolts
fault bit 3 seen 1 time(s), fault bit 5 seen 0 time(s)
gain settings available: 4The second line is the one to look at. fault_counts[3] was incremented once, so 1 is unsurprising. fault_counts[5] was never touched at all, and it reads as a dependable 0 — guaranteed by §3's rule, because = {0} covered every element of the array.
That is the whole value of the idiom. A counter array is usable the moment it is declared: no filling loop, and every count starts from a number you can rely on. Had the declaration been int fault_counts[8];, that same printf would be reading an indeterminate value, with no warning to tell you. That is the whole value of the idiom: a counter array is usable immediately, with no filling loop, and every count starts at a number you can rely on.
Three choices in that program are deliberate. step_microvolts uses int a[] = {...} because the initializer is the definition of the table, so the count should follow it. fault_counts states its size explicitly, because 8 is a property of the hardware rather than of the list. And the element count comes from the sizeof idiom rather than a repeated literal.
10. What You Should Be Able to Say Now
- A braced initializer supplies element values in order, at the point the array comes into existence.
int a[] = {...}lets the initializer decide the count, so the two cannot disagree.- Every element an initializer does not supply is set to zero — a C guarantee.
= {0}is therefore the idiom for a fully zeroed array, at any size.- C17 forbids empty braces;
= {}is C23, accepted by clang under C17 as an extension. - An array with no initializer at all is not covered — its elements are indeterminate.
- The compiler warns about an uninitialised scalar and says nothing about an uninitialised array element.
- Too many initializers is a warning, not an error; the extra value is discarded.
- Designated initializers
{[0]=5,[7]=9}set named positions and zero the rest; the index must be a constant expression. - A non-constant size creates a variable-length array — optional since C11, silent without
-Wvla, and not this curriculum's answer to a size known only at run time.
11. Exercises
Exercise 1 — predict every element. Do not compile yet. For each declaration, give all the element values, or say that they are indeterminate.
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {1, 2};
int c[5] = {0};
int d[5];
int e[] = {7, 8};
int f[5] = {[1] = 9};Exercise 2 — give the counts. For each array in Exercise 1, give sizeof x / sizeof x[0] assuming int is 4 bytes.
Exercise 3 — explain the asymmetry. Say why int d[5]; produces no warning when d[0] is read, while int scalar; does. Then say what that means for how much you can rely on the compiler here.
Exercise 4 — choose the form. For each, say which declaration form you would use and why.
- sixteen counters, all starting at zero
- a fixed table of five calibration constants
- a buffer of 256 elements that a loop will completely fill before anything reads it
- an array of 12 month lengths, where you want to be certain the count matches the list
Exercise 5 — find the bug. This is meant to count how many readings fall in each of four buckets. Say what is wrong, what the symptom would be, and give the one-token repair.
int bucket[4];
for (int i = 0; i < reading_count; i++) {
bucket[classify(readings[i])] = bucket[classify(readings[i])] + 1;
}Exercise 6 — classify three declarations. For each, say whether it compiles cleanly, warns, or is an error on a C17 baseline, and what the resulting array contains.
int p[3] = {1, 2, 3, 4};
int q[3] = {};
int r[] = {};Exercise 7 — explain the discarded value. §5's excess initializer is a warning rather than an error. Give one argument for why a warning is defensible and one for why you would compile with it treated as an error in a serious project.
Exercise 8 — size it correctly. A colleague writes int samples[n]; where n is read from a configuration value. Explain what they have created, which flag would have told them, two reasons a project might forbid it, and what they should use instead for a count known only at run time.
12. Summary
- A braced initializer fills elements in order, at the declaration, with no partially formed window.
int a[] = {...}deduces the count from the initializer.- Any initializer zeroes every element it does not reach — guaranteed by C.
= {0}is the idiom for a zeroed array;= {}is C23.- No initializer means no zeroing, and unlike a scalar, no warning.
- Excess initializers warn and the extra values are discarded.
- Designated initializers set named positions and zero everything else.
- A non-constant size is a variable-length array — use a constant, add
-Wvla, and use Module 9 for run-time sizes.
An array can now be brought into existence with exactly the contents you intended. What has been used informally in every example since Chapter 6.1 — the loop that walks the elements — deserves the same treatment, because the bounds of that loop are where most array bugs are made, and because the pairing between zero-based indexing and the half-open loop condition turns out to make a whole family of mistakes impossible rather than merely unlikely.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
