实用经验 37 switch-case的陷阱

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标号相同的情形。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值