大家不要只收藏不关注呀,哪怕只是点个赞也可以呀!🤣
之前让大家发邮箱,好多人因为隐私问题不好意思发,为了大家方便,我把笔记放到了同名公众号里,大家可以自取。
输入软考笔记即可自取
另外附赠大家免费的小程序,输入小程序即可弹出免费的微信小程序,祝大家考试顺利。
最近读了一本C语言书《C陷阱与缺陷》,还不错,挺适合刚刚工作后的人,特此分享读书笔记,写代码时应注意这些问题,笔记已做精简,读完大概需要30min。如果读起来感觉很吃力,那我推荐你读一读书,书上的例子比较多,看着能轻松一点,老鸟看起来应该不是很难,作为复习一下,应该还不错,如果需要pdf文档,请私信我,我发给你,这本书今天读完了,说一下感受,如果只是入门的话,读完感受并不强烈,如果在工作了一段时间后,就会发现书中的错误确实是新手容易犯而且一些之前不理解的地方确实有了更新的认识,值得一读。
一、词法陷阱
1、= 不同于 ==
将比较值放在左边,变量放到右边,例如if(NULL==p->s)
2、&和|不同于&&和||
按位运算符 &(相同1才取1) 和 |(有一个1便为1) ;逻辑运算符 &&(and)和|| (or) ;
0111 1101 & 1111 0000 =0111 0000 ;0111 1101 |1111 0000 = 1111 1101
If(a>10&&b10) if(a>10 || b10)
3、单字符符号与多字符符号
每个符号应该包含尽可能多的字符:从左到右一个一个字符读入,如果字符组成一个符号,那么再读入下一个字符,判断已经读入的两个字符串是否可能是一个符号的组成部分,如果可能,继续上面的判断,知道读入的字符不再可能组成有意义的符号。
例如:(a*(b+c)))
a—b 与a-- -b 含义相同,但与 a- --b含义不同,y = x /*p 这里是会出现错误,应该写作y= x / *p ;或者y = x /(*p)
4、整形常量
如果一个整形常量的第一个字符是0,那么该常量将被视作8进制数。因此,10和010的含义截然不同,转换为十进制 1010,0108
5、字符与字符串 ||
字符用 char hello[ ]={‘h’,’e’,’l’,’l’,’o’,’\n’} 与 char hello[ ] ={“hello\n”} 等效
二、语法陷阱
1、理解函数声明
当计算机启动时,硬件将调用首地址为0位置的子例程
假设fp是一个指向返回值为void类型的函数的指针。
void (fp) ();
(fp) ();
对常数进行一个类型转换,将其转型为该变量的类型,如将常数0,转型为 “指向返回值为void的函数的指针”类型,如( void () () ) 0
用( void () () ) 0替换fp
(( void () () ) 0) ();
2、运算符的优先级问题
逻辑运算符(&& 、||)的优先级低于关系运算符(>、<、=)
移位运算符(&、|)的优先级比算数运算符(+、-、*、/、%)要低,但比关系运算符要高
3、注意作为语句结束标志的分号
例如
if(x[i]>big);
Big =x[i];
与
if(x[i]>big)
Big =x[i];
截然不同。
还例如
If(n<3)
return
date = x[0]; time= x[1]; code= x[2];
这里的return 会跳转到 date = x[0]; 而不是返回。
If(n<3)
Return;
date = x[0]; time= x[1]; code= x[2];
第三种
Struct log{
Int date;
Int time;
Int code;
}
main()
{
}
main()的类型为log型
与
Struct log{
Int date;
Int time;
Int code;
};
main()
{
}
main()的类型未定
4、switch语句
不要忘记break;语句
5、函数调用
假设f是一个函数,那么
f(); //是一个函数调用
f; //什么也没做,准确来说,这个语句计算函数f的地址,但并不调用该函数
6、悬挂 else引发的问题
if(x==0)
if(y==0) error();
else{
z=x+y;
f(&z);
}
if(x==0)
{
if(y==0)
error();
else{
z=x+y;
f(&z);
}
}
if(x==0)
{
if(y==0)
error();
}
else{
z=x+y;
f(&z);
}
建议用{ }封装if与else部分的执行函数,在这里插入代码片
三、语义陷阱
1、 指针与数组
1)C语言只有一维数组,但是数组内可以放任何类型的对象,当然也可以放进去一个数组。
2)对于数组我们只能做两件事:确定数组的大小,以及获得指向该数组下标为0的元素的指针,其他的有关数组的操作,实际上都是通过指针进行的。
理解数组声明:
int a[3];
int *p;
struct {
int p[4];
double x;
}b[17];
int cal[12][31]; cal[4]含义是什么? p=cal[4]; p的含义是什么?
int *p ; p = cal; 非法
int (*p)[31]; p = cal; 合法
int *p ; int i; p=&i; 合法
int *p ; int i[3] p= i; 合法
int *p ; int i[3] p=& i; 在ANSI C中不合法
2、 非数组的指针
C语言以空字符(’\0‘)作为字符串常量的结束标志,将字符串s和r连接成一个字符串,标准操作为char *r ;
r =malloc(strlen(s)+strlen(t)+1);
if(!r)
{
exit(1);
}
strcpy(r,s);
strcat(r,t);
free(r);
3、 作为参数的数组声明
char hello[]=”hello“;
传参 hello 等价于 &hello[0]
将数组作为参数传参时
函数strlen(char s[ ])等价于strlen(char *s)
但作为定义声明char s[ ] 与 char *s 截然不同。
4、 避免”举隅法“
char *p,*q;
p=”xyz”;
q=p;
q[1] = ? q[1] = ‘y’;
不要混淆指针与指针指向的数据,复制指针时数据并不复制。
5、 空指针并非空字符串
将0赋值给指针变量时,绝对不能使用该指针指向的内存中存储的内容。
if(p==(char ) 0) 合法
if(strcmp(p,(char)0)==0) 非法
6、 边界计算与不对称性
注意数组下标与数组大小。
7、 求值顺序
|| 首先对左侧操作数求值,若为1,则不对右侧求值,若为0,则对右侧求值。
&& 对左求值,若为0,则不对右侧执行,若为1,则对右侧求值。
a ? b:c 三目运算符,若a为真,则执行b,若为假,则执行C。
8、 运算符&&、|| 和 !
按位运算符&、|、~位运算
逻辑运算符&&、||、!逻辑运算
9、 整数溢出
两个无符号整数运算不存在溢出,一个有符号整数,一个无符号整数运算,将有符号整数转换为无符号整数,也不会发生溢出,只有两个有符号整数运算时才有可能溢出,且溢出的结果是未知的。
例如去检查溢出:if(a+b <0)
complain( );
如果a+b发生溢出,那么if的检查就会失败,因为加法运算将设置一个内部寄存器的四个状态为:正、负、零、溢出。
正确的方式是将a与b都要转换为无符号整数,INT_MAX代表可能的最大整数值
if((unsigned) a+(unsigned)b >INT_MAX)
complain( );
10、 为函数main提供返回值
main 函数执行完后需要增加返回值。
四、 连接
1、 什么是连接器
C语言中的重要思想是分别编译,若干个源程序可以在不同的时候单独进行编译,连接器将若干个C源程序合并为一个整体。该整体能够被操作系统直接执行。如果C语言实现中提供了lint程序,一定要使用,可以检测出很多错误。
2、声明与定义
int a; 如果未初始化,编辑器应该默认初始化为0,(有些编辑器不能保证)
extern int a;外部引用别的地方的定义的int a;
如果在不同的源文件中定义了同一个变量,并各指定一个初始值,例如一个文件中指定int a=7,另一个文件中定义int a =9,这个时候外部引用时,大多数系统会拒绝接受该程序,如果有多个定义但未初始化,一些系统会接受。最好的解决办法是,每个外部变量只定义一次。
3、命名冲突与static修饰符
1)避免与库函数命名冲突
2)static可以将变量的作用域限制在一个源文件中,以减少命名冲突。,同样 static可以修饰函数,效果一样。
4、形参、实参与返回值
函数的形参有无都行,如果函数的形参列表为空,那么被调用时实参列表也为空。
任何C函数都有返回值,任何函数在调用它的每个文件中,都在第一次被调用之前进行声明或定义,就不会有任何与返回类型相关的麻烦。
要注意形参与实参数据类型的一致。
5、 检查外部类型
如果一个源文件中声明 extern int n; 另一个源文件中却是 long n;这样会导致什么问题?
1)如果编译器足够强大,可以检测出来这种错误。
2)如果是32位计算机,可能能正常工作,但是这种写法绝对是错误的
3)虽然两个实例的存储空间大小不同,但是却共享存储空间的方式能够满足,赋给其中一个的值,对另一个也有效,错误的程序因为某个巧合可以工作。
4)两个变量n共享存储空间的方式,对其中一个赋值,等同于给另一个赋了完全不同的值,程序不能正常工作。
6、 头文件
为了避免上面的错误,我们遵循一个简单的规则:每个外部对象只在一个地方声明。这个地方就是在一个头文件中,定义该外部对象的模块也应该包括这个头文件。例如:
在file.h中包含声明 extern char filename[ ];
如果其他外部c源文件中需要用到filename时,只需要在C文件中添加
#include “file.h”
五、 库函数
1、 返回整数的getchar函数
尽量使用系统头文件
1、返回整数的getchar函数
char c;
while((c=getchar())!=EOF)
putchar(c);
这个存在三种可能:
1)某些合法的输入字符在被”截断“后使得c的取值与EOF相同,程序将在文件复制的中途终止。
2)c取不到EOF这个值,程序将陷入死循环。
3) 因为巧合可以工作。
2、更新顺序文件
访问文件时,不能同时进行读写操作,如果想要同时进行,必须插入fseek函数调用。
类似:
read
seek
write
seek
第二次的seek改变文件状态,使得下一次文件可以正常读写。
3、缓冲输出与内存分配
setbuf(stdout,buf);可以将输出暂存起来,然后大块写入
使用时,为了避免错误,可以用两种方法:
1)将缓冲区成为静态数组,例如 static char buf[BUFSIZE];setbuf(stdout,buf);
2)动态分配缓冲区,例如:setbuf(stdout,malloc(BUFSIZE));
4、 使用errno检测错误
正确使用errno的方式
/*调用库函数*/
if(返回的错误值)
检查 errno;
5、 库函数signal
signal(signal type,handler function);
捕获异步事件的一种方式,信号非常复杂,而且具有一些从本质上而言不可移植的特性,我们要让signal处理函数尽可能简单。
例如: 打印一条错误消息,然后直接exit退出程序。
六、 预处理器
预处理器的重要性可以由两个主要的原因说明:
1)预处理器可以实现只需要改动一处数值,便可将用到该值的地方的所以地方都修改,将这个数定义为显式常量。
2)没有函数调用带来的系统开销
1、 不能忽视宏定义中的空格
1、不能忽视宏定义中的空格
例如:
#define f (x) ((x)-1)
f(x) 代表 ((x)-1) 错误
f 代表 (x) ((x)-1) 正确
#define f(x) ((x)-1)
这样才能实现 f(x) 代表 ((x)-1)
2、 宏并不是函数
宏定义中最好把每个参数都用括号括起来,来避免错误。
例如
#define abs(x) x>0?x:-x
abs(a)+1 会得到 a>0 ? a : -a+1
但我们的本意不是这样。
如果宏实现比较复杂,那么尽量使用函数来实现。
宏的展开可能会产生庞大的展开式。
3、 宏并不是语句
这里的例子有点晦涩,不知如何表达,只是记住宏并不能直接当作语句去使用,并且要注意分号即可。
4、 宏并不是类型定义
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b,c;
这样我们只需要修改一行代码就可以改变abc的类型,但是我们并不推荐如此使用,最好还是使用类型定义
typedef struct foo FOOTYPE;
七、 可移植性缺陷
因为编译环境的不同,可移植性就成了一个重要话题,本节讨论几个最常见的错误来源。
1、 应对C语言标准变更
新标准无法在旧编译器中使用,为了增强可移植性,我们最好的做法在旧的环境和新的环境收益中做好选择。
2、 标识符名称的限制
不要用大写的函数名称与库函数同名,例如malloc和Malloc,假如你自己写的是Malloc,但是编译器如果不区分外部名称大小写,这里就会出错,要避免这种情况。
3、 整数的大小
非正式情况下short 与 int 是16位 long 是32位,这与机器的字符长度有关。
可移植性最好的办法就是声明该变量为long型,但在这种情况下我们去定义一个新的类型更加清晰:例如
typedef long tenmil;
4、 字符是有符号还是无符号整数
将char型转换为int型需要做出选择;如果要转换为有符号数,那么编译器将char类型扩展为int 型时需要复制符号位,如果是转换为无符号数,那么编译器只需要在多余的位上直接填充0即可。如果最高位是1,那么将这数定义成为无符号字符
,无论什么编译器,在该字符转换为整型时都只需在多余的位填充0即可。
一个常见错误:如果c是一个字符变量,使用(unsigned)c 强制类型转换成无符号整型,这里会出现错误。因为转换为无符号时,会先转换为int型,这里会得到非预期的结果。
正确的方法是(unsigned char)c;
5、 移位运算符
在向右移位时,空出的位是由0填充还是由符号位的副本填充?
如果将操作的变量声明为无符号类型,那么空出的位是由0填充,如果是有符号位,那么二者都可以。
移位计数(即移位操作的位数)允许的取值范围是什么?
如果被移位的对象是n位,那么移位计数应该大于0且小于n,为什么要加这个限制,因为加上这个限制后,我们就能够在硬件上高效地实现移位运算。
6、 内存位置为0
null指针并不指向任何对象,除非是用于赋值或比较运算,出于其他任何目的使用null指针都是非法的。
7、 除法运算时发生的截断
q=a/b
r=a%b
性质1:q*b+r==a;性质2:如果改变a的正负号,我们希望只改变q的符号,但不改变q的绝对值;性质3:当b>0时,我们希望r>=0且r<b;
这三条性质不可能同时在计算机中成立,所以在实现整数除法截断运算时,会放弃三条原则中的至少一条,一般是第三条。
最好是避免a为负数这样的情况,并且声明a为无符号数
8、 随机数的大小
与机器的整数长度有关,如果是32位,那么最大为2的31次方-1。
RAND_MAX为随机数的最大取值。
9、 大小写转换
使用宏定义
#define _toupper© (©+’A’-’a’)
#define _tolower© (©+’a’-’A’)
10、 首先释放、然后重新分配
malloc:申请分配一块新内存
realloc:指向已分配的内存,重新定义大小,可能涉及到内存的拷贝
free :释放内存
八、 建议
在代码编写中,如何减少程序错误,以下是一些通用的建议:
1、通用建议
1、不要说服自己相信错误
2、直接了当地表明意图
3、考察最简单的特例
4、使用不对称边界
5、注意潜伏在暗处的bug
6、防御性编程:能想到可能发生的错误就一定会发生,所以应做出规避