C / ARRAYS
Initialisers, partial initialisation and zero fill
Initialise C arrays so you know exactly what is in every element: partial lists, {0}, designated indices, and where zero fill does and does not happen.
What you will learn
- Guarantee a known state with int a[N] = {0}; instead of trusting an uninitialised array
- Predict which elements a partial initialiser leaves at zero and which stay indeterminate
- Set sparse array slots with designated initialisers like {[3] = 7, [9] = 1}
- Spot the memset(a, 1, sizeof a) trap that fills bytes, not int values
Understanding Initialisers, partial initialisation and zero fill
An array initialiser is a brace-enclosed list, and its values are handed to elements starting at index 0 in the order written. The declared bound and the list interact two ways: in int a[5] = {1, 2}; the bound wins and the list covers only a prefix, while in int a[] = {1, 2}; the list decides and the array has exactly two elements. What you may not do is supply more initialisers than the bound allows; int a[2] = {1, 2, 3}; is a constraint violation, and a compiler that merely warns and discards the 3 is doing you no favour.
The rule that catches people out is what happens to the elements you never mentioned. Once an array has an initialiser at all, every element not covered by it is initialised exactly as an object with static storage duration would be: integers become 0, floating types 0.0, pointers become null, and struct or nested-array members are treated the same way recursively. That is the entire reason int a[1000] = {0}; works — you write one initialiser and the rule supplies the other 999. Omit the initialiser inside a function, as in int a[1000];, and no rule applies: the elements hold indeterminate values and reading them before assignment is undefined behaviour.
C99 designated initialisers let you address elements by index rather than by position, so int t[8] = {[2] = 5, [6] = 1}; sets those two slots and zero-fills the rest, and an undesignated value written after a designator continues from the next index. Because the zero fill is a language rule rather than a loop you wrote, the compiler is free to implement it cheaply: an all-zero array with static storage duration usually costs nothing but a reservation in .bss, while a zeroed automatic array becomes a bulk clear when control reaches its declaration. That is the one performance caveat worth remembering — = {0} on a large local array is O(n) work each time the block is entered.
Deduced lengths are also worth using deliberately. Writing int sizes[] = {10, 20, 30}; means the length can never disagree with the list, and sizeof sizes / sizeof sizes[0] recovers it; with designated initialisers the deduced length is one past the largest index reached, so int t[] = {[9] = 1}; has ten elements, nine of them zero.
<stdio.h>
int main(void)
{
int counts[6] = {4, 9}; /* first two given, rest zero filled */
int flags[6] = {0}; /* one initialiser, whole array zero */
int marks[6] = {[1] = 7, [4] = 3}; /* C99 designated initialisers */
int sizes[] = {10, 20, 30}; /* length deduced from the list */
for (int i = 0; i < 6; i++)
printf("i=%d counts=%d flags=%d marks=%d\n",
i, counts[i], flags[i], marks[i]);
printf("sizes has %zu elements\n", sizeof sizes / sizeof sizes[0]);
return 0;
}
An initialiser list that touches even one element forces the compiler to zero-fill every element you did not mention, while no initialiser at all leaves the whole array indeterminate.
Worked examples
{1} is not "fill with 1", and memset is not either
Shows that a single initialiser only sets element 0, and that reaching for memset to fill ints with 1 writes bytes instead.
<stdio.h>
<string.h>
int main(void)
{
int a[4] = {1};
int b[4];
memset(b, 1, sizeof b);
printf("a: %d %d %d %d\n", a[0], a[1], a[2], a[3]);
printf("b: %d %d %d %d\n", b[0], b[1], b[2], b[3]);
return 0;
}
Example explained
Line 1int a[4] = {1}; puts 1 in a[0] and zero-fills a[1] to a[3]; the 1 is not repeated.
Line 2memset(b, 1, sizeof b); writes the byte 0x01 into all 16 bytes, not the value 1 into four ints.
Line 3Each int therefore reads back as 0x01010101, which is 16843009 where int is 4 bytes.
Line 4memset with 0 is the one case that looks correct, because all-bits-zero and integer 0 coincide on mainstream platforms.
Designators can jump backwards
Demonstrates how an undesignated value continues from the index after a designator, and that designators may appear out of order.
<stdio.h>
int main(void)
{
int t[8] = {[2] = 5, 6, 7, [0] = 9};
printf("%d", t[0]);
for (int i = 1; i < 8; i++)
printf(",%d", t[i]);
printf("\n");
return 0;
}
Example explained
Line 1[2] = 5 stores 5 at index 2 and moves the implicit position to index 3.
Line 26 and 7 carry no designator, so they land at indices 3 and 4.
Line 3[0] = 9 jumps back to index 0; designators need not be in increasing order.
Line 4Indices 1, 5, 6 and 7 were never named, so the zero fill covers them.
Static arrays are zeroed without any initialiser
Shows that file-scope and static local arrays start at zero by a different rule, so only automatic arrays need an explicit = {0}.
<stdio.h>
int global_table[3];
void count_calls(void)
{
static int calls[2];
calls[0]++;
printf("calls[0]=%d calls[1]=%d\n", calls[0], calls[1]);
}
int main(void)
{
printf("global_table: %d %d %d\n",
global_table[0], global_table[1], global_table[2]);
count_calls();
count_calls();
return 0;
}
Example explained
Line 1global_table has static storage duration, so it is zero initialised with no initialiser written at all.
Line 2static int calls[2]; is zeroed once before main runs, not on each call, which is why calls[0] prints 1 then 2.
Line 3calls[1] is never assigned and stays 0 — the same zeros as = {0}, but produced by the storage-duration rule.
Line 4Remove the static keyword and calls would be an ordinary local array with indeterminate contents.
Important notes
Empty braces, as in int a[5] = {};, are only standard from C23; write {0} if the code must build under C99 or C17, even though GCC and Clang accept {} as an extension.
Zero fill is defined per type, not per byte: = {0} on an array of pointers produces genuine null pointers and on doubles produces 0.0, whereas clearing bytes only happens to give the same result on common hardware.
Common mistakes
Assuming a local array like int buf[64]; starts at zero because a file-scope array does. Its contents are indeterminate, so the program can work in a debug build and produce nonsense in an optimised one.
Writing int a[4] = {1}; expecting every element to be 1. Only a[0] is 1 and the rest are 0, so any loop that assumes an all-ones array computes the wrong result silently.
Ignoring the warning from int a[3] = {1, 2, 3, 4};. It is a constraint violation, and if warnings are off the fourth value is quietly dropped rather than resizing the array.
Try it yourself
Change, predict, then run
Declare int week[7] = {[0] = 8, [6] = 3};, print all seven elements with a loop, then change the declaration to int week[] = {8}; and print sizeof week / sizeof week[0] to watch the deduced length drop from 7 to 1.
Open the C workspaceCheck your understanding
Two declarations sit side by side in the same function: int p[4] = {7}; and int q[4];. Before either array is assigned to, how do their contents differ?
- p holds 7, 0, 0, 0 while q holds indeterminate values, so reading q is undefined behaviour
- Both hold 7, 0, 0, 0, because C zero-fills every array declared inside a function
- p holds 7, 7, 7, 7 while q holds 0, 0, 0, 0
- p holds 7, 0, 0, 0 and q holds 0, 0, 0, 0, because the compiler clears the stack frame on entry
Show answer
The zeros in p come from the presence of an initialiser, not from p being an array, so q gets nothing: its elements are indeterminate and reading them is undefined behaviour. Option 4 is tempting because many toolchains hand out stack memory that happens to be zero in unoptimised builds, but nothing in the language promises that, and the same code can print leftover values once optimisation or a different call path reuses the stack.