C陷阱与缺陷 要点总结

 

C陷阱与缺陷 要点总结

 

‍词法

词法分析中的贪心法

每一个符号应该包含尽可能多的字符,也就是说编译器把程序分解为符号的方法是,从左到右一个字符一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分,如果可能重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号,这个策略被称为“贪心法”

y/*x 实际想表达的是y/(*x) 但是/*会被理解为一段注释的开始,这样的准二义性问题会招致麻烦。

习题

a+++++b

整形常量

如果一个整形常量的第一个字符是0,那么该常量将被视为8进制。

字符与字符串

  • 单引号和双引号有时候会被弄混,编译器有时候不会报错,在运行时产生难以预料的错误。
  • 单引号引起的字符实际上代表一个整数。
  • 错误:printf('\n')会产生错误

语法

变量声明由两部分组成:类型和一组类似表达式的声明符。

模拟开机时启动程序,(*(void(*)())0)()

float ff()表达的含义是ff()求值的结果是一个float,也就是说ff表示一个返回结果为float的函数

float *ptr表达的含义是*ptr的结果是一个float,也就是说ptr是一个float的指针

float *g(),表达的含义是*g()求值的结果是float,但是g和右边()结合的优先级高于左边的*,因此g表示一个返回float指针的函数

float (*h)(), 表达的含义是 (*h)()求值的结果是float,h表示一个返回float的函数的指针

类型的转换符:把一个变量声明中的变量名和结尾的分号去掉,再将剩余部分用一个括号封装起来。比如前面的(float (*)())

函数指针的调用,将设fp表示一个函数指针,那么(*fp)()表示调用该函数指针指向的函数,可以简写为fp(),但是*两边的()不能去掉,因为()运算符的优先级高于*,如果省掉括号表示对函数执行结果解引用。

void (*sfp)(int) sfp表示一个函数指针

void (*signal(something))(int) 表达的意思是(*singnal(something))的结果是一个传入参数为int,返回结果为void的函数,那么signal的表达的意思就是传入somenthing返回一个函数指针。

正常人的写法应该是 typedef void (*HANDLER)(int); HANDLER signal(int, HANDLER);

关于运算符的优先级,靠谱的做法还是增加括号,明确的表达。

表达式中的分号,if、while等语句如果后面加了分号则表示独立的语句。如果struct 定义后面少加了分号,则可能出现下面类似的错误:

struct { int xxx; int ccc}

main()

{}表达式的意思可能就变成返回一个结构体。 奇葩的联想。

switch 中的break也是一个比较容易出错的地方,如果真的需要省略break,可以在对应的位置增加注释

case 1:

/*此处没有break*/

case 2:

break;

default:

break;

if else对括号的使用要一致,避免出现else悬挂的问题,看下面的错误例子

if (x==0)

if (y == 0) error();

else {

z = x + y;

f(&z);

}

c语言允许初始化列表出现多余的逗号,这样在初始化列表很长的时候,需要分成多行,每行都是以逗号结尾,这种语法上的相似性可以方便代码编辑工具进行处理。

语义

数组

c语言的数组需要注意两点,

第一点,c语言只有一维数组,但是数组的成员可以是任何类型,包括数组类型,所以可以模拟出多维数组。

第二点,c语言中对数组只能有两种操作,第一种指定数组大小,第二种获取下标为0的元素的地址。其他的元算,即使是下标的运算也是通过指针元算进行的。

对于一个数组变量a,sizeof(a)表示这个数组的大小,而其他时候a都表示下标为0元素的地址。*(a+i) 简记为a[i]

非数组的指针常见的:

第一,不检查malloc的返回值

第二,分配的内存没有及时释放

第三,申请的内存长度不足。

作为参数传递的数组名会被转换成指向数组第一个元素的地址。

虽然指针和数组可以转换,但是如果一个指针变量不代表一个数组,那么不要把他声明为一个数组类型,避免出现误导。

混淆指针和指针所指向的数据:复制指针并不同时复制指针指向的数据。

空指针不是空字符串,可以把0赋值给一个指针变量,但是不能尝试访问该指针变量指向内存存储的内容,空指针不能解引用。

边界计算与不对称边界

栏杆错误或者说差一错误,100英尺围栏,每10英尺需要一个栏杆,总共需要多少栏杆

边界的计算两个通则:

通则一,计算最简单的特例,在此结果上外推

通则二,仔细计算边界,绝不掉以轻心

避免出现栏杆错误的编程技巧,即不对称边界:采用第一个入界点和第一个出界点表示一个范围,这里的下界是入界点表示包含在取值范围之内,上界为出界点,不包含在取值范围之内。

另一种考虑不对称边界的方式是,把上界作为某序列中第一个被占用的元素,把下界作为第一个被释放的元素。

对于指针bufptr,是让它始终指向最后一个已占用的字符,还是让它指向第一个未被占用字符。根据不对称边界的原则,选择后一种更为合适。

依据不对称边界的原则,判断指针到达缓冲区尾部的方法是if(bufpter == &buf[N]) 而不是 if(bufptr > &buf[N-1])。尽管buf[N]的原色是不存在的,他的地址仍然可以用来比较,但是不能访问不存在的元素。

求值顺序

c语言中只有四个运算符 (‘&&’, ‘||’, ‘?’, ‘,’)存在规定的求值顺序,运算符&&和||都是先求左值,如果有需要再求右值,表达式a?b:c也是先求a的值如果有需要再计算b或c,而逗号运算符,先对左侧操作求值,然后该值被丢弃,再对右侧操作求值。

f(x,y)中x和y的求值顺序时未定义的。g((x,y))中x和y的求值顺序是先计算x,丢弃之后再计算y,而函数g传入的参数也是只有一个。

赋值操作符并不保证任何求值顺序,比如下面的例子:

x[i] = y[i++],并不能保证左侧先执行。

不要使用位运算符 (&,|,~)来替代逻辑运算符(&&,||,!),有时候运行结果正常只是巧合,意义不同,并且逻辑运算符有求值顺序,能够避免错误访问。

整数溢出,这里不是类型长度导致的溢出,而是指有符号整型的溢出,两个有符号整型计算,结果可能会溢出。而溢出的结果是未定义的,各编译器实现不同,因此不能使用类似下面的方式来检测溢出if(a+b < 0),而应该把他们都转换成无符号整型进行比较 if ((unsigned)a + (unsigned)b > INT_MAX)

main函数应该有返回值。

连接

4.1什么是连接器

连接器不理解C语言,编译器的责任是把C语言翻译成连接器能够理解的形式。连接器的作用是把编译器或者汇编器生成的若干目标模块,整合成一个载入模块或者一个可执行文件的实体。

程序中每个函数或者外部变量没有声明为static,就都是一个外部对象。连接器通常把一个目标模块看成是一组外部对象组成。

工作过程:连接器的输入是目标模块,输出是载入模块,连接器读入目标模块和库文件,同时生成载入模块,对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象,如果没有就写入载入模块,如果有就进行同名冲突处理。

一个目标模块中引用了其他目标模块的外部对象时,生成载入模块时需要记录这些引用,直到读入定义该外部对象的模块时,修改载入模块中的标记,标记该外部对象不再是未定义的。

4.2声明与定义

变量的定义如果出现在所有函数体之外,称为外部对象的定义。

外部变量的定义如果没有指定初始值,那么多次定义可能能够编译通过,但是不应该这样做,每个外部变量只应该定义一次。

4.3命名冲突与static修饰符

为了避免可能出现的命名冲突,如果一个函数仅被同一文件中的其他函数调用,我们应该把它声明为static。

4.4形参实参返回值

任何一个函数,在被调用的每个文件中,都在第一次被调用之前进行了声明或者定义就不会出现参数或者返回值的错误。

也就是说在某些时候,调用函数之前可以不声明或者定义该函数,此时返回值和参数有有一些默认的规则。这不是常规做法?

4.5检查外部类型

保证一个特定的名称的所有外部定义在每个目标模块中具有相同的类型,是程序员的责任,编译器可能检测不到这种错误。

如:extern int n; long n; char filename[] = "/ect/passwd" ; extern char* filename;

4.6头文件

避免上述问题的方法是,外部对象在一个地方声明,这个地方应该是一个头文件中。定义外部变量的地方也应高包含该头文件。

库函数

5.1 getchar

返回整形的getchar,看一个例子c=getchar,如果c被定义为char类型,结果是c无法容纳下所有字符,包括EOF,有时候编译器在这类情况下会把返回结果截断。

5.2

文件操作,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作,不能随后紧跟着一个输出操作,如果需要同时进行输入和输出操作,需要在其中插入fseek函数

5.3缓冲输出与内存分配

程序输出的两种方式:第一种叫做及时处理,另外一种方式是先暂存起来,然后大块写入。前者旺旺造成较高的系统负担。通过调用库函数setbuf(stdout, buf)来实现。在使用setbuf时,需要注意buf变量的生命周期,在指针执行打印之前buf不能被释放 。

5.4使用errno检测错误

在调用库函数时,应该先检查返回值,在确认函数执行失败的情况下,再查看errno查看错误原因。这么做的原因是在执行成功情况下errno也可能被上次库函数设置,而没有清零。

5.5 signal

信号处理函数非常复杂棘手,而且具有一些从本质上而言的不可移植性,signal处理函数应该尽可能的简单,并把他们组织在一起。事件处理函数必须是不对全局变量产生影响的可重入的函数。

常用的做法是打印错误信息,然后调用longjmp或者exit退出程序。

‍预处理器

宏只对程序文本起作用。宏提供了一种对组成程序字符进行变换的方式,而并不是作用于程序中的对象。

6.1注意宏定义中的空格

6.2宏不是函数,所以我们需要:

宏定义中的每个参数都用括号括起来

宏定义整个表达式也应该用括号括起来

宏中的参数可能被求值多次,所以要确保宏的参数没有副作用,不如不能传递类似(a++)这类的参数

宏可能产生非常庞大的表达式

6.3宏不是语句,看下面的例子

#define assert(e) if(!e) assert_error(__FILE__, __LINE__)

if (x>0 && y > 0)

assert(x>y)

else

assert(y>x)

最终展开之后else错误的宏定义中的if结合了。

6.4宏不是类型定义

#define T1 struct foo *

T1 a,b;

展开表达式,b并不是我们想要的类型。

因此我们应该使用typedef来定义类型。

‍可移植性缺陷

7.1 c标准的变更

7.2 标示符名称的限制

7.3整数的大小

7.4字符是有符号整数还是无符号整数

字符转换为较大的数时,如果字符的最高位是1,那么转换成无符号数还是有符号数?

可以声明为unsigned char类型,这样编译器在转换时,只是把多余的位补0

注意,不能使用(unsigned)c的方式获得无符号整数,因为这个操作会先把c转换为int,而这个结果可能是非预期的。

7.5移位运算

向右移位时多出来的空位是由0来填充,还是由符号位副本来填充?

无符号数进行移位,空位用0来填充,有符号数移位,可以用0也可以用符号位来填充,所以,如果关注右移空出的位,应该要转换为无符号数。

移位计数(移动的位数)的取值范围是?

如果被移位对象的长度为n,那么移位计数的取值范围应该大于等于0,而严格小于n,加上如此限制可以更高效的在硬件执行移位运算。

移位运算比除法运算快

右移运算和/有时候结果是不同的(-1) >> 1不等于0,而-1/2等于0

7.6 内存为置0

null指针不指向任何对象,因为除非是用于赋值或者比较运算,出于其他任何目的使用null指针都是非法的。

7.7 除法运算发生的截断

7.8 随机数的大小,有的系统大小为0到2^31 -1 有的大小为0到2^15 -1

7.9 大小写转换 toupper tolowerr有的系统实现的是函数,_toupper _tolower是通过宏实现的,宏的使用需要对传入的参数慎重。

7.10 realloc的实现

7.11

通过n+‘0'的这种方式获取n对应的字符是不可以移植的,可移植的方法是“0123456789”[n]

基于2的补码的计算所能表示的负数的范围要大于所能表示整数的范围,一个long类型整数有k位和一个符号位,该long类型整数能表示-2^k却不能表示2^k,所以一个有符号的long不能通过添加负号来转换为整数,可能会溢出。负值转换为正值时把-n赋值给unsigned long类型,而不是赋值给long类型

当除法运算中有一个操作数为负数时,他的表现与具体实现有关。n为负数时,n%10可能是一个正数。

建议

直截了当表明意图,主要是说在操作符优先级方面,使用括号表达明确意思。

考虑最简单的特例,输入数据为空或者仅有一个元素,考虑程序设计或者验证程序。

使用不对称边界

潜伏在深处的bug,避免使用生僻的语言特性,这样可以方便移植,也可以避免编译器bug。

防御性编程 (防范式编程 代码大全2 第8章)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值