Switch-case语句主要用在多分支条件的环境中,在这种环境中使用if语句会存在繁琐而且效率不高的弊端。Switch-case语句的一般使用格式为:
switch(expression)
{
case const expression1:
......
break;
case const expression2:
......
break;
default:
......
break;
}
在执行过程中,expression的值会与每个case的值比较,实现switch语句的功能。关键字case和它所关联的值被称作case标号。每个case标号的值都必须是一个常量表达式。除此之外还有一个特殊的case标号–default标号。
如果expression值与其中一个case标号匹配,则程序将从该标号后面的第一个语句开始依次执行各个语,直到switch结束或者遇到break语句为止。如果没有发生与之匹配的case标号(并且也没有default标号),则程序会从switch语句后面的第一条语句继续执行。
小心陷阱
- 关于switch一般存在这种误解:以为程序只会执行匹配的case标号相关的语句。实际上并非如此,该标号只是程序会执行的起始点,程序会从该点执行,并跨越case边界继续执行其他语句,直到switch结束或遇到break语句为止。
- break语句的使用,是switch-case语句的核心。因为在大多数情况下,在下一个case标号前面必须加上一个break语句。
考虑下面这段键盘输入的执行处理实现。
int iInputChar = 0;
// 读取键盘输入
iInputChar = GetInputFromKeyBoard();
// 根据键盘输入执行对应操作
switch(iInputChar)
{
case CTRL_C:
// 执行拷贝操作
DoCpyWork();
case CTRL_V:
// 执行粘贴操作
DoPasteWork();
case CTRL_F
// 执行查找操作
DoFindWork();
default:
}
我们先思考一下这段代码导致了什么结果,假设键盘输入了一个ctrl+c,我们会发现DoCpyWork()、DoPasteWork()和DofindWork()都被执行了一遍。这也许会让你吃惊,本意是执行DoCpyWork()函数的,实际上三个函数却都执行了。
我们在看这个版本,在每个case语句结束时都添加一个break语句。考虑下面这个版本。
int iInputChar = 0;
// 读取键盘输入
iInputChar = GetInputFromKeyBoard();
// 根据键盘输入执行对应操作
switch(iInputChar)
{
case CTRL_C:
// 执行拷贝操作
DoCpyWork();
break;
case CTRL_V:
// 执行粘贴操作
DoPasteWork();
break;
case CTRL_F
// 执行查找操作
DoFindWork();
break;
default:
break;
}
针对这个版本,如果键盘输入ctrl+c,你会发现程序确实仅执行了DoCpyWork()函数。因此我们可以看出break语句在switch-case中的关键核心作用。
小心地雷:对于switch结构,漏写break语句是常见的一种错误,在编程过程中我们应引以注意。
最佳实践:尽管C++标准中,并没有要求在switch结构的最后一个标号之后指定break语句,但是,为了安全起见,最好在每个标号后提供一个break语句,即使最后一个标号也不例外。这样如果因为某种特殊需要在switch的最后一个标号后面又添加一个新的标号,则不用在在前面加break语句了。
虽然我们一直在强调break的重要性。但不得不说有这么一种情况,你确实希望在case标号后省略break语句,允许程序向下执行多个case标号。例如下面这段统计元音总数的代码。
// 统计元音的总数
int iVovelCnt = 0;
// ...
switch(chChar)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++iVovelCnt;
break;
}
实际上每个case标号不一定要另起一行。为了强调每个case标号表示的是一个要匹配的范围,可以将他们全部在一行中列出。这种写法更能体现你的本意。
// 统计元音的总数
switch(chChar)
{
case 'a': case 'e': case 'i': case 'o': case 'u':
++iVovelCnt;
break;
}
最佳实践:故意省略break是一种特别罕见的用法,因此在这种形式的代码附近,请务必添加一些注释,说明其运行逻辑。
讨论完了break的使用问题,我们接着讨论switch-case使用的其他问题。这些问题包括内部变量的定义,case标号和default标号。
在switch-case结构中,只能在最后一个case标号或default中定义内部变量:
case TRUE:
// error: 不能采用这种形式定义变量
string strFileName = GetFileName();
break;
case FALSE:
......
break;
指定这种规则是为了避免出现代码跳过变量定义和初始化的情况。
我们分析这个规则存在的原因:一般如果定义了一个变量,此变量便从此定义点开始有效,直到所在的语句块结束。如果在两个case中间定义一个变量,那么对于定义变量的case标号的后面其他标号都可以使用这个变量了。但是如果switch从那些后续case标号开始执行呢?这是显而易见的,可能这个变量还没有定义就使用了,这是我们所不想见到的。为了实现在case中可以定义变量,我们可以引进语句块思想实现。在该语句块中定义变量,从而保证这个变量在使用前定义了和初始化。而出了这个语句块该变量就非法了。
case TRUE:
{
// OK: case标号中定义一个变量
string strFileName = GetFileName();
break;
}
case FALSE:
......
break;
最佳实践:在case语句块中,最好不要定义变量,所需的变量应在switch之前进行定义和初始化。如果必须定义变量,请谨慎而为。采用语句块方式实现内部变量的定义。
default标号提供了相当于else子句的功能,如果所有的case标号和switch表达式的值都不匹配,并且default标号存在。这是switch将执行default标号后面的语句。例如:
// 统计元音的总数及其他字符的个数
switch(chChar)
{
case 'a': case 'e': case 'i': case 'o': case 'u':
++iVovelCnt;
break;
default:
++iOtherCnt;
break;
}
最佳实践:对于那些哪怕没有语句在default标号下执行的环境中,定义default标号依然是有用的。定义default标号可明确的告诉读者,这种情况已经考虑到了,只是没有什么可以执行的。
注意:default标号不能单独存在,它必须位于语句之前。如果switch以default结束,而default分支没有什么任务需要执行,那么default标号后面必须添加一个空语句。
最后,我们讨论一下case标号,这里需要说明的是:除了default标号外,其他case标号必须是整型常量表达式。所以case标号不能是浮点数,变量。例如,下面这种使用形式就是错误的。
// 不正确的标号
case 3.14159: // 非整数
case iVal: // 非长整数
除此之外,还有一点需要注意的是任何两个case标号不能具有相同的值,这是为编译器所不容的。在编译时会导致编译错误。
请谨记
- 在使用switch-case结构时,在每个case标号分支语句最后,请务必添加一个break。如果在某些特殊情况下break不在需要了。建议你在此处添加注释进行说明。
- 除default标号外,每个case标号必须是一个常量表达式。且不能存在两个case标号相同的情形。