《C陷阱与缺陷》总结

    这本书不是对C语言的批判,而是列出了一些使用C语言常见的错误,以及一些看似简单的陷阱。对于提升对C语言的使用及认识有很大的作用!
   
    该书主要讲解了词法分析、语法分析以及语义细节问题。接着讲解了链接、预处理以及可移植性问题。讲解的都是基础的,容易出错的,都是C语言的基本问题。

    词法陷阱很常出现。
    1、赋值运算符=与逻辑运算符==的区别、按位运算符& |与逻辑运算符&& ||的区别。
    2、词法分析中的“贪心法”(尽量多匹配运算符)。
    3、运算符中不能出现空白, y = x/*p(/*的注释作用与除以取指针)。
    4、字符串与字符的区别(""和''),字符串最后的'\0'。
    5、另外,不同的C语言版本以及不同的编译器可能出现不同的结果,不能一概而论,对于移植性要求时要考虑各种情况。

    语法陷阱是常见的理解错误。
    1、函数指针在我们平时的C语言编程中较少使用,(*(void(*)())0)()表示将0转换为返回值为void的函数指针,然后调用该函数。 除了常见的类型,我们还要善于分析由组合而成的一些类型,比如void(*)(int),另外要善于用typedef来简化声明。
    2、当用到多个运算符时,运算符的优先级需要认真考虑,否则会出现意外的结果。单目运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符 > 条件运算符 > 逗号运算符。
    3、注意作为语句结束标志的分号,单独的分号是空语句,但会匹配上面的语句。
    4、switch语句中的break的使用,既是优势,又是一大弱点。
    5、避免“悬挂”else引发的问题。

    语义陷阱。
    1、数组与指针的关系要深入掌握,否则可能出现问题。a[i]和i[a]具有同样的含义,都是指针计算*(a+i)。 
    2、int b[3],取b[3]仍可能,注意越界的问题。
    3、int (*month)[31] month指向拥有31个整型元素的数组。
    4、C的字符串中注意最后的'\0'。
    5、数组做函数参数时,自动转换为相应的指针,不能通过数组名把数组的大小传过去。
    6、复制指针并不同时复制指针所指向的数据。
    7、由0转换而来的指针不等于任何有效的指针,对其操作的行为是未定义的,在不同的机器上会有不同的效果,应当注意。
    8、注意不对称边界的使用,左闭右开,在很多情况下可使计算简单,数组就采用左闭右开,while(--n >= 0) 将循环n次。
    9、求值顺序和运算符的优先级不一个概念,C语言中只有四个运算符(&&、||、?:和,)存在规定的求值顺序,逻辑运算符的短路求值对于程序的正常执行至关重要 if(y != 0 && x/y > t){} 。
    10、整数的溢出。
    11、main函数返回0代表程序执行成功,非0表示失败,main的返回值主要来告知操作系统,所以对于某些关注程序执行结果的系统,必须有正确的main函数返回值。

    连接。
    1、C语言由多个分别编译的部分组成,需要连接器把程序合并成一个整体。
    2、在连接时,如果多个文件存在同名的外部变量,则容易发生命名冲突,要掌握声明和定义的区别,学会用extern来引入其他文件定义的变量,static来声明一个变量的作用域限制在该文件中。
    3、形参、实参和返回值问题对不同版本的C标准可能不同,需要注意。
    4、可以用头文件来声明一些公用的外部变量。

    库函数。
    1、getchar()函数和getch()的区别,前一个函数的输入在命令框里回显,且会把键盘里的输入缓存起来一个一个得到,后一个函数不回显,常用语程序暂停,按键后继续。
    2、一个输入操作随后不能直接紧跟一个输出操作,反之亦然,即fread()后不能直接fwrite(),若想同时进行输入输出,必须在其中插入fseek函数的调用,fseek(fp, 0L, 1)将文件指针定位到当前。
    3、 程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入,setbuf函数可以用来设置缓冲区(setbuf(stdout, buf))。
    4、很多与操作系统有关的函数,执行失败时常会通过一个外部变量error来告知,可以来检测这个变量。
    5、所有C语言实现中都包括有signal库函数,作为捕获异常事件的一种方式,信号复杂棘手,要让signal函数尽可能简单。

    预处理器。
    1、不能忽视宏定义中的空格。
    2、宏不是函数,最好把宏定义的每个参数都用括号括起来,#define abs(x) (((x) >= 0) ? (x) : -(x)),另外即使宏定义中的各个参数与整个表达式都被括起来,也仍存在其他问题,比如说,一个操作数如果两次被用到,就会被求值两次biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]))。
    3、宏展开可能产生庞大的表达式,占用远超过编程者所期望的空间。
    4、宏并不是语句,以assert宏为例,如果我们这样定义 #define assert(e) if(!e) assert_error(__FILE__, __LINE__),则该句中的if容易与其它的else配对,加{}也会出现奇怪的行为,其实际定义类似于一个表达式 #define assert(e) ((void) ((e) || _assert_error(__FILE__, __LINE__)))。
    5、宏并不是类型定义,宏只是简单的展开,类型定义时typedef更通用一些。

    可移植性缺陷。
    1、由于C语言的实现不同,机器上的编译器不同,所以移植性是个很重要的问题,解决移植性问题需要作很多工作。
    2、C语言标准的变更。
    3、标示符名称的限制。
    4、整数的大小(用typedef来定义解决可移植性)。
    5、字符是有符号整数还是无符号整数。
    6、移位运算符的移出位的填充方式。 
    7、内存位置0,在所有C程序中,误用null指针的效果都是未定义的。
    8、除法运算时发生的截断,(-3)/2 等于-1余-1还是等于-2余1,随机数的大小,不同的机器可能不一样。     9、大小写的转换,函数和宏定义两种实现,可以在速度和方便之间自由选择。
    10、首先释放,然后重新分配,下面的代码是合法的:free(p); p=realloc(p, newsize); 并非所有的C实现在某块内存被释放后还能较长时间的保留之。
    11、可移植性问题的一个例子,若n<0,不能简单的令n=-n来得到正数,因为最小的负数-2^k没有对应的正数2^k,会发生溢出;有的机器上字符集中数字的顺序可能不是连续的,此时应用一张代表数字的字符表,下面是合法的, "0123456789"[4] 等于4,字符串常量相当于字符数组,可以替换数组名。

    建议。知道自己在做什么。
    1、不要说服自己相信“皇帝的新装”,while(c == '\t' || c = ' ' || c == '\n') {},很多人理所当然的认为其正确,发现不了这个错误。
    2、直截了当的表明意图,对可能出错的地方,在代码编写时做到实现预防,如上式写成while('\t' == c || ' ' == c || '\n' == c){}。    
    3、考察最简单的特例。
    4、使用不对称边界。
    5、注意潜在暗处的Bug。
    6、防御性编程。C编译器能够捕捉编译错误最好,任何C语言实现都无法捕捉到所有的程序错误。

    《C陷阱与缺陷》提出了C语言中很容易忽视的问题,对自己理解、编写C语言由很多的帮助。对编程意识的提升也有很大帮助。平时很少考虑C语言标准和编译器的问题,这在考虑移植性时必须考虑,不同的标准和编译器对同样的代码有很大的不同。另外就是一些细节的地方,比如n<0,直接n=-n并不好。


    编程是个麻烦、不断修改找错误完善的过程,要想写出高质量的代码,急不得!
    

    下面是书本提到的一些知识点的测试代码:

#include
#include
//#include "file.h" //extern char filename[];

int main()
{
char c[] = "hello";
// FILE *f = fopen("doc.txt", "r");
// while (c = ' ' || c == '\t' || c == '\n') //c = 1会一直循环,死循环不同编译器可能不一样,getc(f)如果跳出的话
// {
// c = getc(f);
// }

int x = 1;
int *p = &x;

int a = 9;
int b = 5;
//a+++++b; 错误 根据贪心法 解析为 a++ ++ +b,而a++的结果不能作为左值

int aa[] = {1,2,3,5,};
int i = 2;
int calendar[12][31];
int (*ap)[31]; //指向数组的指针
char ch[] = {'f', '4'}; //strlen(ch)的长度不定,因为\0存在的位置不定
char *r, *malloc();
char *s = "231";
  char *t = "tew";


char *p1 = 0;
char *p2;
//char cc = *p1;   define NULL ((void *)0)  0转换而来的指针不指向任何有效的地址


r = malloc(strlen(s) + strlen(t)); //应该+1,虽然这样结果也对
strcpy(r,s);
strcat(r,t);

//printf("%s\n", filename);
printf("%s\n", r); //231tew
printf("%d\n", strlen(r)); //6

printf("%d\n", strlen(ch)); //1251  不确定数

ap = calendar;


printf("%d %d",*ap,**ap); //不定的值,第一个为地址,第二个为int型

printf("%d %d\n",aa[i],i[aa]); //aa[i]和i[aa]都相当于aa+i,结果都对,不推荐用i[aa]
printf("%d\n", aa[4]);
aa[4] = 90;

printf("%d\n", 0.1); //输出的结果为 -12546234,毫无意义的结果
fprintf(stderr, "error\n");
printf("%%s\nfsd");
printf("%s\nfsd");
printf("%s\n", NULL);

x <<= *p; //x = x << *p

printf("\n"); //printf('\n')会报错


i = getchar();
printf("%d\n", i);
i = getchar();
printf("%d\n", i);
getch();
}













  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值