C陷阱与缺陷阅读笔记(上)

时间:2022-01-02 09:38:51
词法陷阱
1.贪心法
C编译器对C语言符号的识别,基于每一个符号应该包含尽可能多的字符原则。
如果输入流截止至某个字符之前都已经分解成为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串。
因此
x = y/*p,中的/*被解释为注释符号,而非除以p所指内容。
此类问题即是所谓准二义性问题(near-ambiguity).因此a+++++b,实际被编译器解释为:a ++ ++ + b
2.如果一个整形常量的第一个字符是0,则被处理为八进制,因此,实际编程中要注意,例如可能为了数据对齐,在常数前加0等。
3.单引号引起的一个字符实际是一个整数,但双引号引起的则是一个指向无名数组起始字符的指针。
语法陷阱
1.关于申明
我们根据表达式的申明,可以得到该类型的类型转换符,即只需要把申明中的变量名和申明末尾的分号去掉,再将剩余的部分用一个括号整个括起来即可。例如:
float (*fun)()
表示一个指向返回值为浮点类型的函数的指针,所以
(float (*)())表示一个指向返回值为浮点类型的函数的指针的类型转换符,
对于该指针所指函数的调用,则用:
(*fun)();
ANSIC标准允许程序员将上式简写为fun()
对于一个常数进行这样的类型转换,如下:
(float (*)())0
如果该0表示地址,则我们可以这样表示在0地址出的子程序:
(void (*)())0
换句话说,就是0指向一个返回值为float的函数,也就是说,0是一个指向返回值为浮点类型的函数的指针。那么,这样调用0所指函数:
(*(void(*)())0)()
如果用typedef 来表示,则如下:
typedef void (*fun)();//即fun是一个指向无返回的函数的指针的类型转换符。那么对0转换如下:
(fun)0
0地址子程序调用如下:
(*(fun)0)();
 
以上也出现在signal函数。
所以,实际上,typedef void (*fun)(),定义的是一种数据类型,是一个指向无返回值无形参的函数的指针,实际在使用中,需要定义该数据类型的实体,并指向该种类型的函数,才可用用该指针访问所指函数。
 
2.关于优先级
    实际上,关于优先级的问题,要看平时常用那些,这里只提示自己不常用的那些:
2.1单目运算符,如!,~,++,--,-,(T),*,&,sizeof,这些是自右向左的结合性。即他们相同优先级,从右开始结合处理。*p++,表示*(p++),即取*p,再p++。
2.2移位运算符比关系运算符优先级高。可以直接:
if (t << 1 > 2)
{
}
2.3关系运算6个优先级并不相同,< <= >= > 的优先级比== != 要高。
eg:
tax_rate = income > 40000 && residency < 5 ? 3.5 : 2.0;
先income > 40000 后residenecy < 5 然后 ? 最后 =、
3 关于switch
某些只要操作某一种情况的条件,即可与其他情况类似的实现的case,我们可以用switch省去break的方式实现,例如,减法就是加法的一种,即减数取负。
语义陷阱
int a[10];
for (int i = 0; i <= 10; ++i)
{
a[i] = 0;
}
这里发生死循环的关键在于,如果编译这段程序的编译器按照内存地址递减的方式给变量分配内存,那么a最后一个元素之后的一个字实际上分配给了变量i,也就是a[10]= 0编程i = 0;导致出现死循环。
 
#define NULL 0
在C中除了0,其他整数转换成指针时取决于具体C编译器的实现,编译器保证由0转换而来的指针不等于任何有效的指针。当0转换成指针时,该指针绝对不能解除引用。
关于求值顺序
if (count != 0 && sum / count < k)
C语言中只有四个运算符(&&, || ?:, 和,,)。
其中&& 和||先计算左侧,再根据需要计算右侧。
a?b:c, 先计算a,再根据a,计算b或者c。
逗号运算符,先对左侧求值,然后该值被丢弃,再对右侧求值。
而其他所有运算符的运算顺序,都是未定义的。
i= 0;
while(i < tabsize && tab[i] != x)
{
i++;
} 
i = tabsize时,在表中没有找到要找的x,其他情况,就是找到了。
关于溢出
有符号类型的数据溢出检测,如下:
if ((unsigned)a + (unsigned)b > INT_MAX)
{
}
ANSIC标准的limits.h中定义了INT_MAX
或者
if (a > INT_MAX - b)
{
}
链接
有一点要说明的是,链接器是独立于C语言实现的,因此,链接器对于编译器无法检测到的错误,同样束手无策。此时可以用lint。
另外,变量的多处引用,命名冲突等,可以将需要引用的变量,extern在单个.h文件中,在需要用到的.c中包含该.h文件,而在这些文件的某一个.c文件中,定义该变量即可。
库函数
1.为了保持与过去不能同时读写的程序向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。如果同时要进行输入输出操作,必须增加fseek函数,调整文件状态。
关于库函数,以后有机会读C标准库。
预处理器
宏定义的对空格要求很严格,然后,在宏调用中,不如此严格。。。
#define f(x) ((x) - 1)
f(3) 等于2.f (3)也等于2.。。。。。
 
原来断言宏,还可以这样写:
#define assert(e) \
	((void)((e)||_assert_error(__FILE_,__FILE__)))
利用||操作符的计算顺序,即保证效率,又稳定。 
补充:关于#define TEST (100 * 20),要分清楚的是,#define自始至终都是在预处理阶段,做宏展开,而真正的100 * 20是在编译阶段,编译器检测出常量运算而计算出其值的。
 
 

关于可移植性的缺陷
1、ANSIC 标准保证,C实现必须能够区别出前6个字符不同的外部名称。而且,这个定义中不区分大小写。因此,要注意的是,你编写的标示符名称,state,State可能在某些编译环境下,是一样的。
2.对于数据长度,short,int,long,有以下规定,short < int < long,一个int整数能容纳任何数组下标,字符长度由硬件决定。
有些硬件的字符长度是8位,有些是9位,另外,还有一些是16位,用来处理如日语之类的语言的大字符集。
ANSIC 要求long型整数的长度至少应该是32位。因此,要考虑程序移植性问题,那么最好用long型数据类型。
3.char转换成较大整数时,需要考虑应该讲该字符作为有符号数还是无符号数,如果你关注字符的最高位为1的char型数据是有符号数,还是无符号数,则可以将该字符定义为unsigned char,这样,无论什么编译器,都将其处理为无符号数,而如果申明为一般的字符变量,则视编译器而定。
因此,(unsigned)c,无法得到与c等价的整数,只能用(unsigned char)c.
4.移位运算符,被移除后的位用什么填充。
	如果是无符号数,填0,有符号数,则可能填0,也可能填用符号位的副本填充。而移位的范围是0~n-1.
5.内存位置0
	有的编译器,对内存位置0,设置强制硬件保护,有的则只读,但也有可以读写的情况,因此,NULL指针的使用要注意。
6.早期的realloc函数实现要求待重新分配的内存区域必须首先释放掉。
7.除法截断问题:
定我们让a除以b,商为q,余数为r:
那么,
q = a / b;
r = a % b;
假定b > 0,那么a,b,q,r之间维持怎样的关系呢?
1.q * b + r = a
2.如果我们改变a的正负号,我们希望只改变q的符号,不改变q的绝对值。
3.当b > 0,我们希望保证r >= 0 且 r < b.
实际上,许多语言不能同时保证以上3条,C和大多数语言一样,也是只保证以上前2条。
因此,就会出现,(-3) / 2 = -1, (-3) % 2 = -1.因此, r = a % b,最好定义a为无符号数。

一些建议:
1.直接了当的表明自己的意图,例如用()等方式。
2.考查最简单的特列。
3.使用不对称边界。
4.注意潜伏的bug。
5.防御性编程。