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).

Conditional Execution

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.

While-loops

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:

while (n--);
  y *= 3;

while (n--)
  y *= 3;

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.

Do-loops

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).

For-loops

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-expressioncontinuation-expressioniteration-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++;
}


Switch Statements

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

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

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 Goto Statement, and Labels

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:

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