C Traps And Pitfalls(一)
2016-08-03 本文已影响25人
每晚一句安
第0章 导读
0.0 程序的两种错误
- 能被编译器检测出来的
- 顺利通过编译、没有任何警告或者错误消息,但结果不是想要的
0.1 心智模式
人们深植心中,对于周遭世界如何运作的看法和行文
先入为主
0.2 章节介绍
- 词法分析
- 语法细节
- 语义细节
- 连接
- 库函数
- 预处理
- 可移植性
- 预防性程序设计
第1章 词法“陷阱”
1.0 词法分析器
词法分析器:编译器中负责将程序分解为一个一个符号的部分
符号:token, 程序的一个基本组成单元(符号组成相同的字符序列,在不同环境可能不同)
Note: 在C语言中,符号之间的空白(包括制表符、空格符或换行符)将被忽略
1.1 = 不同于 ==
本意比较是否相等却写成赋值
1.2 & 和 | 不同于 && 和 ||
& | 为按位运算符,逐位进行与、或操作
&& ||为逻辑运算符
1.3 词法分析中的“贪心法”
- 单符号(/、*、=等)多符号(/*、==等)
- 编译器的贪心法:每一个符号应该包括尽可能多的字符,直到读入的字符组成的字符串不再可能组成一个有意义的符号
y = x/*p /* p指向除数 */ //此处/*被认为是注释开始,不断读入字符直到遇到*/
1.4 整形常量
为了对齐,无意将十进制前面补0,变为了八进制
1.5 字符与字符串
- 分清 '' 与 ""
char c = 'hello'
猜猜会发生什么?
做法一:依次用后一个字符覆盖前一个字符,变为c = 'o'
- "/*" 中 /* 属于字符串的一部分,/*""*/ 中 "" 属于注释一部分
第2章 语法“陷阱”
2.1 理解函数声明
- 构造规则:按照使用的方式来声明
float f; //浮点型变量
float ff(); //返回值是浮点型的函数
float *g(); //返回值是指向浮点型的指针,指针函数
float (*f)(); //h是一个函数指针,指向的函数的返回值为浮点型
(float (*)()); //“指向返回值是浮点类型的函数指针”的类型转化符
2.2 运算符优先级问题
优先级 | 运算符 | 结合方向 |
---|---|---|
1 | () [] -> . | 自左向右 |
2 | ! ~ ++ -- - (type) * & sizeof | 自右向左 |
3 | / * % | 自左向右 |
4 | + - | 自左向右 |
5 | >> << | 自左向右 |
6 | > >= < <= | 自左向右 |
7 | == != | 自左向右 |
8 | & | 自左向右 |
9 | ^ | 自左向右 |
10 | l | 自左向右 |
11 | && | 自左向右 |
12 | ll | 自左向右 |
13 | ?: | 自右向左 |
14 | = /= *= %= += -= <<= >>= &= ^= l= | 自右向左 |
15 | , | 自左向右 |
非正常 -> 单目 -> 算术 -> 移位 -> 关系 -> 逻辑 -> 赋值 -> 条件 -> 逗号
2.3 注意作为语句结束的分号
if/while/struct
等结尾
2.4 switch语句
switch (number) {
case 1: //TODO; break;
case 2: //TODO; break;
...
case n: //TODO; break;
default: //TODO; break;
}
2.5 函数调用
在函数调用时即使函数不带参数,也应该包括参数列表
f(); //函数调用
f; //计算f的地址,却并不调用该函数
2.6 “悬挂” else 引发的问题
else 始终与同一对括号内最近的 if 结合
习题
int day[] = {1, 2, 3, 4, 5, 6,}; //多余的逗号,让自动化的代码工具生成代码,
//保持每项格式一样,方便添加、删除、重排各项
第3章 语义“陷阱”
3.1 指针与数组
- 多维数组可以看做是一维数组组成,只不过每个元素也是数组
int can=lendar[12][31]; //拥有12个数组类型的元素,每个元素都是拥有31个整形元素的数组
- ANSI C 规定数组大小必须在编译期就作为一个常数确定下来
C99 允许变长数组(VLA)
- 多维数组的指针
// 清空calendar数组,数组指针补上界
int calendar[12][31];
int (*)monthp[31];
for (monthp = calendar; monthp < &calendar[12]; monthp++) {
int *dayp;
for (dayp = *monthp; dayp < &(*monthp)[31]; dayp++) {
*dayp = 0;
}
}
3.2 非数组的指针
// 将字符串s和t连接起来成为r
char *r;
strcpy(r, s);
strcat(r, t);
eg.1:不能满足要求,因为不能确定r指向何处,不仅要让其转向一个地址,而且该地址应该能容纳字符串。
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
eg.2:还是错的。
- malloc可能无法提供请求的内存
- 给r分配的内存使用完后应该释放
- 字符串结尾还有个'\0'
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if (!r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
free(r);
3.3 作为参数的数组声明
int strlen(char s[]) //值传递,
{
//TODO
}
int strlen(char *s) //地址传递
{
//TODO
}
eg.1 写法相同, 可以认为作用等价。但值传递,数组元素需全放入栈中,而且编译程序需要专门产生一部分用来复制初始数组的代码
3.4 避免“举隅法”
char *p, *q;
p = "xyz";
q = p;
q[1] = 'Y';
- p 指向的内存中变为了"xYz"
- ANSI C 禁止对字符串常量修改, C99 可以
3.5 空指针并非空字符串
define NULL 0
将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。
if(p == (char *)0) ... //合法
if(strcmp(p, (char *)0) == 0)... //非法
3.6 边界计算与不对称边界
int i, a[10];
for (i = 0; i <= 10; i++) {
a[i] = 0;
}
i 计到10,循环体内将并不存在的
a[10]
设置为0,实际是将计算器 i 的值设为0,死循环
栏杆错误
- 考虑最简单特例,再外推
- 仔细计算边界
不对称边界
x >= 16 && x <= 37
入界点16
出界点38
- 可以利用数组“溢界”元素的地址,但不能引用该元素
3.7 求值顺序
- 某些运算符按已知、规定顺序求值
- 存在特定的求值顺序
- && 与 || 先对左侧求值,只在需要时才对右侧求值
- ?: 在
a ? b: c
中a先被求值,再根据a的值求b或者c- , 先对左侧的求值,然后舍弃,在对右侧求值
f(x, y)
中求值顺序未定义,此时逗号不是逗号运算符
g((x, y))
先求x后舍弃,再求y,让其作为唯一的参数
3.8 && 、|| 和 !
1. 按位运算符
& | ~
对操作数逐位操作
2. 逻辑运算符
&& || !
结果只有真假
3.9 整数溢出
// 检查 a+b 是否溢出
int a, b;
if (a+b < 0)
complain();
不能正常运行。在某些机器上,通过设置状态寄存器来记录,这样就需判断状态位
- 将 a 和 b 转化为无符号型整数
if ((unsigned)a+(unsigned)b) < INT_MAX) //<limits.h> complain();
- 不需要转换
if(a > INT_MAX - b) complain();
3.10 main函数的返回值
return 0
返回值代表程序是否成功执行- 缺省返回值类型,默认为
int
型