浅谈C语言中存在的陷阱和缺陷
下面介绍C语言的陷阱和缺陷:
词法陷阱:
1、 = 不同于==不要在程序中将两者写错,小心。将表达式与常量比较时,可将常量放在左边。
2 、&和| 不同于&& 和 ||.
3、 词法分析中的贪心法:每个符号应该包含尽可能多的字符。如果(编译器的)输入流截至某个字符前都已经分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个字符的最长字符串。如y = x/*p,那么/*将作为一个符号对待。
4、 如果一个整形变量第一个字符是0,那么该常量被视为8进制数。
5 、Char c = ‘cxf’。在vc和Gcc中,依次用后一个字符覆盖前一个字符,最后得到的整数值是最后一个字符的整数值。
语法陷阱:
1 、c变量声明由类型和一组类似表达式的声明符组成。声明符与表达式类似,对他求值返回一个声明中给定类型的结果。如float f, ((f))。
知道了如何声明一个变量,那么该类型的类型转换符就很容易得到了:将声明中的变量名和分号去掉,再将剩余的部分用个括号“封装”起来即可。如float(*h) (),则float(*)()就是“指向返回值是浮点类型的函数的指针”的类型转换符。(*(void(*)())0)()调用地址为0位置的的例程。
2 、运算符优先级:单目运算符,算术运算符,移位,关系,逻辑,条件, 赋值。
3 、switch语句中case中,不要忘记break,若刻意要省略,请加注释。
4 、C语言中只有一维数组,而且数组的大小必须在编译期间就作为一个常数确定下来。多维数组是通过一维数组仿真的,因为数组的元素可以是任何对象,当然也可以是数组。
对数组,我们只能做两件事,确定其大小,以及获得指向该数组下标为0的元素的指针。其它的有关数组的操作,实际上是通过指针进行的。
语义陷阱:
1 、空指针并不等于空字符串。编译器保证由0转换而来的指针不等于任何有效的指针。当将0赋值非一个指针变量时,绝对不能企图使用该指针指向的内存中存储的内容。
2 、在使用范围时,使用不对称边界方式。第一个是“入界点”(序列中第一个被占用的元素),第二个是“出界点”(序列中第一个被释放的元算)。For(int I = 0 ; I < 10; i++)。尽量不要使用For(int I = 0 ; I <=9; i++)。
3 、数组的下标如果用入界口加出界口来表达(即10个元素,其下标为0 <= n < 10 ),则元素个数即为上界与下界之差,即下界。若为空,则上界等于下界。任何情况下上界也永远不可能小于下界。
尽量采用非对称边界法。
一个有N个元素的数组 ,我们可以使用a[N]进行比较和赋值,但不能引用其内容。
4 、C语言中只有4个运算符存在规定的求值顺序:&&,| |, ?:和,。其他的运算符对器操作数求值的顺序是未定义的。特别的是,赋值运算符并不保证任何求值顺序。Y[i]=X[i++] 错误。
5 、记得为main提供返回值。
连接:
1 、为避免命名冲突,请对变量或函数使用static修饰符。为了定义与库函数中同名的函数,可将文件中要定义的函数加static修饰。
2 、使用外部函数前,一定要声明。否则,没有声明,函数返回值将默认为整型。
3 、外部声明要与定义类型一致。不能声明是extern int n,而定义是long n.
4 、同一个外部变量在不同的地方被声明为不同的类型,这种错误大部分编译器是检不出来的。
char file[]= "/etc/password";
与
extern char* file;
是不一样的。
库函数:
1 、注意getchar()返回整型,不是字符型。
2 、为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之依然,如果要同时进行输入和输出操作,必须在其中插入fseek函数的调用。例:
FILE *fp; struct record rec; while (fread((char *)&rec, sizeof(rec),1,fp) = 1) { if(/* */) { fseek(fp, -(long)sizeof(rec), 1); fwrite((char *)&rec, sizeof(rec), 1,fp); fseek(fp, 0l,1); } }
3、 缓冲输出和内存分配:
可通过setbuf函数控制程序的缓冲输出。
#include <stdio.h> void main(void) { int c; char buf[BUFSIZ]; setbuf(stdout,buf); while((c = getchar()) != EOF) putchar(c); }
这个是不对的。buf最后一次被清空是在什么时候?答案是在main函数结束之后,作为程序交回控制给操作系统之前C运行时库所必须进行的清理工作的一部分。但是在此之前buf已经被释放。
解决方法一是加上static 声明。也可以把buf声明完全移到main函数之外。第二种办法是动态分配缓冲区,在程序中并不主动释放分配的缓冲区
4 、不能直接使用errno检测错误,应先检测作为错误指示的返回值,确定程序已经执行失败。然后,再检查errno,搞清原因。
/* 调用库函数 */
if(返回的错误值)
检查errno
5 、库函数signal
从理论上说,一个信号可能在C程序执行期间的任何时刻上发生,甚至可能出现在某些复杂的库函数(如malloc)的执行过程中。
因此从安全的角度讲,信号的处理函数不应该调用上述类型的库函数。基于同样的原因,从signal处理函数中使用longjump退出,通常情况下也是不安全的:因为信号可能发生在malloc 或者其它库函数开始更新某个数据结构,却又没有最后完成的过程中。因此signal处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。
然而,就算这样做也并不总是安全的。当一个算术运算错误引发一个信号时,某些机器在signal处理函数返回后还将重新执行失败的操作。因此对于算术运算错误,signal处理函数的惟一安全、可移植的操作就是打印一条出错消息,然后使用longjump或exit立即退出程序。
当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是缓冲。
预处理器:
1、 不要忽视宏中的括号。
2、 宏不是函数。将宏中的参数都加上括号,将整个结果表达式也括起来。防止副作用。
3、 宏不是语句:#define assert(e) ((void)((e)||_assert_error(_FILE_,_LINE_)))
4 、宏不是类型定义;不要用#define定义类型,而是用typedef定义新类型。
可移植性:
1 、因为字符串常量可以用来表示一个字符数组,所以在数组名出现的地方都可以用字符串常量末端替换。 如: "0123456789"[n%10]
2、 注意C标准的变化,新特性的使用。
3 、c标准所能保证的只是,c实现必须能够区别出前6个字符不同的外部名称,且并没有要求区分大小写。避免诸如:print_char(),print_int()等
4 、整数长度的相对长度规定:short类型的值肯定能被int型容纳,int型肯定能被long型整数容纳;一个普通(int类型)整数足够大以容纳任何数组下标;字符长度由硬件特性决定。