C语言缺陷与陷阱(笔记)

 
C 言像一把雕刻刀, 利,并且在技 手中非常有用。和任何 利的工具一 C 到那些不能掌握它的人。本文介 C 害粗心的人的方法,以及如何避免 害。
第一部分 研究了当程序被划分 为记 生的 问题 第二部分 继续 研究了当程序的 号被 编译 声明、表达式和 会出 问题 第三部分 研究了由多个部分 成、分 别编译 定到一起的 C 程序。 第四部分 理了概念上的 解:当一个程序具体 生的事情。 第五部分 研究了我 的程序和它 所使用的常用 系。在 第六部分 中,我 注意到了我 所写的程序也 并不是我 所运行的程序; 预处 理器将首先运行。最后, 第七部分 讨论 了可移植性 问题 :一个能在一个 实现 中运行的程序无法在另一个 实现 中运行的原因。
法分析器( lexical analyzer 检查组 成程序的字符序列,并将它 划分 为记 号( token )一个 号是一个由一个或多 个字符构成的序列,它在 言被 编译时 具有一个(相 地) 一的意
C 程序被两次划分 为记 首先是 预处 理器 取程序 它必 须对 程序 号划分以 发现标识 宏的 标识 符。通 过对每 个宏 行求 来替 最后, 经过 宏替 的程序又被 集成字符流送 给编译 器。 编译 器再第二次将 个流划分 为记 号。
1.1= 不是 == C 是用 = 表示 赋值 而用 == 表示比 是因 为赋值 率要高于比 ,因此 其分配更短的符号。 C 赋值视为 一个运算符,因此可以很容易地写出多重 赋值 (如 a = b = c ),并且可以将 赋值 嵌入到一个大的表达式中。
C 言参考手册 明了如何决定: 如果 入流到一个 定的字符串 止已 识别为记 号, 则应该 包含 下一个字符以 成能 构成 号的最 的字符串 ” “ 子串原
    赋值 运算符如 += 实际 上是两个 号。因此,
a + /* strange */ = 1
a += 1
是一个意思。看起来像一个 独的 号而 实际 上是多个 号的只有 一个特例。特 地,
p - > a
是不合法的。它和
p -> a
不是同 义词
另一方面,有些老式 编译 是将 =+ 视为 一个 独的 号并且和 += 是同 义词
引号中的一个字符只是 写整数的另一 方法。 个整数是 定的字符在 实现 照序列中的一个 对应 。而一个包 在双引号中的字符串,只是 写一个有双引号之 的字符和一个附加的二 值为 零的字符所初始化的一个无名数 的指 的一 种简 短方法。
使用一个指 来代替一个整数通常会得到一个警告消息 (反之亦然),使用双引号来代替 引号也会得到一个警告消息(反之亦然)。但 于不 检查 参数 型的 编译 器却除外。
由于一个整数通常足 大,以至于能 放下多个字符,一些 C 编译 器允 在一个字符常量中存放多个字符。 意味着用 'yes' 代替 "yes" 将不会被 发现 。后者意味着 包含 y e s 和一个空字符的四个 连续 器区域中的第一个的地址 ,而前者意味着 在一些 实现 式中表示由字符 y e s 合构成的一个整数 两者之 的任何一致性都 属巧合。
理解 号是如何构成声明、表达式、 句和程序的。
C 量声明都具有两个部分:一个 型和一 具有特定格式的、期望用来 对该类 型求 的表达式。
float *g(), (*h)();
表示 *g() (*h)() 都是 float 表达式。由于 () * 定得更 密, *g() *(g()) 表示同 西: g 是一个返回指 float 的函数,而 h 是一个指向返回 float 的函数的指
当我 知道如何声明一个 型的 量以后,就能 很容易地写出一个 型的模型( cast ):只要 量名和分号并将所有的 西包 在一 对圆 括号中即可。
float *g();
声明 g 是一个返回 float 的函数,所以 (float *()) 就是它的模型。
(*(void(*)())0)(); 硬件会 用地址 0 的子程序
(*0)(); 这样 并不行,因 * 运算符要求必 有一个指 它的操作数 。另外, 个操作数必 是一个指向函数的指 ,以保 * 果可以被 用。需要将 0 转换为 一个可以描述 指向一个返回 void 的函数的指 型。 ( Void(*)())0
里,我 解决 问题时 没有使用 typedef 声明。通 使用它,我 可以更清晰地解决 问题
typedef void (*funcptr)(); // typedef funcptr void (*)() ; 指向返回 void 的函数的指针
(*(funcptr)0)();
// 调用地址为 0 处的子程序
定得最 密的运算符并不是真正的运算符:下 、函数 用和 选择 些都与左 关联
接下来是一元运算符。它 具有真正的运算符中的最高 。由于函数 用比一元运算符 定得更 密,你必 (*p)() p 指向的函数; *p() 表示 p 是一个返回一个指 的函数。 转换 是一元运算符,并且和其他一元运算符具有相同的 。一元运算符是右 合的,因此 *p++ 表示 *(p++) ,而不是 (*p)++
在接下来是真正的二元运算符。其中数学运算符具有最高的 ,然后是移位运算符、 系运算符、 逻辑 运算符、 赋值 运算符,最后是条件运算符。 需要 住的两个重要的 西是:
1.    所有的 逻辑 运算符具有比所有 系运算符都低的
2.    移位运算符比 系运算符 定得更 密,但又不如数学运算符。
乘法、除法和求余具有相同的 ,加法和减法具有相同的 ,以及移位运算符具有相同的
有就是六个 系运算符并不具有相同的 == != 比其他 系运算符要低。
逻辑 运算符中,没有任何两个具有相同的 。按位运算符比所有 序运算符 定得都 密, 每种 与运算符都比相 的或运算符 定得更 密,并且按位异或( ^ )运算符介于按位与和按位或之
    三元运算符的 比我 提到 的所有运算符的 都低。
个例子 还说 明了 赋值 运算符具有比条件运算符更低的 是有意 的。另外, 所有的 赋值 运算符具有相同的 并且是自右至左 合的
具有最低 的是逗号运算符。 赋值 是另一 运算符,通常具有混合的
或者是一个空 句,无任何效果;或者 编译 器可能提出一个 断消息,可以方便除去掉它。一个重要的区 是在必 跟有一个 句的 if while 句中。 另一个因分号引起巨大不同的地方是函数定 前面的 构声明的末尾 下面的程序片段:
struct foo {
    int x;
}

f() {
    ...
}
挨着 f 的第一个 } 后面 失了一个分号。它的效果是声明了一个函数 f ,返回 值类 型是 struct foo 构成了函数声明的一部分。如果 里出 了分号, f 将被定 义为 具有默 的整型返回 [5]
C 中的 case 标签 是真正的 标签 :控制流程可以无限制地 入到一个 case 标签 中。
    看看另一 形式,假 C 程序段看起来更像 Pascal
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
并且假 color 2 则该 程序将打印 yellowblue ,因 控制自然地 入到下一个 printf() 用。
既是 C switch 句的 点又是它的弱点。 它是弱点,是因 很容易忘 一个 break 句,从而 致程序出 现隐 晦的异常行 它是 点,是因 故意去掉 break 句,可以很容易 实现 其他方法 实现 的控制 构。尤其是在一个大型的 switch 句中,我 们经 发现对 一个 case 理可以 化其他一些特殊的 理。
和其他程序 设计语 言不同, C 要求一个函数 用必 有一个参数列表,但可以没有参数。因此,如果 f 是一个函数,
f();
就是 对该 函数 用的 句,而
f;
也不做。它会作 函数地址被求 ,但不会 用它 [6]
一个 else 是与其最近的 if 关联
一个 C 程序可能有很多部分 成,它 被分 别编译 ,并由一个通常称 为连 接器、 编辑 器或加 器的程序 定到一起。由于 编译 器一次通常只能看到一个文件,因此它无法 检测 到需要程序的多个源文件的内容才能 发现 错误
你有一个 C 程序,被划分 两个文件。其中一个包含如下声明:
int n;
而令一个包含如下声明:
long n;
不是一个有效的 C 程序,因 一些外部名称在两个文件中被声明 不同的 型。然而,很多 实现检测 不到 错误 ,因 为编译 器在 编译 其中一个文件 并不知道另一个文件的内容。因此, 检查类 型的工作只能由 接器(或一些工具程序如 lint )来完成;如果操作系 接器不能 识别 数据 型, C 编译 器也没法 多地 制它。
    个程序运行 时实际 生什 有很多可能性:
1.    实现 够聪 明,能 够检测 型冲突。 会得到一个 断消息, n 在两个文件中具有不同的 型。
2.    你所使用的 实现 int long 视为 相同的 型。典型的情 况是机器可以自然地 32 位运算。在 这种 情况下你的程序或 工作,好象你两次都将 量声明 long (或 int )。 这种 程序的工作 属偶然。
3.    n 的两个 例需要不同的存 ,它 以某 方式共享存 区,即 其中一个的 赋值对 另一个也有效。 可能 生,例如, 编译 器可以将 int 安排在 long 的低位。不 论这 是基于系 是基于机器的, 这种 程序的运行同 是偶然。
4.    n 的两个 例以另一 方式共享存 区,即 其中一个 赋值 的效果是 另一个 以不同的 。在 这种 情况下,程序可能失
这种 情况 生的 一个例子出奇地 繁。程 序的某一个文件包含下面的声明:
char filename[] = "etc/passwd";
而另一个文件包含 这样 的声明:
char *filename;
    尽管在某些 境中数 和指 的行 非常相似,但它 是不同的。在第一个声明中, filename 是一个字符数 的名字。尽管使用数 的名字可以 生数 第一个元素的指 ,但 个指 只有在需要的 候才 生并且不会持 。在第二个声明中, filename 是一个指 的名字。 个指 可以指向程序 员让 它指向的任何地方。如果程序 没有 一个 ,它将具有一个默 0 NULL )( [ ] 实际 上,在 C 中一个 初始化的指 通常具有一个随机的 是很危 的!)。
    两个声明以不同的方式使用存 区,它 不可能共存。
    避免 这种类 型冲突的一个方法是使用像 lint 这样 的工具(如果可以的 )。 了在一个程序的不同 编译单 元之 间检查类 型冲突,一些程序需要一次看到其所有部分。典型的 编译 器无法完成,但 lint 可以。
    避免 该问题 的另一 方法是将外部声明放到包含文件中 这时 ,一个外部 象的 一次 [7]
    一些 C 运算符以一 已知的、特定的 其操作数 行求 。但另一些不能。例如,考 下面的表达式:
a < b && c < d
C 言定 义规 a < b 首先被求 。如果 a 小于 b c < d 须紧 接着被求 算整个表达式的 。但如果 a 大于或等于 b c < d 根本不会被求
a < b 编译 a b 的求 就会有一个先后。但在一些机器上,它 是并行 行的。
C 中只有四个运算符 && || ?: , 指定了求 值顺 序。 && || 最先 的操作数 行求 ,而右 的操作数只有在需要的 候才 行求 。而 ?: 运算符中的三个操作数: a b c ,最先 a 行求 ,之后 仅对 b c 中的一个 行求 取决于 a , 运算符首先 的操作数 行求 ,然后抛弃它的 的操作数 行求 [8]
C 中所有其它的运算符 操作数的求 值顺 序都是未定 的。事 上, 赋值 运算符不 值顺 序做出任何保
    出于 个原因,下面 这种 将数 x 中的前 n 个元素 制到数 y 中的方法是不可行的:
i = 0;
while(i < n)
    y[i] = x[i++];
其中的 问题 y[i] 的地址并不保 i 之前被求 。在某些 实现 中, 是可能的;但在另一些 实现 中却不可能。另一 情况出于同 的原因会失
i = 0;
while(i < n)
    y[i++] = x[i];
而下面的代 是可以工作的:
i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}
当然, 可以
for(i = 0; i < n; i++)
    y[i] = x[i];
    在很多 言中,具有 n 个元素的数 其元素的号 和它的下 是从 1 n 对应 的。但在 C 中不是 这样
个具有 n 个元素的 C 中没有下 标为 n 的元素,其中的元素的下 是从 0 n - 1 。因此从其它 C 言的程序 员应该 小心地使用数
int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;
    下面的程序段由于两个原因会失
double s;
s = sqrt(2);
printf("%g\n", s);
    第一个原因是 sqrt() 需要一个 double 它的参数,但没有得到。第二个原因是它返回一个 double 但没有 这样 声名。改正的方法只有一个:
double s, sqrt();
s = sqrt(2.0);
printf("%g\n", s);
C 中有两个 简单 规则 控制着函数参数的 转换 (1) int 短的整型被 转换为 int (2) double 短的浮点 转换为 double 。所有的其它 不被 转换 确保函数参数 型的正确性是程序 任。
因此,一个程序 如果想使用如 sqrt() 这样 接受一个 double 型参数的函数,就必 须仅传递给 float double 型的参数。常数 2 是一个 int ,因此其 型是 错误 的。
    当一个函数的 被用在表达式中 ,其 会被自 转换为 适当的 型。然而, 了完成 个自 动转换 编译 器必 知道 函数 实际 返回的 型。没有更 声名的函数被假 返回 int ,因此声名 这样 的函数并不是必 的。然而, sqrt() 返回 double ,因此在成功使用它之前必 要声名。
里有一个更加壮 的例子:
main() {
    int i;
    char c;
    for(i = 0; i < 5; i++) {
        scanf("%d", &c);
        printf("%d", i);
    }
    printf("\n");
}
    表面上看, 个程序从 入中 取五个整数并向 出写入 0 1 2 3 4 实际 上,它并不 这么 做。譬如在一些 编译 器中,它的 0 0 0 0 0 1 2 3 4
    ?因 c 的声名是 char 而不是 int 。当你令 scanf() 取一个整数 ,它需要一个指向一个整数的指 。但 里它得到的是一个字符的指 。但 scanf() 并不知道它没有得到它所需要的:它将 入看作是一个指向整数的指 并将一个整数存 到那里。由于整数占用比字符更多的内存, 这样 做会 影响到 c 附近的内存。
    c 附近确切是什 编译 器的事;在 这种 情况下 有可能是 i 的低位。因此, 当向 c 入一个 i 就被置零。当程序最后到达文件 scanf() 不再 尝试 c 中放入新 i 才可以正常地增 ,直到循 环结 束。
 
C 程序通常将一个字符串 转换为 一个以空字符 尾的字符数 。假 有两个 这样 的字符串 s t ,并且我 想要将它 们连 一个 独的字符串 r 。我 通常使用 函数 strcpy() strcat() 来完成。下面 这种 的方法并不会工作:
char *r;
strcpy(r, s);
strcat(r, t);
是因 r 没有被初始化 指向任何地方。尽管 r 可能潜在地表示某一 内存,但 并不存在,直到你分配它。
    试试 r 分配一些内存:
char r[100];
strcpy(r, s);
strcat(r, t);
只有在 s t 所指向的字符串不很大的 候才能 工作。不幸的是, C 要求我 们为 指定的大小是一个常数,因此无法确定 r 是否足 大。然而,很多 C 实现带 有一个叫做 malloc() 函数,它接受一个数字并分配 这么 多的内存。通常 有一个函数称 strlen() ,可以告 一个字符串中有多少个字符:因此,我 可以写:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
    然而 个例子会因 两个原因而失 。首先, malloc() 可能会耗尽内存,而 个事件 静静地返回一个空指 来表示。
    其次,更重要的是, malloc() 并没有分配足 的内存。一个字符串是以一个空字符 束的。而 strlen() 函数返回其字符串参数中所包含字符的数量,但不包括 尾的空字符。因此,如果 strlen(s) n s 需要 n + 1 个字符来盛放它。因此我 需要 r 分配 外的一个字符。再加上 检查 malloc() 是否成功,我 得到:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r) {
    complain();
    exit(1);
}
strcpy(r, s);
strcat(r, t);
  法( Synecdoche, sin-ECK-duh-key )是一 文学手法,有点 似于明 或暗 ,在牛津英文 典中解 如下: “a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc. (将全面的 位用作不全面的 位,或反之;如整体 局部或局部 整体、一般 特殊或特殊 一般,等等。)
住的是, 制一个指 并不能 制它所指向的 西
将一个整数 转换为 一个指 果是 实现 的( implementation-dependent ),除了一个例外。 个例外是常数 0 ,它可以保 转换为 一个与其它任何有效指 都不相等的指 通常 这样
#define NULL 0
但其效果是相同的。要 住的一个重要的事情是,当用 0 针时 它决不能被解除引用 话说 ,当你将 0 赋给 一个指 针变 量后,你就不能 访问 它所指向的内存。不能 这样 写:
if(p == (char *)0) ...
也不能 这样 写:
if(strcmp(p, (char *)0) == 0) ...
strcmp() 是通 其参数来 看内存地 址的。
如果 p 是一个空指 这样 写也是无效的:
printf(p);
printf("%s", p);
C 于整数操作的上溢或下溢定 得非常明确。
    只要有一个操作数是无符号的, 果就是无符号的,并且以 2n 模,其中 n 。如果两个操作数都是 符号的, 则结 果是未定 的。
例如,假 a b 是两个非 整型 量,你希望 测试 a + b 是否溢出。一个明 法是 这样 的:
if(a + b < 0)
    complain();
通常, 是不会工作的。
    一旦 a + b 生了溢出, 果的任何 注都是没有意 的。例如,在某些机器上,一个加法运算会将一个内部寄存器 :正、 、零或溢出。 这样 的机器上, 编译 器有 将上面的例子 实现为 首先将 a b 加在一起,然后 检查 内部寄存器状 是否 为负 。如果 运算溢出,内部寄存器将 于溢出状 测试 会失
    使 个特殊的 测试 成功的一个正确的方法是依 于无符号算 的良好定 ,即要在有符号和无符号之 间进 转换
if((int)((unsigned)a + (unsigned)b) < 0)
    complain();
两个原因会令使用移位运算符的人感到 烦恼
1.    在右移运算中,空出的位是用 0 填充 是用符号位填充?
2.    移位的数量允 使用哪些数?
第一个 问题 的答案很 简单 ,但有 实现 的。如果要 行移位的操作数是无符号的,会移入 0 。如果操作数是 符号的, 则实现 决定是移入 0 是移入符号位。如果在一个右移操作中你很 心空位,那 unsigned 来声明 量。 这样 你就有 空位被 0
    第二个 问题 的答案同 样简单 :如果待移位的数 n 移位的数量必 大于等于 0 并且 格地小于 n 。因此,在一次 独的操作中不可能将所有的位从 量中移出
例如,如果一个 int 32 位,且 n 是一个 int ,写 n << 31 n << 0 是合法的,但 n << 32 n << -1 是不合法的。
    注意,即使 实现 将符号 移入空位, 一个 符号整数的右移运算和除以 2 的某次 也不是等价的。 一点,考 (-1) >> 1 是不可能 0 的。 [ 注: (-1) / 2 果是 0 ]
下面的程序:
#include

main() {
    char c;
//int c

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    段程序看起来好像要将 制到 出。 实际 上,它并不完全会做 些。
    原因是 c 被声明 字符而不是整数。 意味着它将不能接收可能出 的所有字符包括 EOF
因此 里有两 可能性。有 一些合法的 入字符会 c EOF 相同的 ,有 又会使 c 无法存放 EOF 。在前一 情况下,程序会在文件的中 停止 制。在后一 情况下,程序会陷入一个无限循
    实际 上, 存在着第三 可能:程序会偶然地正确工作。 C 言参考手册 格地定 了表达式
((c = getchar()) != EOF)
果。其 6.1 中声明:
当一个较长的整数被转换为一个短的整数或一个char,它会被截去左;超出的位被简单弃。
7.14 声明:
存在着很多赋值运算符,它都是从右至左合的。它都需要一个左的操作数,而赋值表达式的型就是其左的操作数的型。其就是已经赋过值的左操作数的
两个条款的 合效果就是必 过丢 getchar() 果的高位,将其截短 字符,之后 个被截短的 再与 EOF 行比 。作 为这 个比 的一部分, c 一个整数,或者采取将左 的位用 0 填充,或者适当地采取符号 展。
然而,一些 编译 器并没有正确地 实现这 个表达式。它 getchar() 的低几位 赋给 c 。但在 c EOF 的比 中,它 却使用了 getchar() 这样 做的 编译 器会使 个事例程序看起来能 正确地 工作。
立即安排 出的 示通常比将其 暂时 保存在一大 一起 出要昂 得多。因此, C 实现 通常允 程序 控制 生多少 出后在 实际 地写出它
    个控制通常 一个称 setbuf() 函数。如果 buf 是一个具有适当大小的字符数
setbuf(stdout, buf);
将告 I/O 写入到 stdout 中的 出要以 buf 一个 冲,并且等到 buf 了或程序 直接 fflush() 实际 写出。 冲区的合适的大小 在中定 义为 BUFSIZ
因此,下面的程序解 了通 使用 setbuf() 讲标 制到 出:
#include

main() {
    int c;

    char buf[BUFSIZ];
    setbuf(stdout, buf);

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    不幸的是, 个程序是 错误 的,因 一个 微的原因。
    要知道毛病出在哪,我 需要知道 冲区最后一次刷新是在什 么时 候。答案;主程序完成之后, 将控制交回到操作系 之前所 行的清理的一部分。在 刻, 冲区已 放了
    有两 方法可以避免 问题
    首先,使用静 态缓 冲区,或者将其 式地声明
static char buf[BUFSIZ];
或者将整个声明移到主函数之外。
    另一 可能的方法是 动态 地分配 冲区并且从不 放它:
char *malloc();
setbuf(stdout, malloc(BUFSIZ));
注意在后一 情况中,不必 检查 malloc() 的返回 ,因 如果它失 了,会返回一个空指 。而 setbuf() 可以接受一个空指 其第二个参数, 将使得 stdout 成非 冲的。 会运行得很慢,但它是可以运行的。
由于宏可以象函数那 ,有些程序 就会将它 们视为 等价的。因此,看下面的定
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏体中所有的括号。它 了防止出 a b 有比 > 低的表达式的情况。
    一个重要的 问题 是,像 max() 这样 个操作数都会出 两次并且会被求 两次 。因此,在 个例子中,如果 a b 大, a 就会被求 两次:一次是在比 候,而另一次是在 max() 候。
    是低效的, 错误
bi ggest = x[0];
i = 1;
while(i < n)
    biggest = max(biggest, x[i++]);
max() 是一个真正的函数 会正常地工作,但当 max() 是一个宏的 候会失 。譬如,假 x[0] 2 x[1] 3 x[2] 1 。我 来看看在第一次循 环时 生什 赋值语 句会被
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先, biggest x[i++] 行比 。由于 i 1 x[1] 3 系是 。其副作用是, i 2
    由于 系是 x[i++] 赋给 biggest 。然而, 这时 i 2 了,因此 赋给 biggest x[2] ,即 1
避免 问题 的方法是保 max() 宏的参数没有副作用:
biggest = x[0];
for(i = 1; i < n; i++)
    biggest = max(biggest, x[i]);
有一个危 的例子是混合宏及其副作用 是来自 UNIX 第八版的中 putc() 宏的定
#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc() 的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据 构的指 。注意第一个参数完全可以使用如 *z++ 西,尽管它在宏中两次出 ,但只会被求 一次。而第二个参数会被求 两次(在宏体中, x 了两次,但由于它的两次出 在一个 : 的两 ,因此在 putc() 的一个 例中它 之中有且 有一个被求 )。由于 putc() 中的文件参数可能 有副作用, 会出 现问题 。不 ,用 手册文档中提到: 由于 putc() 实现为 宏,其 stream 可能会具有副作用。特 putc(c, *f++) 不能正确地工作。 但是 putc(*c++, f) 实现 中是可以工作的。
有些 C 实现 很不小心。例如,没有人能正确 putc(*c++, f) 。另一个例子,考 很多 C 中出 toupper() 函数。它将一个小写字母 转换为 的大写字母,而其它字符不 。如果我 所有的小写字母和所有的大写字母都是相 的(大小写之 可能有所差距) ,我 可以得到 这样 的函数:
toupper(c) {
    if(c >= 'a' && c <= 'z')
        c += 'A' - 'a';
    return c;
}
在很多 C 实现 中, 了减少比 实际计 要多的 开销 ,通常将其 实现为 宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' - 'a') : (c))
很多 比函数要快。然而,当你 着写 toupper(*p++) ,会出 奇怪的 果。
    另一个需要注意的地方是使用宏可能会 生巨大的表达式。例如, 继续 max() 的定
#define max(a, b) ((a) > (b) ? (a) : (b))
们这 个定 a b c d 中的最大 。如果我 直接写:
max(a, max(b, max(c, d)))
它将被
((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ?
 (a) : (((b) > (((c) > (d) ?
(c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
出奇的 大。我 可以通 平衡操作数来使它短一些:
max(max(a, b), max(c, d))
会得到:
((((a) > (b) ? (a) : (b))) > (((c) > (d) ? (c) : (d))) ?
 (((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
看起来 是写:
biggest = a;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
好一些。
宏的一个通常的用途是保 不同地方的多个事物具有相同的 型:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;
程序 可以通 只改 程序中的一行就能改 a b c 型,尽管 a b c 可能声明在很 的不同地方。
    使用 这样 的宏定 义还 有着可移植性的 优势 —— 所有的 C 编译 器都支持它。很多 C 编译 器并不支持另一 方法:
typedef struct foo FOOTYPE;
FOOTYPE 义为 一个与 struct foo 等价的新 型。
    种为类 型命名的方法可以是等价的,但 typedef 更灵活一些。例如,考 下面的例子:
#define T1 struct foo *
typedef struct foo * T2;
两个定 使得 T1 T2 都等价于一个 struct foo 的指 。但看看当我 们试图 在一行中声明多于一个 量的 候会 生什
T1 a, b;
T2 c, d;
第一个声明被
struct foo * a, b;
a 被定 义为 一个 构指 ,但 b 被定 义为 一个 构(而不是指 )。相反,第二个声明中 c d 都被定 义为 指向 构的指 ,因 T2 的行 好像真正的 型一
今天,一个 C 程序 如果想写出 于不同 境中的用 都有用的程序就必 知道很多 微的差
一个标识符是一个字符和数字序列,第一个字符必是一个字母。下划线_算作字母。大写字母和小写字母是不同的。只有前八个字符是名,但可以使用更多的字符。可以被多种汇编器和加器使用的外部标识符,有着更多的限制:
下面 著的函数:
char *Malloc(unsigned n) {
    char *p, *malloc();
    p = malloc(n);
    if(p == NULL)
        panic("out of memory");
    return p;
}
    个函数是保 耗尽内存而不会 致没有 检测 的一个 简单 法。程序 可以通 过调 Mallo() 来代替 malloc() 。如果 malloc() 不幸失 ,将 panic() 示一个恰当的 错误 消息并 止程序。
然而,考 函数用于一个忽略大小写区 的系 生什 这时 ,名字 malloc Malloc 是等价的。 话说 函数 malloc() 被上面的 Malloc() 函数完全取代了,当 malloc() 用的是它自己。 然,其 果就是第一次 尝试 分配内存就会陷入一个 递归 并随之 生混乱。但在一些能 区分大小写的 实现 个函数 是可以工作的。
C 程序 提供三 整数尺寸:普通、短和 有字符,其行 像一个很小的整数。 C 言定 义对 整数的大小不作任何保
1.    整数的四 尺寸是非 减的。
2.    普通整数的大小要足 存放任意的数
3.    字符的大小 应该 特定硬件的本
代机器具有 8 位字符,不 过还 有一些具有 7 9 位字符。因此字符通常是 7 8 9 位。
    整数通常至少 32 位,因此一个 整数可以用于表示文件的大小。
    普通整数通常至少 16 位,因 太小的整数会更多地限制一个数 的最大大小。
    短整数 是恰好 16 位。
更可移植的做法是定 一个 新的 型:
typedef long tenmil;
在你就可以使用 型来声明一个 量并知道它的 度了,最坏的情况下,你也只要改 变这 独的 型定 就可 以使所有 量具有正确的 型。
问题 在将一个 char 转换为 一个更大的整数 时变 得尤 重要。 于相反的 转换 ,其 果却是定 良好的:多余 的位被 简单 弃掉。但一个 编译 器将一个 char 转换为 一个 int 却需要作出 选择 :将 char 视为带 符号量 是无符号量?如果是前者,将 char int 制符号位;如果是后者, 要将多余的位用 0 填充。
个决定的 于那些在 理字符 时习惯 将高位置 1 的人来 非常重要。 决定着 8 位的字符范 是从 -128 127 是从 0 255 又影响着程序 员对 哈希表和 转换 表之 西的 设计
    如果你 心一个字符 最高位置一 是否被 视为 一个 数,你 应该显 式地将它声明 unsigned char 这样 就能保 转换为 整数 是基 0 的,而不像普通 char 量那 在一些 实现 中是 符号的而在另一些 实现 中是无符号的。
另外, 有一 种误 解是 认为 c 是一个字符 ,可以通 (unsigned)c 来得到与 c 等价的无符号整数。 错误 的,因 一个 char 行任何操作(包括 转换 )之前 转换为 int 这时 c 会首先 转换为 一个 符号整数再 转换为 一个无符号整数, 生奇怪的 果。
    正确的方法是写 (unsigned char)c
里再一次重 :一个 心右移操作如何 行的程序最好将所有待移位的量声明 无符号 的。
b a 得到商 q 余数 r
q = a / b;
r = a % b;
们暂时 b > 0
1.    最重要的,我 期望 q * b + r == a ,因 为这 余数的定
2.    如果 a 的符号 生改 ,我 期望 q 的符号也 生改 ,但 绝对值
3.    希望保 r >= 0 r < b 。例如,如果余数将作 一个哈希表的索引,它必 要保 证总 是一个有效的索引。
    三点清楚地描述了整数除法和求余操作。不幸的是,它 不能同 时为 真。
3 / 2 ,商 1 0 (1) 这满 足第一点。而 -3 / 2 呢?根据第二点,商 应该 -1 ,但如果是 这样 ,余数必 也是 -1 这违 反了第三点。或者,我 可以通 将余数 标记为 1 足第三点,但 这时 根据第一点商 应该 -2 反了第二点。
因此 C 和其他任何 实现 了整数除法舍入的 言必 放弃上述三个原 中的至少一个。
很多程序 设计语 言放弃了第三点,要求余数的符号必 和被除数相同。 可以保 第一点和第二点。很多 C 实现 也是 这样 做的。
  尽管有些 候不需要灵活性, C 是足 可以 令除法完成我 所要做的、提供我 所想知道的。例如,假 有一个数 n 表示一个 标识 符中的字符的一些函数,并且我 想通 除法得到一个哈希表入口 h ,其中 0 <= h <= HASHSIZE 。如果我 知道 n 是非 的,我 可以 简单 地写:
h = n % HASHSIZE;
然而,如果 n 有可能是 的, 这样 写就不好了,因 h 可能也是 的。然而,我 知道 h > -HASHSIZE ,因此我 可以写:
h = n % HASHSIZE;
if(n < 0)
    h += HASHSIZE;
    ,将 n 声明 unsigned 也可以。
个尺寸是模糊的, 库设计 影响。在 PDP-11 [10] 机器上运行的 有的 C 实现 中,有一个称 rand() 的函数可以返回一个( )随机非 整数。 PDP-11 中整数 度包括符号位是 16 位,因此 rand() 返回一个 0 215-1 的整数。
    C VAX-11 实现时 ,整数的 变为 32 。那 VAX-11 上的 rand() 函数返回 是什 呢?
    个系 ,加利福尼 大学的 认为 rand() 的返回 值应该 涵盖所有可能的非 整数,因此它 rand() 版本返回一个 0 231-1 的整数。
    AT&T 的人 则觉 得如果 rand() 函数仍然返回一个 0 215 可以很容易地将 PDP-11 中期望 rand() 返回一个小于 215 的程序移植到 VAX-11 上。
    因此, 写出不依 赖实现 rand() 函数的程序。
toupper() tolower() 函数有着 似的 史。他 最初都被 实现为 宏:
#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
些宏确 有一个缺陷,即:当 定的 西不是一个恰当的字符,它会返回垃圾。因此,下面 个通 使用 些宏来将一个文件 转为 小写的程序是无法工作的:
int c;
while((c = getchar()) !=
EOF)
    putchar(tolower(c));
写:
int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);
一点, AT&T 中的 UNIX 开发组织 提醒我 toupper() tolower() 都是事先 经过 一些适当的参数 测试 的。考 虑这样 重写 些宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ?
(c) + 'a' - 'A' : (c))
但要知道, c 的三次出 都要被求 会破坏如 toupper(*p++) 这样 的表达式。因此,可以考 toupper() tolower() 重写 函数。 toupper() 看起来可能像 这样
int toupper(int c) {
    if(c >= 'a' && c <= 'z')
        return c + 'A' - 'a';
    return c;
}
tolower() 似。
    个改 变带 来更多的 问题 次使用 些函数的 候都会引入函数 开销 。我 的英雄 认为 一些人可能不愿意支付 开销 ,因此他 个宏重命名
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
就允 户选择 方便或速度。
    里面其 只有一个 问题 :伯克利的人 和其他的 C 实现 者并没有跟着 这么 做。 意味着一个在 AT&T 写的使用了 toupper() tolower() 的程序,如果没有 传递 正确大小写字母参数,在其他 C 实现 中可能不会正常工作。
    如果不知道 史,可能很 难对这类错误进 行跟踪。
很多 C 实现为 提供了三个内存分配函数: malloc() realloc() free() malloc(n) 返回一个指向有 n 个字符的新分配的内存的指 个指 可以由 程序 使用。 free() 传递 一个指向由 malloc() 分配的内存的指 可以使 这块 内存得以再次使用。通 一个指向已分配区域的指 和一个新的大小 realloc() 可以将 这块 内存 大或 小到新尺寸, 程中可能要 制内存。
    有人会想,真相真是有点微妙啊。下面是 System V 接口定 中出 realloc() 的描述:
realloc一个由ptr指向的size个字,并返回该块(可能被移)的指在新旧尺寸中比小的一个尺寸之下的内容不会被改 此外, 包含了描述 realloc() 的另外一段:
如果在最后一次mallocrealloccalloc放了ptr所指向的realloc依旧可以工作;因此,freemallocrealloc序可以利用malloc压缩找策略。
因此,下面的代 片段在 UNIX 第七版中是合法的:
free (p);
p = realloc(p, newsize);
    一特性保留在从 UNIX 第七版衍生出来的系 中:可以先 放一 区域,然后再重新分配它。 意味着,在 些系 放的内存中的内容在下一次内存分配之前可以保 。因此,在 些系 中,我 可以用下面 这种 奇特的思想来 放一个 表中的所有元素:
for(p = head; p != NULL; p = p->next)
    free((char *)p);
而不用担心 free() p->next 不可用。
不用 这种 是不推荐的,因 不是所有 C 实现 都能在内存被 放后将它的内容保留足 够长 时间 。然而,第七版的手册 留了一个未声明的 问题 realloc() 的原始 实现实际 上是必 要先 放再重新分配的。出于 个原因,一些 C 程序都是先 放内存再重新分配的,而当 些程序移植到其他 实现 就会出 现问题
下面的程序 有两个参数:一个 整数和一个函数(的指 )。它将整数 转换 位十 制数,并用代表 其中 一个数字的字符来 定的函数。
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}
    个程序非常 简单 。首先 检查 n 是否 为负 数;如果是, 打印一个符号并将 n 变为 正数。接下来, 测试 是否 n >= 10 。如果是, 它的十 制表示 中包含两个或更多个数字,因此我 们递归 printnum() 来打印除最后一个数字外的所有数字。最后,我 打印最后一个数字。
    个程序 —— 由于它的 简单 —— 具有很多可移植性 问题 。首先是将 n 的低位数字 转换 成字符形式的方法。用 n % 10 取低位数字的 是好的,但 它加上 '0' 得相 的字符表示就不好了。 个加法假 机器中 序的数字所 对应 的字符数 序的,没有 隔,因此 '0' + 5 '5' 是相同的,等等。尽管 个假 设对 ASCII EBCDIC 字符集是成立的,但 于其他一些机器可能不成立。避免 问题 的方法是使用一个表:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}
    另一个 问题发 生在当 n < 0 这时 程序会打印一个 号并将 n -n 赋值 生溢出,因 在使用 2 补码 的机器上通常能 表示的 数比正数要多。例如,一个( )整数有 k 位和一个附加位表示符号, -2k 可以表示而 2k 却不能。
    解决 问题 有很多方法。最直 的一 是将 n 赋给 一个 unsigned long 。然而,一些 C 便一起可能没有 实现 unsigned long ,因此我 来看看没有它怎 么办
    在第一个 实现 和第二个 实现 的机器上,改 一个正整数的符号保 不会 生溢出。 问题仅 出在改 一个 数的符号 。因此,我 可以通 避免将 n 变为 正数来避免 问题
    当然,一旦我 打印了 数的符号,我 就能 数和正数 视为 是一 的。 下面的方法就 制在打印符号之后 n 为负 数,并且用 完成我 所有的算法。如果我 们这么 做,我 就必 程序中打印符号的部分只 行一次;一个 简单 的方法是将 个程序划分 两个函数:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        printneg(n, p);
    }
    else
        printneg(-n, p);
}

void printneg(long n, void (*p)()) {
    if(n <= -10)
        printneg(n / 10, p);
    (*p)("0123456789"[-(n % 10)]);
}
    printnum() 在只 检查 要打印的数是否 为负 数;如果是的 话则 打印一个符号。否 ,它以 n 负绝对值 printneg() 。我 printneg() 的函数体来适 n 数或零 一事
    得到什 ?我 使用 n / 10 n % 10 n 的前 数字和 尾数字( 经过 适当的符号 变换 )。 用整数 除法的行 在其中一个操作数 为负 候是 实现 的。因此, n % 10 有可能是正的! 这时 -(n % 10) 数,将会超出我 的数字字符数 的末尾。
    了解决 问题 ,我 建立两个 临时变 量来存放商和余数。作完除法后,我 们检查 余数是否在正确的范 内,如果不是的 话则调 两个 量。 printnum() 没有改 ,因此我 只列出 printneg()
void printneg(long n, void (*p)()) {
    long q;
    int r;
    if(r > 0) {
        r -= 10;
        q++;
    }
    if(n <= -10) {
        printneg(q, p);
    }
    (*p)("0123456789"[-r]);
}
    The C Programming Language 》( Kernighan and Ritchie, Prentice-Hall 1978 )是最具 威的 C 著作。它包含了一个 秀的教程,面向那些熟悉其他高 级语 言程序 设计 的人,和一个参考手册, 简洁 地描述了整个 言。尽管自 1978 年以来 这门语 生了不少 化, 书对 于很多主 仍然是个定 时还 包含了本文中多次提到的 “C 言参考手册
    The C Puzzle Book 》( Feuer, Prentice-Hall, 1982 )是一本少 的磨 文法能力的 收集了很多 谜题 (和答案),它 的解决方法能 够测试读 C 言精妙之 的知
    C: A Referenct Manual 》( Harbison and Steele, Prentice Hall 1984 )是特意 为实现 写的一本参考 料。其他人也会 发现 它是特 有用的 —— 他能从中参考 细节
1. 是基于 图书 C Traps and Pitfalls 》( Addison-Wesley, 1989, ISBN 0-201-17928-8 )的一个 充,有 趣的 者可以 它。 
 
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值