C专家编程——这不是Bug,而是语言特性

时间:2023-02-11 20:04:31
Bug是迄今为止地球上最庞大最成功的试题类型,有近百万种已知的品种,在这个方面,他比其他任何已知的生物种类的综合还要多,而且至少要多出4倍。
在C语言中,若遇到了malloc(strlen(str)),几乎可以断定它是错误的,而malloc(strlen(str)+1)才是正确的。
编程语言缺陷归为三类:不该做的做了,该做的没做,该做的做的不合适。简称为“多做之过”,“少做之过”,“误做之过”。


多做之过——语言中存在的不该存在的特性,包括switch语句,相邻字符串常量的自动连接和缺省全局作用域。
switch有三个缺陷,这些缺陷可以用下列程序来观察:
1、对case可能出现的值太过放纵——语句从匹配表达式的case开始执行。
2、它内部的任何语句都可以加上标签
3、每个case标签后面的语句执行完毕后不会自动终止。——这称为fall through
本着改进未来编程语言的探索精神, 让我们对C语言进行望闻问切,详细记录病例。


#include <stdio.h>


void switch_fall_through(int);
void switch_label(int);


int main()
{
    switch_fall_through(2);
    printf("分隔行\n");
    switch_fall_through(4);
    switch_label(2);
    return 0;
}


void switch_fall_through(int n)
{
    int a = 0;
    switch(n)
    {
        int i = 9;
        a = 9;
    case 1:printf("1 a=%d\n", a);
    case 2:printf("2 a=%d\n", a);
    case 3:printf("3 a=%d\n", a);
    default:printf("default a=%d\n", a);
    case 4:printf("4 a=%d\n", a);
    }
}


void switch_label(int n)
{   
    int i = 1;
    switch(n)
    {
    case 1+1:do_again:
    case 3:printf("LOOP %d\n", i);
        i++;
        if (i == 5)
            break;
        goto do_again;
    case 4:printf("4 \n");break;
    default:printf("default\n");break;
    }
}

     
        
root@duyuqi-OptiPlex-380:~/workspace/ExpertC/chap02# make
gcc -c switch.c
gcc -o switch switch.o
root@duyuqi-OptiPlex-380:~/workspace/ExpertC/chap02# ./switch 
2 a=0
3 a=0
default a=0
4 a=0
分隔行
4 a=0
LOOP 1
LOOP 2
LOOP 3
LOOP 4




ANSI C引入的另一个特性是相邻字符串常量将被自动合并成一个字符串。新旧对比便知端的。
旧的风格:
printf("Hello \
World\n");
新的风格:
printf("Hello "
"World\n");


function apple(){/*默认,在任何地方缺省可见*/}
extern function pear(){/*在任何地方缺省可见,extern关键字是冗余的*/}//
static function turnip(){/*static限制对该函数的访问,在这个文件之外不可见*/}
    几乎没人在函数名前添加存储类型说明的习惯,因此,多数函数都是全局可见的。
    经验表明,缺省的全局可见性被证明是有害的。软件对象在大多数情况下应该缺省的采用有限可见性。
    C语言的大范围全局可见性与其另一个特性interpositioning相互影响。
interpostioning是用户编写和库函数同名的函数并取而代之的行为。




误做之过
C语言存在的一个问题是,它过于简洁,以致语言中有误导性质或不适当的特性。
C语言中的符号重载(我个人认为称为“重载”不够好),就是根据上下文环境可能符号所表达的意义不一样。
eg:static在函数内修饰变量表示变量保持延续,修饰函数表明函数只对本文件可见
另外形如这样的符号还有:extern,void,*,&,<,()等。
重载存在的问题,有两个费解的语句:
p = N * sizeof * q;//q前面的操作符是解除引用操作符,sizeof操作符前面的*是乘号
apple = sizeof (int) *p;//*号是乘号,良好的习惯应写成p * sizeof(int)
要使程序具有可读性,必须对此有所了解,养成良好习惯并加以避免。
#include <stdio.h>

int main()
{
    int p, a = 2, N = 3;
    int *q = &a;
    p = N * sizeof * q;
    int apple = sizeof(int) * p;
    printf("p=%d,apple=%d\n", p, apple);
    return 0;
}

root@duyuqi-OptiPlex-380:~/workspace/ExpertC/chap02# ./symbol 
p=12,apple=48

注:在表达式中如果有布尔操作、算术运算、位操作等混合计算,你应该始终在适当的地方加上括号,是指清楚明了。
大部分编程语言并未明确规定操作数计算的顺序,因此写程序是要依赖意群计算的先后次序,就不是良好的编程风格。例如:x=f()+g()*h();就不是良好风格,
f,g,h函数的执行顺序是不一定的,只有+和*的顺序是一定的。
 

C语言中符号不正常优先级,Dennis Ritchie说,这是历史的偶然而产生的:
.高于*:*p.f <=> *(p.f)
[]高于*:int *ap[] <=> int *(ap[]) ap是指针数组
()高于*: int *fp() <=> int *(fp()) fp是函数
==和!=高于位操作符:val & mask != 0 <=> val & (mask != 0)
==和!=高于赋值符:c=getchar() != EOF <=> c=(getchar() != EOF)
算术运算符高于移位运算符:msb << 4 + lsb <=> msb << (4+lsb)
逗号运算符在所有运算符中优先级最低:i=1,2 <=> (i=1),2


拖尾巴的逗号:int a[] = {1, 2, 3, 4,} 最后的逗号不是打字错误,而是从最早的C语法中继承下来的东西,存在与否都没什么意义。


操作符的结合性:在几个操作符具有相同优先级时决定先执行哪一个。位操作符(例如|和&)具有左结合性
int a, b=1, c=2;
a = b = c;//所有赋值符(包括复合赋值符)都具有右结合性,a的值为2


隐藏于C语言中的问题
历史上一些库函数存在的问题例如,gets()现在使用fgets()代替,等等。



少做之过——空格所引起的麻烦
1、转义:例如,\n 与 \ n完全不一样,老式的字符串常量连接
2、空格弃之不用而引起的麻烦:ANSI C的最大一口策略:如果下一个标记有超过一种的解释方案,编译器将选取能组成最长字符序列的方案。
例如:z = y+++x;等价于z=y++ + x;
再例如:z = y+++++x;等价于z = y++ ++ + x;编译器将显示错误。空格解决:z = y++ + ++x;
3、ratio= *x/*y; /*将被解释成注释的上半部分,从而引起问题。空格解决:ratio= *x/ *y;

#include <stdio.h>

int main()
{
    int z, x = 2, y = 3;
    z = y+++x;
    printf("x=%d,y=%d,z=%d\n", x, y, z);
    z = y++ + ++x; 
    printf("x=%d,y=%d,z=%d\n", x, y, z);
    return 0;
}

root@duyuqi-OptiPlex-380:~/workspace/ExpertC/chap02# ./blank 
x=2,y=4,z=5
x=3,y=5,z=7


    lint程序,在Unix上早期的C语言,语言设计者做了一个明确的决定,把编译器中所有的语义检查措施都分离出来。错误检查由一个单独的程序完成,
这个程序被称为lint。把lint程序分离出来,这是个大失误,虽然使得编译器更小、目标更为转移,但是其代价是:代码中悄然混进了大量的Bug和不可靠的编码风格。
最好的编码原则是,使程序始终为lint_clean(能顺利通过lint程序的检查)。这样,可以去除现有Bug,减少将来出现Bug的可能性。
现在lint程序加入到编译器中是一大趋势,经验证明,把lint程序作为独立的工具通常意味着把lint程序束之高阁。