在紧挨着f的第一个}后面丢失了一个分号。它的效果是声明了一个函数f,返回值类型是struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则f将被定义为具有默认的整型返回值[5]。
2.4 switch语句
通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断:
switch(color) { case 1: printf (\ break;
case 2: printf (\ break; case 3: printf (\ break; }
case color of 1: write ('red'); 2: write ('yellow'); 3: write ('blue'); end
这两个程序片段都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片段非常相似,只有一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。
看看另一种形式,假设C程序段看起来更像Pascal:
switch(color) { case 1: printf (\case 2: printf (\case 3: printf (\}
并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。
这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可以简化其他一些特殊的处理。
例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句:
11
case SUBTRACT: opnd2 = -opnd2; /* no break; */ case ADD: ...
另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外:
case '\\n': linecount++; /* no break */ case '\\t': case ' ': ...
2.5 函数调用
和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数, f();
就是对该函数进行调用的语句,而 f;
什么也不做。它会作为函数地址被求值,但不会调用它[6]。
2.6 悬挂else问题
在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。
考虑下面的程序片断:
if(x == 0)
if(y == 0) error(); else {
z = x + y; f(&z); }
写这段程序的程序员的目的明显是将情况分为两种:x = 0和x != 0。在第一种情况中,程序段什么都不做,除非y = 0时调用error()。第二种情况中,程序设置z = x + y并以z的地址作为参数调用f()。
12
然而, 这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写:
if(x == 0) { if(y == 0) error(); else {
z = x + y; f(&z); } }
换句话说,当x != 0发生时什么也不做。如果要达到第一个例子的效果,应该写:
if(x == 0) { if(y ==0) error(); } else { z = z + y; f(&z); } 3 连接
一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为连接器、连接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。
在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。
3.1 你必须自己检查外部类型
假设你有一个C程序,被划分为两个文件。其中一个包含如下声明: int n;
而令一个包含如下声明: long n;
这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由连接器(或一些工具程序如lint)来完成;如果操作系统的连接器不能识别数据类型,C编译器也没法过多地强制它。
13
那么,这个程序运行时实际会发生什么?这有很多可能性:
实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。 你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。 n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。
n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。
这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明:
char filename[] = \
而另一个文件包含这样的声明:
char *filename;
尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(NULL)([译注]实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险的!)。
这两个声明以不同的方式使用存储区,它们不可能共存。
避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。
避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次[7]。
4 语义缺陷
一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。
我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的C实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到第7节讨论可以执行问题为止。
4.1 表达式求值顺序
一些C运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式:
14
a < b && c < d
C语言定义规定a < b首先被求值。如果a确实小于b,c < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。
要对a < b求值,编译器对a和b的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。
C中只有四个运算符&&、||、?:和,指定了求值顺序。&&和||最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而?:运算符中的三个操作数:a、b和c,最先对a进行求值,之后仅对b或c中的一个进行求值,这取决于a的值。,运算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值[8]。
C中所有其它的运算符对操作数的求值顺序都是未定义的。事实上,赋值运算符不对求值顺序做出任何保证。
出于这个原因,下面这种将数组x中的前n个元素复制到数组y中的方法是不可行的: i = 0; while(i < n) y[i] = x[i++];
其中的问题是y[i]的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能。另一种情况出于同样的原因会失败: i = 0; while(i < n) y[i++] = x[i];
而下面的代码是可以工作的: i = 0; while(i < n) { y[i] = x[i]; i++; }
当然,这可以简写为:
for(i = 0; i < n; i++) y[i] = x[i];
4.2 &&、||和!运算符
C中有两种逻辑运算符,在某些情况下是可以交换的:按位运算符&、|和~,以及逻辑运算符&&、||和!。一个程序员如果用某一类运算符替换相应的另一类运算符会得到某些奇怪的效果:程序可能会正确地
15
百度搜索“77cn”或“免费范文网”即可找到本站免费阅读全部范文。收藏本站方便下次阅读,免费范文网,提供经典小说综合文库C语言缺陷与陷阱(笔记)(3)在线全文阅读。
相关推荐: