多做之过就是指语言中存在某些不应该存在的特性
例如容易出错的switch语句,相邻字符串常量的自动连接,缺省全局作用域。
由于存在fall through, switch语句会带来麻烦
//1.
switch(表达式) {
case 常量表达式:零条或多条语句
default: 零条或多条语句
case 常量表达式:零条或多条语句
}
每个case结构由3个部分组成:关键字case;紧随其后的常量值或常量表达式;再紧接一个冒号。当表达式的值与case中的常量匹配时,该case后面的语句就会执行。default(如果有的话)可以出现在case列表的任何位置,它在其他的case均无法匹配时被选中执行。如果没有default,而且所有的case均不匹配,则整条switch语句便什么也不做。
/*
** default in switch statement.
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
int cond;
cond = 1;
switch( cond ){
default: printf( "condition is illegal!\n" ); break;
case 0: printf( "condition is equal to 0!\n" ); break;
case 2: printf( "condition is equal to 2.\n" ); break;
}
return EXIT_SUCCESS;
}
输出:
各个case和default的顺序可以是任意的,但习惯上总是把default放在最后。
一个遵循标准的C编译器至少允许一条switch语句中有257个case标签,这是为了允许switch满足一个8比特字符的所有情况(256个可能的值加上EOF)。
switch存在一些问题,其中之一就是它对case可能出现的值太过于放纵了。例如,可以在switch的左花括号之后可以声明一些变量,从而进行一些局部存储的分配。在最初的编译器里,这是一个技巧---绝大多数用于处理任何复合语句的代码都可以被复用,可以用于处理switch语句中由花括号包住的那部分代码。所以在这个位置上声明一些变量会被编译器很自然地接受,尽管在switch语句中为这些变量加上初始值没有什么用处,因为它绝不会被执行---语句从匹配表达式的case开始执行。 在C语言中,几乎从来不进行运行时错误检查---对进行解除引用操作的指针进行有效性检查大概是唯一的例外,而且在MS-DOS系统里甚至连这点很有限的检查都无法保证。
MS-DOS的运行时检查
在所有的虚拟内存体系结构中,一旦一个指针进行解除引用操作时所引用的内存地址超出了虚拟内存的地址空间,操作系统会中止这个进程。
MS-DOS并不支持虚拟内存,即使内存访问失败,它也无法立即捕捉到这种情况。MS-DOS可以在程序结束之后检测解除引用空指针的情况。具体方法是在进入程序前,保存内存地址为零时存储的内容。在程序结束时,系统检查这个地址与原先的是否相同。如果不同,基本可以肯定你的程序使用了空指针来访问内存,运行时系统会打印一条“null pointer assignment"(空指针赋值)信息。
运行时检查与C语言的设计理念相违背。按照C语言的理念,程序员应该知道自己正在干什么,而且保证自己的所作所为是正确的。
//2.
小启发
需要一些临时变量吗?把它放在块的开始处。在C语言中,当建立一个块时,一般总是这样开始的:
{
语句
你总是可以在两者之间增加一些声明,如:
{
声明
语句
当分配动态内存代价较高时,你可能会采用这种局部存储的方法,但有可能的话要尽量避免。编译器可以自由地忽略它,它可以通过函数调用来分配所有局部块需要的内存空间。
另一种用法是声明一些完全局部于当前块的变量。
声明一些完全局部于当前块的变量。
if (a > b)
/*交换a, b*/
{
int temp = a;
a = b;
b = temp;
}
//C++在这方面又进了一步,允许语句和声明以任意的顺序交叉出现,甚至允许变量的声明出现在for表达式的内部。
for (int i = 0; i < 100; i++) {
...
}
switch的另一个问题是它内部的任何语句都可以加上标签,并在执行时跳转到那里,这就有可能破坏程序流的结构化。
switch(i) {
case 5 + 3: do_again;
case 2: printf("I loop unremittingly\n"); goto do_again;
default: i++;
case 3: ;
}
//3.
顺便提一句,在C语言中,const关键字并不真正表示常量。
const int two = 2;
switch(i) {
case 1: printf("case 1\n");
case two: printf("case 2\n");
//**error** ^^^integral constant expression expected
case 3: printf("case 3\n");
default: ;
}
/*
** const_var_2.c
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
const int two = 2;
int i;
i = 2;
switch(i) {
case 1: printf("case 1\n");
/*
** case two: printf("case 2\n");
** can't pass compilation:
** because of [Error] case label does not reduce to an integer constant.
*/
//**error** ^^^integral constant expression expected?
case 3: printf("case 3\n");
default: ;
}
return EXIT_SUCCESS;
}
输出:
switch语句最大的缺点是它不会在每个case标签后面的语句执行完毕后自动中止。一旦执行某个case语句,程序将会执行后面所有的case,除非遇到break语句。
"fall through"意思是:如果case语句后面不加break,就依次执行下去,以满足某些特殊情况的要求。 实际上,这是一个非常不好的特性,因为几乎所有的case都需要以break结尾。大部分lint程序在发现“fall through”情况时会发出警告信息。
switch(2) {
case 1: printf("case 1\n");
case 2: printf("case 2\n");
case 3: printf("case 3\n");
case 4: printf("case 4\n");
default: printf("default\n");
}
软件信条
缺省采用“fall through”,在97%的情况下都是错误的
在编译可能具有一个或两个操作数的操作符时:
switch(operator->num_of_operands) {
case 2: process_operand(operator->operand_2);
/*fall through*/
case 1: process_operand(operator0->operand_1);
break;
}
//4.
switch的另一个问题---break中断了什么
它证明了在C语言中,人们太容易低估break语句对控制结构的影响。
network code() {
switch(line) {
case THING1:
doit1();
break;
case THING2:
if (x == STUFF) {
do_first_stuff();
if (y == OTHER_STUFF) {
break;
}
do_latter_stuff();
} /*代码的意图是跳到这里...*/
initialize_modes_pointer();
break;
default:
processing();
} /*...但事实上跳到了这里*/
use_modes_pointer(); /*致使modes_pointer未初始化*/
}
break语句事实上跳出最近的那层循环或switch语句。
//5.
粉笔也成了可用的硬件
ANSI引入的另一个新特性是“相邻的字符串常量将被自动合并成一个字符串。
这就省掉了过去在书写多行信息时必须在行末加“\”的做法,后续的字符串可以出现在每行的开头。
/*旧风格*/
printf("A favorite children's book \
is 'muffy Gets It: the hilarious table of a cat, \
a boy, and his machine gun");
现在可以用一连串相邻的字符串常量来代替它,它们会在编译时自动合并。除了最后一个字符串外,其余每个字符串末尾的'\0'字符会被自动删除。
/*新风格*/
printf("A favorite children's book "
"is 'muffy Gets It: the hilarious table of a cat, "
"a boy, and his machine gun");
然而,这种合并意味着字符串在初始化时,如果不小心漏掉了一个逗号,编译器将不会发出错误信息,而是悄无声息地把两个字符串合并在一起。
char *available_resources[] = {
"color monitor",
"big disk",
"Cray" /*哇!少了个逗号*/
"on-line drawing routhiness",
"mouse",
"keyboard",
"power cables", /*这个多余的逗号会引起什么问题吗?*/
};
顺便提一句,最后那个字符末尾的逗号并不是打字错误,而是从最早的C语法中继承下来的东西,不管存在与否都没有什么意义。ANSI C Rationale对它进行了辩护,称它使C语言在自动生成(automated generation)时更容易一些。我想,这种拖尾巴如果在其他由逗号分隔的列表(如枚举声明、单行多变量声明等)中也允许使用,那还说得过去,可惜事实并非如此。
//6.
小启发
使一段代码在第一次执行时的行为与以后执行时不同
这种方法能使分支和条件测试减少到最小程度。
void generate_initializer(char *string)
{
static char separator = ' ';
printf("%c %s\n", separator, string);
separator = ',';
}
//7.
太多的缺省可见性
定义C函数时,在缺省情况下函数的名字全局可见的,跟加一个冗余的extern关键字效果是一样的。这个函数对于链接到它所在的目标文件的任何东西都是可见的。如果想限制对这个函数的访问,就必须加static关键字。
function apple(); {/*在任何地方均可见*/}
extern function pear(); {/*在任何地方均可见*/}
static function trunip() {
/*在这个文件之外不可见*/
}
根据实际经验,这种缺省的全局可见性多次证明是个错误,这已是盖棺定论。软件对象在大多数情况下应该缺省地采用有限可见性。当程序员需要让它全局可见时,应该采用显式的手段。
这种太大范围的全局可见性会与C语言的另一个特性相互产生影响,那就是interpositioning。
interpositioning就是用户编写和库函数同名的函数并取而代之的行为。
作用域过宽的问题常见于库中:一个库需要让一个对象在另一个库中可见。唯一的办法是让它变得全局可见。但这样一来,它对于链接到该库的所有对象都是可见的了。这就是all_or_nothing---一个符号要么全局可见,要么对其他文件都不可见。
Pascal中那样在一个函数内部嵌套另一个函数的定义,Ada和Modula-2就是在各个程序单元中明确说明那些符号是引入的,那些是引出的。