Although it is possible to chain many if-else statements together, this is difficult to read. Consider the following program:
enum Colors
{
COLOR_BLACK,
COLOR_WHITE,
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
};
void PrintColor(Colors eColor)
{
using namespace std;
if (eColor == COLOR_BLACK)
cout << "Black";
else if (eColor == COLOR_WHITE)
cout << "White";
else if (eColor == COLOR_RED)
cout << "Red";
else if (eColor == COLOR_GREEN)
cout << "Green";
else if (eColor == COLOR_BLUE)
cout << "Blue";
else
cout << "Unknown";
}
Because doing if-else chains on a single variable testing for equality is so common, C++ provides an alternative conditional branching operator called a switch. Here is the same program as above in switch form:
void PrintColor(Colors eColor)
{
using namespace std;
switch (eColor)
{
case COLOR_BLACK:
cout << "Black";
break;
case COLOR_WHITE:
cout << "White";
break;
case COLOR_RED:
cout << "Red";
break;
case COLOR_GREEN:
cout << "Green";
break;
case COLOR_BLUE:
cout << "Blue";
break;
default:
cout << "Unknown";
break;
}
}
The overall idea behind switch statements is simple: the switch expression is evalutated to produce a value, and each case label is tested against this value for equality. If a case label matches, the statements after the case label are executed. If no case label matches the switch expression, the code under the default label is executed (if it exists).
Let’s examine each of these concepts in more detail.
We start a switch statement by using the switch keyword, followed by the expression that we would like to evaluate. Typically this expression is just a single variable, but it can be something more complex like nX + 2 or nX - nY. The one restriction on this expression is that it must evaluate to an integral type (that is, char, short, int, long, or enum). Floating point variables and other non-integral types may not be used here.
Following the switch expression, we declare a block. Inside the block, we use labels to define all of the values we want to test for equality. There are two kinds of labels.
The first kind of label is the case label, which is declared using the case keyword, and followed by a constant expression. A constant expression is one that evaluates to a constant value — in other words, either a literal (such as 5), an enum (such as COLOR_RED), or a constant integral variable (such as nX, when nX has been defined as a const int).
The constant expression following the case label is tested for equality against the expression following the switch keyword. If they match, the code under the case label is executed. Typically, we end each case with a break statement, which tells the compiler that we are done with the case.
It is worth noting that all case label expressions must evaluate to a unique value. That is, you can not do this:
switch (nX)
{
case 4:
case 4: // illegal -- already used value 4!
case COLOR_BLUE: // illegal, COLOR_BLUE evaluates to 4!
};
It is possible to have multiple case labels refer to the same statements. The following function uses multiple cases to test if the cChar parameter is an ASCII number.
bool IsNumber(char cChar)
{
switch (cChar)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return true;
default:
return false;
}
}
If any of the case labels match, the next statement is executed. In this case, that is the statement return true;, which returns the value true to the caller.
The second kind of label is the default label, which is declared using the default keyword. The code under this label gets executed if none of the cases match the switch expression. The default label is optional. It is also typically declared as the last label in the switch block, though this is not strictly necessary.
One of the trickiest things about case statements is the way in which execution proceeds when a case is matched. When a case is matched (or the default is executed), execution begins at the first statement following that label and continues until one of the following conditions is true:
1) The end of the switch block is reached
2) A return statement occurs
3) A goto statement occurs
4) A break statement occurs
Note that if none of these conditions are met, cases will overflow into other cases! Consider the following program:
switch (2)
{
case 1: // Does not match -- skipped
cout << 1 << endl;
case 2: // Match! Execution begins at the next statement
cout << 2 << endl; // Execution begins here
case 3:
cout << 3 << endl; // This is also executed
case 4:
cout << 4 << endl; // This is also executed
default:
cout << 5 << endl; // This is also executed
}
This program prints the result:
2 3 4 5
Probably not what we wanted! When execution flows from one case into another case, this is called fall-through. Fall-through is almost never desired by the programmer, so in the rare case where it is, it is common practice to leave a comment stating that the fall-through is intentional.
In order to prevent fall-through, we have to use a break, a return, or a goto statement at the end of our case statements. A return statement terminates the current function immediately, and a value is possibly returned to the caller. This makes a lot of sense in short functions with a single purpose, such as the IsNumber() function example above.
However, it is often the case that we want to terminate the case statements without also terminating the entire function. To do this, we use a break statement. A break statement (declared using the break keyword) tells the compiler that we are done with this switch (or while, do while, or for loop), and execution continues with the statement after the end of the switch block.
Let’s look at our last example with break statements properly inserted:
switch (2)
{
case 1: // Does not match -- skipped
cout << 1 << endl;
break;
case 2: // Match! Execution begins at the next statement
cout << 2 << endl; // Execution begins here
break; // Break terminates the switch statement
case 3:
cout << 3 << endl;
break;
case 4:
cout << 4 << endl;
break;
default:
cout << 5 << endl;
break;
}
// Execution resumes here
Warning: Forgetting the break statements in a switch block is one of the most common C++ mistakes made!
Quiz
1) Write a function called Calculate() that takes two integers and a char representing one of the following mathematical operations: +, -, /, or *. Use a switch statement to perform the appropriate mathematical operation on the integers, and return the result. If an invalid operator is passed into the function, the function should print “Error” and the program should exit (use the exit() function).
Quiz answers
5.4 — Goto statements
|
Index
|
5.2 — If statements
|
5.4 — Goto statements
Index
5.2 — If statements
Alex, could you show one or two programs from the examples above worked fully? for example, could you show how the code below might evaluate in the main function. Thanks. Also, shouldn’t the symbolic constant enumerators in the “enum Colors” block end with a ‘,’ instead of a ‘;’?
enum Colors { COLOR_BLACK; COLOR_WHITE; COLOR_RED; COLOR_GREEN; COLOR_BLUE; }; void PrintColor(Colors eColor) { using namespace std; switch (eColor) { case COLOR_BLACK: cout < < "Black"; break; case COLOR_WHITE: cout << "White"; break; case COLOR_RED: cout << "Red"; break; case COLOR_GREEN: cout << "Green"; break; case COLOR_BLUE: cout << "Blue"; break; default: cout << "Unknown"; break; } } int Main() { // Please show how the code in the above blocks might evaluate. }You’re right, the enums should be separated by commas, not semicolons. I fixed the example.
Here’s a piece of sample code:
int main() { Colors ePenColor = COLOR_BLACK; Colors eEraseColor = COLOR_WHITE; cout < < "I am drawing in: "; PrintColor(ePenColor); cout << endl; cout << "I am erasing in: "; PrintColor(eEraseColor); cout << endl; return 0; }This produces the result:
Not the most useful example, perhaps.
Here’s another example:
int main() { while (1) { cout < < "Enter a character (ctrl-c to quit): "; char chChar; cin >> chChar; cout < < chChar << " is a "; if (IsNumber(chChar)) cout << "number" << endl; else cout << "letter" << endl; }; return 0; }Sample output:
Thanks for the examples. I have another question: What are the rules for adding a semi-colon to the end of a block? If I remove the semi-colon from the end of the enum Colors block, it will not compile.
As far as I can tell, all statements should be terminated by a semicolon, EXCEPT for compound statements (blocks).
A declaration is considered a statement, and thus should be terminated by a semicolon. An enum is a declaration so it needs to be terminated with a semicolon. Same with a class declaration. The statements inside a switch are considered compound statements, so they do not need a terminating semicolon.
Alex -
There is a logical error in the quiz question and answer: If we pass e.g. 1,2,- or 1,-1,* to the function Calculate, it will return a -1, which is our error code, when in fact an error has not occured since -1 is the correct result (2 - 1 = -1, 1 * -1 = -1). Wouldn’t it be much better to return the string value “ERROR” instead?
i.e.:
default: return "ERROR";You are correct that this function contains a logic error in this regard. I’ll change the question and answer to avoid this issue.
Returning an error string won’t work because the function is set up to return an integer. Incidentally, the function will also crash if the user tries to do a divide operation and the second parameter (the denominator) is 0.
In a real-world application, where we are concerned with the possible perils of integer division, would it be better to have Calculate() return a double? And, if so, does the following work:
... case '/': if (nY == 0) { cout < < "ERROR. Division by zero."; exit(2); } return (static_cast<double>(nX)) / nY; ...Will the other cases (e.g., addition of two integers) automatically convert the integer sum to a double return value without the compiler complaining?
Whether it’s more appropriate to return an int or a double depends entirely on your needs. For some tasks, integer division might be exactly what you want. Others, maybe not. If your function is going to return a double, you might want to modify it to take double parameters as well so you can do floating point addition, subtraction, and multiplication.
I believe your case would work as you intend. And yes, an integer return value will be implicitly converted to a double if the function has been declared to return a double.
How come you did not use break statements in your solution ?
I didn’t use break statements because I used return statements instead. I wanted to return a different value to the caller for each case. Keep in mind that return statements cause the function to immediately return to the caller, thus the switch statement will stop executing at that point. Break is used to keep cases from overflowing into each other — but this also happens with a return, since the entire function stops executing. Thus, there’s no need to use a break statement in the cases where you are using a return.
Wait, for the quiz answer, shouldn’t there be a ‘using namespace std;’ at the beginning of the function?
Yes.