Chapter 6: Exploring Conditional Program Flow

Not only do the values of variables change when a program runs but the flow of execution can also change through a program. The order of statement execution can be altered depending upon the results of conditional expressions. In a conditional program flow, there is one mandatory branch and one optional branch. If the condition is met, the first branch, or path, will be taken; if not, the second path will be taken.

We can illustrate such a branching with the following simple example:

Is today Saturday?

If so, do the laundry.

Else, go for a walk.

In this conditional statement, we are instructed to do the laundry if today is Saturday. For the remaining days that are not Saturday, we are instructed to go for a walk.

This is a simple conditional statement; it has a single conditional expression, a single mandatory branch, and a single optional branch.

The following topics will be covered in this chapter:

  • Understanding various conditional expressions
  • Using if()… else… to determine whether a value is even or odd
  • Using a switch()… statement to give a message based on a single letter
  • Determining ranges of values using if()… else if()… else if()… else…
  • Exploring nesting if()… else… statements
  • Understanding the pitfalls of nesting if()… else… statements

In this chapter, we will explore conditional statements that evaluate multiple conditions. We will also explore conditional statements where multiple branches may be taken.

Technical requirements

As detailed in the Technical requirements section of Chapter 1, Running Hello, World!, continue to use the tools you have chosen.

The source code for this chapter can be found at https://github.com/PacktPublishing/Learn-C-Programming-Second-Edition/tree/main/Chapter06.

Understanding conditional expressions

We have seen how execution progresses from one statement to the next with simple statements. We have also seen how program execution can be diverted or redirected via a function call; after executing the function body, it returns to the place of the function call and continues execution. We are now going to see how the flow of execution can change and how statements can be executed or skipped with some of C's complex statements.

Execution flow will be determined by the result of evaluating conditional expressions, which we learned about in the previous chapter. Conditional expressions can be either simple or complex. Complex conditional statements should be clear and unambiguous. If they cannot be made clear and unambiguous, they should be reworked to be less complex. However, when reworking their results in more awkward code, the complex conditional expression should be thoroughly commented. Careful consideration should be given to the valid inputs and expected results of the conditional expression.

Conditional expressions appear in very specific places within a complex statement. In every case, the conditional expression appears enclosed between ( and ) in the complex statement. The result will always be evaluated as true or false, regardless of the expression's complexity.

Some conditional expressions are given as follows:

( bResult == true ) 
( bResult )          /* A compact alternative. */
( status != 0 )
( status )         /* A compact alternative where status is */
                     /* only ever false when it is 0        */
( count < 3 ) 
( count > 0 && count <= maxCount ) 
                                /* Both must be true for the */
                                /* overall expression to be true. */

In each case, the result is one of two possible outcomes. In this manner, we can use the result to perform one pathway, or branch, or the other branch.

Introducing the if()… else… complex statement

if()… else… is a complex statement that can have two forms – a simple form where the if()… part is present, and a complete form where both the if()… and else… parts are present.

In the if()… else… complex statement, either the true path or the false path will be executed. The path that is not taken will be ignored. In the simplest if()… statement where there is no false path, the true path is executed only if the conditional expression is true.

The statement has two syntactical forms, as follows:

  • The simplest form (no false path), as illustrated in the following code snippet:

    if( expression )

      statement1

    statement3         /* next statement to be executed */

  • The complete form (both the true path and the false path), as illustrated in the following code snippet:

    if( expression )     

      statement1

    else                 

      statement2

    statement3       /* next statement to be executed */

In both the if()… (simple) and if()… else… (complete) statements, expression is evaluated. If the result is true, statement1 is executed. In the complete form, if the expression result is false, statement2 is executed. In either case, the execution continues with the next statement, statement3.

Note that the statements do not indicate whether there is a semicolon or not. This is because statement1 or statement2 may either be simple statements (which are terminated with a ;) or they may be compound statements, enclosed between { and }. In the latter case, each statement with the statement block will be terminated by ; but not the overall block itself.

A simple use of this statement would be determining whether an integer value is even or odd. This is a good opportunity to put the % modulo operator into action. A function that does this using the simple form is illustrated in the following code block:

bool isEven( int num )  {
  bool isEven = false;  // Initialize with assumption that 
                        // it's not false.
  if( (num % 2) == 0 )
    isEven = true;
  return isEven;
}

In the preceding function, we first assume the value is not even, and then test it with a simple if statement and return the result. Notice that the if()… branch is a single statement that is only executed if num % 2 is 0. We had to use a relational operator in the condition because when num is even, num % 2 evaluates to 0, which would then be converted to the Boolean as false, which would not be the correct result. If the condition is false, the if branch is not executed and we return the value of isEven.

This function could also be written in a slightly condensed form using multiple return statements instead of the num local variable, as follows:

bool isEven ( int num)  {
  if( num % 2 ) 
    return false;
  return true;
}

In the preceding function, we use a very simple conditional that will be non-zero only if num is odd; therefore, we must return false for the function to be correct. This function has two exit points, one that will be executed if the test condition is true and one that will be executed when the if branch is not executed. When num is even, the second return statement will never be executed.

The most condensed version of this function would be as follows:

bool isEven ( int num)  {
  return !(num % 2 ) 
}

When num is even, num % 2 gives 0 (false), and so we have to apply NOT to return the correct result.

A function that uses the complete form of this complex statement would be as follows:

bool isOdd ( int num)  {
  bool isOdd;
  if( num % 2 ) isOdd = true;
  else          isOdd = false;
  return isOdd;
}

In the preceding function, isOdd is not initialized. We must, therefore, ensure that whatever value is given as input to the isOdd function is assigned either true or false. As in the preceding examples, each branch of the complex statement is a single statement assigning a value to isOdd.

To explore various uses of this complex statement, let's begin exploring a function that calculates leap years. Leap years in Western civilization first appeared in 45 BC when the Julian calendar (named after Julius Caesar) added a day in February every 4 years. To mirror this reality, we use a modulo of 4 operations. So, in our first approximation of calculating leap years, our function would look like this:

bool isLeapYear( int year )  {
  if( (year % 4) == 0 ) return true;
  return false;
}

In this function block, each time we know whether the given year is a leap year or not, we return from the function block with that result using return logic to stop the execution of statements within the function. Note that there are several ways in which we could have written the second conditional expression, some of which are shown in the following code block:

if(  (year % 4) == 0 ) … 
if(  (year % 4) < 1 ) …
if( !(year % 4) ) …

Of these, the first way is the most explicit, while the last is the most compact. All are correct.

The remainder of this program looks like this:

#include <stdio.h>
#include <stdbool.h>
bool isLeapYear( int );
int main( void )  {
  int year;
  printf( "Determine if a year is a leap year or not.

" );
  printf( "Enter year: ");
  scanf( "%d" , &year );
   // A simple version of printing the result.
  if( isLeapYear( year ) )
    printf( "%d year is a leap year
" , year );
  else
    printf( "%d year is not a leap year
" , year );
   // A more C-like version to print the result.
  printf( "%d year is%sa leap year
" , year , 
          isLeapYear( year ) ? " " : " not " );
  return 0;
}

In the main() code block, we use both an if()… else… statement to print out the result and a more idiomatic C-like printf() statement, which uses the ternary (conditional) operator. Either method is fine. In the C-like version, %s represents a string (a sequence of characters enclosed in "…") to be filled in the output string.

Create a new file named leapyear1.c and add the code. Compile the program and run it. You should see the following output:

Figure 6.1 – Screenshot of the output of leapyear1.c

Figure 6.1 – Screenshot of the output of leapyear1.c

We test this program by testing each path through the program; that is, we test our programming using a year we know is not a leap year and one we know is a leap year. But is this program correct? No, it is not, because of leap centuries. The year 2000 is a leap year but 1900 is not. It turns out that the solar year is slightly less than 365.25 days; it is actually approximately 365.2425 days (365 + 1/4 - 1/100 + 1/400). In 1582, Pope Gregory XIII changed the calendar to account for leap centuries. In many countries, this new calendar system was adopted quickly; however, Britain and its colonies did not adopt it until 1752. Note that we'll have to account for 1/100 (every 100 years) and 1/400 (every 400 years). Our function will have to get a bit more complicated when we revisit it after the next section.

Using a switch()… complex statement

With the if()… else… statement, the conditional expression evaluates to only one of two values – true or false. But what if we had a single result that could have multiple values, with each value requiring a different bit of code execution?

For this, we have the switch()… statement. This statement evaluates an expression to a single result and selects a pathway where the result matches the known values that we care about.

The syntax of the switch()… statement is as follows:

switch( expression )  {
  case constant-1 :    statement-1
  case constant-2 :    statement-2
  …
  case constant-n :    statement-n
  default :            statement-default
}

Here, the expression evaluates a single result. The next part of the statement is called the case-statement block, which contains one or more case clauses and an optional default: clause. In the case-statement block, the result is compared to each of the constant values in each case clause. When they are equal, the code pathway for that case clause is evaluated. These are indicated by the case <value>: clauses. When none of the specified constants matches the result, the default pathway is executed, as indicated by the default: clause.

Even though the default: clause is optional, it is always a good idea to have one, even if only to print an error message. By doing so, you can ensure that your switch() statement is being used as intended and that you haven't forgotten to account for a changed or new value being considered in the switch statement.

As before, each statement could be a simple statement or a compound statement.

We could rewrite our leap year calculation to use the switch statement, as follows:

  switch( year % 4 )  {
    case 0 :
      return true;
    case 1 :
    case 2 :
    case 3 :
    default :
      return false;
  }

Note that for case 1, case 2, and case 3, there is no statement at all. The evaluation of the result of the expression continues to the next case. It falls through each case evaluation into the default case and returns false.

We can simplify this to the only value we really care about, which is 0, as follows:

switch( year % 4 )  {
  case 0 :
    return true;
  default :
    return false;
}

In the context of the isLeapYear() function, the return statements exit the switch() statement and also the function block.

However, there are many times when we need to perform some actions for a given pathway and then perform no further case comparisons. The match was found, and we are done with the switch statement. In such an instance, we don't want to fall through. We need a way to exit the pathway as well as the case-statement block. For that, there is the break statement.

The break statement causes the execution to jump out, to the end of the statement block, where it is encountered.

To see this in action, we'll write a calc() function that takes two values and a single-character operator. It will return the result of the operation on the two values, as follows:

double calc( double operand1 , double operand2 , char operator )  {
  double result = 0.0;
 
  printf( "%g %c %g = " , operand1 , operator , operand2 );
  switch( operator )  {
    case '+':
      result = operand1 + operand2;        break;
    case '-':
      result = operand1 - operand2;        break;
    case '*':
      result = operand1 * operand2;        break;
    case '/':
      if( operand2 == 0.0 )  {
        printf( "*** ERROR *** division by %g is undefined.
" , 
                 operand2 );
        return result;
      } 
      else {
        result = operand1 / operand2;
      }
      break;
    case '%':
       // Remaindering: assume operations on integers (cast first).
     result = (int) operand1 % (int) operand2;
     break;
   default:
     printf( "*** ERROR *** unknown operator; 
             operator must be + - * / or %%
" );
     return result;
     break;
  }
  /* break brings us to here */
  printf( "%g
" , result );
  return result;
}

In this function, double data types are used for each operand so that we can calculate whatever is given to us. Through implicit type conversion, int and float will be converted to the wider double type. We can cast the result to the desired type after we make the function call because it is at the function call, where we will know the data types being given to the function.

For each of the five characters-as-operators that we care about, there is a case clause where we perform the desired operation, assigning a result. In the case of /, we do a further check to verify that division by zero is not being done.

Experiment

Comment out this if statement and divide without the check to see what happens.

In the case of %, we cast each of the operands to int since % is an integer-only operator.

Experiment

Remove the casting and add some calls to calc() with various real numbers to see what happens.

Also, notice the benefit of having a default: clause that handles the case when our calc program is called with an invalid operator. This kind of built-in error checking becomes invaluable over the life of a program that may change many times at the hands of many different programmers.

To complete our calc.c program, the rest of the program is as follows:

#include <stdio.h>
#include <stdbool.h>
double calc( double operand1, double operand2 , char operator );
int main( void )  {
  calc(  1.0 , 2.0 , '+' );
  calc( 10.0 , 7.0 , '-' ); 
  calc(  4.0 , 2.3 , '*' );
  calc(  5.0 , 0.0 , '/' );
  calc(  5.0 , 2.0 , '%' );
  calc(  1.0 , 2.0 , '?' );
  return 0;
}

In this program, we call calc() with all of the valid operators as well as an invalid operator. In the fourth call to calc(), we also attempt a division by zero. Save and compile this program. You should see the following output:

Figure 6.2 – Screenshot of the output of calc.c

Figure 6.2 – Screenshot of the output of calc.c

In each case, the correct result is given for both valid and invalid operations. Don't forget to try the experiments mentioned.

The switch()… statement is ideal when a single variable is being tested for multiple values. Next, we'll see a more flexible version of the switch()… statement.

Introducing multiple if()… statements

The switch statement tests a single value. Could we have done this another way? Yes – with the if()… else if()… else if()… else… construct. We could have written the calc() function using multiple if()… else… statements, in this way:

double calc( double operand1 , double operand2 , char operator )  {
  double result = 0.0;
 
  printf( "%g %c %g = " , operand1 , operator , operand2 );
  if( operator == '+' )
      result = operand1 + operand2;
  else if( operator == '-' )
      result = operand1 - operand2;
  else if( operator == '*' )
      result = operand1 * operand2;
  else if( operator ==  '/'
      if( operand2 == 0.0 ) {
        printf( "*** ERROR *** division by %g is undefined.
",
                operand2 );
        return result;
      } else {
        result = operand1 / operand2;
      }
  else if( operator == '%') {  
     // Remaindering: assume operations on integers (cast first).
     result = (int) operand1 % (int) operand2;
  } 
  else {  
     printf( "*** ERROR *** unknown operator; 
             operator must be + - * / or %%
" );
     return result;
  }
  printf( "%g
" , result );
  return result;
}

This solution, while perfectly fine, introduces a number of concepts that need to be clarified, as follows:

  • The first if()… statement and each else if()… statement correlate directly to each case : clause of the switch()… statement. The final else… clause corresponds to the default: clause of the switch statement.
  • In the first three if()… statements, the true path is a single simple statement.
  • In the fourth if()… statement, even though there are multiple lines, the if()… else… statement is itself a single complex statement. Therefore, it is also a single statement.

This if()… statement is slightly different than the other statements; it uses a conditional expression for a different variable, and it occurs in the else if()… branch as a complex statement. This is called a nested if()… statement.

  • Again, in the last two pathways, each branch has multiple simple statements. To combine them into a single statement, we must make them a part of a compound statement by enclosing them in { and }.

Experiment

Remove { and } from the last two pathways to see what happens.

  • You might argue that, for this case, the switch()… statement is clearer and thus preferred, or you may not think so.

In this code sample, we are comparing a single value to a set of constants. There are instances where a switch statement is not only unusable but cannot be used. One such instance is when we want to simplify ranges of values into one or more single values. Consider the describeTemp() function, which, when given a temperature, provides a relevant description of that temperature. This function is illustrated in the following code block:

void describeTemp( double degreesF )  {
  char * message;
  if(      degreesF >= 100.0 )  message = "hot! Stay in the shade.";
  else if( degreesF >=  80.0 )  message = "perfect weather for swimming.";
  else if( degreesF >=  60.0 )  message = "very comfortable.";
  else if( degreesF >=  40.0 )  message = "chilly.";
  else if( degreesF >=  20.0 )  message = "freezing, but good skiing weather.";
  else                          message = "way too cold to do much of anything!" ;
    printf( "%g°F is %s
" , degreesF , message );
}

In this function, a temperature is given as a double type, representing °F. Based on this value, a series of if()… else… statements select the appropriate range and then print a message describing that temperature. The switch()… statement could not be used for this.

We could also have written describeTemp() using the && logical operator, as follows:

void describeTemp( double degreesF )  {
  char* message;
  if( degreesF >= 100.0 ) 
    message = "hot! Stay in the shade.";
  if( degreesF < 100.0 && degreesF >= 80.0 ) 
    message = "perfect weather for swimming.";
  if( degreesF <  80.0 && defgredegreesF >= 60.0 ) 
    message = "very comfortable.";
  if( degreesF <  60.0 && degreesF >= 40.0 ) 
    message = "chilly.";
  if( degreesF <  40.0  degreesF >= 20.0 ) 
    message = "freezing, but good skiing weather.";
  if( degreesF < 20.0 ) 
    message= "way too cold to do much of anything!" ;
  printf( "%g°F is %s
" , degreesF , message );
}

In this version of describeTemp(), each if()… statement checks for a range of degreesF values. Most of the conditional expressions test an upper limit and a lower limit. Note that there is no else()… clause for any of these. Also, note that for any given value of degreesF, one – and only one – if()… statement will be satisfied. It is important that all ranges of possible values are covered by this kind of logic. This is called fall-through logic, where executions fall through each if()… statement to the very end. We will see further examples of fall-through logic in the next section.

The full program, which also exercises the various temperature ranges, is found in temp.c. When you compile and run this program, you see the following output:

Figure 6.3 – Screenshot of the output of temp.c

Figure 6.3 – Screenshot of the output of temp.c

We can now return to our leapYear.c program and add the proper logic for leap centuries. Copy the leapYear1.c program to leapYear2.c, which we will modify. We keep some parts from the preceding function, but this time, our logic includes both leap centuries (every 400 years) and non-leap centuries (every 100 years), as follows:

  // isLeapYear logic conforms to algorithm given in 
  // https://en.wikipedia.org/wiki/Leap_year.
  //
bool isLeapYear( int year )  {
  bool isLeap = false;
    // Gregorian calendar leap year calculation.
    // Cannot use years before 1 using BC or BCE year notation.
  if( year % 4 ) != 0 )     // Year is not a multiple of 4.
    isLeap = false;
  else if( ( year % 400 ) == 0 )  // Year is a multiple of 400.
    isLeap = true;
  else if( (year % 100) == 0 )    // Year is multiple of 100.
    isLeap = false;
  else
    isLeap = true; // Year is a multiple of 4 (other conditions 400
                 // years, 100 years) have already been considered.
  return isLeap;
}

This underlying logic we are trying to mimic lends itself naturally to a series of if()…else… statements. We now handle 100-year and 400-year leap years properly. Save leapYear2.c, compile it, and run it. You should see the following output:

Figure 6.4 – Output of if()… else… statements

Figure 6.4 – Output of if()… else… statements

We are again using a sequence of if()… else if()… else… statements to turn a year value into a simple Boolean result. However, in this function, instead of returning when the result is known, we assign that result to a local variable, isLeap, and only return it at the very end of the function block. This method is sometimes preferable to having multiple return statements in a function, especially when the function becomes long or contains particularly complicated logic.

Note that it correctly determines that 2000 is a leap year and that 1900 is not.

Using nested if()… else… statements

Sometimes, we can make the logic of if()… else… statements clearer by nesting if()… else… statements within either one or both clauses of the if()… else… statements.

In our isLeap() example, someone new to the intricacies of Gregorian calendar development and the subtleties of a century leap year calculation might have to pause and wonder a bit about our if/else fall-through logic. Actually, this case is pretty simple; much more tangled examples of such logic can be found. Nonetheless, we can make our logic a bit clearer by nesting an if()… else statement within one of the if()… else… clauses.

Copy leapYear2.c to leapYear3.c, which we will now modify. Our isLeap() function now looks like this:

bool isLeapYear( int year )  {
  bool isLeap = false;
    // Gregorian calendar leap year calculation.
    // Cannot use years before 1 using BC or BCE year notation.
    //
  if( (year % 4 ) != 0 )  // Year is not a multiple of 4.
    isLeap = false;
  else {                   // Year is a multiple of 4.
    if( (year % 400 ) == 0 )
      isLeap = true;
    else if( (year % 100 ) == 0 )
      isLeap = false;
    else
      isLeap = true;
  } 
  return isLeap;
}

Again, we use a local variable, isLeap. In the last else clause, we know at that point that a year is divisible by four. So, now, we nest another if()… else… series of statements to account for leap centuries. Again, you might argue that this is clearer than before, or you might not.

Note, however, that we enclosed – nested – the last series of if()… else… statements in a statement block. This was done not only to make its purpose clearer to other programmers but also to avoid the dangling else problem.

The dangling else problem

When multiple statements are written in if()… else… statements where a single statement is expected, surprising results may occur. This is sometimes called the dangling else problem and is illustrated in the following code snippet:

if( x == 0 ) if( y == 0 ) printf( "y equals 0
" );
else printf( "what does not equal 0
" );

To which if()… does the else… belong – the first one or the second one? To correct this possible ambiguity, it is best to always use compound statements in if()… and else… clauses to make your intention unambiguous. Many compilers will give an error such as the following: warning: add explicit braces to avoid dangling else [-Wdangling-else].

On the other hand, some do not. The best way to remove doubt is to use brackets to associate the else… clause with the proper if()… clause. The following code snippet will remove both ambiguity and the compiler warning:

if( x == 0 )  { 
  if( y == 0 ) printf( "y equals 0
" );
}
else printf( "x does not equal 0
" );

In the usage shown in the preceding code block, the else… clause is now clearly associated with the first if()…, and we see x does not equal 0 in our output.

In the following code block, the else… clause is now clearly associated with the second if()… clause, and we'll see y does not equal 0 in our output. The first if()… statement has no else… clause, as can be seen here:

if( x == 0 )  {
  if( y == 0 ) printf( "y equals 0
" );
  else printf( "y does not equal 0
" );
}

Notice how the if( y == 0 ) statement is nested within the if( x == 0 ) statement.

In my own programming experience, whenever I have begun writing an if()… else… statement, rarely has each clause been limited to a single statement. More often, as I add more logic to the if()… else… statement, I have to go back and turn them into compound statements with { and }. As a consequence, I now always begin writing each clause as a compound statement to avoid having to go back and add them in. Before I begin adding the conditional expression and statements in either of the compound statements, my initial entry looks like this:

if( )  {
  // blah blah blahblah
} else {
  // gabba gabba gab gab
}

You may find that the } else { line is better when broken into three separate lines. The choice is a matter of personal style. This is illustrated in the following code snippet:

if( )  {
  // blah blah blahblah
}
else 
{
  // gabba gabba gab gab
}

In the first code example, the else… closing block and the if() opening block are combined on one line. In the second code example, each is on a line by itself. The former is a bit more compact, while the latter is less so. Either form is acceptable, and their use will depend upon the length and complexity of the code in the enclosed blocks.

Stylistically, we could also begin each of the if blocks with an opening { on its own line, as follows:

if( )
{
  // blah blah blahblah
} else {
  // gabba gabba gab gab
}

Alternatively, we could do this:

if( ) {
  // blah, blah blah blah.
} 
else
{
  // gabba gabba, gab gab.
}

Some people prefer the block opening { on its own line, while others prefer the block opening at the end of the conditional statement. Again, either way is correct. Which way is used depends on both personal preference and the necessity of following the conventions of the existing code base – consistency is paramount.

Summary

From this chapter, we learned that we can not only alter program flow with function calls but also execute or omit program statements through the use of conditional statements. The if()… else… statement has a much wider variety of forms and uses. The switch()… statement operates on a single value, comparing it to the desired set of possible constant values and executing the pathway that matches the constant value. if()… else… statements can be chained into longer sequences to mimic the switch()… statement and to provide a richer set of conditions than possible with switch()…. Also, if()… else… statements can be nested in one or both clauses to either make the purpose of that branch clear or the condition for each pathway less complex.

In these conditional statements, execution remains straightforward, from top to bottom, where only specific parts of the statement are executed. In the next chapter, we'll see how to perform multiple iterations of the same code path, with various forms of looping and the somewhat stigmatized goto statement.

Questions

  1. Are both statements of an if()… else... statement executed?
  2. Can you replace every if()… statement with a switch()… statement?
  3. Can you replace every switch()… statement with a complex if()… statement?
  4. How do you avoid the dangling else problem?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset