Back to main
Control of Program Flow
Programs normally proceed one statement at a time, in order.
However, only trivial programs behave this way all the way through.
Flow of control constructs exist to provide facilities for
conditional execution, and iteration (looping, etc).
The if..else statement allows us to perform some operations under
certain conditions: if x>0 do something otherwise
do something else. The simplest form involves a single branch of execution,
where a statement is executed only if a condition is true:
The if Statement's Simplest Form
if (expression) /* Single branch */
statement
Remember that a statement
can be a
compound statement,
i.e. one or more statements collected between {braces}
Alternatively a multi-way branch may give several paths of execution:
Multiple Branches of Execution
if (expression1) /* 1st branch */
statement1
else if (expression2) /* 2nd branch */
statement2
else /* Otherwise branch */
statement3
If expression1 is TRUE, then statement1 executes.
If not, expression2 is evaluated and if that is TRUE, statement2
executes. If expression2 is false also, statement3
is executed.
The "Dangling Else" Problem
Consider the following code fragments:
Two Dangling Elses
if (a == 1)
if (b == 2)
x = 0;
else
x = 1;
if (a == 1)
if (b == 2)
x = 0;
else
x = 1;
Under what circumstances does x get set to 1? The indentation is
only decorative in C: both of the above fragments mean exactly the same
thing to the compiler. In the first, the suggestion is that x is set to 1 when
a == 1 && b != 2,
but in the second, the suggestion is that it is set to 1 simply when
a != 1. So which indentation is helpful and which is
confusing?
The answer is arbitrary: the original designers of the C language decided
that in the case of such ambiguity, the else statement shall be
associated with the most recent if statement.
Therefore, the first indentation conveys the correct meaning. If you need the second
meaning, you should write something like:
if (a == 1) {
if (b == 2)
x = 0;
} else
x = 1;
The braces force the grouping of the statements in an unambiguous way.
In fact, even if you want the first behaviour, putting in some cosmetic braces
might not be such a bad idea. The compiler reads the program and parses it
before any code is generated, so any cosmetic elements have no impact
upon code size or speed, but might considerably increase readabilty and
thus help with maintenance and debugging.
A while-loop is a part of a program which is executed zero or more times.
It has the general form:
while (expression)
statement
Once again, statement can be, and usually is, compound.
The expression is evaluated and if the result is TRUE,
the statement is executed. The statement is executed repeatedly until
the expression evaluates as FALSE. If expression is FALSE when the
while-loop is reached for the first time, statement
is not executed at all. So you had better make sure the statement
does something which changes the value of expression if you
want the program ever to finish the while-loop!
Here are some examples using while
An Infinite Loop
while (1)
;
The infinite loop never ever terminates; it executes for as long as 1 is TRUE
(i.e., forever). The body of the loop is an example of a null statement: it
does nothing, but since the langauge syntax calls for a statement of some sort,
the ';' is required.
If you run this program, the computer will waste a lot of time going around in circles
until you interrupt or suspend it (Ctrl-C or Ctrl-Z). This isn't a very useful program,
but infinite loops do come in very handy when the statement isn't null: maybe you
really don't want the program ever to quit (like the one controlling your video
recorder or TV), or maybe the compound statement cotrolled by the while
has a break in it.
The next example shows a while-loop being used as nature intended.
To understand it, you need to know that the
scanf()
function reads input from the keyboard into given variables, and that it
returns the number of variables successfully read. It follows that if we keep
asking for one floating-point value, scanf() will continue to read from
the console until the user enters and End-of-File charcter (Ctrl-D in
ASCII). When this happens,
scanf() changes no variables, but returns 0 to indicate nothing was
read. All this will be covered in detail in the section on the
Standard
I/O Library
Common Programming Mistakes with while-loops
Misplaced semicolons usually cause havoc at compile time, but
at least that is easy to fix. The common mistake with flow-of-control
constructs is to introduce a null statement accdentally by inserting
a mis-placed semicolon immediately after the while, if
or for statement. Compare the following:
The code on the left multiplies y by 3 raised to the nth
power if n is a non-negative integer. Judging from the indentation, this
is what the programmer had in mind. If the code on the right had been
compiled instead, the extra semicolon would mean that the while loop
would have no effect on y; it would loop without effect until n
is reduced to 0 (FALSE). y is then multiplied by 3 whatever the
initial value of n.
The particular problem with this kind of bug is that no compiler errors are
produced for the "incorrect" version of the program; both versions of the
program are entirely legal C. The indentation which gives us a hint that the
code on the left hand side is probably incorrect, but this indentation is entirely
cosmetic and is ignored by the compiler.
Similar illustrations can be made for many of the flow-control constructs.
The only reliable way to isolate and remove such errors is to observe the
behaviour of the running program using a debugger.
Whereas the body of a while-loop is executed zero or more times,
the body of a do-loop is executed one or more times. The decision as
to whether or not to reiterate the loop is made by evaluating an expression
after the controlled statement has been executed.
The general form of the do-loop is:
do
statement
while (expression);
do-loops are frequently used to check input from the user. If the computer
is lucky, the user will get things right the first time, but humans are rather error-prone
so one often sees C code like this:
Checking User Input with do...while
int n = 0;
...
do {
puts("Type in a number bigger than 20");
scanf("%d", &n);
} while (n <= 20);
The program keeps on asking for a number until one bigger than 20 is entered,
and it always asks at least once. If this were a while-loop instead, and
n happened to be more than 20 before reaching it, the user wouldn't be
prompted and no input would be read, but with the do-loop, the user
is asked at least once.
Some other programming languages prefer to provide a do...until contruct
rather than a do...while. If you are already used to writing in such
languages, remember that you need to negate the expression in the while
clause because do...until(i == 10) means the same as
do...while(i != 10).
Every loop you can think of can be done with do-loops and
while-loops, but after a time you will find yourself writing a
frequently occuring form, which is why the for construct
is provided. The following two code fragments are synonymous:
initial-expression;
while (continutaion-expression) {
statement
iteration-expression;
}
for (initial-expression; continuation-expression; iteration-expression)
statement
The following two programs print out the numbers from 0 to 9 using the two different
approaches.
Two Ways of Counting up to Nine
Using a for-loop
int i;
for (i=0; i<10; i++)
printf("%d\n", i);
Using a while-loop
int i;
i = 0;
while (i<10) {
printf("%d\n", i);
i++;
}
Just as the for statement can be thought of as
an abbreviation for a particular sort of commonly used
while-loop, the switch control structure
behaves similarly to a multi-way if. It is used when
it is desired to control a list of alternatives based on the
value of a single expression. The general form is:
switch (expression) {
case constant-expression-1:
0 or more statements
case constant-expression-2:
0 or more statements
...
case constant-expression-n:
0 or more statements
default:
0 or more statements
}
The switch statement evaluates expression and executes the statement(s)
after constant-expression whose value matches
expression. If there is no match with one of the constant-expressions,
the statement(s) associated with the default keyword are executed.
If the optional default keyword is absent, control passes to the statement
following the switch block.
The switch statement can only test for equality. Constant-expressions must be
either int or char. It isn't allowed ot check for a range of values,
except by having a case statement for each possible value. The reason
for this is that the compiler looks upon the switch statement as a jump
table, with each of the case keywords representing a possible
jump target. In this respect, it is slightly more restrictive than a multi-branched
if, although advanced compilers like gcc can take advantage
of these restrictions to produce more highly optimised code.
In most cases, the group of statements associated with each case will have
a break as their last member. This stops control
"falling through" to the next case, and makes each statement group specific
to the associated case. This is almost always the desired semantic.
The code below shows how to build a simple menu using switch.
Here the various different options from the menu are checked for suitable
operations performed. If none of these were recognised, the default option
is to inform the user that the option was invalid. After the action requested
by the user is complete, the code will then return to the start of the do
loop ready to ask for another option. Such code is commonly found at the
heart of console-oriented programs which interact through menus with users.
int c;
do {
puts("Choose an option:");
puts("\to - Open a file");
puts("\ts - Save option");
puts("\tp - Print option");
puts("\tq - Quit");
c = getchar(); /* Get user option from keyboard */
switch (c) {
case 'o': /* Open option */
puts("Open file...");
...
break; /* Jump to end of switch */
case 's': /* Save option */
puts("Save file...");
...
break;
/* Other options go here, until... */
default: /* Option wasn't recognised */
puts("Invalid option, please try again");
}
} while ( (c != 'q') & (c != 'Q') );
The break statement is used to exit the enclosing structure immediately.
It is most often used to terminate a case in the switch
control structure as previously described. It can also be used to break out of a while,
do..while or for loop.
Some programmers argue that the break statement is just a
goto in disguise, and there is always a better
way of coding an algorithm expressed with break without resorting
to what essentially amounts to an unconditional jump instruction. The golden
rule is to use the form which you think is most readable.
By way of example, suppose we wanted to search an array to find the index of the first negative element, never passing the end of the array. The return value will be -1 if no negative value is found, or the index in the array of the first negative value otherwise. Here are two approaches, one using a break, and one not.
Two Ways of Searching an Array
int scanArray(int a[], int size)
{
int i = 0;
while (i < size)
if (a[i] < 0)
return i;
else
i++;
return -1;
}
int scanArray(int a[], int size)
{
int i, result;
for (i = 0, result=-1;
i < size;
i++)
if (a[i] < 0) {
result = i;
break;
}
return result;
}
The example on the left uses break to terminate a for-loop
early, whereas the example on the right uses return to achieve the
same effect. Which is better is a matter of taste: some consider the break
statement bad; some consider multiple instances of the return keyword
in a single function poor style. One should choose whichever structure most
elucidates the operation of the program.
The continue statement causes the enclosing loop construct to
start its next iteration immediately. Similar arguments apply as with the
break statement as to whether the use of continue is to
be encouraged.
The following code performs an operation on all positive elements of an integer array. It is a rather contrived example, because one could avoid the continue by simply inverting the sense of the if statement. However, it serves to demonstrate how an alternative construction can be used to draw attention to a particular aspect of the algorithm.
Using the continue Statement
/* Perform some operation on all of the positive
elements in an integer array */
int i;
for(i = 0; i < size; ++i)
{
if (a < 0)
continue;
/* Perform the operation on a[i] */
...
}
The "case" part of a switch statement is an
example of a label. In fact you can put a label at any point in your code
and jump straight to it with goto.
goto is evil. You never need to use it, so don't.
Here's why goto is so evil and useless. The first reason is it is very bad style
to jump around a program willy-nilly. The author is really saying, "I am confused about
how to structure this program, and don't really understand what it is doing":
there isn't any occasion when you really have use goto when you
couldn't have coded the same thing using while-, do-, or
for-loops with a handful of break and contiune statements.
You can also force the compiler to jump over implicit code, for example
code associated with variable initialisation, with disasterous and unforseeable
results. Consider the following:
Using goto Illegally
main()
{
int b=2;
goto here;
{
int b=3;
here:
printf("Inside block, b is %d\n", b);
}
printf("Outside block, b is %d\n", b);
}
Unfortunately, this is legal C. One is permitted to declare local variables at the
beginning of any block, so the second delcaration of the integer variable b
and its initialisation "hides" the first variable, and "vapourises" at the end of the
block making the initially declared variable visible once again. With the goto
statement commented out, this code runs as expected:
$ ./a.out
Inside block, b is 3
Outside block, b is 2
With the goto in place, the result is different:
$ ./a.out
Inside block, b is 0
Outside block, b is 2
And it's worse than you think. Not only was the second b never initialised,
it never had memory reserved for it either!. If we had started assiging values
to the local version of b inside the block, the outcome would have been
anyone's guess. It could even have modified the value of a different variable
somewhere else in the program! Debug that!!
In the above example it is clear where the bug has been introduced. In practice, if you
use goto, similar bugs are likely to crop up, possibly spanning many files.
goto is evil. You never need to use it, so don't.
There is one place where it might just about be OK to use a jump if you really, really,
really have to, and even then it probably isn't. Suppose you have written a program
which reads in data from a file, for example, which has a very, very complex structure.
So you break up the parsing of the input file into nice, bite-sized chunks of complexity,
creating a program which descends through several levels of subroutine calls before it
can actually read the data. Suppose further that the input file is somehow corrupt, and
that it is really important that your program exits sensibly rather than just crashing if
this happens. Your code is now riddled with checks which keep on repeating the
same tests as each level of subroutine exits. It would be good to be able to jump to
the "top level" where the file parsing began and invoke a "bail-out" clause to try to
recover the situation before exiting sensibly.
C doesn't benefit from the try...except construction available in some
other languages, so to enable such a jump to happen without introducing the sort of
error explained in the previous example, one has to use longjmp(). Take a
look at the following code:
#include <setjmp.h>
void doSomethingHard(jmp_buf bailOut);
int main()
{
jmp_buf env;
if (!setjmp(env))
doSomethingHard(env);
else
puts("Oh no!");
}
void doSomethingHard(jmp_buf bailOut)
{
...
/* Something nasty happened */
longjmp(bailOut, 1);
...
}
When the program calls setjmp, the environment is stored in the named
variable and the value 0 is returned. In our example, this means that the
function which parses the file is called, and passed the context created by
setjmp. When an error occurs from which recovery isn't possible,
longjmp is invoked with a second arguemnt of 1. This causes
the current execution to be aborted, and flow of control to return to the place
where setjmp was originally called, but in such a way that it appears
that setjmp has just returned the given value, in this case 1.
So now the else-part of the if-statement is executed, and the
program does its best to clean up before exiting.
Even the manual page for setjmp says
setjmp() and sigsetjmp() make programs hard
to understand and maintain. If possible an alternative should be used.
You have been warned.
Summary:
All but the simplest programs take advantage of flow-control constructs
which make the program from deviating from "straight-line code". The code
might be executed conditionally as the result of an if-
or switch-statement, or repeated a number of
times using a while-, do-
or for-statement.
The behaviour of loops can be modified using break
or continue statements, but these should be
used sparingly and only if the code is more easily understood as a result.
It is possible to force a jump to a particular part of the code using goto,
but this is diabolical.
System Requirements
To view this web resource, you will need:
-
A web browser which supports CSS-2 Style Sheets and
HTML-4, such
as Mozilla or Netscape.
Internet Explorer
may work but is not recommended, because of
problems with
Java integrity in Microsoft(TM) applications;
-
A Java runtime environment which is at least
Version 1.4.1.
Copyright and Acknowledgements
The tools upon which this course relies are Copyright the
Free Software Foundataion
where they are made
available under the GPL (GNU Public Licence).
The content of this course was derived from that generated
by many ex-colleagues
at the University of Leeds, Department of Electronics and
Electrical Engineering. Much
of the content has been reworked, and substantially
augmented, but
Dr N J Bailey, Centre for Music Technology,
The University
of Glagsow. This manifestation is Copyright
N J Bailey; some of the
content is Copyright The University of Leeds.
Diagrams on this resource are drawn in XFig and are rendered
by the browser
using The University of Hamburg's
Simple
FIG viewer applet which is Copyright
(C) 1996-2002 F.N.Hendrich,
hendrich@informatik.uni-hamburg.de.
The source code, programming examples and exercises are all
specific to this
course, and are Copyright, Dr N J Bailey.
The applet for viewing and demonstrating C programs is
Copyright
Dr N J Bailey, and is to be found documented
and
with its source code on the Centre for Music
Technology website under
Software