《C陷阱与缺陷》读书笔记

第一章、词法“陷阱”
编译器中负责将程序分解成为一个一个符号的部分,一般称为词法分析器。
1.1 =不同与==
在C语言中,=是赋值符号;而==则表示比较符,也就是等于的意思。虽然这两种方式使用起来蛮方便,但是有时候也会不小心,将==写成了=,比如:

0_1524560480065_QQ图片20180424170049.png
这段代码并不能按照我们想的那样输出 hello world,因为小手一抖,少了个=号,最终表达式的值为0,按照C语言的语法规范,这样是完全没问题,不过if的条件一直为假罢了。
对于等于比较符,这里是有一个小技巧的,就是当条件是一个变量和一个常量或者定值的时候,我们最好采用下面这种写法:
0_1524561376810_QQ图片20180424171540.png

即,把常量写在左边,这样写就算不小心少写了个=符号,那编译器还是会告诉你的因为你将值赋给一个常量了,然后自己改下语法就可以,不会造成没必要的逻辑错误。

1.2 &和|不同于&&和||
&和|两者是对位进行运算的运算符,一般用来操作二进制数,而&&和||则属于逻辑运算符,用来判断多个表达式的真假关系。在使用||运算符的时候,我们需要注意,当 || 左边的表达式为真时,那么右边的表达式将不再执行,如下图:
0_1524562398103_QQ图片20180424173116.png
这段代码的输出结果为:2,即没有执行b=1这这个表达式。
1.3 词法分析中的“贪心法”
每个符号应该尽可能包含多的字符,从左到右读入字符,直到字符组成的字符串不能组成一个有意义的符号为止。需要注意的是,除了字符串和字符常量,符号中间不能嵌有空白(空格符、制表符和换行符)。对于多个符号一起连着出现的情况,最好用括号表明自己想要表达的意思,提高代码的可读性,没必要去刻意的使用运算符的优先级。
1.4 整形常量
八进制数是0开头,十六进制数以0x开头,十进制数是1~9开头。
1.5 字符与字符串
用单引号引起的字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。
用双引号引起的字符串,代表的是一个指向无名数组的起始字符的指针。
由于一个代表整数,一个代表指针,故二者不能混用。

第二章、语法“缺陷”

2.1 理解函数声明
任何C变量声明都由两部分组成:类型以及一组类似表达式的声明符。
float pf;
这个声明的含义是
pf是一个指向了浮点变量,也就是说,pf是一个指向浮点数的指针。
float g(), (h)();
表示
g()与(h)()是浮点表达式。因为()的优先级高于g()也就是(g()):g是一个函数,该函数的返回值类型为指向浮点类型的指针。h是一个函数指针,h所指向函数的返回值为浮点类型。我们一般也称
g()是一个指针函数,而h则叫做函数指针。
某种类型的类型转换符:只需要把声明中的变量名声明末尾的分号去掉,再将剩余的部分用一个括号整个“封住”起来。
float (h)(); 的类型转换符为:(float ( ) ()),表示一个“指向返回值为浮点类型的函数的指针”

2.2 运算符的优先级问题

2.3 注意语句结束标志的分号

结构体遗漏了表示结束的分号,因此,上图代码段的实际效果是声明函数main()的返回值的为logrec类型。
2.4 switch语句
若需要结束switch代码块,不要忘了break语句。
2.5 函数调用
f(); //是一个函数调用语句。
f; //这个语句只是计算f函数的地址,并不调用该函数。
2.6 “悬挂”else引发的问题

这段代码的本意是else与第一个if语句配对,但实际是,else是与它最近的if配对,也就是,与if(0 == y) 这个if配对了,解决办法就是用大括号把if(0 == y) 这条语句包起来。

第三章 语义“缺陷”

3.1 指针与数组
C语言的数组有两个值得注意的地方:
1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。C语言中数组的元素可以是任何类型的对象,当然也可以是另外的一个数组。即,所谓的多维数组就可以理解是数组的元素是另一个一维数组。
2.操作数组的方式:确定数组的大小,以及获得指向该数组下标为0的元素的指针。其余的操作都是通过指针来完成的,即,数组下标的运算都等同于一个对应的指针运算。
给一个指针加上一个整数,与给一个指针的二进制加上同样的整数,两者的含义完全不同。如果ip指向一个整数,那么ip+1指向的是内存中的下一个整数,而的二进制加1表示的内存中的下一个地址单元(这个内存单元中的值并不一定是一个整数)。
int a[3];
int p;
p = a; //把数组a中下标为0的元素的地址赋值给p
// p = &a; //这句表示把一个指向数组的指针的值赋值给p,而p是指向int类型的指针,故不可以。
除了a被用作运算符sizeof的参数这种情况,其它所有情形下数组名a都代表指向数组a中下标为0的元素的指针。可以用a+1访问数组的第二个元素,但不能用a++访问,因为a是一个常量。
3.2 非数组的指针
字符串常量代表了一块包括字符串中所有字符以及一个空字符('\0')的内存区域的地址。比如:
“abc” + 1; 这个语句的结果是返回一个指向“b”的指针。
3.3 作为参数的数组声明
C语言中会自动的将作为参数的数组声明转换为相应的指针声明。
int strlen(char
 s) {
/具体内容/
}
3.4 避免“举隅法”
char p;
p = "xyz";
我们需要注意p和
p的区别,p的值是字符串起始元素的指针,*p表示指针所指的变量值。
3.5 空指针并非空字符串
当常数0被转换为指针使用时,这个指针绝对不能被解除引用(解除引用:取指针所指内容的值)。
3.6 边界计算与不对称边界

这段代码会陷入死循环,并不会报我们以为的数组越界的错误,因为a[i]j就是指针操作。导致并不存在的a[10]被设置为0,也就是内存中在数组a之后的一个字的内存被设置为0。
用第一个入界点和第一个出界点来表示一个数的范围,用这种方式避免“栏杆错误”。比如:把 x>=16 且 x<=37,写成x>=16 且 x<38。
3.7 求值顺序
C语言中有四个运算符(&&、||、?: 和 , )存在所谓的“短路”原则。
3.9 整数溢出
在无符号的算术运算中,没有所谓的“溢出”一说。
算术运算符中一个操作数是有符号数,另一个是无符号数,那有符号数会被转换为无符号数,“溢出”就不会发生了。
两个有符号数做算术运算时就可能发生“溢出”。

第四章 连接
4.1 什么是连接器
C语言的一个重要思想就是将多个源程序文件分别单独编译,然后再经过连接这一步将多个文件连接一起。
连接器把由编译器或者汇编器生成的若干个目标模块。整合成一个被称为载入模块或可执行文件的实体,这个实体可被操作系统执行。
连接器通常把目标模块看成一组外部对象,这里的外部对象是指目标模块中的函数和外部变量(函数外定义的变量),若有外部对象的引用,连接器还需要解析引用。

4.2 声明和定义
extern int a;
这句话不是对a的定义,只是声明a是一个外部整形变量。它说明了a的存储空间是在其他地方分配的,即a变量在其它地方中定义的,而不是本语句。这里的a只是对外部对象的显示引用。
4.3 命名冲突与static修饰符
static关键字是将变量或函数的作用域限制在一个源文件内,对其他源文件不可见。利用static关键字可以在不同文件中定义相同名称的变量而不产生命名冲突。
4.4 形参,实参与返回值
如果函数的调用与声明不在同一文件中,那我们需要在调用它的文件中声明该函数。
个人觉得最好在函数声明时指定函数的形参类型,不要利用什么自动转换参数而不写类型。

正常情况下,这个程序应该是输出的是:0 1 2 3 4
实际上,这个程序并不一定得到上面的结果。在某些编译器上,它的输出是:0 0 0 0 0 1 2 3 4
问题在于,这里的c被声明为char类型。当程序要求scanf读入一个整数,应该传递给它一个指向整数的指针,但实际是得到的一个指向char的指针,程序也会将这个指向字符的指针作为指向整数的指针而接受,并在指针指向的内存空间中存储一个整数。而整数所占的存储空间要大于字符所占的存储空间,所以字符c附近内将被覆盖。在某些编译器下,每次读一个值到c,都会将i的低端部分覆盖为0,有i的高端部分本就是0,即i被重新置0了。
4.5 检查外部类型
不能将同一个外部变量名在两个不同的文件中被声明为不同的类型。如果将同一个变量名声明为不同的数据类型,可能发生很多意想不到的情况。当然,如果编译器足够好的话,那么编译器就会报告出这个情况。
假如,一个文件中有如下语句:
char name[] = "zxcvb";
另一个文件中包含声明:
extern char* name;
以上两个语句的变量类型并不相同,其中第一个语句的name的类型是“字符数组”,第二个声明中的name是“字符指针”。这两个name使用存储空间的方式不同。

4.6 头文件
避免命名冲突的一个好方法是:每个外部对象只在一个地方声明。利用一个单独的文件专门用来包括所有的外部对象。而且定义这个外部对象的模块也应该包含这个头文件。

第五章 库函数
5.1 返回整数的getchar()函数
char c = getchar();
用getchar()函数进行字符的输入,并不是每输入一个字符马上将字符的值赋给c,而是先送至“输入缓冲区”,遇到回车键时结束输入。输入缓冲区是一个字符的队列,其中存储了所有你尚未读取的字符。每次调用getchar函数,它就会从输入缓冲区中读出第一个字符,并把这个字符从输入缓冲区中清除。
5.2 更新顺序文件
对一个文件同时进行文件的读(fread())和写(fwrite())操作时,我们需要在两个函数之间插入fseek()函数。为什么要这样做,我们可以这样理解,首先,我们需要知道,程序是不会直接操作磁盘上的文件的,每次都是先将磁盘的内容读到缓冲区,然后程序从缓冲区取或写数据。在缓冲区内部有一个光标,用来记录程序当前读写操作的位置,每次写fwrite()完以后光标就在此处写的结束位置处。接着直接调用fread()函数的话,那么就会从光标处继续往下读,并不能读取到刚才写入的数据。解决办法就是在fwrite()和fread()函数之间插入fseek()函数,改变光标的位置(在有些编译器中,fseek()函数还有刷新缓存的作用)。
5.3 缓冲输出与内存分配
setbuf(stdout, buf);
语句将会把所有要写入到stdout文件的内容都先用缓冲区buf缓存,直到buf填满或显示的调用fflush(),这样缓冲区的内容才真正的写到了stdout中。如果直到程序结束也没有调用fflush,那很有可能出现作为缓冲区的buf被释放了,可是还没有刷新buf的内容,结果当系统自己去刷新的时候,此时缓冲区的内容已空。打印信息将于自己期望不符。
当程序这样编码时,输出结果将会乱码。因为buf数组在main函数结束后就释放了,然后系统才会将buf内存区域的内容刷新至stdout,并不能得到需要的内容。解决办法,将buf的声明移到main函数外面或者将buf定义为static类型。当然也可以在函数结束前,显示的刷新。
5.4 使用errno检测错误
库函数调用成功时,不一定需要强制将errno清零,当然也可以对errno的值进行重新设置
在调用库函数时,首先检测错误的返回值,确定程序是否执行失败。然后检查errno,搞清楚出错原因。
5.5 库函数signal
我们先了解下,同步和异步。同步:主动请求后,需要自己每隔一定的时间去轮询操作结果,检查操作结果是否就绪。异步:主动请求后便去做自己的事情,当请求的事情操作完成后,被请求的对象主动通知发出请求的对象,并返回操作结果。
而signal函数的作用就是用来捕获异步事件的结果。

第六章 预处理器
程序如果要用到常数,我们最好是用宏定义的变量替代程序中直接使用常数。
需要注意的是,宏只是简单的文本替换,不同于函数调用。
6.1 宏定义中的空格
宏定义标识符和内容之间通过空格区分。
6.2 宏不是函数
当程序遇到宏的标识符时,只是简单的文本替换。特别注意有参数传入的场景。所以定义时,最好把宏的各个参数和整个宏的表达式结果都用括号括起来。我们还需要注意的是,宏展开可能产生非常庞大的表达式,占用空间会比较大。
6.3 宏不是语句
在使用宏的时候要考虑清楚,是否需要加分号或者大括号这些。列如关于if与else的配对问题,如果宏展开后有if等语句,需要注意它们的配对问题。有时候可以把宏定义成一个类似表达式形式的,不仅仅只想到把宏定义成类似语句形式的。
6.4 宏不是类型定义
可以利用宏来实现多个变量的类型定义,还是一样的,宏改动代码时比较方便。
我们也可以利用语句:typedef struct foo FOOTYPE;
这个语句定义了FOOTYPE为一个新的类型,与 struct foo类型完全一样。

第七章 可移植性缺陷
一个好的编程习惯就是,将所有的外部变量和宏定义的变量标识符都大写。
7.3 整数的大小
0_1525007442105_a3fdbc49-390d-46c0-91ba-d9dd3a88aa98-image.png
7.4 有符号数与无符号数
将char类型转换到int类型时,编译器会同时复制符号位。
7.5 移位运算
移位运算有算术移位与逻辑移位。而逻辑左移与算术左移具有相同的效果,都是在低位空出位置补0。算术右移:无符号数空出的高位补0,有符号数空出的位用符号位补充。逻辑右移:不管是有无符号的数,空出的位都用0补充。
7.8 随机数
我们利用rand函数及随机数种子产生的随机数都称为伪随机数,这些随机数都是根据设计的算法所产生。因为计算机是没办法产生真正意义上的随机数的。
7.10 内存的释放,分配问题
malloc(n); //返回一个指向一块新分配的可以容纳n个字符的内存块的指针
free(p); //将malloc返回的指针作为参数传入,可以释放上面分配的那块内存
realloc(p,newsize); //调整(扩大或缩小)已分配的内存块为新的指定大小
7.11 可移植性问题的列子
0_1525010959708_158daf88-9966-4b92-bde6-aa84f7a33f0d-image.png
当试着把一个负整数转换为正整数时,需要注意溢出问题,因为对于有符号数取值范围来说,负数的最小值的绝对值比正的最大值的绝对值大1。所以可能溢出。解决办法可以是,把负数转换为unsigned long类似这样的类型,不能简单的对-n操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值