引言

你可能有过这样的经历:在酒吧或派对上,你打赌能否完成一项看似简单的任务,结果却发现,由于人体生理特性或物理定律的古怪之处,这项任务根本不可能完成。打赌的人知道,他挑战的人越多,他赢的可能性就越大,因为这些生理特性和定律,即使不为人知,也相当恒定和可预测。

同样地,如果你让一大群人去学习一项复杂的任务,比如学习 C 编程语言,其中许多人会遇到完全相同的问题,并提出完全相同的问题。这些问题和困难在任务最初设计时可能无法预测,事后看来答案似乎显而易见,但人们确实会反复遇到一些相同的问题,并且相同的疑问确实会不断涌现。这些问题和困难并不一定表明任务不可能完成,仅仅是说明它足够困难,因而引人入胜。

不出所料,这些问题和困难经常出现在互联网上,尤其是在被称为 Usenet 的交互式讨论新闻组中。收集常见问题的合理且显而易见的想法催生了“常见问题解答”或 FAQ 列表的传统。FAQ 列表并不总是能达到其预期的减少常见问题发生率的目的,但如果问题确实是一致的,那么它们被选中收录到 FAQ 列表中这一事实就表明,它们可能与你或其他本书读者提出的问题相匹配。

关于本书

大多数(关于 C 或任何其他主题的)书籍都是从作者的视角撰写的。它们讨论作者认为你应该了解的主题,并以作者认为有道理的方式呈现。如果这种方式对你来说没有道理(而且,在某种程度上,它是不可能的,因为作者假定他已经掌握了材料,而你则不掌握),你可能会留下深刻的、未解答的问题。

然而,本书围绕着大约 400 个问题组织,这些问题都基于真实的人在尝试学习或编程 C 时提出的真实问题。本书的目标不仅仅是作者认为重要的主题;它关注的是真实读者根据他们提出的问题而认为重要的主题。如果你正在学习或使用 C,并且你有一个关于 C 的问题,而你在检查过的其他书中都找不到答案,那么很可能在这里能找到答案。

本书无法承诺回答你在编写 C 程序时会遇到的每一个问题,因为许多将要出现的问题都与你的问题领域有关,而本书仅涵盖 C 语言本身。正如它无法涵盖任何人可能尝试用 C 解决的每一个问题的每一个方面一样,本书也无法涵盖任何人可能尝试为之编写 C 程序操作系统的每一个方面,或者任何人可能尝试用 C 实现每一个算法。具体的问题、特定的操作系统和通用算法,都在专门讨论这些主题的书籍或其他资料中进行了讨论。然而,涉及操作系统和算法的某些问题相当常见,因此第 19 章和第 20 章提供了对其中一些问题的简短、入门级的回答,但请不要期望这些内容是完整的。

本书中的问题是人们在阅读完 C 入门教材或参加 C 编程课程后通常会遇到的问题。因此,本书不会教你 C,也不会讨论任何 C 教材应涵盖的基本问题。此外,本书的答案在大多数情况下旨在具有明确的正确性,并避免传播任何误解。因此,一些答案比乍看起来可能必需的更详尽:它们提供了完整的图景,而不是过度简化或遗漏重要的细节。(毕竟,过度简化或遗漏的细节是导致本书的问题和答案所针对的许多误解的根源。)在详尽的答案中,你会找到必要的捷径和简化,并且在 xxx 页开始的术语表中,你会找到准确解释通常需要使用的精确术语的定义。捷径和简化当然是安全的:它们不应导致后续的误解,如果你以后想要完整的故事,你可以随时回顾更完整的解释,或查阅一些参考资料。

正如我们将在第 3 章和第 11 章中特别看到的,C 的标准定义并没有规定所有可以编写的 C 程序的行为。有些程序属于各种灰色地带:它们可能在某些系统上运行,它们可能并不严格违法,但它们不能保证在所有地方都能运行。本书是关于可移植 C 编程的,因此其答案建议尽可能避免使用不可移植的结构。

本书背后在线 FAQ 列表的编写过程就像一场对话:当人们不理解时,他们会说出来。这种反馈在完善答案的形式方面非常有价值。虽然印刷书籍显然更加静态,但这种对话仍然是恰当的:欢迎你的评论、批评和建议。如果你能够访问互联网,可以将评论发送至scs@aw.com,或者通过邮寄方式发送给出版商。本书中发现的任何错误列表将得到维护并在互联网上提供;有关信息,请参阅第 20.40 节。

问题格式

本书大部分内容由一系列问答对组成。许多答案还包含参考文献和注释。注释(如果出现)类似于脚注;如果你觉得它们过于挑剔,可以跳过它们。有几本受尊敬的参考文献被反复引用,缩写如下:

其他参考文献通过其全称引用;所有参考文献的完整引用见 xxx 页的参考文献列表。

这种等宽字体用于表示 C 语法(函数名和变量名、关键字等),也用于表示一些操作系统命令(cc等)。偶尔出现的 tty(4) 形式的标注表示 Unix Programmer's Manual 第 4 章中的“tty”部分。

代码示例

这是一本关于 C 的书,所以其中许多小片段必然是用 C 编写的。这些示例主要为了清晰地说明问题。它们总是以最高效的方式编写的;使它们“更快”通常会使它们不那么清晰。(有关代码效率的更多信息,请参阅第 20.13 节。)它们主要使用现代的、ANSI 风格的语法编写;如果你仍然使用“经典”编译器,请参阅第 11.29 节以获取转换技巧。

作者和出版商邀请你使用和修改这些代码片段用于你自己的程序,但如果你这样做,我们当然会感谢你的注明。(一些片段来自其他来源,并已注明;如果你使用这些代码,请注明那些贡献者。)较大示例的源代码可通过匿名 FTP 从 aw.com 的 cseng/authors/summit/cfaq 目录在互联网上获取(另请参阅第 18.12 节)。

为了强调某些观点,不幸的是,有必要包含一些代码片段,它们是应该做的例子的示例。在答案中,此类代码片段会用明确的注释标记,例如/* 错误 */以提醒你不要效仿。(问题中的代码片段通常不会这样标记;问题中的代码片段应该很明显是可疑的,因为问题通常是“为什么这段代码不行?”)

组织结构

如前所述,本书的问题基于真实的人提出的真实问题,而真实世界的问题并不总是能被清晰地归类。许多问题涉及多个主题:看似内存分配问题实际上可能反映了不正确的声明。(跨章节界限的几个问题同时出现在两个章节中,以便于查找。)无论如何,这不是一本你必须按顺序阅读的书:使用目录、从 xxx 页开始的问题列表、索引以及问题之间的交叉引用来查找你感兴趣的主题。(而且,如果你有一些空闲时间,你可能会发现自己仍然按顺序阅读;也许你会遇到一个你还没来得及问过的问题的答案。)

通常,在你开始编写代码之前,你必须声明你的数据结构,因此第 1 章 从讨论声明和初始化开始。C 的结构体、联合体和枚举类型足够复杂,值得专门用一章来讨论;第 2 章 讨论了如何声明和使用它们。

程序的大部分工作是由表达式语句完成的,这是第 3 章 的主题。

第 4 章到第 7 章讨论了许多初学者 C 程序员的痛点:指针。第 4 章 讨论了指针的通用性,第 5 章 重点关注空指针的特殊情况,第 6 章 描述了指针和数组之间的关系,第 7 章 探讨了当指针行为异常时真正的问题所在:底层内存分配。

几乎所有的 C 程序都操作字符和字符串,但这些类型是在语言的底层实现的。程序员通常负责正确管理这些类型;在这样做时出现的一些问题被收集在第 8 章 中。类似地,C 没有正式的布尔类型;第 9 章 简要讨论了 C 的布尔表达式以及在需要时实现用户定义布尔类型的适当方法。

C 预处理器(编译器负责处理#include#define指令,实际上是所有以#开头的行)是足够独立的,它几乎代表一门独立的语言,并在其自己的章节,第 10 章 中进行介绍。

ANSI C 标准化委员会(X3J11)在澄清 C 的定义并使其被世界接受的过程中,引入了许多新特性并进行了一些重大的修改。与 ANSI/ISO 标准 C 特有的问题被收集在第 11 章 中。如果你有使用 ANSI C 之前的 C(也称为“K&R”或“经典”C)的经验,你会发现第 11 章 是了解这些差异的有用介绍。另一方面,如果你能够熟练使用 ANSI C,那么 ANSI C 之前的特性和 ANSI C 特性之间的区别可能并不有趣。无论如何,第 11 章 中与 C 预处理器、库函数等其他主题相关的所有问题,也都出现在其他章节中,或者通过其他章节进行了交叉引用。

C 的定义相对来说比较简练,部分原因在于许多特性并没有内置到语言中,而是通过库函数访问。其中最重要的是“标准 I/O”或“stdio”函数,这些函数在第 12 章 中进行了讨论。其他库函数在第 13 章 中进行了介绍。

14 章 和第 15 章 讨论了两个更高级的主题:浮点数和可变参数列表。无论使用什么系统或语言,浮点计算往往都很棘手;第 14 章 概述了一些一般的浮点问题以及一些 C 特有的浮点问题。一个函数接受可变数量参数的可能性,虽然可能不是必需的或危险的,但有时也很方便,并且是 C 的printf函数的核心;第 15 章 讨论了处理可变参数列表的技术。

16 章 中隐藏了一些问题,如果你已经熟悉了前面大部分内容,你可能想先跳到这里:它们涉及到程序中偶尔会出现的奇怪问题和神秘错误,这些错误可能非常令人沮丧,难以追踪。

当编写程序有两种或多种同样“正确”的方法时(而且通常确实存在),一种方法可能基于主观标准,这些标准不仅仅是代码是否能正确编译和运行。第 17 章 讨论了一些这些难以捉摸的编程风格问题。

你无法孤立地构建 C 程序:你需要一个编译器,并且可能需要一些额外的文档、源代码或工具。第 18 章 讨论了一些可用的工具和资源,包括lint,这是一个几乎被遗忘但曾经是不可或缺的工具,用于检查程序的正确性和可移植性的某些方面。

如前所述,C 语言并没有规定你编写一个实际程序所必需的一切。诸如“如何读取一个字符而不等待 RETURN 键?”和“如何找到文件大小?”这样的问题非常普遍,但 C 并没有定义这些答案;这些操作取决于底层操作系统提供的设施。第 19 章 介绍了许多这些问题以及针对流行操作系统的简短答案。

最后,第 20 章 收集了不适合其他任何地方的杂项问题:位操作、效率、算法、C 与其他语言的关系,以及一些琐碎的问题。(第 20 章 的介绍包含对其各种内容的更详细的分解。)

结束本引言,这里有两个初步问题,与其说是关于 C,不如说是关于本书:

问:如果这本书可以在网上免费获得,为什么我还要买这本书?

答:本书包含的材料比发布到 comp.lang.c. 的版本多三倍以上,尽管电子文档有其优势,但以印刷形式处理如此多的信息确实可以更容易。 (你将花费大量时间从网上下载这么多信息并打印出来,而且排版也不会那么漂亮。)

问:“FAQ”怎么读?

答:我读作“eff ay kyoo”,我相信这在 FAQ 列表刚被“发明”时就是最初的读法。现在许多人读作“fack”,这很形象地让人联想到“fact”这个词。我读复数形式,比如本书标题中的,是“eff ay kyooze”,但许多人读作“fax”。这些读音没有绝对的对错之分;“FAQ”是一个新词,大众的使用在塑造任何术语的演变中都起着一定的作用。

(顺便说一句,同样无法确定的是,“FAQ”指的是问题本身,还是问题加上其答案,还是整个问答列表。)

但现在,让我们进入正题吧!