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
FundamentalAn 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.
TIP
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 if
s and two else
s. 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.
INFO
Exercises Section 5.3.1
Exercise 5.5: Using an if
–else
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 if
–else
statement.
Exercise 5.7: Correct the errors in each of the following code fragments:
(a)if (ival1 != ival2)
(b)if (ival < minval)
(c)if (int ival = get_value())
(d)if (ival = 0)
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 case
s or until the program explicitly interrupts it. To avoid executing code for subsequent case
s, 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 case
s 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;
}
TIP
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.
TIP
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
.
TIP
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
INFO
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.