1. 前言
本文从英文 C-FAQ (2004 年 7 月 3 日修订版) 翻译而来。本文的 中文版权为朱群英和孙云所有。 本文的内容可以*用于个人目的,但 是不可以未经许可出版发行。 英文版权为 Steve Summit 所有,详情 见下面的英文版权说明。
The English version of this FAQ list is Copyright 1990-2004 by Steve Summit. Content from the book 《C Programming FAQs: Frequently Asked Questions》 is made available here by permission of the author and the publisher as a service to the community. It is intended to complement the use of the published text and is protected by international copyright laws. The on-line content may be accessed freely for personal use but may not be republished without permission.
最新的 HTML 中译版本可以在 http://c-faq-chn.sourceforge.net/ 取得。 另外在同一地址还提供 PDF 版本的下载。在 http://sourceforge.net/projects/c-faq-chn 可以得到本文的 LATEX 源文件。
有关英文原文的问题,请咨询 Steve Summit (scs@eskimo.com)。 有关中文译稿的问题, 请联系孙云 (sunyun.s@gmail.com, 1-12章) 和朱群英 (zhu.qunying@gmail.com, 13-20章、LATEX文件编辑)。
12.34 我很吃惊, ANSI 标准竟然有那么多没有定义的东西。 标准的唯一任务不就是让这些东西标准化吗?
某些构造随编译器和硬件的实现而变化, 这一直是 C 语言的一个特点。 这种有意的不严格规定可以让编译器生成效率更高的代码, 而不必让所有
程序为了不合理的情况承担额外的负担。因此, 标准只是把现存的实践 整理成文。
编程语言标准可以看作是语言使用者和编译器实现者之间的协议。协议的 一部分是编译器实现者同意提供, 用户可以使用的功能。而其它部分则包 括用户同意遵守,
编译器实现者认为会被最受的规则。只要双方都恪守
自己的保证, 程序就可以正确运行。如果任何一方违背它的诺言, 则结果 肯定失败。
参见问题 12.35。
12.35 有人说 i = i++ 的行为是未定义的, 但是我刚在一个兼容 ANSI 的编译器上测试,
得到了我希望的结果。
面对未定义行为的时候, 包括范围内的实现定义行为和未确定行为, 编译器 可以做任何实现,
其中也包括你所有期望的结果。但是依靠这个实现却不明智。 参见问题 7.4, 11.31, 11.32 和 11.34。
12.31 为什么声称兼容 ANSI 的 Frobozz Magic C 编译器不能 编译这些代码?我知道这些代码是 ANSI 的, 因为 gcc 可以编译。
许多编译器都支持一些非标准的扩展, gcc 尤甚。你能确认被拒绝的代码
不依赖这样的扩展吗?通常用试验特定的编译器来确定一种语言的特性是 个坏主意; 使用的标准可能允许变化, 而编译器也可能有错。 参见问题 12.35。
12.7 我不明白为什么我不能象这样在初始化和数组维度中使用常量: const int n = 5; int a[n];
const
限定词真正的含义是 ``只读的"; 用它限定的对象是运行时 (同常) 不能被赋值的对象。因此用 const 限定的对象的值并 不完全是一个真正的常量。
在这点上 C 和 C++ 不一样。如果你需要真正的运行 时常量, 使用预定义宏 #define (或enum)。
参考资料: [ISO,
Sec. 6.4]; [H&S, Secs. 7.11.2,7.11.3 pp. 226-7]。
12.8 既然不能修改字符串常量, 为什么不把它们定义为字符常量的数组?
一个原因是太多的代码包含
char *p = "Hello,
world!";
这样并不正确的语句。这样的语句要受诊断信息的困扰,但真正的问题却出现在 改变 p 所指目的的任何企图。
参见问题 2.13。
2.13 以下的初始化有什么区别?char a[] = "string literal"; char *p = "string literal"; 当我向
p[i] 赋值的时候, 我的 程序崩溃了。
字符串常量有两种稍有区别的用法。用作数组初始值 (如同在 char a[] 的声明中),
它指明该数组中字符的初始值。其它情况下, 它会转化为一个 无名的静态字符数组, 可能会存储在只读内存中, 这就是造成它不一定能被修改。 在表达式环境中,
数组通常被立即转化为一个指针 (参见第 6 章), 因此第二个声明把 p 初始化成 指向无名数组的第一个元素。
为了编译旧代码,
有的编译器有一个控制字符串是否可写的开关。
参见问题 2.11、 7.1、 7.2 和 7.6。
参考资料:
[K&R2, Sec. 5.5 p. 104]; [ISO, Sec. 6.1.4, Sec. 6.5.7]; [Rationale, Sec. 3.1.4]; [H&S, Sec. 2.7.4 pp. 31-2]。
7.2 可是我听说 char a[ ] 和 char *a 是一样的。
并非如此。(你所听说的应该跟函数的形式参数有关;参见问题 6.4)
数组不是指针。 数组定义 char a[6] 请求预留 6 个 字符的位置, 并用名称 ``a" 表示。也就是说, 有一个称为 ``a" 的位置, 可以放入 6
个字符。 而指针申明 char *p, 请求一个位置放置一个指 针, 用名称 ``p" 表示。 这个指针几乎可以指向任何位置: 任何字符和 任何连续的字符,
或者哪里也不指(参见问题 5.1 和 1.10)。
一个图形胜过千言万语。声明
char a[] =
"hello";
char *p = "world";
将会初始化下图所示的数据结果:
+---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
+---+---+---+---+---+---+
+-----+ +---+---+---+---+---+---+
p: | *======> | w | o | r | l | d |\0 |
+-----+ +---+---+---+---+---+---+
根据 x 是数组还是指针, 类似 x[3]
这样的引用会生成不同的代码。 认识到这一点大有裨益。以上面的声明为例, 当编译器看到表达式 a[3] 的时候, 它生成代码从 a 的位置开始跳过 3 个,
然后取出那个 字符. 如果它看到 p[3], 它生成代码找到 ``p" 的位置, 取出其中的指 针值, 在指针上加 3 然后取出指向的字符。换言之, a[3]
是 名为 a 的对象 (的起始位置) 之后 3 个位置的值, 而 p[3] 是 p 指向的对象的 3 个位置之后的值. 在上例中, a[3] 和 p[3]
碰巧都是 'l' , 但是编译器到达那里的途径不尽相同。 本质的区别在于类似 a 的数组和类似 p 的指针一旦在表达式中出现就会 按照不同的方法计算,
不论它们是否有下标。下一问题继续深入解释。 参见问题 2.13。
参考资料: [K&R2, Sec. 5.5 p. 104]; [CT&P, Sec. 4.5 pp. 64-5]。
12.23 a[3] = "abc"; 合法吗?它是什么意思?
尽管只在极其有限的环境下有用, 可它在 ANSI C
(可能也包括一些 ANSI 之前的系统) 中是合法的。它声明了一个长度为 3 的数组, 把它的 三个字符初始化为 'a', 'b' 和 'c',
但却没有通常的 '\0' 字符。因此该数组并不是一个真正的 C 字符串从而不能用在 strcpy, printf %s 等当中。
多数时候,
你应该让编译器计算数组初始化的初始值个数, 在初始值 ``abc" 中, 计算得长度当然应该是 4。
参考资料: [ISO, Sec. 6.5.7]; [H&S, Sec. 4.6.4 p. 98]。
20.1 怎样从键盘直接读入字符而不用等 RETURN 键?怎样 防止字符输入时的回显?
唉, 在 C
里没有一个标准且可移植的方法。在标准中跟本就 没有提及屏幕和键盘的概念, 只有基于字符 ``流" 的简单输入输出。
在某个级别,
与键盘的交互输入一般上都是由系统取得一行的输入才 提供给需要的程序。这给操作系统提供了一个加入行编辑的机会 (退格、 删除、消除等), 使得系统地操作具一致性,
而不用每一个程序自己 建立。当用户对输入满意, 并键入 RETURN (或等价的键)后, 输入行才被提供 给需要的程序。即使程序中用了读入单个字符的函数 (例如
getchar() 等), 第一次调用就会等到完成了一整行的输入才会返回。这时, 可能 有许多字符提供给了程序, 以后的许多调用 (象 getchar()
的函数) 都会马上返回。
当程序想在一个字符输入时马上读入, 所用的方式途径就采决于行处理在 输入流中的位置, 以及如何使之失效。在一些系统下
(例如 MS-DOS, VMS 的某些模态), 程序可以使用一套不同或修改过的操作系统函数 来扰过行输入模态。在另外一些系统下 (例如 Unix, VMS 的
另一些模态), 操作系统中负责串行输入的部分 (通常称为 ``终端驱动") 必须 设置为行输入关闭的模态, 这样, 所有以后调用的常用输入函数 (例如
read(), getchar() 等) 就会立即返回输入的字符。 最后, 少数的系统 (特别是那些老旧的批处理大型主机) 使用外围处理器 进行输入,
只有行处理模式。
因此, 当你需要用到单字符输入时 (关闭键盘回显也是类似的问题), 你需要 用一个针对所用系统的特定方法,
假如系统提供的话。新闻组 comp.lang.c 讨论的问题基本上都是 C 语言中有明确支持的, 一般上你会从针对个别系统的
新闻组以及相对应的常用问题集中得到更好的解答, 例如 comp.unix.questions 或 comp.os.msdos.programmer。
另外要注意, 有些解答即使是对相似系统的变种也不尽相同, 例如 Unix 的不同变种。同时也要记住, 当回答一些针对特定系统的问题时, 你的答案在你
的系统上可以工作并不代表可以在所有人的系统上都工作。
然而, 这类问题被经常的问起, 这里提供一个对于通常情况的简略回答。
某些版本的 curses 函数库包含了 cbreak(), noecho() 和 getch() 函数,
这些函数可以做到你所需的。如果你只是想要 读入一个简短的口令而不想回显的话, 可以试试 getpass()。在 Unix 系统下, 可以用 ioctl()
来控制终端驱动的模式, ``传统"系统下有 CBREAK 和 RAW 模式, System V 或 POSIX 系统下有 ICANON, c_cc[VMIN]
和 c_cc[VTIME] 模式, 而 ECHO 模式 在所有系统中都有。必要时, 用函数 system() 和 stty 命令。
更多的信息可以查看所用的系统, 传统系统下, 查看 <sgtty.h> 和 tty(4), System V 下, 查看
<termio.h> 和 termio(4), POSIX 下, 查看 <termios.h> 和 termios(4)。在
MS-DOS 系统下, 用函数 getch() 或 getche(), 或者相对应的 BIOS 中断。在 VMS 下, 使用屏幕管理例程 (SMG$), 或
curses 函数库, 或者低层 $QIO 的 IO$_READVBLK 函数, 以及 IO$M_NOECHO 等其它函数。也可以通过设置 VMS
的终端驱动, 在单字符输入或 ``通过" 模式间切换。 如果是其它操作系统, 你就要靠自己了。
另外需要说明一点, 简单的使用
setbuf() 或 setvbuf() 来设置 sdtin 为无缓冲, 通常并不能切换到单字符输入模式。
如果你在试图写一个可移植的程序,
一个比较好的方法是自己定义三套函数: 1) 设置终端驱动或输入系统进入单字符输入模式, (如果有必要的话), 2) 取得字符, 3)
程序使用结束后的终端驱动复原。理想上, 也许有一天, 这样的一组函数可以成为标准的一部分。本常用问题集的扩充版 (参见问题 20.36)
含有一套适用于几个流行系统的函数。
参见问题 20.2
参考资料: [PCS, Sec. 10 pp. 128-9, Sec. 10.1 pp. 130-1]; [POSIX, Sec. 7]。
13.18 为什么大家都说不要使用 scanf()?那我该用什么来代替呢?
scanf() 有很多问题 --- 参见问题 13.15,
13.16 和 13.17。而且, 它的 %s 格式有着和 gets() 一样的问题 (参见问题 13.20) --- 很难保证接收缓冲不溢出。
更一般地讲, scanf() 的设计使用于相对结构化的, 格式整齐的输入。设计上, 它的名称就是来自于 ``scan
formatted"。如果你注意到, 它会告诉你成功或 失败, 但它只能提供失败的大略位置, 至于失败的原因, 就无从得知了。 对 scanf()
多得体的错误恢复几乎是不可能的; 通常先用类似 fgets() 的函数 读入整行, 然后再用 sscanf() 或其它技术解释。strtol(),
strtok() 和 atoi() 等函数通常有用; 参见问题 14.4。如果你真的要用任何 scanf 的变体, 你要确保检查返回值,
以确定找到了期待的值。而使用 %s 格式的时候, 一定要 小心缓冲区溢出。
参考资料: [K&R2, Sec. 7.4 p. 159]。
13.24 既然 fflush() 不能, 那么怎样才能清除输入呢?
这取决于你要做什么。如果你希望丢掉调用 scanf() (参见问题
12.16 - 12.17) 之后所剩下的换行符和未预知的输入, 你可能需要重写你的 scanf() 或者换掉它, 参见问题 13.18。或者你可以用下边这样
的代码吃掉一行中多余的字符
while((c = getchar()) != '\n' && c !=
EOF)
/* 丢弃 */ ;
你也可以使用 curses 的 flushinp() 函数。
没有什么标准的办法可以丢弃标准输入流的未读取字符, 即使有, 那也不够, 因为未读取字符也可能来自其它的操作系统级的输入缓冲区。如果你希望严格
丢弃多输入的字符 (可能是预测发出临界提示), 你可能需要使用系统相关的 技术; 参加问题 20.1 和 20.2。
参考资料: [ISO, Sec. 7.9.5.2]; [H&S, Sec. 15.2]。
14.18 我不断得到库函数未定义错误, 但是我已经 #inlude 了所有用到的头文件了。
通常, 头文件只包含外部说明。某些情况下,
特别是如果是非标准函数, 当你 连接程序时, 需要指定正确的函数库以得到函数的定义。#include 头文件 并不能给出定义。参见问题 11.10, 12.29, 14.19, 15.3 和 20.39。
11.10 我在编译一个程序, 看起来我好像缺少需要的一个或多个头文件。 谁能发给我一份?
根据 ``缺少的" 头文件的种类, 有几种情况。
如果缺少的头文件是标准头文件, 那么你的编译器有问题。你得向你的供货商 或者精通你的编译器的人求助。
对于非标准的头文件问题更复杂一些。有些完全是系统或编译器相关的。 某些 是完全没有必要的, 而且应该用它们的标准等价物代替。 例如,
用 <stdlib.h> 代替 <malloc.h>。 其它的头文件, 如跟流行的附加库相关的, 可能有相当的可移植性。
标准头文件存在的部分原因就是提供适合你的编译器, 操作系统和处理器的定义。 你不能从别人那里随便拷贝一份就指望它能工作, 除非别人跟你使用的是同样的 环境. 你可能事实上有移植性问题 (参见第 20 章) 或者编译器问题。 否则, 参见问题 19.18。
20.43 我不能使用这些非标准、依赖系统的函数, 程序需要兼容 ANSI!
你很不走运。要么你误解了要求, 要么这不可能做到。 ANSI/ISO C 标准 没有定义做这些事的方法; 它是个语言的标准,
不是操作系统的标准。 国际标准 POSIX (IEEE 1003.1, ISO/IEC 9945-1) 倒是定义了许多这方面 的方法, 而许多系统 (不只是
Unix) 都有兼容 POSIX 的编程接口。
可以做到, 也是可取的做法是使程序的大部分兼容 ANSI, 将依赖系统的功能
集中到少数的例程和文件中。这些例程或文件可以大量使用 #ifdef 或针对 每一个移植的系统重写。
8.21 我必须在程序退出之前释放分配的所有内存吗?
你不必这样做。一个真正的操作系统毫无疑问会在程序退出的时候回收所有的 内存和其它资源。
然而, 有些个人电脑据称不能可靠地释放内存, 从 ANSI/ISO C 的角度来看这不过是一个 ``实现的质量问题"。
参考资料: [ISO, Sec. 7.10.3.2]。
8.20 我在分配一些结构, 它们包含指向其它动态分配的对象的指针。 我在释放结构的时候, 还需要释放每一个下级指针吗?
是的。一般地,
你必须分别向 free() 传入 malloc() 返回的每一个指针, 仅仅一次 (如果它的确要被释放的话)。一个好的经验法则是对于程序中的每 一个
malloc() 调用, 你都可以找到一个对应的 free() 调用以释放 malloc() 分配的内存。
参见问题 8.21
15.1 一个 float 变量赋值为 3.1 时, 为什么 printf 输出的值为 3.0999999?
大多数电脑都是用二进制来表示浮点和整数的。在十进制里, 0.1 是个简单、精确 的小数, 但是用二进制表示起来却是个循环小数
0.0001100110011 ...。 所以 3.1 在十进制内可以准确地表达, 而在二进制下不能。
在对一些二进制中无法精确表示的小数进行赋值或读入再输出时, 也就是从十进制 转成二进制再转回十进制, 你会观察到数值的 不一致.
这是由于编译器二进制/十进制转换例程的精确度引起的, 这些例程也用在 printf 中。 参见问题 15.6。
15.4 浮点计算程序表现奇怪, 在不同的机器上给出不同的结果。
首先阅读问题 15.2.
如果问题并不是那么简单,
那么回想一下, 电脑一般都是用一种浮点的格式来近似 的模拟实数的运算, 注意是近似, 不是完全。下溢、误差的累积和其它非常规性是 常遇到的麻烦。
不要假设浮点运算结果是精确的, 特别是别假设两个浮点值可以进行等价比较。也 不要随意的引入 ``模糊因素"; 参见问题 15.5。
这并不是 C 特有的问题, 其它电脑语言有一样的问题。浮点的某些方面被通常 定义为 ``*处理器 (CPU) 是这样做的" (参见问题
12.34), 否则在一 个没有 ``正确" 浮点模型的处理器上, 编译器要*做代价非凡的仿真。
本文不打算列举在处理浮点运算上的潜在难点和合适的做法。一本好的有关数字 编程的书能涵盖基本的知识。参见下面的参考资料。
参考资料: [K&P, Sec. 6 pp. 115-8]; [Knuth, Volume 2 chapter 4]; [Goldberg]。
3.7 是否有自动比较结构的方法?
没有。编译器没有简单的好办法实现结构比较 (即, 支持结构的 == 操作符), 这也符合 C 的低层特性。
简单的按字节比较会由于结构中没有用到的 ``空洞'' 中的随机数据 (参见问题 2.10) 而失败; 而按域比较在处理大结构时需要难以接受的大量重复代码。
如果你需要比较两个结构, 你必须自己写函数按域比较。
参考资料:
[K&R2, Sec. 6.2 p. 129]; [Rationale, Sec. 3.3.9]; [H&S, Sec. 5.6.2 p. 133]。
4.2 使用我的编译器,下面的代码 int i=7; printf("%d\n", i++ * i++); 返回 49?不管按什么顺序计算,
难道不该打印出56吗?
尽管后缀自加和后缀自减操作符 ++ 和 -- 在输出其旧值之后 才会执行运算,
但这里的``之后"常常被误解。没有任何保证 确保自增或 自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进 行。也不能保证变量的更新会在表达式
``完成" (按照 ANSI C 的术语, 在下一个 ``序列点" 之前, 参见问题 3.7) 之前的某个时刻进行。 本例中,
编译器选择使用变量的旧值相乘以后再对二者进行自增运算。
包含多个不确定的副作用的代码的行为总是被认为未定义。(简单而言, ``多个不确定副作用"
是指在同一个表达式中使用导致同一对象修改两 次或修改以后又被引用的自增, 自减和赋值操作符的任何组合。这是一 个粗略的定义; 严格的定义参见问题 4.7,
``未定义" 的含义 参见问题 11.32。) 甚至都不要试图探究这些东西在你的编译 器中是如何实现的 (这与许多 C 教科书上的弱智练习正好相反);
正如 K&R 明智地指出, ``如果你不知道它们在不同的机器上如何实现,
这样的无知可能恰恰会有助于保护你。begintex2html_deferred
参考资料: [K&R1, Sec.
2.12 p. 50]; [K&R2, Sec. 2.12 p. 54]; [ISO, Sec. 6.3]; [H&S, Sec. 7.12 pp. 227-9]; [CT&P, Sec. 3.7 p. 47]; [PCS, Sec. 9.5 pp. 120-1]。
6.11 为什么有那么多关于空指针的疑惑?为什么这些问题如此经常地出现?
C 程序员传统上喜欢知道很多
(可能比他们需要知道的还要多) 关于机器实现的细节。 空指针在源码和大多数机器实现中都用零来表示的事实导致了很多无根据的猜测。 而预处理宏
(NULL) 的使用又似乎在暗示这个值可能在某个时刻或者在某种怪异的机 器上会改变。``if(p == 0)" 这种结构又很容易被误认为在比较之前把 p
转成了整 数类型, 而不是把 0 转成了指针类型。最后, 术语 ``空" 的几种用法 (如上文 问题 6.10 所列出的) 之间的区别又可能被忽视。
冲出这些迷惘的一个好办法是想象 C 使用一个关键字 (或许象 Pascal 那样, 用 ``nil")
作为空指针常数。编译器要么在源代码没有歧义的时候把 ``nil" 转成适 当类型的空指针, 或者有歧义的时候发出提示。现在事实上, C 语言的空指针常
数关键字不是 ``nil" 而是 ``0", 这在多数情况下都能正常工作, 除了一个未加修饰 的 ``0" 用在非指针上下文的时候, 编译器把它生成整数 0
而不是发出错误信息, 如果那个未加修饰的 0 是应该是空指针常数, 那么生成的程序不行。
本文是在编程论坛上的一位大牛发表的,然后整理下来学习