练习13:Switch语句

原文:Exercise 13: Switch Statement

译者:飞龙

在其它类似Ruby的语言中,switch语句可以处理任意类型的表达式。一些语言比如Python没有switch语句,因为带有布尔表达式的if语句可以做相同的事情。对于这些语言,switch语句比if语句更加灵活,然而内部的机制是一样的。

C中的switch语句与它们不同,实际上是一个“跳转表”。你只能够放置结果为整数的表达式,而不是一些随机的布尔表达式,这些整数用于计算从swicth顶部到匹配部分的跳转。下面有一段代码,我要分解它来让你理解“跳转表”的概念:

  1. #include <stdio.h>
  2. int main(int argc, char *argv[])
  3. {
  4. if(argc != 2) {
  5. printf("ERROR: You need one argument.\n");
  6. // this is how you abort a program
  7. return 1;
  8. }
  9. int i = 0;
  10. for(i = 0; argv[1][i] != '\0'; i++) {
  11. char letter = argv[1][i];
  12. switch(letter) {
  13. case 'a':
  14. case 'A':
  15. printf("%d: 'A'\n", i);
  16. break;
  17. case 'e':
  18. case 'E':
  19. printf("%d: 'E'\n", i);
  20. break;
  21. case 'i':
  22. case 'I':
  23. printf("%d: 'I'\n", i);
  24. break;
  25. case 'o':
  26. case 'O':
  27. printf("%d: 'O'\n", i);
  28. break;
  29. case 'u':
  30. case 'U':
  31. printf("%d: 'U'\n", i);
  32. break;
  33. case 'y':
  34. case 'Y':
  35. if(i > 2) {
  36. // it's only sometimes Y
  37. printf("%d: 'Y'\n", i);
  38. }
  39. break;
  40. default:
  41. printf("%d: %c is not a vowel\n", i, letter);
  42. }
  43. }
  44. return 0;
  45. }

在这个程序中我们接受了单一的命令行参数,并且用一种极其复杂的方式打印出所有原因,来向你演示switch语句。下面是swicth语句的工作原理:

  • 编译器会标记swicth语句的顶端,我们先把它记为地址Y。
  • 接着对switch中的表达式求值,产生一个数字。在上面的例子中,数字为argv[1]中字母的原始的ASCLL码。
  • 编译器也会把每个类似case 'A'case代码块翻译成这个程序中距离语句顶端的地址,所以case 'A'就在Y + 'A'处。
  • 接着计算是否Y+letter位于switch语句中,如果距离太远则会将其调整为Y+Default
  • 一旦计算出了地址,程序就会“跳”到代码的那个位置并继续执行。这就是一些case代码块中有break而另外一些没有的原因。
  • 如果输出了'a',那它就会跳到case 'a',它里面没有break语句,所以它会贯穿执行底下带有代码和breakcase 'A'
  • 最后它执行这段代码,执行break完全跳出switch语句块。

译者注:更常见的情况是,gcc会在空白处单独构建一张跳转表,各个偏移处存放对应的case语句的地址。Y不是switch语句的起始地址,而是这张表的起始地址。程序会跳转到*(Y + 'A')而不是Y + 'A'处。

这是对swicth语句工作原理的一个深究,然而实际操作中你只需要记住下面几条简单的原则:

  • 总是要包含一个default:分支,可以让你接住被忽略的输入。
  • 不要允许“贯穿”执行,除非你真的想这么做,这种情况下最好添加一个//fallthrough的注释。
  • 一定要先编写casebreak,再编写其中的代码。
  • 如果能够简化的话,用if语句代替。

你会看到什么

下面是我运行它的一个例子,也演示了传入命令行参数的不同方法:

  1. $ make ex13
  2. cc -Wall -g ex13.c -o ex13
  3. $ ./ex13
  4. ERROR: You need one argument.
  5. $
  6. $ ./ex13 Zed
  7. 0: Z is not a vowel
  8. 1: 'E'
  9. 2: d is not a vowel
  10. $
  11. $ ./ex13 Zed Shaw
  12. ERROR: You need one argument.
  13. $
  14. $ ./ex13 "Zed Shaw"
  15. 0: Z is not a vowel
  16. 1: 'E'
  17. 2: d is not a vowel
  18. 3: is not a vowel
  19. 4: S is not a vowel
  20. 5: h is not a vowel
  21. 6: 'A'
  22. 7: w is not a vowel
  23. $

记住在代码的开始有个if语句,当没有提供足够的参数时使用return 1返回。返回非0是你提示操作系统程序出错的办法。任何大于0的值都可以在脚本中测试,其它程序会由此知道发生了什么。

如何使它崩溃

破坏一个switch语句块太容易了。下面是一些方法,你可以挑一个来用:

  • 忘记写break,程序就会运行两个或多个代码块,这些都是你不想运行的。
  • 忘记写default,程序会在静默中忽略你所忘记的值。
  • 无意中将一些带有预料之外的值的变量放入switch中,比如带有奇怪的值的int
  • switch中是否未初始化的值。

你也可以使用一些别的方法使这个程序崩溃。试着看你能不能自己做到它。

附加题

  • 编写另一个程序,在字母上做算术运算将它们转换为小写,并且在switch中移除所有额外的大写字母。
  • 使用','(逗号)在for循环中初始化letter
  • 使用另一个for循环来让它处理你传入的所有命令行参数。
  • 将这个switch语句转为if语句,你更喜欢哪个呢?
  • 在“Y”的例子中,我在if代码块外面写了个break。这样会产生什么效果?如果把它移进if代码块,会发生什么?自己试着解答它,并证明你是正确的。