| 目录 |

2 你好,世界!

2.1 C 能带来什么

“这些楼梯通向哪儿?”
“它们向上。”

——雷·斯坦茨与彼得·文克曼,《捉鬼敢死队》

C 是一门底层语言。

从前可不是这样。当年人们还用花岗岩雕凿穿孔卡片的时候,C 是一种了不起的解脱方式,让人不必再承受汇编19这类更底层的语言的苦役。

但到了现代,当代的语言提供了各种各样 1972 年 C 被发明时还不存在的特性。这意味着 C 是一门相当基础的语言,特性并不多。它什么都能做,但会让你为此付出努力。

那我们今天为什么还要用它呢?

如果你熟悉另一门语言,C 的很多东西都很简单。C 启发了许多其他语言,在 Go、Rust、Swift、Python、JavaScript、Java 以及各种各样别的语言里,你都能看到它的影子。那些部分你会觉得眼熟。

C 里真正让人卡住的只有一样东西:指针。其他几乎一切都是熟悉的,唯独指针很古怪。指针背后的概念你很可能早就知道,但 C 逼着你把它明确表达出来,用的还是你大概从没见过的运算符。

这尤其阴险,因为一旦你吃透了指针24,它们就突然变得简单了。但在那一刻之前,它们就是滑溜溜的鳗鱼。

C 里其余的一切,不过是记住另一种(有时甚至是同一种!)做你早已做过的事情的方式。指针才是那个古怪的部分。而且可以说,就连指针也不过是某个你大概熟悉的主题的变奏。

那么,准备好踏上一场欢快的冒险吧——在不使用汇编的前提下,尽可能贴近计算机的核心,用的还是有史以来最具影响力的计算机语言25。坐稳了!

2.2 你好,世界!

这就是 C 程序最经典的示例。人人都用它。(注意,左边的数字只是供读者参考的行号,并不属于源代码。)

/* Hello world 程序 */

#include <stdio.h>

int main(void)
{
    printf("Hello, World!\n");  // 在这里真正干活
}

我们要戴上长袖重型橡胶手套,抓起手术刀,把这玩意儿剖开,看看它到底靠什么运转。所以,好好刷手消毒吧,因为要开始了。下刀要非常轻……

先说个简单的:/* 和 */ 这对符号之间的任何内容都是注释,编译器会完全忽略它。一行中 // 之后的任何内容也一样。这样你就可以给自己和别人留消息,等你在遥远的将来回头读自己的代码时,能知道当时到底想干什么。相信我,你会忘的;这种事就是会发生。

那么,这个 #include 又是什么?真丑!好吧,它告诉 C 预处理器把另一个文件的内容取出来,就地插入到代码那里。

等一下——什么是 C 预处理器?问得好。编译有两个阶段26:预处理器和编译器。任何以井号(pound sign、hash symbol,或者叫 “octothorpe”)开头的东西(#),都是预处理器在编译器动手之前先处理的。这些通常称为预处理指令,常见的有 #include 和 #define。后面还会详细讲。

在继续之前,我为什么非得指出井号还叫 octothorpe 呢?答案很简单:我觉得 octothorpe 这个词实在太好笑了,所以一有机会就要无缘无故地到处宣扬它。Octothorpe。Octothorpe,octothorpe,octothorpe。

所以总之。C 预处理器把所有东西预处理完之后,结果就交给编译器,由它生成汇编代码27、机器码28,或者它打算生成的任何东西。机器码是 CPU 能理解的“语言”,而且它能非常快地理解。这也是 C 程序往往很快的原因之一。

现在先别操心编译的技术细节;你只要知道,源代码先过预处理器,预处理器的输出再过编译器,然后编译器生成一个可执行文件供你运行。

那这一行的其余部分呢?<stdio.h> 是什么?这就是所谓的头文件。末尾那个 .h 就露了馅。实际上,它是“标准 I/O”(stdio)头文件,你以后会逐渐认识并喜欢上它。它让我们能够使用一系列 I/O 功能29。在我们的演示程序里,要输出字符串“Hello, World!”,所以特别需要用到 printf() 函数来完成这件事。<stdio.h> 文件就提供了这个入口。基本上,如果我们不 #include <stdio.h> 就使用 printf(),编译器就会向我们抱怨。

我怎么知道用 printf() 就需要 #include <stdio.h>?答案:文档里写着呢。如果你在 Unix 系统上,敲 man 3 printf,手册页最上面就会告诉你需要哪些头文件。或者看本书的参考章节。:-)

我的天哪。讲了这么多,才只讲完了第一行!不过,面对现实吧,它已经被彻底解剖了。不会再有谜团了!

那就歇口气……回头再看看示例代码。只剩下几行简单的了。

欢迎休息归来!我知道你其实没休息;我只是哄哄你罢了。

下一行是 main()。这是函数 main() 的定义;那对花括号({ 和 })之间的所有内容都属于这个函数定义。

(话说回来,怎样调用另一个函数呢?答案就在 printf() 那一行里,我们马上就会讲到。)

现在,main 函数在很多方面都很特殊,但有一方面尤为突出:程序开始执行时,会自动调用这个函数。在 main() 之前,你的任何代码都不会被调用。就我们的示例而言,这完全没问题,因为我们想做的只是打印一行然后退出。

哦,还有一点:一旦程序执行过 main() 的末尾,也就是下面那个收尾的花括号处,程序就会退出,而你会回到命令提示符。

现在我们知道,那个程序引入了一个头文件 stdio.h,并声明了一个会在程序启动时执行的 main() 函数。那么 main() 里有什么好东西呢?

你问得太好了。真的!我们只有一样好东西:一次对函数 printf() 的调用。有很多办法能看出这是函数调用而不是函数定义,其中一个标志就是它后面没有花括号。调用末尾要用分号收尾,这样编译器就知道表达式的结束了。你会看到,几乎什么后面都要加分号。

你给函数 printf() 传了一个实参:一个在调用时会被打印出来的字符串。哦耶——我们在调用函数了!我们太厉害了!等等,等等——别得意忘形。字符串末尾那个奇怪的 \n 是什么?嗯,字符串里的大多数字符都会按它们存储的样子打印出来。但有些字符没法在屏幕上好好打印,它们就以反斜杠加字符的两字符代码形式嵌入。最常见的一个是 \n(读作 “backslash-N”,或者干脆读 “newline”),它对应换行字符。正是这个字符让后续输出从下一行行首继续,而不是挤在当前行。就像在一行末尾敲了回车。

把这段代码复制到一个名为 hello.c 的文件里,然后构建它。在类 Unix 平台(例如 Linux、BSD、Mac 或 WSL)上,你可以在命令行用类似下面这样的命令来构建:

gcc -o hello hello.c

(这里的含义是“编译 hello.c,并输出一个名为 hello 的可执行文件”。)

完成之后,你应该会得到一个名为 hello 的文件,可以用下面这条命令运行它:

./hello

(开头的 ./ 告诉 shell“从当前目录运行”。)

看看会发生什么:

Hello, World! 

搞定,也测过了!发布吧!

2.3 编译细节

我们再稍微多聊聊如何构建 C 程序,以及这背后都发生了什么。

和其他语言一样,C 也有源代码。不过,取决于你来自哪种语言,你可能从来不需要把源代码编译成可执行文件。

编译就是把你的 C 源代码变成一个操作系统可以执行的程序的过程。

JavaScript 和 Python 开发者根本不习惯单独的编译步骤——尽管它在幕后确实一直在发生!Python 会把你的源代码编译成一种叫字节码的东西,由 Python 虚拟机执行。Java 开发者则习惯了编译,但那产生的是供 Java 虚拟机使用的字节码。

编译 C 时生成的是机器码。这就是那些 1 和 0,CPU 可以直接、快速地执行它们。

通常不经过编译的语言被称为解释型语言。但正如我们提到的 Java 和 Python,它们也有编译步骤。而且也没有哪条规则说 C 不能被解释执行。(世界上确实有 C 解释器!)简而言之,这中间有一大堆灰色地带。总的来说,编译不过是把源代码转换成另一种更易于执行的形式。

C 编译器就是执行编译工作的那个程序。

正如我们已经说过的,gcc 是安装在许多 类 Unix 操作系统上的编译器30。它通常是在终端里从命令行运行的,但并非总是如此。你也可以从 IDE 中运行它。

那么,我们怎么做命令行构建呢?

2.4 用 gcc 构建

如果当前目录下有一个名为 hello.c 的源文件,你可以在终端里输入下面这条命令,把它构建成一个名为 hello 的程序:

gcc -o hello hello.c

-o 的意思是“输出到这个文件”31。而末尾的 hello.c 就是我们要编译的文件名。

如果你的源代码分散在多个文件里,可以把所有 .c 文件都放到命令行上,从而一起编译它们(几乎就像它们是同一个文件,不过实际规则比这更复杂):

gcc -o awesomegame ui.c characters.c npc.c items.c

它们就会一起构建成一个大的可执行文件。

这些足够你起步了——后面我们会详细讨论多个源文件、目标文件以及各种有趣的东西。

2.5 用 clang 构建

在 Mac 上,系统自带的编译器不是 gcc——而是 clang。不过系统也安装了一个包装器,所以你可以运行 gcc,它照样能工作。

你也可以通过 Homebrew32 或其他方式安装真正的 gcc 编译器。

2.6 从 IDE 构建

如果你使用的是集成开发环境(IDE),那你大概不必从命令行构建。

在 Visual Studio 中,CTRL-F7 用于构建,CTRL-F5 用于运行。

在 VS Code 中,你可以按 F5 通过调试器运行。(你需要安装 C/C++ 扩展。)

在 XCode 中,你可以用 COMMAND-B 构建,用 COMMAND-R 运行。要获取命令行工具,可以在网上搜索“XCode command line tools”,你会找到安装说明。

作为入门,我建议你也试试从命令行构建——那可是历史!

2.7 C 语言版本

这些年来 C 一路发展,有过许多带名字的版本号,用来描述你使用的是这门语言的哪种方言。

这些版本号通常指规范发布的年份。

最著名的是 C89、C99、C11 和 C23。本书将重点讲最后一个。

不过下面是一张更完整的表:

版本 说明
K&R C 1978 年,最初的版本。以 Brian Kernighan 和 Dennis Ritchie 命名。Ritchie 设计并编写了这门语言,Kernighan 则与人合著了关于它的书。如今你很少见到原始的 K&R 代码。如果见到了,它看起来会很古怪,就像中古英语在现代英语读者眼中那样古怪。
C89、ANSI C、C90 1989 年,美国国家标准学会(ANSI)发布了一份 C 语言规范,为此后延续至今的 C 定下了基调。一年后,缰绳交到了国际标准化组织(ISO)手中,它制定了与之完全相同的 C90。
C95 对 C89 的一次鲜少被提及的补充,加入了宽字符支持。
C99 第一次大规模翻修,加入了大量语言特性。大多数人记得最清楚的是加入了 // 风格的注释。截至本文写作时,这是使用最广泛的 C 版本。
C11 这次大版本更新包含 Unicode 支持和多线程。请注意,一旦你开始使用这些语言特性,可能就要牺牲与那些仍停留在 C99 时代的地方之间的可移植性。不过说实话,1999 年已经是相当久远的事了。
C17、C18 C11 的缺陷修复更新。C17 似乎是正式名称,但发布一直推迟到 2018 年。据我所知,这两个名字可以互换使用,但更推荐 C17。
C23 最新的规范。

你可以用 -std= 命令行参数强制 GCC 使用其中某个标准。如果你希望它对标准更挑剔,可以再加上 -pedantic。

例如:

gcc -std=c11 -pedantic foo.c

在本书中,我按 C23 标准编译程序,并打开所有警告:

gcc -Wall -Wextra -std=c23 -pedantic foo.c


| 目录 |