CHAPTER 10:

STATEMENTS

Every procedural language provides statements for determining the flow of control within programs. Although declarations are a type of statement, in C the unqualified word statement usually refers to procedural statements rather than declarations. In this chapter we are concerned only with procedural statements.

In the C language, statements can be written only within the body of a function; more specifically, only within compound statements. The normal flow of control among statements is sequential, proceeding from one statement to the next. However, as we shall see, most of the statements in C are designed to alter this sequential flow so that algorithms of arbitrary complexity can be implemented. This is done with statements that control whether or not other statements execute and, if so, how many times. Furthermore, the ability to write compound statements permits the writing a sequence of statements wherever a single, possibly controlled, statement is allowed. These two features provide the necessary generality to implement any algorithm, and to do it in a structured way.

The C language uses semicolons as statement terminators. A semicolon follows every simple (non-compound) statement, even the last one in a sequence.

When one statement controls other statements, a terminator is applied only to the controlled statements. Thus we would write

		if(x > 5) x = 0; else ++x; 

with two semicolons, not three. Perhaps one good way to remember this is to think of statements that control other statements as "super" statements that "contain" ordinary (simple and compound) statements. Then remember that only simple statements are terminated. This implies, as stated above, that compound statements are not terminated with semicolons. Thus

		while(x < 5) {func(); ++x;}

is perfectly correct. Notice that each of the simple statements within the compound statement is terminated.

Null Statements

The simplest C statement is the null statement. It has no text, just a semicolon terminator. As its name implies, it does exactly nothing. Why have a statement that serves no purpose? Well, as it turns out, statements that do nothing can serve a purpose. As we saw in Chapter 9, expressions in C can do work beyond that of simply yielding a value. In fact, in C programs, all of the work is accomplished by expressions; this includes assignments and calls to functions that invoke operating system services such as input/output operations. It follows that anything can be done at any point in the syntax that calls for an expression. Take, for example, the statement

		while(...) ;

in which the ellipsis stands for some expression that controls the execution of the null statement following. We are free to write ... so that it produces side effects of any kind in addition to controlling the loop. In other words, the effects of the loop can be accomplished in the controlling expression rather than the controlled statement. The result of this practice is more efficient object code. For a specific example see the discussion of the while statement below. The null statement is just one way in which the C language follows a philosophy of attaching intuitive meanings to seemingly incomplete constructs. The idea is to make the language as general as possible by having the least number of disallowed constructs.

Compound Statements

The terms compound statement and block both refer to a collection of statements that are enclosed in braces to form a single unit. Compound statements have the form

		{ObjectDeclaration?... Statement?... }

ObjectDeclaration?... is an optional set of local declarations. If present, Small C requires that they precede the statements; in other words, they must be written at the head of the block. Statement?... is a series of zero or more simple or compound statements. Notice that there is not a semicolon at the end of a block; the closing brace suffices to delimit the end.

The power of compound statements derives from the fact that one may be placed anywhere the syntax calls for a statement. Thus any statement that controls other statements is able to control units of logic of any complexity.

When control passes into a compound statement, two things happen. First, space is reserved on the stack for the storage of local variables that are declared at the head of the block. Then the executable statements are processed.

When control leaves a compound statement, the stack is restored to its original condition. For more on local declarations, see Chapters 4-6.

One important limitation in Small C is that a block containing local declarations must be entered through its leading brace. This is because bypassing the head of a block effectively skips the logic that reserves space for local objects. Since the goto and switch statements (below) could violate this rule, Small C mutually excludes from any function goto statements and declarations at levels below the function body, and it disallows declarations within switch statements. Full C has no such restrictions, however. As it turns out, these restrictions are seldom encountered, since most people declare all of their locals at the head of function bodies, and it is fairly unusual to see locals declared in blocks within switch statements.

Expression Statements

The C language is unusual in its provision for expression statements--expressions which the compiler accepts as complete statements. We saw in Chapter 8 that this generalization leads to the elimination of procedures as a distinct class of subroutine and the elimination of special call statements for invoking them. This feature and the provision of assignment operators also leads to the elimination of a special assignment statement.

The value produced by an expression statement is not used under normal conditions. In fact, some compilers optimize such values into oblivion. But Small C goes ahead and produces them in its primary register and then ignores them. It is possible, however, to utilize these values by inserting assembly language code after the expression statement. See Chapter 11 for details.

As we noted above, the work in C programs is really done by expressions. Function calls, assignments, increments, and decrements are all performed during expression evaluation. That being the case, it follows that C must provide for the normal sequential evaluation of expressions; it must support expression statements.

Some examples of valid expression statements are:

		func();
		++i;
		i = 15, j = 16, k = 17;
		x += func(x, y = 12) * 100;  

The Goto Statement

Goto statements break the sequential flow of execution by causing control to jump abruptly to designated points. They have the general form

		goto Name ;

where Name is the name of a label which must appear in the same function. It must also be unique within the function. Labels have the form

		Name :

where Name obeys the C naming conventions ( Chapter 2). Notice that labels are terminated with a colon. This highlights the fact that they are not statements but statement prefixes which serve to label points in the logic as targets for goto statements. When control reaches a goto, it proceeds directly from there to the designated label. Both forward and backward references are allowed, but the range of the jump is limited to the body of the function containing the goto statement.

As we observed above, goto statements, cannot be used in functions which declare locals in blocks which are subordinate to the outermost block of the function.

Goto statements should be used sparingly, if at all. Over reliance on them is a sure sign of sloppy thinking. Strictly speaking, they are never absolutely necessary. In fact the original Small C compiler did not even support goto statements. But situations do occasionally arise in which a goto can save the writing of redundant statements. To keep the meaning of your logic clear, you should try to limit the distance between goto statements and their target labels. It also helps to write labels so they stand out from their surrounding code; for instance, placing a blank line above a label helps.

The If Statement

If statements provide a non-iterative choice between alternate paths based on specified conditions. They have either of two forms

		if ( ExpressionList ) Statement

or
		if ( ExpressionList ) Statement 

else Statement

ExpressionList is a list of one or more expressions and Statement is any simple or compound statement. First, ExpressionList is evaluated and tested. If more than one expression is given, they are evaluated from left to right and the right-most expression is tested. If the result is true (non-zero), then the first (or only) statement is executed and the one following the keyword else (if present) is skipped. If it is false (zero), the first statement is skipped and the second (if present) is executed. For example, the statement

		if (ch) {
			putchar(ch);
			++i;
			}
		else return (i);

tests ch. If it is not zero, putchar() (Chapter 12) writes it to the standard output file and i is incremented; otherwise, the value of i is returned to the calling function.

The Switch Statement

Switch statements provide a non-iterative choice between any number of paths based on specified conditions. They compare an expression to a set of constant values. Selected statements are then executed depending on which value, if any, matches the expression. Switch statements have the form

		switch ( ExpressionList ) { Statement?...}

where ExpressionList is a list of one or more expressions. Statement?... represents the statements to be selected for execution. They are selected by means of case and default prefixes--special labels that are used only within switch statements. These prefixes locate points to which control jumps depending on the value of ExpressionList. They are to the switch statement what ordinary labels are to the goto statement. They may occur only within the braces that delimit the body of a switch statement.

The case prefix has the form

		case ConstantExpression : 

and the default prefix has the form

		default: 

The terminating colons are required; they heighten the analogy to ordinary statement labels. Any expression involving only numeric and character constants and operators is valid in the case prefix.

After evaluating ExpressionList, a search is made for the first matching case prefix. Control then goes directly to that point and proceeds normally from there. Other case prefixes and the default prefix have no effect once a case has been selected; control flows through them just as though they were not even there. If no matching case is found, control goes to the default prefix, if there is one. In the absence of a default prefix, the entire compound statement is ignored and control resumes with whatever follows the switch statement. Only one default prefix may be used with each switch. Full C compilers reject multiple case prefixes with the same value; however, Small C accepts them but effectively sees only the first one.

If it is not desirable to have control proceed from the selected prefix all the way to the end of the switch block, break statements may be used to exit the block. Break statements have the form

		break; 

Some examples may help clarify these ideas. The statement

		switch (ch) {
			case 'Y':
			case 'y': cptr = "yes";
				 break;
			case 'N':
			case 'n': cptr = "no";
				 break;
			default: cptr = "error";
			}

tests ch. If it equals 'Y' or 'y' the character pointer cptr is set to the address of a string containing "yes" and control exits the switch block. If it equals 'N' or 'n' then cptr is set to the address of a string containing "no" and control exits from the block. Those cases failing, cptr is set to the address of a string containing "error" and control exits through the end of the block.

The statement

		switch (i) {
			default: putchar ('a');
			case 2: putchar ('b');
			case 3: putchar ('c');
			}

tests i for values 2 and 3. Those failing, the characters abc are written to the standard output file. If i is 2 then just bc is written; and if it is 3 then only c is written.

The body of the switch is not a normal compound statement since local declarations are not allowed in it or in subordinate blocks. This restriction enforces the Small C rule that a block containing declarations must be entered through its leading brace.

The While Statement

The while statement is one of three statements that determine the repeated execution of a controlled statement. This statement alone is sufficient for all loop control needs. In fact it was the only such statement supported by the original Small C compiler. The other two merely provide an improved syntax and an execute-first feature. While statements have the form

		while ( ExpressionList ) Statement

where ExpressionList is a list of one or more expressions and Statement is an simple or compound statement. If more than one expression is given, the right-most expression yields the value to be tested. First, ExpressionList is evaluated. If it yields true (non-zero), then Statement is executed and ExpressionList is evaluated again. As long as it yields true, Statement executes repeatedly. When it yields false, Statement is skipped, and control continues with whatever follows.

In the example

		i = 5;
		while (i) array[--i] = 0;

elements 0 through 4 of array[ ] are set to zero. First i is set to 5. Then as long as it is not zero, the assignment statement is executed. With each execution i is decremented before being used as a subscript.

It is common to see while statements where the statement being controlled is null and all of the work is done in the expression being tested. In the statement

		while (*dest++ = *sour++) ;

dest and sour are pointers. With every iteration, the object at sour is obtained (before incrementing sour) and is then assigned to the object at dest (before incrementing dest). Since the assignment operator yields the value assigned, that becomes the value of the expression in parentheses. If it is not zero, the null statement executes (doing nothing) and another evaluation is performed. The process repeats until a value of zero has been assigned. This illustrates how strings in C are usually copied.

Two other statements are handy for use with the while (or any loop controlling) statement when it controls a compound statement. The continue statement has the form

		continue; 

It causes control to jump directly back to the top of the loop for the next evaluation of the controlling expression. If loop controlling statements are nested, then continue affects only the innermost surrounding statement. That is, the innermost loop statement containing the continue is the one that starts its next iteration.

The break statement (described earlier) may also be used to break out of loops. It causes control to pass on to whatever follows the loop controlling statement. If while (or any loop or switch) statements are nested, then break affects only the innermost statement containing the break. That is, it exits only one level of nesting.

It is not uncommon to see while statements such as

		while (1) {
			...
			if(...) break;
			...
			}

Notice that the expression is always true. When a specific condition arises, control breaks out of the loop. This is the usual way out of such a loop. Of course, the program could use a goto or a return statement (below), or it could terminate execution by calling the exit() or abort() (Chapter 12).

The For Statement

The for statement also controls loops. It is really just an embellished while in which the three operations normally performed on loop-control variables (initialize, test, and modify) are brought together syntactically. It has the form

		for ( ExpressionList? ;
			ExpressionList? ;
			ExpressionList? ) Statement

For statements are performed in the following steps:

  1. The first ExpressionList is evaluated. This is done only once to initialize the control variable(s).
  2. The second ExpressionList is evaluated to determine whether or not to perform Statement. If more than one expression is given, the right-most expression yields the value to be tested. If it yields false (zero), control passes on to whatever follows the for statement. But, if it yields true (non-zero), Statement executes.
  3. The third ExpressionList is then evaluated to adjust the control variable(s) for the next pass, and the process goes back to step 2.
The example given previously, in which a five-element array is set to zero, could be written as

		for (i = 4; i >= 0; --i) array[i] = 0; 

or a little more efficiently as

		for (i = 5; i; array[--i] = 0) ; 

Any of the three expression lists may be omitted, but the semicolon separators must be kept. If the test expression is absent, the result is always true. Thus

		for (;;) {...break;...} 

will execute until the break is encountered. This syntax is not as desirable as while(1)..., however, since Small C implements for statements with more hidden jumps than while statements (Chapter 19).

As with the while statement, break and continue statements may be used with equivalent effects. A break statement makes control jump directly to whatever follows the for statement. And a continue skips whatever remains in the controlled block so that the third ExpressionList is evaluated, after which the second one is evaluated and tested. In other words, a continue has the same effect as transferring control directly to the end of the block controlled by the for.

The Do Statement

The do statement is the third loop controlling statement in C. It is really just an execute-first while statement. It has the form

		do Statement while ( ExpressionList ) ;

Statement is any simple or compound statement. The do statement executes in the following steps:

  1. Statement is executed.
  2. Then, ExpressionList is evaluated and tested. If more than one expression is given, the right most expression yields the value to be tested. If it yields true (non-zero), control goes back to step 1; otherwise, it goes on to whatever follows.

As with the while and for statements, break and continue statements may be used. In this case, a continue causes control to proceed directly down to the while part of the statement for another test of ExpressionList. A break makes control exit to whatever follows the do statement.

The example of the five-element array could be written as

		i = 4;
		do {array[i] = 0; --i;} while (i >= 0); 

or as
		i = 4;
		do array[i--] = 0; while (i >= 0); 

or as
		i = 5;
		do array[--i] = 0; while (i); 

The Return Statement

The return statement is used within a function to return control to the caller. Return statements are not always required since reaching the end of a function always implies a return. But they are required when it becomes necessary to return from interior points within a function or when a useful value is to be returned to the caller. Return statements have the form

		return ExpressionList? ; 

ExpressionList? is an optional list of expressions. If present, the last expression determines the value to be returned by the function. I f absent, the returned value is unpredictable.

Missing Statements

It may be surprising that nothing was said about input/output, program control, or memory management statements. The reason is that such statements do not exist in the C language proper.

In the interest of portability these services have been relegated to a set of standard functions in the run-time library. Since they depend so heavily on the run-time environment, removing them from the language eliminates a major source of compatibility problems. Each implementation of C has its own library of standard functions which perform these operations. Since different compilers have libraries that are pretty much functionally equivalent, programs have very few problems when they are compiled by different compilers. Chapter 12 describes the standard functions of the Small C compiler.

Go to Chapter 11 Return to Table of Contents