首页 > C/C++语言 > C/C++基本语法 > C 语言编程常见错误 2
2006
10-17

C 语言编程常见错误 2

    我对原文作了一定的删改,使之符合当今的 C 语言标准。如果原作者觉得不好的话,请跟帖告知,我将马上删除。谢谢!



1. = 不等于 ==


    从 Algol 派生出来的语言,如 Pascal 和 Ada,用 := 表示赋值而用 = 表示比较。而 C 语言则是用 = 表示赋值而用 == 表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。此外,C 可以多重赋值(如 a = b = c),并且可以将赋值嵌入到一个大的表达式或者语句中。这种便捷导致了一个潜在的问题:需要用比较的地方却写成了赋值。下面的语句看起来好像是要检查 x 是否等于 y :
        if ( x = y )
            foo();
而实际上是将 y 的值赋值给 x ,并检查结果是否非零。再看看下面的一个希望跳过空格、制表符和换行符的循环:
        while ( c == ‘ ‘ || c = ‘\t’ || c == ‘\n’ )
            c = getc(f);
在应该与 ‘\t’ 进行比较的地方程序员错误地使用了 =,而不是==。这个“比较”实际上是将’\t’ 赋给 c,然后判断 c 的(新的)值是否为零。因为 ‘\t’ 不为零,所以这个“比较”一直为真,因此这是一个死循环。
    一些编译器会对形如 e1 = e2 的条件给出一个警告以提醒用户。当你确实需要对一个变量进行赋值,然后再检查变量是否“非零”时,为了避免这种警告信息,应显式给出比较符。也就是将:
        if ( x = y )
            foo();
改写为:
        if ( ( x = y ) != 0 )
            foo();



2. 多字符符号


    一些 C 符号,如 /、* 或 =,只有一个字符。还有些 C 符号,如 /* 、 == 或标识符,具有多个字符。当编译器遇到紧连在一起的 / 和 * 时,它必须决定是将这两个字符识别为两个符号还是一个单独的符号。C 语言标准规定:“如果一个字符被识别为符号,则应该包含下一个字符看看包含此字符后构成的字符串是否仍然可以构成符号,如果可以则继续包含下一个字符,一直到不能构成符号为止。”。因此,如果 / 是符号的第一个字符,并且 / 后面紧随着一个 *,则这两个字符构成注释符开始标记。下面的语句看起来像是将 y 的值设置为 x 的值除以 p 所指向的值:
        y = x/*p    /* p 指向除数 */;
实际上,因为 /* 是注释符开始标记,因此编译器会简单地“吞噬”程序文本,直到 */ 出现为止。换句话说,这条语句仅仅把 y 的值设置为 x 的值,而根本没有看到 p。我们应该将这条语句改为:
        y = x / *p    /* p 指向除数 */;
或者:
        y = x / (*p)    /* p指向除数 */;



3. else 问题


    考虑下面的程序片断:
        if ( x == 0 )
            if ( y == 0 )
                error();
        else {
            z = x + y;
            f(&z);
        }
    写这段程序的程序员的目的明显是想将情况分为两种:x == 0 和x != 0。在第一种情况中,如果 y == 0,则调用 error()。第二种情况中,程序执行 z = x + y; 和 f(&z); 。
    然而, 这段程序的实际效果却大为不同。其原因是 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);
        }



4. 表达式求值顺序


    一些运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些则不是。例如下面的表达式:
        a < b && c < d
C 标准规定 a < b 首先被求值。如果 a 确实小于 b,c < d 必须紧接着被求值以计算整个表达式的真假性。但如果 a 大于或等于 b,则 c < d 根本不会被求值。而对 a < b 求值时,到底是先取 a 的值,还是先取 b 的值,标准并没有定义。
    C 中只有四个运算符(&&、||、?: 和 ,)指定了求值顺序。&& 和 || 最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而 ?: 运算符中的三个操作数中,先对最左边的进行求值,然后根据它的值决定到底应该求中间的操作数的值,还是求最右边的操作数的值。逗号运算符(,)的求值顺序为从左到右。
    C 中所有其它运算符的操作数的求值顺序都是未定义的。特别要说的是,赋值运算符也没有对求值顺序做出任何保证。
    出于这个原因,下面这种将数组 x 中的前 n 个元素复制到数组 y 中的方法是不可行的:
        j = 0;
        while ( j < n )
            y[j] = x[j++];
因为标准没有保证 y[j] 在 j 增长之前被求值。到底 y[j] 先求值,还是 x[j++] 先求值是依赖编译器的!所以我们不应该这么写!另一种方案基于同样的原因也不可行:
        j = 0;
        while ( j < n )
            y[j++] = x[j];
下面的代码才是正确的:
        j = 0;
        while ( j < n ) {
            y[j] = x[j];
            j++;
        }
当然,也可以这么写:
        for ( j = 0; j < n; j++ )
            y[j] = x[j];



5. &&、|| 和 ! 运算符


    C 规定 0 代表“假”,非零代表“真”。这些运算符返回 1 表示“真”而返回 0 表示“假”。&& 和 || 运算符如果可以通过左边的操作数确定整个表达式的真假性,就不会对右边的操作数进行求值。!10 返回 0,因为 10 非零;10 && 12 返回 1,因为 10 和 12 的值都不是 0;10 || 12 也是 1,因为 10 非零。这个表达式中的 12 不会被求值,因为左边的 10 就足够确定整个表达式为真。同理 :10 || f() 中的 f() 也不会被求值。



6. 下标从零开始


    C 语言中,一个具有 n 个元素的数组中没有下标为 n 的元素,元素的下标是从 0 到n-1。下面的程序可能会崩溃:
        int i, a[10];
        for ( i = 1; i <= 10; i++ )
            a = 0;
应该改成:
        int i, a[10];
        for ( i = 0; i < 10; i++ )
            a = 0;



7. getchar 函数的返回值为整型(int)


    请看以下程序:
        #include <stdio.h>


        int main( void )
        {
            char c;


            while ( ( c = getchar() ) != EOF )
                putchar(c);


            return 0;
        }


    这段代码存在一个小小的,但已经足以致命的错误:c 被声明为字符型(char)而不是整型。这意味着 c 可能不能正确接收 EOF,从而导致程序不能退出!正确的写法是:将 char 改成int。


留下一个回复