第1章 词法“陷阱”
=
不同于==
,一般情况下,如果是和常量比较,常量写到前面&
和|
不同于&&
和||
,- 词法分析中的“贪心法”;
y= x/*p
,会贪心的以为是注释开始,应该要插入空格,表明后面是指针; - 整型常量,如果第一个字符是
0
,则被认为是八进制数; - 字符和字符串;单引号的字符实际代表一个整数,而双引号引起的单个字符是字符串,代表的却是一个指向无名数组起始字符的指针。
第2章 语法“陷阱”
- 理解函数声明
(*(void(*)())0)();
任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符。
float *g(); /* g是一个函数,该函数的返回值类型为指向浮点数的指针 */
float (*h)(); /* h 是一个函数指针,h所指向函数的返回值为浮点类型*/
(float (*)()); /* 表示一个“指向返回值为浮点类型的函数的指针”的类型转换符*/
(*fp)(); /* 调用该函数的方式*/
void (*fp)(); /* 指向返回值为void类型的函数的指针*/
void (*)())0; /* 将常数0转型为“指向返回值为void的函数的指针"类型*/
- 运算符的优先级问题;
if (flags & FLAG != 0) // 需改为 if (flags & (FLAG != 0 ))
!=
运算符的优先级要高于 &
运算符
+,-
的运算优先级要高于移位运算符
数组下标、函数调用操作符各结构成员选择操作符,它们都是自左于右结合的,因此 a.b.c
的含义是 (a.b).c
而不是 a.(b.c)
单目运算符的优先级仅次于前述运算符,在所有的真正意义上的运算符中,它们的优先级最高。
优先级比单目运算符要低的,接下来就是双目运算符,在双目运算符中,算术运算符的优先级最高,移位运算符次之,关系运算符再次之,接着是逻辑运算符,赋值运算符,最后是条件运算符;在所有的运算符中,逗号运算符的优先级最低;
-
注意作为语句结束标志的分号;一个重要的例外情况是在
if
或者while
语句之后多了一个分号;例如:
if (x[i] > big); big = x[i];
如果不是多了一个分号,而是遗漏了一个分号,同样会招致麻烦,例如:
if (n < 3)
return
logrec.date = x[0];
-
switch
语句,每个case
都会跟一个break
-
函数调用,
f()
和f
; -
“悬挂”else
引发的问题;
if (x==0)
if (y == 0) error();
else
{
z = x + y;
f(\&z);
}
所以处理策略是,在每一个if
和 else
下面的执行语句都要加上 {}
第3章 语义“陷阱”
- 指针与数组;
C
语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来;对于一个数组,我们只能够做两件事,确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们看上去是以数组下标进行运算的,实际上都是通过指针进行的。
int calendar[12][31]
; 这个语句声明了calendar
是一个数组,该数组拥有12个
数组类型的元素,其中每个元素都是一个拥有31
个整型元素的数组,而不是一个拥有31
个数组类型的元素的数组。
- 非数组的指针;在
C
语言中,字符串常量代表了一块包含字符串中所有字符以及一个空字符(’\0’)
的内存区域的地址。因为C语言要求字符串常量以空字符作为结束标志,对于其他字符串,C程序员通常也沿用了这一惯例。
char *r;
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
应改为:
char *r;
r = malloc(strlen(s) + strlen(t) + 1);
if (!r)
{
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
/* */
free(r);
-
作为参数的数组声明,将数组作为函数参数毫无意义,所以,
C
语言中会自动地将作为参数的数组声明转换为相应的指针声明。 -
避免
“举隅法”
:混淆指针与指针所指向的数据;复制指针并不同时复制指针所指向的数据;
-
空指针并非空字符串
当常数
0
被转换为指针使用时,这个指针绝对不能被接触引用,换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容; -
边界计算与不对称边界
通用原则:
a. 首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则一;
b. 仔细计算边界,绝不掉以轻心,这是原则二;
-
求值顺序,
a < b && c < d;
只要a < b
为真时,才比较c
与d
;C
语言中只有四个运算符(&&
、||
、?
:和,
)存在规定的求值顺序,运算符&&
和 运算符||
首先对左侧操作数求值,只在需要时才对右侧操作数求值。运算符?:
有三个操作数,在a?b:c
中,操作数a
首先被求值,根据a
的值再求操作数b
或c
的值。而逗号运算符,首先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值; -
运算符
&&
、||
和!
按位运算符
&
、|
和~
; 逻辑运算符&&
、||
、!
; -
整数溢出
如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发送。当两个操作数都是有符号整数时,“溢出”就有可能发生;
if (a+b < 0)
complain()
用a+b
与0
比较,当加法操作发生“溢出”时,这个内部寄存器的状态是溢出而不是负,那么 if
的检查就会失败;
一种正确的方式是将 a
和b
都强制转换为无符号整数;
if ((unsigned) a + (unsigned)b > INT_MAX)
complain();
INT_MAX
是一个已定义常量,代表可能的最大整数值;
- 为函数
main
提供返回值
第4章 连接
某些C
语言实现提供了一个称为lint
的程序,可以捕获大量的此类错误,但遗憾的是并非全部的C
语言实现都提供了该程序,如果能够找到诸如lint
的程序,就一定要善加利用。
-
什么是链接器?
典型的链接器把编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行,其中某些目标模块是直接作为输入提供给链接器的;另外一些目标模块则是根据连接过程的需要
-
声明与定义
-
命名冲突与
static
修饰符如果在两个不同的源文件中都包括了定义
int a
;那么它或者表示程序错误,或者在两个源文件中共享
a
的同一个实例;
static
修饰符是一个能够减少此类命名冲突的有用工具,例如:以下声明语句
static int a;
其含义与下面的语句相同;
只不过,a
的作用域限制在一个源文件内,对于其他源文件,a
是不可见的。如果若干个函数需要共享一组外部对象,可以将这些函数放到一个源文件中,把它们需要用到的对象也都在同一个源文件中以static
修饰符声明。
static
修饰符不仅适用于变量,也适用于函数,如果函数 f 需要调用另一个函数 g
,而且只有函数f
需要调用函数 g
, 我们可以把函数f
与函数 g
都放到同一个源文件中,并且声明函数g
为static
。
-
形参、实参与返回值
任何一个C
函数都有返回类型,要么是void
,要么是函数生成结果的类型。函数的返回类型理解起来要比参数类型相对容易一些。
如果一个函数在被定义或声明之前被调用,那么它的返回类型就默认为整型。 -
检查外部类型
char filename[] = "/etc/passwd"; /* 文件1 */
extern char* filename; /* 文件2 */
修改为如下
char filename[] = "/etc/passwd"; /* 文件1 */
extern char filename[]; /* 文件2 */
或
char* filename = "/etc/passwd"; /* 文件1 */
extern char* filename; /* 文件2 */
- 头文件
第5章 库函数
C
语言中没有定义输入/输出语句,任何一个有用的C
程序都必须调用库函数来完成最基本的输入/输出操作;有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件。如果库文件的编写者已经提供了精确描述库函数的头文件,不去使用它们就真是愚不可及;
- 返回整数的 getchar 函数
/* 许多编译器对getchar的返回值做了截断处理,并把低端字节部分赋值给了变量c,
但是,它们在比较表达式中并不是比较c 与 EOF,而是比较 getchar 函数的返回值与 EOF! */
while (( c = getchar()) != EOF)
-
更新顺序文件
-
缓冲输出与内存分配
C语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。这种控制能力一般是通过库函数
setbuf
实现的。
/* 通知输入/输出库,所有写入到stdout的输出都应该使用buf 作为输出缓冲区,
直到buf 缓冲区被填满或者程序员直接调用fflush;调用fflush 将导致输出缓冲区的
内容被实际写入该文件*/
setbuf(stdout, buf);
-
使用
errno
检测错误 -
库函数
signal
实际上所有的
C
语言实现中都包括有signal
库函数,作为捕获异步事件的一种方式,要使用该库函数,需要在源文件中加上
#include <signal.h>
可以这样调用 signal 函数, signal (signal type, handler function)
当一个程序异常终止是,程序输出的最后几行常常会丢失,原因是什么?我们能够采取怎么样的措施来解决这个问题?
答:一个异常终止的程序可能没有机会来清空其输出缓冲区,因此,该程序生成的输出可能位于内存的某个位置,但却永远不会被写出了,
对于试图调试这类程序的编程者来说,这种丢失输出的情况经常会误导他们,因为它会造成这样一种印象,程序发生失败的时刻比实际上运行失败的真正时刻要早得多。解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下:
setbuf(stdout, (char*)0);
这个语句必须在任何输出被写入到stdout
之前执行。该语句最恰当的位置就是作为main
函数的第一个语句;
第6章 预处理器
虽然宏非常有用,它提供了一种对C
程序的字符进行编号的方式,而并不作用于程序中的对象;因为宏既可以使一段看上去完全不合语法的代码成为一个有效的C
程序,也能使一段看上去无害的代码成为一个可怕的怪物;
-
不能忽视宏定义中的空格
-
宏并不是函数;宏从表面上看其行为与函数非常相似,注意宏定义中出现的所有这些括号,它们的作用是预防引起与优先级有关的问题;因此,最好在宏定义中把每个参数都用括号括起来,同样,整个结果表达式也应该用括号括起来,
使用宏的另一个危险是,宏展开可能产生非常庞大的表达式,占用的空间远远超过了编程者所期望的空间;
-
宏并不是语句
-
宏并不是类型定义
宏的一个常见用途是,使多个不同变量的类型可在一个地方说明。
宏定义的优点——可移植性,得到了所有C编译器的支持,但是,我们最好还是使用类型定义;
#define T1 struct foo *
typedef struct foo * T2;
T1 a, b; /* 被扩展为 struct foo * a, b; 与实际要表达的意思有所差别 */
T2 a, b; /* 可以正确的将指针类型一并引入 */
第7章 可移植性缺陷
-
应对C语言标准变更
-
标识符名称的限制,长度限制,会截断,
-
整数的大小,字符长度由硬件特性决定;
-
字符是有符号整数还是无符号整数?
-
移位运算符
右移时,空出的位是由
0
填充,还是由符号位的副本填充C语言实现既可以用0填充空出的位,也可以用符号位的副本填充空出的位。
移位计数允许的取值范围是什么?如果被移位的对象长度是
n
位,那么移位计数必须大于或等于0
,而严格小于n
-
内存位置
0
,null
指针并不指向任何对象,除非用于赋值或比较运算,其他都非法。 -
除法运算时发生的截断;正数和负数除以一个数,它们的商和余数不一定满足(商 * 除数 + 余数 = 被除数)
-
随机数的大小范围,根据硬件决定
-
大小写转换;
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
首先释放,然后重新分配
malloc, realloc, free,
如果ptr
指向的是一块最近一次调用malloc
, realloc
, calloc
分配的内存,即使这块内存已被释放, realloc
函数仍然可以工作,因此,可以通过调节 free
, malloc
和 realloc
的调用顺序,充分利用 malloc
函数的搜索策略来压缩存储空间;
第8章 建议
- 不要说服自己相信“皇帝的新装”,
- 直截了当地表明意图,用括号或其他方式让你的意图尽可能清楚明了;
- 考察最简单的特例;
- 使用不对称边界
- 注意潜伏在暗处的bug
- 防御性编程;