C Traps And Pitfalls(一)

2016-08-03  本文已影响25人  每晚一句安

第0章 导读


0.0 程序的两种错误

  1. 能被编译器检测出来的
  2. 顺利通过编译、没有任何警告或者错误消息,但结果不是想要的

0.1 心智模式

人们深植心中,对于周遭世界如何运作的看法和行文

先入为主

0.2 章节介绍

  1. 词法分析
  2. 语法细节
  3. 语义细节
  4. 连接
  5. 库函数
  6. 预处理
  7. 可移植性
  8. 预防性程序设计

第1章 词法“陷阱”


1.0 词法分析器

词法分析器:编译器中负责将程序分解为一个一个符号的部分

符号:token, 程序的一个基本组成单元(符号组成相同的字符序列,在不同环境可能不同)

Note: 在C语言中,符号之间的空白(包括制表符、空格符或换行符)将被忽略

1.1 = 不同于 ==

本意比较是否相等却写成赋值

1.2 & 和 | 不同于 && 和 ||

& | 为按位运算符,逐位进行与、或操作
&& ||为逻辑运算符

1.3 词法分析中的“贪心法”

  1. 单符号(/、*、=等)多符号(/*、==等)
  2. 编译器的贪心法:每一个符号应该包括尽可能多的字符,直到读入的字符组成的字符串不再可能组成一个有意义的符号
y = x/*p   /* p指向除数 */  //此处/*被认为是注释开始,不断读入字符直到遇到*/

1.4 整形常量

为了对齐,无意将十进制前面补0,变为了八进制

1.5 字符与字符串

  1. 分清 '' 与 ""
  2. char c = 'hello' 猜猜会发生什么?
    做法一:依次用后一个字符覆盖前一个字符,变为 c = 'o'
  3. "/*" 中 /* 属于字符串的一部分,/*""*/ 中 "" 属于注释一部分

第2章 语法“陷阱”


2.1 理解函数声明

  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 指针与数组

  1. 多维数组可以看做是一维数组组成,只不过每个元素也是数组
int can=lendar[12][31]; //拥有12个数组类型的元素,每个元素都是拥有31个整形元素的数组
  1. ANSI C 规定数组大小必须在编译期就作为一个常数确定下来
    C99 允许变长数组(VLA)
  1. 多维数组的指针
// 清空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:还是错的。

  1. malloc可能无法提供请求的内存
  2. 给r分配的内存使用完后应该释放
  3. 字符串结尾还有个'\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';
  1. p 指向的内存中变为了"xYz"
  2. 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,死循环

栏杆错误

  1. 考虑最简单特例,再外推
  2. 仔细计算边界

不对称边界

  1. x >= 16 && x <= 37 入界点16 出界点38
  2. 可以利用数组“溢界”元素的地址,但不能引用该元素

3.7 求值顺序

  1. 某些运算符按已知、规定顺序求值
  2. 存在特定的求值顺序
  • && 与 || 先对左侧求值,只在需要时才对右侧求值
  • ?: 在 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();

不能正常运行。在某些机器上,通过设置状态寄存器来记录,这样就需判断状态位

  1. 将 a 和 b 转化为无符号型整数
if ((unsigned)a+(unsigned)b) < INT_MAX)  //<limits.h>
    complain();
  1. 不需要转换
if(a > INT_MAX - b)
    complain();

3.10 main函数的返回值

  1. return 0 返回值代表程序是否成功执行
  2. 缺省返回值类型,默认为int
上一篇下一篇

猜你喜欢

热点阅读