Team LiB
Previous Section Next Section

5.3. Conditional Statements

 

C++ provides two statements that allow for conditional execution. The if statement determines the flow of control based on a condition. The switch statement evaluates an integral expression and chooses one of several execution paths based on the expression’s value.

 

5.3.1. The if Statement

 
Image

An if statement conditionally executes another statement based on whether a specified condition is true. There are two forms of the if: one with an else branch and one without. The syntactic form of the simple if is

 

if (condition)
     statement

 

An if else statement has the form

 

if (condition)
     statement
else
     statement2

 

In both versions, condition must be enclosed in parentheses. condition can be an expression or an initialized variable declaration (§ 5.2, p. 174). The expression or variable must have a type that is convertible (§ 4.11, p. 159) to bool. As usual, either or both statement and statement2 can be a block.

 

If condition is true, then statement is executed. After statement completes, execution continues with the statement following the if.

 

If condition is false, statement is skipped. In a simple if, execution continues with the statement following the if. In an if else, statement2 is executed.

 
Using an if else Statement
 

To illustrate an if statement, we’ll calculate a letter grade from a numeric grade. We’ll assume that the numeric grades range from zero to 100 inclusive. A grade of 100 gets an “A++,” grades below 60 get an “F,” and the others range in clumps of ten: grades from 60 to 69 inclusive get a “D,” 70 to 79 a “C,” and so on. We’ll use a vector to hold the possible letter grades:

 

 

vector<string> scores = {"F", "D", "C", "B", "A", "A++"};

 

To solve this problem, we can use an if else statement to execute different actions for failing and passing grades:

 

 

// if grade is less than 60 it's an F, otherwise compute a subscript
string lettergrade;
if (grade < 60)
    lettergrade = scores[0];
else
    lettergrade = scores[(grade - 50)/10];

 

Depending on the value of grade, we execute the statement after the if or the one after the else. In the else, we compute a subscript from a grade by reducing the grade to account for the larger range of failing grades. Then we use integer division (§ 4.2, p. 141), which truncates the remainder, to calculate the appropriate scores index.

 
Nested if Statements
 

To make our program more interesting, we’ll add a plus or minus to passing grades. We’ll give a plus to grades ending in 8 or 9, and a minus to those ending in 0, 1, or 2:

 

 

if (grade % 10 > 7)
    lettergrade += '+';         // grades ending in 8 or 9 get a +
else if (grade % 10 < 3)
    lettergrade += '-';         // those ending in 0, 1, or 2 get a -

 

Here we use the modulus operator (§ 4.2, p. 141) to get the remainder and decide based on the remainder whether to add plus or minus.

 

We next will incorporate the code that adds a plus or minus to the code that fetches the letter grade from scores:

 

 

// if failing grade, no need to check for a plus or minus
if (grade < 60)
    lettergrade = scores[0];
else {
    lettergrade = scores[(grade - 50)/10]; // fetch the letter grade
    if (grade != 100)  // add plus or minus only if not already an A++
        if (grade % 10 > 7)
            lettergrade += '+';  // grades ending in 8 or 9 get a +
        else if (grade % 10 < 3)
            lettergrade += '-';  // grades ending in 0, 1, or 2 get a -
}

 

Note that we use a block to enclose the two statements that follow the first else. If the grade is 60 or more, we have two actions that we need to do: Fetch the letter grade from scores, and conditionally set the plus or minus.

 
Watch Your Braces
 

It is a common mistake to forget the curly braces when multiple statements must be executed as a block. In the following example, contrary to the indentation, the code to add a plus or minus happens unconditionally:

 

 

if (grade < 60)
    lettergrade = scores[0];
else  // WRONG: missing curly
    lettergrade = scores[(grade - 50)/10];
    // despite appearances, without the curly brace, this code is always executed
    // failing grades will incorrectly get a - or a +
    if (grade != 100)
        if (grade % 10 > 7)
            lettergrade += '+';  // grades ending in 8 or 9 get a +
        else if (grade % 10 < 3)
            lettergrade += '-';  // grades ending in 0, 1, or 2 get a -

 

Uncovering this error may be very difficult because the program looks correct.

 

To avoid such problems, some coding styles recommend always using braces after an if or an else (and also around the bodies of while and for statements).

 

Doing so avoids any possible confusion. It also means that the braces are already in place if later modifications of the code require adding statements.

 

Image Best Practices

Many editors and development environments have tools to automatically indent source code to match its structure. It is a good idea to use such tools if they are available.

 

 
Dangling else
 

When we nest an if inside another if, it is possible that there will be more if branches than else branches. Indeed, our grading program has four ifs and two elses. The question arises: How do we know to which if a given else belongs?

 

This problem, usually referred to as a dangling else, is common to many programming languages that have both if and if else statements. Different languages solve this problem in different ways. In C++ the ambiguity is resolved by specifying that each else is matched with the closest preceding unmatched if.

 

Programmers sometimes get into trouble when they write code that contains more if than else branches. To illustrate the problem, we’ll rewrite the innermost if else that adds a plus or minus using a different set of conditions:

 

 

// WRONG: execution does NOT match indentation; the else goes with the inner if
if (grade % 10 >= 3)
    if (grade % 10 > 7)
        lettergrade += '+';  // grades ending in 8 or 9 get a +
else
    lettergrade += '-'; // grades ending in 3, 4, 5, 6 will get a minus!

 

The indentation in our code indicates that we intend the else to go with the outer if—we intend for the else branch to be executed when the grade ends in a digit less than 3. However, despite our intentions, and contrary to the indentation, the else branch is part of the inner if. This code adds a '-' to grades ending in 3 to 7 inclusive! Properly indented to match the actual execution, what we wrote is:

 

 

// indentation matches the execution path, not the programmer's intent
if (grade % 10 >= 3)
    if (grade % 10 > 7)
        lettergrade += '+';  // grades ending in 8 or 9 get a +
    else
        lettergrade += '-';  // grades ending in 3, 4, 5, 6 will get a minus!

 
Controlling the Execution Path with Braces
 

We can make the else part of the outer if by enclosing the inner if in a block:

 

 

// add a plus for grades that end in 8 or 9 and a minus for those ending in 0, 1, or 2
if (grade % 10 >= 3) {
    if (grade % 10 > 7)
        lettergrade += '+';  // grades ending in 8 or 9 get a +
} else                  // curlies force the else to go with the outer if
    lettergrade += '-'; // grades ending in 0, 1, or 2 will get a minus

 

Statements do not span block boundaries, so the inner if ends at the close curly before the else. The else cannot be part of the inner if. Now, the nearest unmatched if is the outer if, which is what we intended all along.

 

Exercises Section 5.3.1

 

Exercise 5.5: Using an ifelse statement, write your own version of the program to generate the letter grade from a numeric grade.

Exercise 5.6: Rewrite your grading program to use the conditional operator (§ 4.7, p. 151) in place of the ifelse statement.

 

Exercise 5.7: Correct the errors in each of the following code fragments:

(a) if (ival1 != ival2)
    ival1 = ival2
else ival1 = ival2 = 0;

 

(b) if (ival < minval)
    minval = ival;
    occurs = 1;

 

 

(c) if (int ival = get_value())
    cout << "ival = " << ival << endl;
if (!ival)
    cout << "ival = 0\n";

 

(d) if (ival = 0)
    ival = get_value();

 

Exercise 5.8: What is a “dangling else”? How are else clauses resolved in C++?


 

5.3.2. The switch Statement

 

A switch statement provides a convenient way of selecting among a (possibly large) number of fixed alternatives. As one example, suppose that we want to count how often each of the five vowels appears in some segment of text. Our program logic is as follows:

 

• Read every character in the input.

 

• Compare each character to the set of vowels.

 

• If the character matches one of the vowels, add 1 to that vowel’s count.

 

• Display the results.

 

For example, when we run the program on the text of this chapter, the output is

 

Number of vowel a: 3195
Number of vowel e: 6230
Number of vowel i: 3102
Number of vowel o: 3289
Number of vowel u: 1033

 

We can solve our problem most directly using a switch statement:

 

 

// initialize counters for each vowel
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch) {
    // if ch is a vowel, increment the appropriate counter
    switch (ch) {
        case 'a':
            ++aCnt;
            break;
        case 'e':
            ++eCnt;
            break;
        case 'i':
            ++iCnt;
            break;
        case 'o':
            ++oCnt;
            break;
        case 'u':
            ++uCnt;
            break;
    }
}
// print results
cout << "Number of vowel a: \t" << aCnt << '\n'
     << "Number of vowel e: \t" << eCnt << '\n'
     << "Number of vowel i: \t" << iCnt << '\n'
     << "Number of vowel o: \t" << oCnt << '\n'
     << "Number of vowel u: \t" << uCnt << endl;

 

A switch statement executes by evaluating the parenthesized expression that follows the keyword switch. That expression may be an initialized variable declaration (§ 5.2, p. 174). The expression is converted to integral type. The result of the expression is compared with the value associated with each case.

 

If the expression matches the value of a case label, execution begins with the first statement following that label. Execution continues normally from that statement through the end of the switch or until a break statement.

 

We’ll look at break statements in detail in § 5.5.1 (p. 190), but, briefly, a break interrupts the current control flow. In this case, the break transfers control out of the switch. In this program, the switch is the only statement in the body of a while. Breaking out of this switch returns control to the enclosing while. Because there are no other statements in that while, execution continues at the condition in the while.

 

If no match is found, execution falls through to the first statement following the switch. As we already know, in this example, exiting the switch returns control to the condition in the while.

 

The case keyword and its associated value together are known as the case label. case labels must be integral constant expressions (§ 2.4.4, p. 65):

 

 

char ch = getVal();
int ival = 42;
switch(ch) {
    case 3.14: // error: noninteger as case label
    case ival: // error: nonconstant as case label
    // . . .

 

It is an error for any two case labels to have the same value. There is also a special-case label, default, which we cover on page 181.

 
Control Flow within a switch
 

It is important to understand that execution flows across case labels. After a case label is matched, execution starts at that label and continues across all the remaining cases or until the program explicitly interrupts it. To avoid executing code for subsequent cases, we must explicitly tell the compiler to stop execution. Under most conditions, the last statement before the next case label is break.

 

However, there are situations where the default switch behavior is exactly what is needed. Each case label can have only a single value, but sometimes we have two or more values that share a common set of actions. In such instances, we omit a break statement, allowing the program to fall through multiple case labels.

 

For example, we might want to count only the total number of vowels:

 

 

unsigned vowelCnt = 0;
// ...
switch (ch)
{
    // any occurrence of a, e, i, o, or u increments vowelCnt
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
       ++vowelCnt;
       break;
}

 

Here we stacked several case labels together with no intervening break. The same code will be executed whenever ch is a vowel.

 

Because C++ programs are free-form, case labels need not appear on a new line. We can emphasize that the cases represent a range of values by listing them all on a single line:

 

 

switch (ch)
{
    // alternative legal syntax
    case 'a': case 'e': case 'i': case 'o': case 'u':
       ++vowelCnt;
       break;
}

 

Image Best Practices

Omitting a break at the end of a case happens rarely. If you do omit a break, include a comment explaining the logic.

 

 
Forgetting a break Is a Common Source of Bugs
 

It is a common misconception to think that only the statements associated with the matched case label are executed. For example, here is an incorrect implementation of our vowel-counting switch statement:

 

 

// warning: deliberately incorrect!
switch (ch) {
    case 'a':
        ++aCnt;  // oops: should have a break statement
    case 'e':
        ++eCnt;  // oops: should have a break statement
    case 'i':
        ++iCnt;  // oops: should have a break statement
    case 'o':
        ++oCnt;  // oops: should have a break statement
    case 'u':
        ++uCnt;
}

 

To understand what happens, assume that the value of ch is 'e'. Execution jumps to the code following the case 'e' label, which increments eCnt. Execution continues across the case labels, incrementing iCnt, oCnt, and uCnt as well.

 

Image Best Practices

Although it is not necessary to include a break after the last label of a switch, the safest course is to provide one. That way, if an additional case is added later, the break is already in place.

 

 
The default Label
 

The statements following the default label are executed when no case label matches the value of the switch expression. For example, we might add a counter to track how many nonvowels we read. We’ll increment this counter, which we’ll name otherCnt, in the default case:

 

 

   // if ch is a vowel, increment the appropriate counter
   switch (ch) {
       case 'a': case 'e': case 'i': case 'o': case 'u':
           ++vowelCnt;
           break;
       default:
           ++otherCnt;
           break;
   }
}

 

In this version, if ch is not a vowel, execution will start at the default label and we’ll increment otherCnt.

 

Image Best Practices

It can be useful to define a default label even if there is no work for the default case. Defining an empty default section indicates to subsequent readers that the case was considered.

 

 

A label may not stand alone; it must precede a statement or another case label. If a switch ends with a default case that has no work to do, then the default label must be followed by a null statement or an empty block.

 
Variable Definitions inside the Body of a switch
 

As we’ve seen, execution in a switch can jump across case labels. When execution jumps to a particular case, any code that occurred inside the switch before that label is ignored. The fact that code is bypassed raises an interesting question: What happens if the code that is skipped includes a variable definition?

 

The answer is that it is illegal to jump from a place where a variable with an initializer is out of scope to a place where that variable is in scope:

 

 

case true:
    // this switch statement is illegal because these initializations might be bypassed
    string file_name; // error: control bypasses an implicitly initialized variable
    int ival = 0;     // error: control bypasses an explicitly initialized variable
    int jval;         // ok: because jval is not initialized
    break;
case false:
    // ok: jval is in scope but is uninitialized
    jval = next_num(); // ok: assign a value to jval
    if (file_name.empty()) // file_name is in scope but wasn't initialized
        // ...

 

If this code were legal, then any time control jumped to the false case, it would bypass the initialization of file_name and ival. Those variables would be in scope. Code following false could use those variables. However, these variables would not have been initialized. As a result, the language does not allow us to jump over an initialization if the initialized variable is in scope at the point to which control transfers.

 

If we need to define and initialize a variable for a particular case, we can do so by defining the variable inside a block, thereby ensuring that the variable is out of scope at the point of any subsequent label.

 

 

case true:
    {
       // ok: declaration statement within a statement block
       string file_name = get_file_name();
       // ...
    }
    break;
case false:
       if (file_name.empty())  // error: file_name is not in scope

 

Exercises Section 5.3.2

 

Exercise 5.9: Write a program using a series of if statements to count the number of vowels in text read from cin.

Exercise 5.10: There is one problem with our vowel-counting program as we’ve implemented it: It doesn’t count capital letters as vowels. Write a program that counts both lower- and uppercase letters as the appropriate vowel—that is, your program should count both 'a' and 'A' as part of aCnt, and so forth.

Exercise 5.11: Modify our vowel-counting program so that it also counts the number of blank spaces, tabs, and newlines read.

Exercise 5.12: Modify our vowel-counting program so that it counts the number of occurrences of the following two-character sequences: ff, fl, and fi.

Exercise 5.13: Each of the programs in the highlighted text on page 184 contains a common programming error. Identify and correct each error.

 

 
Team LiB
Previous Section Next Section