Books / Introduction to C Programming Language / Chapter 12
Statements and control structures
Simple & Compound Statements
The bodies of C functions (including the main
function) are made up of statements. These can either be simple statements that do not contain other statements, or compound statements that have other statements inside them. Control structures are compound statements like if/then/else, while, for, and do..while that control how or whether their component statements are executed.
Simple statements
The simplest kind of statement in C is an expression (followed by a semicolon, the terminator for all simple statements). Its value is computed and discarded. Examples:
x = 2; /* an assignment statement */
x = 2+3; /* another assignment statement */
2+3; /* has no effect---will be discarded by smart compilers */
puts("hi"); /* a statement containing a function call */
root2 = sqrt(2); /* an assignment statement with a function call */
Most statements in a typical C program are simple statements of this form.
Other examples of simple statements are the jump statements return
, break
, continue
, and goto
. A return
statement specifies the return value for a function (if there is one), and when executed it causes the function to exit immediately. The break
and continue
statements jump immediately to the end of a loop (or switch
; see below) or the next iteration of a loop; we’ll talk about these more when we talk about loops. The goto
statement jumps to another location in the same function, and exists for the rare occasions when it is needed. Using it in most circumstances is a sin.
Compound statements
Compound statements come in two varieties: conditionals and loops, which we’ll explore in depth in the next sections.
Conditionals
These are compound statements that test some condition and execute one or another block depending on the outcome of the condition. The simplest is the if
statement:
if(houseIsOnFire) {
/* ouch! */
scream();
runAway();
}
The body of the if
statement is executed only if the expression in parentheses at the top evaluates to true (which in C means any value that is not 0).
The braces are not strictly required, and are used only to group one or more statements into a single statement. If there is only one statement in the body, the braces can be omitted:
if(programmerIsLazy) omitBraces();
This style is recommended only for very simple bodies. Omitting the braces makes it harder to add more statements later without errors.
if(underAttack)
launchCounterAttack(); /* executed only when attacked */
hideInBunker(); /* ### DO NOT INDENT LIKE THIS ### executed always */
In the example above, the lack of braces means that the hideInBunker()
statement is not part of the if
statement, despite the misleading indentation. This sort of thing is why I generally always put in braces in an if
.
An if
statement may have an else
clause, whose body is executed if the test is false (i.e. equal to 0).
if(happy) {
smile();
} else {
frown();
}
A common idiom is to have a chain of if
and else if
branches that test several conditions:
if(temperature < 0) {
puts("brrr");
} else if(temperature < 100) {
puts("hooray");
} else {
puts("ouch!");
}
This can be inefficient if there are a lot of cases, since the tests are applied sequentially. For tests of the form _
/* print plural of cow, maybe using the obsolete dual number construction */
switch(numberOfCows) {
case 1:
puts("cow");
break;
case 2:
puts("cowen");
break;
default:
puts("cows");
break;
}
This prints the string “cow” if there is one cow, “cowen” if there are two cowen, and “cows” if there are any other number of cows. The switch
statement evaluates its argument and jumps to the matching case
label, or to the default
label if none of the cases match. Cases must be constant integer values.
The break
statements inside the block jump to the end of the block. Without them, executing the switch
with numberOfCows
equal to 1 would print all three lines. This can be useful in some circumstances where the same code should be used for more than one case:
switch(c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
type = VOWEL;
break;
default:
type = CONSONANT;
break;
}
or when a case “falls through” to the next:
switch(countdownStart) {
case 3:
puts("3");
case 2:
puts("2");
case 1:
puts("1")
case 0:
puts("KABLOOIE!");
break;
default:
puts("I can't count that high!");
break;
}
Note that it is customary to include a break
on the last case even though it has no effect; this avoids problems later if a new case is added. It is also customary to include a default
case even if the other cases supposedly exhaust all the possible values, as a check against bad or unanticipated inputs.
switch(oliveSize) {
case JUMBO:
eatOlives(SLOWLY);
break;
case COLLOSSAL:
eatOlives(QUICKLY);
break;
case SUPER_COLLOSSAL:
eatOlives(ABSURDLY);
break;
default:
/* unknown size! */
abort();
break;
}
Though switch
statements are better than deeply nested if/else-if constructions, it is often even better to organize the different cases as data rather than code. We’ll see examples of this when we talk about function pointers.
Nothing in the C standards prevents the case
labels from being buried inside other compound statements. One rather hideous application of this fact is Duff’s device.
Loops
There are three kinds of loops in C.
The while loop
A while
loop tests if a condition is true, and if so, executes its body. It then tests the condition is true again, and keeps executing the body as long as it is. Here’s a program that deletes every occurrence of the letter e
from its input.
#include <stdio.h>
int
main(int argc, char **argv)
{
int c;
while((c = getchar()) != EOF) {
switch(c) {
case 'e':
case 'E':
break;
default:
putchar(c);
break;
}
}
return 0;
}
Note that the expression inside the while
argument both assigns the return value of getchar
to c
and tests to see if it is equal to EOF
(which is returned when no more input characters are available). This is a very common idiom in C programs. Note also that even though c
holds a single character, it is declared as an int
. The reason is that EOF
(a constant defined in stdio.h
) is outside the normal character range, and if you assign it to a variable of type char
it will be quietly truncated into something else. Because C doesn’t provide any sort of exception mechanism for signalling unusual outcomes of function calls, designers of library functions often have to resort to extending the output of a function to include an extra value or two to signal failure; we’ll see this a lot when the null pointer shows up in the chapter on pointers.
The do..while loop
The do
..while
statement is like the while
statement except the test is done at the end of the loop instead of the beginning. This means that the body of the loop is always executed at least once.
Here’s a loop that does a random walk until it gets back to 0 (if ever). If we changed the do
..while
loop to a while
loop, it would never take the first step, because pos
starts at 0.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
main(int argc, char **argv)
{
int pos = 0; /* position of random walk */
srandom(time(0)); /* initialize random number generator */
do {
pos += random() & 0x1 ? +1 : -1;
printf("%d\n", pos);
} while(pos != 0);
return 0;
}
examples/statements/randomWalk.c
The do
..while
loop is used much less often in practice than the while
loop.
It is theoretically possible to convert a do
..while
loop to a while
loop by making an extra copy of the body in front of the loop, but this is not recommended since it’s almost always a bad idea to duplicate code.
The for loop
The for
loop is a form of syntactic sugar that is used when a loop iterates over a sequence of values stored in some variable (or variables). Its argument consists of three expressions: the first initializes the variable and is called once when the statement is first reached. The second is the test to see if the body of the loop should be executed; it has the same function as the test in a while
loop. The third sets the variable to its next value. Some examples:
/* count from 0 to 9 */
for(i = 0; i < 10; i++) {
printf("%d\n", i);
}
/* and back from 10 to 0 */
for(i = 10; i >= 0; i--) {
printf("%d\n", i);
}
/* this loop uses some functions to move around */
for(c = firstCustomer(); c != END_OF_CUSTOMERS; c = customerAfter(c)) {
helpCustomer(c);
}
/* this loop prints powers of 2 that are less than n*/
for(i = 1; i < n; i *= 2) {
printf("%d\n", i);
}
/* this loop does the same thing with two variables by using the comma operator */
for(i = 0, power = 1; power < n; i++, power *= 2) {
printf("2^%d = %d\n", i, power);
}
/* Here are some nested loops that print a times table */
for(i = 0; i < n; i++) {
for(j = 0; j < n; j++) {
printf("%d*%d=%d ", i, j, i*j);
}
putchar('\n');
}
A for
loop can always be rewritten as a while
loop.
for(i = 0; i < 10; i++) {
printf("%d\n", i);
}
/* is exactly the same as */
i = 0;
while(i < 10) {
printf("%d\n", i);
i++;
}
Loops with break, continue, and goto
The break
statement immediately exits the innermmost enclosing loop or switch
statement.
for(i = 0; i < n; i++) {
openDoorNumber(i);
if(boobyTrapped()) {
break;
}
}
The continue
statement skips to the next iteration. Here is a program with a loop that iterates through all the integers from -10 through 10, skipping 0:
#include <stdio.h>
/* print a table of inverses */
#define MAXN (10)
int
main(int argc, char **argv)
{
int n;
for(n = -MAXN; n <= MAXN; n++) {
if(n == 0) continue;
printf("1.0/%3d = %+f\n", n, 1.0/n);
}
return 0;
}
examples/statements/inverses.c
Occasionally, one would like to break out of more than one nested loop. The way to do this is with a goto
statement.
for(i = 0; i < n; i++) {
for(j = 0; j < n; j++) {
doSomethingTimeConsumingWith(i, j);
if(checkWatch() == OUT_OF_TIME) {
goto giveUp;
}
}
}
giveUp:
puts("done");
The target for the goto
is a label, which is just an identifier followed by a colon and a statement (the empty statement ;
is ok).
The goto
statement can be used to jump anywhere within the same function body, but breaking out of nested loops is widely considered to be its only genuinely acceptable use in normal code.
Choosing where to put a loop exit
Choosing where to put a loop exit is usually pretty obvious: you want it after any code that you want to execute at least once, and before any code that you want to execute only if the termination test fails.
If you know in advance what values you are going to be iterating over, you will most likely be using a for
loop:
for(i = 0; i < n; i++) {
a[i] = 0;
}
Most of the rest of the time, you will want a while
loop:
while(!done()) {
doSomething();
}
The do
..while
loop comes up mostly when you want to try something, then try again if it failed:
do {
result = fetchWebPage(url);
} while(result == 0);
Finally, leaving a loop in the middle using break
can be handy if you have something extra to do before trying again:
for(;;) {
result = fetchWebPage(url);
if(result != 0) {
break;
}
/* else */
fprintf(stderr, "fetchWebPage failed with error code %03d\n", result);
sleep(retryDelay); /* wait before trying again */
}
(Note the empty for
loop header means to loop forever; while(1)
also works.)