Before you begin
Why this matters
A C program can print the right numbers in every test and still read memory that was never given a value. Another program keeps a count from an earlier call that should have started again. Both build without errors. When you know how long each variable exists and what it holds at first, you can find these bugs by reading the code.
What this helps you reason about
- Predict whether a local variable keeps its value from one call to the next.
- Say what each element of an array holds after a short initializer such as {1}.
- Spot a variable that some path reads before giving it a value, and read gcc’s warning about it.
In Java, the compiler refuses to read a local variable that has no value yet. In Go, every variable starts at zero. C does neither for a local variable, yet it starts every global variable at zero. The C variant of Variables showed both rules. This lesson explains them with one idea, storage duration: how long an object exists. It also separates where a name can be used from how long its object exists, and shows what an initializer fills in.
Already know this? The short version
- C11 6.2.4: an automatic object (a block-scope variable without static) is created each time its block is entered and ends with the block; a static object (file scope, or a static local) exists for the whole run and is initialized once, before main; allocated memory lives from malloc to free.
- An automatic object without an initializer is indeterminate, and reading it is undefined when its address is never taken. gcc -Wall reports some such reads (-Wuninitialized, -Wmaybe-uninitialized) and misses others, such as a read inside a loop in an -O0 build.
- A static object without an initializer is zero, or a null pointer, and its initializer must be a constant expression. A brace initializer shorter than the array zeros the rest, for automatic arrays too: {1} gives 1 0 0 0 0.
- Scope is where a name can be used; lifetime is when its object exists. A static local has block scope and static lifetime, and static at file scope gives a name internal linkage.
- In real programs, watch for loop totals with no starting value, static locals that must start fresh, returned pointers to automatic arrays, and malloc where zeros were assumed: see “What goes wrong with initialization and storage in real programs?”.
Words in this lesson (8)
- object
- In C, a region of memory that holds a value of one type. A variable names an object. This is not the object of object-oriented languages.
- lifetime
- The part of a run during which an object exists: memory is kept for it, and it keeps the last value stored in it. The object’s storage duration decides how long that is.
- automatic storage duration
- The lifetime of a variable declared inside a block without static. Each time the block runs, a new object is created, and it stops existing when the block ends.
- static storage duration
- The lifetime of a variable that exists for the whole run of the program. A local declared static has it: it is initialized before the program starts, not at the first call, and keeps its value between calls.
- indeterminate value
- The contents of a local variable that has not been given a value. It is not a reliable number, and reading it is a bug.
- undefined behavior
- Code for which the C standard sets no requirements. The program may crash, print wrong results, or seem to work, and the result can change between builds.
- initializer
- The = … part of a declaration, which gives a new variable its first value. For an array or a struct it is a list in braces, { … }; elements or members it does not mention are set to 0, or to a null pointer.
- scope
- The part of the program where a name can be used. A name declared inside a block, a pair of braces, can be used from its declaration to the end of that block. A name declared outside every function has file scope: it can be used from its declaration to the end of the file.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Storage duration
How long does a C object exist?
Variables showed that a C variable names an object: a region of memory that holds a value of one type. The part of a run during which an object exists, and keeps the last value stored in it, is its lifetime. Its storage duration decides how long that lifetime is. Three storage durations matter in ordinary programs.
A variable declared inside a block without the keyword static has automatic storage duration. Each time the block runs, a new object is created, and it stops existing when the block ends. A function body is a block, so each call gets its own local variables.
A variable declared outside every function, or inside a block with static, has static storage duration. There is one object for the whole run. Its initializer runs once, before main starts, not each time the program reaches the declaration. So a static local keeps its value from one call to the next. Because the value is set before main, the initializer must be a constant expression, one the compiler can compute: static int desk = first_desk(); is refused with error: initializer element is not constant.
The third kind is allocated storage duration. malloc(n), declared in <stdlib.h>, returns a pointer to a new block of n bytes, and the block exists until the program passes that pointer to free. The end of a block or a call does not end it. Stack and heap, a later lesson, shows malloc and free at work.
void next_desk(void) {
/* one object for the run; set before main */
static int desk = 10;
/* a new object at each call */
int people = 0;
desk = desk + 1;
people = people + 1;
printf("desk %d for person %d\n", desk, people);
}
/* Two calls print:
desk 11 for person 1
desk 12 for person 1 */ An automatic local is a new object at every call; a static object is one object for the whole run, initialized once.
Coming from C++: A C++ static local is initialized the first time a call reaches its declaration, and its initializer may call a function. In C it is set before main, and its initializer must be a constant expression.
Precisely: C11 6.2.4 and 5.1.2
6.2.4p1 lists four storage durations: static, thread, automatic, and allocated. Thread storage duration belongs to objects declared _Thread_local: one object per thread. 6.2.4p2 defines the lifetime as the part of the execution during which storage is guaranteed to be reserved; the object has a constant address and keeps its last-stored value. 6.2.4p3 and 5.1.2p1: an object with static storage duration is initialized once, before program startup, and 6.7.9p4 requires constant expressions (or string literals) in its initializer. 6.2.4p6: an automatic object’s lifetime runs from entry into its block until the block ends in any way; calling a function from the block suspends it but does not end it, and a recursive call creates a new instance. The lifetime starts at entry to the block, before the declaration is reached, but the name is in scope only after its declarator. 7.22.3: allocated objects last from malloc, calloc, or realloc until free or realloc.
Experiment
Call a function with a static local twice
desk has static storage duration: one object for the whole run, initialized once, before main. people has automatic storage duration: each call creates a new people, and its initializer runs when the call reaches the declaration. Without static, desk is automatic too, so each call creates a new desk and sets it to 10.
Why it matters A static local carries its value from one call to the next. The = 10 on a static local does not reset it at each call.
Predict both output lines. Then delete the word static from line 4 and predict both lines again.
#include <stdio.h>
void next_desk(void) {
static int desk = 10;
int people = 0;
desk = desk + 1;
people = people + 1;
printf("desk %d for person %d\n", desk, people);
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
next_desk();
next_desk();
return 0;
}
What to look for
The stepper never stops on line 4: no code runs there, because desk got its 10 before main started. At the first stop in next_desk, line 5, desk already shows 10, and people is not listed yet. In the second call, desk starts at 11, the value the first call left, while people shows 0 again at line 6. The run prints desk 11 for person 1, then desk 12 for person 1. After the edit, the stepper stops on line 4 in each call, desk shows 10 at line 5 both times, and the run prints desk 11 for person 1 twice.
Expected output
desk 11 for person 1
desk 12 for person 102 / No value yet
What does a variable hold before it gets a value?
A static object without an initializer starts at zero: 0 for numbers, and a null pointer for pointers. An automatic object without an initializer holds an indeterminate value: whatever was left in its memory. Each call makes a new object, but the memory under it is used again, so the leftover can come from an earlier call.
Do not read an automatic variable before your code gives it a value. If the program never takes its address with &, the read is undefined behavior: the C standard sets no requirements for the whole program. If the address is taken, the value is still not one you can count on.
gcc helps, but it does not catch every case. With -Wall, which the stepper uses, gcc reports a read that comes before any line could have stored a value. The stepper shows it under Compiler warnings, for example:
warning: ‘extra’ may be used uninitialized [-Wmaybe-uninitialized]
A warning does not stop the build. In the experiment below, extra is read only inside an if that this run never enters, so the run is defined and still gets the warning.
/* static: starts at 0 */
int helped;
void desk_round(void) {
/* static: starts at 0, once */
static int rounds;
/* automatic: indeterminate */
int found;
found = 2;
rounds = rounds + 1;
helped = helped + found;
} A static object without an initializer starts at zero; an automatic one holds an indeterminate value, which the program must not read.
Coming from Java: javac refuses a read of a local that may have no value: variable might not have been initialized. gcc builds the program, and at most warns.
Coming from Go: Go gives every variable its zero value. In C, only objects with static storage duration start at zero; a local starts with whatever its memory held.
Precisely: 6.7.9p10, 6.3.2.1p2, and gcc
6.7.9p10: an automatic object that is not initialized explicitly has an indeterminate value; a static one is set to zero, or a null pointer, and an array or struct is set member by member in the same way. 6.3.2.1p2 makes reading an uninitialized automatic object undefined when it could have been declared register, that is, when its address is never taken. When the address is taken, an indeterminate value is either an unspecified value or a trap representation (6.2.6.1p5); int has no trap representations in the stepper’s build, but compilers optimize as if each read may give a different value. gcc enables -Wuninitialized (“‘total’ is used uninitialized”, when every path reads before storing) and -Wmaybe-uninitialized (when some path does) with -Wall. At -O0, gcc 12 on aarch64 Linux, the stepper’s build, gave no warning for a read inside a loop, or for a read after an if that stores on one path only. Optimized builds find more cases, but not all. Valgrind, and clang’s -fsanitize=memory, can report such reads at run time.
Experiment
Read the warning for a variable with no first value
extra has automatic storage duration and no initializer, so each call creates it with an indeterminate value. gcc sees that line 7 reads extra before any line can have stored a value in it. The warning comes from the code, not from this run: line 7 never runs here. A call such as desks_for(4, 2) would run it, and that read is undefined behavior.
Why it matters Give an automatic variable its first value in its declaration. A “may be used uninitialized” warning points at a read that some input will reach.
In this run, guests is always 0, so the if never runs. Predict whether gcc warns, and what prints. Then change int extra; to int extra = 0; and predict both again.
#include <stdio.h>
int desks_for(int staff, int guests) {
int desks = (staff + 1) / 2;
int extra;
if (guests > 0) {
extra = extra + guests;
desks = desks + extra;
}
return desks;
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
printf("%d desks\n", desks_for(5, 0));
printf("%d desks\n", desks_for(3, 0));
return 0;
}
What to look for
Under Compiler warnings, gcc reports ‘extra’ may be used uninitialized [-Wmaybe-uninitialized] at line 7, with a note that ‘extra’ was declared here, at line 5. In each call the stepper skips line 5: a declaration without an initializer has no code. From line 6, extra shows a number such as 1413568073 or -1456563296 that no line of this program stored there. Those are leftover bytes, and the numbers can change from run to run. The program never reads them, and the run prints 3 desks, then 2 desks. After the edit there is no warning, the stepper stops on line 5, and extra shows 0 from line 6.
Expected output
3 desks
2 desks03 / Initializers
What does an initializer fill in?
An initializer is the = … part of a declaration. int open[5]; declares an array: 5 ints stored side by side, open[0] to open[4]. An array initializer lists values in braces, in order. When the list is shorter than the array, C sets every element it does not mention to 0. So {1} sets open[0] to 1 and the other four elements to 0. {0} is the usual way to write an array of zeros.
A designated initializer names the element: {[2] = 4, [5] = 1} sets those two elements and sets the rest to 0. A value after a designated one goes to the next element: {[2] = 4, 7} sets team[3] to 7.
A string literal can fill a char array. char sign[8] = "Quiet"; stores the five letters, then the null character '\0' that ends a string, then zeros in the last two bytes. Leave room for the '\0': char sign[5] = "Quiet"; still compiles, but stores no '\0', so sign is not a string.
These rules hold for automatic arrays too: an initializer, even {0}, sets every element. An automatic array with no initializer is indeterminate in every element.
int open[5] = {1};
/* open: 1 0 0 0 0 */
int team[6] = {[2] = 4, [5] = 1};
/* team: 0 0 4 0 0 1 */
char sign[8] = "Quiet";
/* sign: Q u i e t \0 \0 \0 */ An initializer shorter than its array sets the rest of the array to zero, for an automatic array too.
Coming from Python: [1] * 5 makes five ones in Python. In C, int open[5] = {1}; makes one 1 and four zeros.
Precisely: C11 6.7.9
6.7.9p21: when a brace list has fewer initializers than the array has elements, or the struct has members, the rest are initialized as objects with static storage duration are: zero, or a null pointer. 6.7.9p17: a designator sets the current position, and the next initializer without a designator goes to the element after it. 6.7.9p14: a string literal initializes a char array byte by byte, including the terminating null character if there is room or the array has no size, as in char sign[] = "Quiet";, which has 6 bytes. Structs use the same rules with member names, {.hours = 30}. The initializer of an automatic array may use variables (int week[4] = {hours};); a static one may not. gcc also accepts [0 ... 7] = 1 for a range of elements, but that is a GNU extension, not C11.
Experiment
Fill three arrays with short initializers
Each initializer lists fewer values than its array has elements, so C sets every element it does not mention to 0. A designator such as [2] chooses the element for the value after it. The string literal "Quiet" fills six bytes, five letters and '\0', and the last two bytes of sign become 0 as well.
Why it matters {1} does not mean “all ones”. To give every element the same value other than 0, write each value, or set the elements in a loop.
Predict both output lines. Then change {1} on line 5 to {[1] = 1} and predict the first line again.
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int open[5] = {1};
int team[6] = {[2] = 4, [5] = 1};
char sign[8] = "Quiet";
printf("%d %d %d\n", open[0], open[4], team[2]);
printf("%s %d\n", sign, sign[7]);
return 0;
}
What to look for
Each array appears in main’s frame after its line runs. open shows {1, 0, 0, 0, 0}, and team shows {0, 0, 4, 0, 0, 1}. sign shows "Quiet\000\000": the debugger writes each null character as \000, and it does not show the last byte, which is also 0. The run prints 1 0 4, then Quiet 0. After the edit, open shows {0, 1, 0, 0, 0}, and the first line is 0 0 4.
Expected output
1 0 4
Quiet 004 / Scope and lifetime
Where can a name be used, and how long does its object exist?
Scope and lifetime answer different questions. Scope, from Scope and shadowing, is where in the source code a name can be used. Lifetime is when, during the run, the object exists.
For an automatic local the two almost match: the name can be used to the end of its block, and the object ends with that block. A static local separates them. Its name has block scope: no code outside its block can use that name. Its object has static storage duration: it exists for the whole run. So a function can return the address of a static local, and the code that called it can use that pointer after the call has returned.
At file scope, outside every function, static means something else. Every variable there already has static storage duration, and static gives its name internal linkage: other source files of the same program cannot refer to it.
int *door_opens(void) {
static int opens = 0;
opens = opens + 1;
return &opens;
}
/* in main: */
int *seen = door_opens();
door_opens();
printf("%d\n", *seen); /* 2 */
/* printf("%d\n", opens); is refused:
the name opens is not in scope here. */ Scope is where a name can be used; lifetime is when its object exists. A static local has block scope and lasts for the whole run.
Precisely: C11 6.2.1, 6.2.2, and 6.2.4p2
6.2.1p4: an identifier declared in a block has block scope, ending at the end of that block. 6.2.2p3: a file-scope object or function declared static has internal linkage; one without static has external linkage, so another file can reach it with an extern declaration. 6.2.4p2: using an object after its lifetime ends is undefined, and every pointer to it becomes indeterminate. In the stepper’s build, gcc 12 on aarch64 Linux, the address of opens begins 0xaaaa, where the program’s own data is loaded, while local variables have addresses beginning 0xffff; that is an observation of this build, not a rule of C.
Experiment
Keep a pointer to a static local
opens has block scope, so only code inside door_opens can use its name. It has static storage duration, so the object exists before the first call and after the last one, and both calls change the same object. seen holds its address, so *seen reads it from main.
Why it matters A pointer to a static local stays valid after the call returns. A pointer to an automatic local does not; the next section quotes the warning gcc gives when a function returns one.
main keeps the address that the first call returns. Predict what prints after the second call. Then change *seen on line 13 to opens and read the compiler error.
#include <stdio.h>
int *door_opens(void) {
static int opens = 0;
opens = opens + 1;
return &opens;
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int *seen = door_opens();
door_opens();
printf("%d\n", *seen);
return 0;
}
What to look for
At the first stop in door_opens, opens already shows 0. After line 11, main’s frame shows seen as an address, then <opens>, the name of the object it points to, then → 1. The address changes from run to run. During the second call, door_opens shows opens = 2 after line 5, and main’s seen shows → 2 at the same stop: one object, seen from two frames. The run prints 2. After the edit, the build fails with error: ‘opens’ undeclared (first use in this function), pointing at line 13.
Expected output
205 / In production
What goes wrong with initialization and storage in real programs?
The rules in this lesson explain four bugs that C programmers meet in real programs.
A loop total with no starting value. int total; followed by a loop that adds to total reads an indeterminate value on the first pass. In the stepper’s build this loop gave no warning, although the same read outside a loop does warn. Give the total its starting value in the declaration, as below.
A static local in a function that must start fresh. static int count = 0; sets count once, before main, not at each call. A function that counts with it is right on its first call and wrong on the next, so a test that calls it once passes. Every caller shares the one object, nothing outside the function can reset it, and two threads (parts of one program that run at the same time) that call it at once can damage the count. Keep the count in an automatic variable, or let the caller keep it and pass its address.
A returned pointer to an automatic array. gcc warns:
warning: function returns address of local variable [-Wreturn-local-addr]
The array’s lifetime ends with the call, so the pointer is dangling: it points to memory that no longer exists. Let the caller pass in an array to fill. Making the array static removes the warning, but then every call returns the same array and overwrites the last result.
Zeros that disappear when a table moves to malloc. A global or static array starts at zero. Memory from malloc is indeterminate, like an automatic variable. calloc(count, size), also from <stdlib.h>, returns memory with every byte set to 0.
int week[5] = {8, 9, 8, 7, 10};
/* bug: int total; this loop gave no warning */
int total = 0;
for (int i = 0; i < 5; i++) {
total = total + week[i];
}
printf("%d\n", total); /* 42 */ Give every automatic variable its first value, keep state that must start fresh out of static locals, and never return the address of an automatic object.
Precisely: library functions with static storage
C11 7.27.3 says asctime, ctime, gmtime, and localtime return pointers to static objects that a later call may overwrite, and strtok keeps its position in static storage between calls. POSIX adds localtime_r and strtok_r, which take the storage from the caller instead. 7.22.3.4: the contents of memory from malloc are indeterminate. 7.22.3.2: calloc sets all bits to zero. That is 0 for every integer type; the standard does not promise that all bits zero is 0.0 or a null pointer, although it is on common platforms. -Wreturn-local-addr is on by default, and the stepper’s -O0 build reported it for the array above.
06 / Try it
Can you say what each variable holds?
Sort the three variables that book uses by storage duration before you read main.
#include <stdio.h>
int booked;
int book(int hours) {
static int calls;
int week[4] = {hours};
calls = calls + 1;
booked = booked + week[0] + week[3];
return calls;
}
int main(void) {
book(3);
int last = book(5);
printf("%d %d\n", booked, last);
return 0;
}
What does it print? What changes if you delete static from the declaration of calls?
Reveal the reasoning
8 2. booked has static storage duration and no initializer, so it starts at 0. calls is a static local: it starts at 0 before main, and the two calls make it 1, then 2, so last is 2. week is automatic: each call makes a new array, hours goes into week[0], and the initializer sets the other three elements to 0. The calls add 3 + 0 and 5 + 0, so booked is 8. Without static, calls is automatic with no initializer, so calls = calls + 1 reads an indeterminate value. That read is undefined behavior, and gcc warns: ‘calls’ is used uninitialized [-Wuninitialized].
Practice tickets
- C-201 · Overtime grows with every employee on the payroll screen → Free
- C-202 · The locker screen finds no free locker once locker 0 is taken → Sign in · Pro
- C-203 · The door app says the first person arrived at midnight → Sign in · Pro
Keep exploring
Primary references
- C11 draft standard (N1570): 6.2.1 Scopes, 6.2.2 Linkages, 6.2.4 Storage durations of objects, 6.7.9 Initialization, 7.22.3 Memory management functions ↗
- cppreference: Storage-class specifiers (C) ↗
- cppreference: Array initialization (C) ↗
- GCC manual: Warning options (-Wuninitialized, -Wmaybe-uninitialized, -Wreturn-local-addr) ↗