3.2. Control of flow
3.2.1. The if statement
The if
statement has two forms:
if(expression) statement if(expression) statement1 else statement2
In the first form, if (and only if) the expression is
non-zero, the statement is executed. If the
expression is zero, the statement is ignored.
Remember that the statement can be compound; that is the way to
put several statements under the control of a single if
.
The second form is like the first except that if the statement shown as statement1 is selected then statement2 will not be, and vice versa.
Either form is considered to be a single statement in the syntax of C, so the following is completely legal.
if(expression) if(expression) statement
The first if (expression)
is followed by a
properly formed, complete if
statement. Since that is legally
a statement, the first if
can be considered to read
if(expression) statement
and is therefore itself properly formed. The argument can be extended as far as you like, but it's a bad habit to get into. It is better style to make the statement compound even if it isn't necessary. That makes it a lot easier to add extra statements if they are needed and generally improves readability.
The form involving else
works the same way, so we can also
write this.
if(expression) if(expression) statement else statement
As Chapter 1 has said already, this is now ambiguous. It is
not clear, except as indicated by the indentation, which of the
if
s is responsible for the else
. If we follow
the rules that the previous example suggests, then the second
if
is followed by a statement, and is therefore itself a
statement, so the else
belongs to the first
if
.
That is not the way that C views it. The rule is that an
else
belongs to the first if above that hasn't already got an
else
. In the example we're discussing, the else goes with the
second if.
To prevent any unwanted association between an else
and an
if
just above it, the if can be hidden away by using a
compound statement. To repeat the example in Chapter 1, here
it is.
if(expression){ if(expression) statement }else statement
Putting in all the compound statement brackets, it becomes this:
if(expression){ if(expression){ statement } }else{ statement }
If you happen not to like the placing of the brackets, it is up to you to put them where you think they look better; just be consistent about it. You probably need to know that this a subject on which feelings run deep.
3.2.2. The while and do statements
The while
is simple:
while(expression) statement
The statement is only executed if the expression
is non-zero. After every execution of the statement, the
expression is evaluated again and the process repeats if it is
non-zero. What could be plainer than that? The only point to watch out for
is that the statement may never be executed, and that if
nothing in the statement affects the value of the expression
then the while
will either do nothing or loop for ever,
depending on the initial value of the expression.
It is occasionally desirable to guarantee at least one execution of the statement following the while, so an alternative form exists known as the do statement. It looks like this:
do statement while(expression);
and you should pay close attention to that semicolon—it is not
optional! The effect is that the statement part is executed before the
controlling expression is evaluated, so this guarantees at least one trip
around the loop. It was an unfortunate decision to use the keyword
while
for both purposes, but it doesn't seem to cause too
many problems in practice.
If you feel the urge to use a do
, think carefully. It is
undoubtedly essential in certain cases, but experience has shown that the
use of do
statements is often associated with poorly
constructed code. Not every time, obviously, but as a general rule you
should stop and ask yourself if you have made the right choice. Their use
often indicates a hangover of thinking methods learnt with other
languages, or just sloppy design. When you do convince yourself
that nothing else will give you just what is wanted, then go ahead - be
daring—use it.
3.2.2.1. Handy hints
A very common trick in C programs is to use the result of
an assignment to control while
and do
loops. It
is so commonplace that, even if you look at it the first time and blench,
you've got no alternative but to learn it. It falls into the category of
‘idiomatic’ C and eventually becomes second nature to anybody who
really uses the language. Here is the most common example of all:
#include <stdio.h> #include <stdlib.h> main(){ int input_c; /* The Classic Bit */ while( (input_c = getchar()) != EOF){ printf("%c was read\n", input_c); } exit(EXIT_SUCCESS); }Example 3.2
The clever bit is the expression assigning to input_c
. It
is assigned to, compared with EOF
(End Of File), and used to
control the loop all in one go. Embedding the assignment like that is a
handy embellishment. Admittedly it only saves one line of code, but the
benefit in terms of readability (once you have got used to seeing it) is
quite large. Learn where the parentheses are, too. They're necessary for
precedence reasons—work out why!
Note that input_c
is an int
. This is because
getchar
has to be able to return not only every possible
value of a char
, but also an extra value, EOF
.
To do that, a type longer than a char
is necessary.
Both the while
and the do
statements are
themselves syntactically a single statement, just like an if
statement. They occur anywhere that any other single statement is
permitted. If you want them to control several statements, then
you will have to use a compound statement, as the examples of
if
illustrated.
3.2.3. The for statement
A very common feature in programs is loops that are controlled by variables used as a counter. The counter doesn't always have to count consecutive values, but the usual arrangement is for it to be initialized outside the loop, checked every time around the loop to see when to finish and updated each time around the loop. There are three important places, then, where the loop control is concentrated: initialize, check and update. This example shows them.
#include <stdio.h> #include <stdlib.h> main(){ int i; /* initialise */ i = 0; /* check */ while(i <= 10){ printf("%d\n", i); /* update */ i++; } exit(EXIT_SUCCESS); }Example 3.3
As you will have noticed, the initialization and check parts of the loop
are close together and their location is obvious because of the presence
of the while
keyword. What is harder to spot is the place
where the update occurs, especially if the value of the controlling
variable is used within the loop. In that case, which is by far the most
common, the update has to be at the very end of the loop: far away from
the initialize and check. Readability suffers because it is hard to work
out how the loop is going to perform unless you read the whole body of the
loop carefully. What is needed is some way of bringing the initialize,
check and update parts into one place so that they can be read quickly and
conveniently. That is exactly what the for statement is designed to do.
Here it is.
for (initialize; check; update) statement
The initialize part is an expression; nearly always an assignment expression which is used to initialize the control variable. After the initialization, the check expression is evaluated: if it is non-zero, the statement is executed, followed by evaluation of the update expression which generally increments the control variable, then the sequence restarts at the check. The loop terminates as soon as the check evaluates to zero.
There are two important things to realize about that last description: one, that each of the three parts of the for statement between the parentheses are just expressions; two, that the description has carefully explained what they are intended to be used for without proscribing alternative uses—that was done deliberately. You can use the expressions to do whatever you like, but at the expense of readability if they aren't used for their intended purpose.
Here is a program that does the same thing twice, the first time using a
while
loop, the second time with a for
. The use
of the increment operator is exactly the sort of use that you will see in
everyday practice.
#include <stdio.h> #include <stdlib.h> main(){ int i; i = 0; while(i <= 10){ printf("%d\n", i); i++; } /* the same done using ``for'' */ for(i = 0; i <= 10; i++){ printf("%d\n", i); } exit(EXIT_SUCCESS); }Example 3.4
There isn't any difference betweeen the two, except that in this case
the for
loop is more convenient and maintainable than the
while
statement. You should always use the for
when it's appropriate; when a loop is being controlled by some sort of
counter. The while
is more at home when an indeterminate
number of cycles of the loop are part of the problem. As always, it needs
a degree of judgement on behalf of the author of the program; an
understanding of form, style, elegance and the poetry of a well written
program. There is no evidence that the software business suffers from a
surfeit of those qualities, so feel free to exercise them if you are
able.
Any of the initialize, check and update expressions in the for statement can be omitted, although the semicolons must stay. This can happen if the counter is already initialized, or gets updated in the body of the loop. If the check expression is omitted, it is assumed to result in a ‘true’ value and the loop never terminates. A common way of writing never-ending loops is either
for(;;)
or
while(1)
and both can be seen in existing programs.
3.2.4. A brief pause
The control of flow statements that we've just seen are quite adequate
to write programs of any degree of complexity. They lie at the core
of C and even a quick reading of everyday C programs will
illustrate their importance, both in the provision of essential
functionality and in the structure that they emphasize. The remaining
statements are used to give programmers finer control or to make it easier
to deal with exceptional conditions. Only the switch
statement is enough of a heavyweight to need no justification for its use;
yes, it can be replaced with lots of ifs
, but it adds a lot
of readability. The others, break
, continue
and
goto
, should be treated like the spices in a delicate sauce.
Used carefully they can turn something commonplace into a treat, but a
heavy hand will drown the flavour of everything else.
3.2.5. The switch statement
This is not an essential part of C. You could do without it, but the language would have become significantly less expressive and pleasant to use.
It is used to select one of a number of alternative actions depending on
the value of an expression, and nearly always makes use of another of the
lesser statements: the break
. It looks like this.
switch (expression){ case const1: statements case const2: statements default: statements }
The expression is evaluated and its value is compared with
all of the const1 etc. expressions, which must all evaluate
to different constant values (strictly they are integral constant
expressions, see Chapter 6 and below). If any of them
has the same value as the expression then the statement
following the case
label is selected for execution. If the
default
is present, it will be selected when there is no
matching value found. If there is no default
and no matching
value, the entire switch
statement will do nothing and
execution will continue at the following statement.
One curious feature is that the cases are not exclusive, as this example shows.
#include <stdio.h> #include <stdlib.h> main(){ int i; for(i = 0; i <= 10; i++){ switch(i){ case 1: case 2: printf("1 or 2\n"); case 7: printf("7\n"); default: printf("default\n"); } } exit(EXIT_SUCCESS); }Example 3.5
The loop cycles with i
having values 0–10. A value of
1
or 2
will cause the printing of the message
1
or 2
by selecting the first of the
printf
statements. What you might not expect is the way that
the remaining messages would also appear! It's because the
switch
only selects one entry point to the body of the
statement; after starting at a given point all of the following statements
are also executed. The case
and default
labels
simply allow you to indicate which of the statements is to be
selected. When i
has the value of 7
, only the
last two messages will be printed. Any value other than 1
,
2
, or 7
will find only the last message.
The labels can occur in any order, but no two values may be the same and
you are allowed either one or no default
(which doesn't have
to be the last label). Several labels can be put in front of one statement
and several statements can be put after one label.
The expression controlling the switch
can be of any of the
integral types. Old C used to insist on only
int
here, and some compilers would forcibly truncate longer
types, giving rise on rare occasions to some very obscure bugs.
3.2.5.1. The major restriction
The biggest problem with the switch
statement is that it
doesn't allow you to select mutually exclusive courses of action; once
the body of the statement has been entered any subsequent statements
within the body will all be executed. What is needed is the
break
statement. Here is the previous example, but amended
to make sure that the messages printed come out in a more sensible order.
The break
statements cause execution to leave the
switch
statement immediately and prevent any further
statements in the body of the switch
from being
executed.
#include <stdio.h> #include <stdlib.h> main(){ int i; for(i = 0; i <= 10; i++){ switch(i){ case 1: case 2: printf("1 or 2\n"); break; case 7: printf("7\n"); break; default: printf("default\n"); } } exit(EXIT_SUCCESS); }Example 3.6
The break
has further uses. Its own section follows
soon.
3.2.5.2. Integral Constant Expression
Although Chapter 6 deals with constant expressions, it is
worth looking briefly at what an integral constant expression is, since
that is what must follow the case
labels in a
switch
statement. Loosely speaking, it is any expression
that does not involve any value-changing operation (like increment or
assignment), function calls or comma operators. The operands in the
expression must all be integer constants, character constants,
enumeration constants, sizeof
expressions and floating-point
constants that are the immediate operands of casts. Any cast operators
must result in integral types.
Much what you would expect, really.
3.2.6. The break statement
This is a simple statement. It only makes sense if it occurs in the body
of a switch
, do
, while
or
for
statement. When it is executed the control of flow jumps
to the statement immediately following the body of the statement
containing the break
. Its use is widespread in
switch
statements, where it is more or less essential to get
the control that most people want.
The use of the break
within loops is of dubious legitimacy.
It has its moments, but is really only justifiable when exceptional
circumstances have happened and the loop has to be abandoned. It would be
nice if more than one loop could be abandoned with a single break but that
isn't how it works. Here is an example.
#include <stdio.h> #include <stdlib.h> main(){ int i; for(i = 0; i < 10000; i++){ if(getchar() == 's') break; printf("%d\n", i); } exit(EXIT_SUCCESS); }Example 3.7
It reads a single character from the program's input before printing the
next in a sequence of numbers. If an ‘s
’ is typed, the
break causes an exit from the loop.
If you want to exit from more than one level of loop, the
break
is the wrong thing to use. The goto
is the
only easy way, but since it can't be mentioned in polite company, we'll
leave it till last.
3.2.7. The continue statement
This statement has only a limited number of uses. The rules for its use
are the same as for break
, with the exception that it doesn't
apply to switch
statements. Executing a continue
starts the next iteration of the smallest enclosing do
,
while
or for
statement immediately. The use of
continue
is largely restricted to the top of loops, where a
decision has to be made whether or not to execute the rest of the body of
the loop. In this example it ensures that division by zero (which gives
undefined behaviour) doesn't happen.
#include <stdio.h> #include <stdlib.h> main(){ int i; for(i = -10; i < 10; i++){ if(i == 0) continue; printf("%f\n", 15.0/i); /* * Lots of other statements ..... */ } exit(EXIT_SUCCESS); }Example 3.7
You could take a puritanical stance and argue that, instead of a
conditional continue,
, the body of the loop should be made
conditional instead—but you wouldn't have many supporters. Most C
programmers would rather have the continue
than the extra
level of indentation, particularly if the body of the loop is large.
Of course the continue
can be used in other parts of a
loop, too, where it may occasionally help to simplify the logic of the
code and improve readability. It deserves to be used sparingly.
Do remember that continue
has no special meaning to a
switch
statement, where break
does have. Inside
a switch
, continue
is only valid if there is a
loop that encloses the switch
, in which case the next
iteration of the loop will be started.
There is an important difference between loops written with
while
and for
. In a while
, a
continue will go immediately to the test of the controlling expression.
The same thing in a for
will do two things: first the update
expression is evaluated, then the controlling expresion is evaluated.
3.2.8. goto and labels
Everybody knows that the goto
statement is a ‘bad
thing’. Used without care it is a great way of making programs hard to
follow and of obscuring any structure in their flow. Dijkstra wrote a
famous paper in 1968 called ‘Goto Statement Considered Harmful’,
which everybody refers to and almost nobody has read.
What's especially annoying is that there are times when it is the most
appropriate thing to use in the circumstances! In C, it is used to
escape from multiple nested loops, or to go to an error handling exit at
the end of a function. You will need a label when you use a
goto
; this example shows both.
goto L1; /* whatever you like here */ L1: /* anything else */
A label is an identifier followed by a colon. Labels have their own
‘name space’ so they can't clash with the names of variables or
functions. The name space only exists for the function containing the
label, so label names can be re-used in different functions. The label can
be used before it is declared, too, simply by mentioning it in a
goto
statement.
Labels must be part of a full statement, even if it's an empty one. This usually only matters when you're trying to put a label at the end of a compound statement—like this.
label_at_end: ; /* empty statement */ }
The goto
works in an obvious way, jumping to the labelled
statements. Because the name of the label is only visible inside its own
function, you can't jump from one function to another one.
It's hard to give rigid rules about the use of goto
s but,
as with the do
, continue
and the
break
(except in switch
statements), over-use
should be avoided. Think carefully every time you feel like using one, and
convince yourself that the structure of the program demands it. More than
one goto
every 3–5 functions is a symptom that should
be viewed with deep suspicion.
Summary
Now we've seen all of the control of flow statements and examples of their use. Some should be used whenever possible, some are not for use line by line but for special purposes where their particular job is called for. It is possible to write elegant and beautiful programs in C if you are prepared to take the extra bit of care necessary; the specialized control of flow statements give you the chance to add the extra polish that some other languages lack.
All that remains to be done to complete the picture of flow of control in C is to finish off the logical operators.