C陷阱与缺陷
第1章 词法“陷阱”
1.1 =不同于==
if中的判断应该表明显性比较,指出不等于0,等于0等。
1.2 &和I不同于&&和II
&和I是按位与或,而&&和II是逻辑与或。
1.3 词法分析中的“贪心法”
字符一直读入直到下一个不能组成有意义的字符。符号的中间不能嵌有空白。
1.4 整数常量
禁止将10写成010,如果一个整型常量的第一个字符是数字0,那么该常量将被视作八进制数。
1.5 字符与字符串
字符‘a’;字符串“hello” == ‘h’、‘e’、‘l’、‘l’、‘o’、‘\0’
数值类型 | 长度 |
|
| 32位机器 |
short [int] | 半个机器周期 |
|
| 2个byte |
int | 一个机器周期 |
|
| 4个byte |
long [int] | 至少一个机器周期 |
|
| 4个byte |
float | 至少一个机器周期 |
|
| 4个byte |
double | 二个机器周期 |
|
| 8个byte |
|
|
|
|
|
字符类型 | 长度 |
|
|
|
char | 一个字节 |
|
| 1个byte |
第2章 语法“陷阱”
2.1 理解函数声明 ???
float *g(), (*h)();
*g()中括号优先级高于*,所以g是一个函数,该函数的返回值类型为指向浮点数的指针。
(*h)(),h为函数指针,h指向函数的返回值为浮点类型。
通过以上规则可以定义函数指针和指针函数
2.2 运算符的优先级问题
r= hi << 4 + low; 错 r = ( hi << 4 ) + low; 对
单目运算符是自右至左结合。
2.3 注意作为语句结束标志的分号
(1)if ( x[i]> big);
big = x[i];
if后“;”多余,但编译器不会报错。
(2)if ( n <3 )
return
logrec.data = x[0];
logrec.time = x[1];
logrec.code = x[2];
return后漏了“;”,但编译器也不会报错。
(3)structlogrec{
intdata;
int time;
int code;
}
main()
{
…
}
struct{}后漏了“;”,但编译器也不会报错。
2.4 switch语句
(1)switch case break default break,每个case、default后一个break
(2)也有no break的用法。
2.5 函数调用
在函数调用时即使函数不带参数,也应该包括参数列表。
2.6 “悬挂”else引发的问题
为了防止else和别的if配对,应该在每个if和每个else后加上括号,且每个if后都应该跟上else。
第3章 语义“陷阱”
3.1 指针与数组
数组的特点:(1)C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,C语言中的数组的元素可以是任何类型的对象,当然也可以是另外一个数组。 (2)对于一个数组:确定数组大小及获得指向该数组下标为0的元素的指针。其他操作都是对通过指针完成的。
p++; //下一个元素,而不是下一个内存
*( a + i ) == a[ i ]; calendar[4][7] == *(*(calendar+4)+7);
int calendar[12][31]; sizeof(calendar[4]) == sizeof(int)*31; calendar[4] == calendar[4][0];
char filename1[]= “etcs”; char *filename2= “etcs”;
sizeof(filename1)等于5;sizeof(filename2)等于?。数组名表示整个数组的内存,而指针表示这个字符串的首地址。
数组和指针的区别
(1)修改内容
字符数组的值可变,而字符串指针的内容不可变。
(2)内容复制与比较
字符串复制与比较中都需要使用strcpy()和strcmp();
但字符串指针复制前需为指针声明内存大小。
(3)计算内容大小
字符数组表示所有大小,而字符串指针只表示头地址。
3.2 非数组的指针
将s,t两字符串连接成一个r字符串:
#include<stdio.h>
#include<string.h>
#include<malloc.h>
int main(void)
{
char*s="12345asdfghjkl",*t="67890qwertyuiop";
char *r;
r = (char *)malloc(strlen(s)+ strlen(t) + 1);
//在内存中找到长度为n的内存,然后将这块内存的首地址给r,即r就指向这块内存
if(!r ) //若申请不到内存
{
printf("error!\n");
exit(1);
}
strcpy(r, s);
strcat(r, t);
printf("%s\n", r);
free(r);
return 0;
}
3.3 作为参数的数组声明
C语言中会自动地将作为参数的数组声明转化为相应的指针声明。即
int strlen( char s[] )与int strlen(char *s )等价。
3.4 避免“举隅法”
char*p; p=”xyz”;
p的值是一个指向’x’,’y’,’z’,’\0’4个字符组成的数组的起始元素的指针,指针表示的是指向地址的指针,并没有具体含义。复制指针并不能同时复制指针所指向的数据。
3.5 空指针并非空字符串
空指针就是没有指向一块有意义的内存,如char *p;或char *p=NULL;NULL就是整数0,与’\0’不同。void *这不是空指针,这叫没有确切类型的指针。
字符串中的一个字符为’\0’,那就表示该字符串的结束,当该字符为整数0或NULL时,也表示结束。空字符串指的是该字符串中的所有字符都为’\0’,即整数0;
3.6 边界计算和不对称边界
for(i=0;i<10; i++) //表示0<= i< 10,不对称区间,总共10个元素。
3.7 求值顺序
if(count!= 0 && sum/count < smallaverge)
printf("OK!");
即使当变量count为0时,也不会产生一个“用0作除数”的错误。
运算符&&和||首先对左侧操作数求值,只在需要时才对右侧操作数求值。
运算符 a?b:c 首先对操作数a求值,根据a的值再求操作数b或c的值。
运算符“,”首先对左侧操作数求值,然后该值被“丢弃”,再对右侧求值。
while(i< 5) while(i < 5)
y[i++] = x[i]; y[i]= x[i++];
以上对i做了太多的假设,这是不正确的,应该将i++;重新写出。
3.8 运算符&&、||、!
&、|、~ 表示按位与或非; &&、||、! 表示逻辑与或非
3.9 整数溢出 ???
当两个操作数都是有符号整数时,“溢出”就有可能发生,而且“溢出”的结果是未定义的。而两个无符号整数、一个无符号一个有符号整数都不会发生“溢出”。
3.10 为函数main提供返回值
intmain()
{
return0;
}
第4章 连接
一个C语言程序可能由多个分别编译的部分组成,这些不同部分通过一个通常叫做连接器的程序合并在一起。
4.1 什么是连接器
在多个目标模块整合在一个载入模块时,连接器的一个重要作用是处理这类命名冲突(函数和变量)。连接器读入目标模块与库文件,同时生成载入模块。如果读者的C语言实现中提供了lint程序(代码检查工具),切记要使用。
4.2 声明与定义
声明指的是告诉编译器,但不分配内存;定义指的是分配内存。声明包括定义与引用。
int a; //声明(定义) int a = 7; //定义 extern int a; //引用
4.3 命名冲突与static修饰符
Static修饰变量和函数,可以防止变量与函数命名冲突,因为static限定了作用域。
4.4 形参、实参与返回值
函数调用前要进行声明;声明函数的形参与返回值要与定义时相同;调用函数时的实参类型要与形参类型相同。
4.5 检查外部类型
externint n;
long n;
不同C源文件中定义的公用变量的类型要相同。
4.6 头文件
文件file.c 文件file.h
#include ”file.h” extern charfilename[];
char filename[];
在XXX.h中声明XXX.c中的变量和函数,可以防止大量修改命名,即在其他.c文件中调用#include ” XXX.h”即可实现变量和函数的扩展,无需在其他.c文件中调用extern。
第5章 库函数
5.1 返回整数的getchar()函数
int getchar(void);
#defineFOF -1
5.2 更新顺序文件
如果要同时进行输入和输出操作,必须在其中插入fseek()函数的调用。
5.3 缓冲输出与内存分配
C语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。
setbuf(stdout, buf); 可以通过调用fflush或直到buf缓冲区被填满,buf缓冲区中内容才实际写入到stdout中。缓冲区大小在<stdio.h>中的BUFSIZ定义。
5.4 使用errno检测错误
许多库函数,特别是那些鱼操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通过程序该函数调用错误。
/*调用库函数 */
if(返回的错误值)
检查errno
5.5 库函数signal
Signal库函数作为捕获异步事件的一种方式。
#include <signal.h>
signal(signaltype; handler function); // type表示signal函数将要捕获的信号类型;function是当指定的事件发生时,将要加以调用的事件处理函数。
signal函数能够做的安全事件只有设置一个标志位然后返回,期待以后主函数能够检查到这个标志位,发现一个信号已经发生。
对于算术运算错误,signal函数的唯一安全、可移植的操作就是打印一条出错消息,然后返回longjmp或exit立即退出程序。
第6章 预处理器
6.1 不能忽视宏定义中的空格
#define f (x) ((x)-1) // f(x)中间不能用空格
6.2 宏并不是函数
#define abs(x) (((x) >= 0) ? (x): -(x)) //这条语句是正确的
#define max(a, b) ((a) > (b) ? (a) : (b)) //这条语句是不完备的
当函数调用的系统开销大大于函数体内实际计算操作时,可选择宏定义来节省开销。
带参数的宏定义注意点:
(1)宏定义中的每个参数都要用括号括起来;
(2)整个结果表达式也应该用括号括起来;
(3)确保宏定义中的参数没有副作用,比如迭代、递增、三目运算等可能会导致参数多次求值。
6.3 宏并不是语句
用宏描述语句时,尽量写成一句,且代入原文中查看是否有错。assert宏,它的参数是一个表达式,如果该表达式为0(false),就使得程序终止执行,并给出一条适当的出错消息。
#define assert(e) ((void) ( (e) ||_assert_error( _FILE_, _LINE_ ))) //打印出错误的文件和行号
6.4 宏并不是类型定义
类型定义使用typedef,不用#define
第7章 可移植性缺陷
7.1 应对C语言标准变更
为了尽量增加程序的可移植性,让过去的工具能够继续工作,而放弃现在可能的收益,这种代价又未免过于昂贵,最好的做法也许是承认我们需要下定决心才能做出选择,因此必须谨慎对待,不能等闲视之。
7.2 标识符名称的限制
C语言区分大小写
7.3 整数大小
(1)short <=int <= long
(2)int型足够容下所有数组下标
(3)字符长度由硬件决定
应使用typedef long INT32; 等写明所占的大小,方便以后移植。
7.4 字符是有符号整数还是无符号整数
用unsignedchar声明字符,则为无符号整数;
用char声明字符,则可能为无符号整数,也可能为有符号整数。
字符转化为无符号整数时,使用(unsignedchar),不用(unsigned)。
7.5 移位运算符
(1)在向右移位时,对象是无符号数时,空出的位由0填充;对象是有符号数时,空出的位由符号位的副本填充。如果关注向右移位时空出的位,那么可以将操作的变量声明为无符号类型(unsigned char unsigned int…),那么空出来的位会被设置成0。
(2)如果被移位的对象长度是n位,那么移植计数必须大于或等于0,而严格小于n。
当有符号整数为负数时,右移并不等于除以2的某次幂。
7.6 内存位置0
在所有C程序中,不允许使用空指针进行除赋值和比较之外的操作。要检查这类问题的最简单办法是,把程序移到不允许读取内存位置0的机器上运行。
7.7 除法运算时发生的截断
在除法运算中,当被除数为负数时,(-3)/2 = -1 (-3)%2 = -1 或 (-3)/2 =-2 (-3)%2 = 1,由编译器决定,但大多数是第一种情况。(3)/2 = 1 (3)%2 = 1
7.8 随机数的大小
C语言应该在开头包含#include<stdlib.h>;C++中应该在开头包含#include <cstdlib.h>。
rand()是伪随机数,即按照一定顺序出现的随机数,每次重新开始,出现的顺序相同。数为0到RAND_MAX之间。rand()%a可得到0~a-1之间的数。srand()为随机数生成器的初始化器,通过指定srand(unsigned int)中的参数,可以实现真正的随机数。RAND_MAX与系统有关,无法修改,只读。
7.9 大小写转换
使用函数实现,健壮但慢 使用宏定义,快但不健壮,需要考虑不为字母的时候
int toupper (int c) #define _toupper(c) ((c) + ‘A’ – ‘a’)
{ #define _tolower(c) ((c) + ‘a’ – ‘A’)
if ( c >= ‘a’ && c <= ‘z’)
return c + ‘A’ - ‘a’;
return c;
}
7.10 内存首先释放,然后重新分配
malloc分配内存大小;realloc调整内存大小;free释放内存。
7.11 可移植性问题的一个例子
因为基于2的补码的计算机一般允许表示的负数取值范围要大于正数的取值范围,所以不能直接将负数直接添加负号转出正数。实际大多数工作是确保边界条件的正确性。