这一篇在干嘛?

这是《C Primer Plus》的起点篇:不写复杂代码,先回答三个问题——C 语言从哪来、为什么值得学、计算机是怎么跑程序的。读完你会明白”源代码 → 编译 → 链接 → 可执行文件”这条流水线,为第 02 章解剖第一个程序打好底子。

C 从何而来(Whence C?)

1972 年,贝尔实验室(Bell Labs)的 Dennis Ritchie 在和 Ken Thompson 一起开发 Unix 操作系统时,创造了 C 语言。C 并非凭空出现——它源自 Thompson 的 B 语言。

C 的出身决定了它的性格:它是为工作中的程序员创造的实用工具,首要目标是”有用”。对比一下同时代的其他语言:

  • Pascal:主要目标是给”良好的编程原则”提供教学基础;
  • BASIC:设计得像英语,方便不懂计算机的学生学习。

这些目标都很好,但不总与”日常干活好用”兼容。C 走的是实用路线,这让它成为几代程序员的选择。

为什么选 C(Why C?)

图:C 语言的优点——代码紧凑、程序小巧、可移植到其他计算机

设计特性

C 融入了计算机科学理论与实践公认 desirable 的控制特性,天然适合自顶向下(top-down)规划、结构化编程(structured programming)和模块化设计,写出的程序更可靠、更易理解。

高效(Efficiency)

C 程序往往紧凑且运行快。它甚至具备一些通常只有汇编语言(assembly language,一种用助记符表示特定 CPU 内部指令的语言)才有的精细控制力——你可以为”最快速度”或”最省内存”微调程序。

可移植性(Portability)

在 A 系统上写的 C 程序,拿到 B 系统上很少或无需修改就能运行。当年把 IBM PC 的 BASIC 程序搬到 Apple BASIC、或把大型机 FORTRAN 搬到 Unix 的人都懂:移植有多麻烦。而 C 的编译器覆盖了从 8 位微处理器到超级计算机的约 40 种系统。

当然,可移植不是绝对的:专门访问某类硬件(如显示器)或操作系统特性(如 Windows API)的那部分代码,通常不可移植

强大灵活、面向程序员

  • 大部分 Unix 操作系统本身就是用 C 写的;FORTRAN、Perl、Python、BASIC 等语言的编译器/解释器也多为 C 所写;
  • C 能访问硬件、操作内存中的单个位(bit),运算符(operator)丰富;
  • C 不像 Pascal 那样严格限制你——自由既是优点也是危险:许多任务(如数据形式转换)在 C 里很简单,但你也可能犯在其他语言里根本不可能犯的错。

缺点

优缺点的根源常常是同一个特性:

  • C 的**指针(pointer)**让你能犯很难追踪的错误——“自由奉献的代价是永恒的警惕”;
  • C 简洁 + 运算符丰富,意味着有能力写出极难读懂的代码(还真有年度”混乱代码大赛” Obfuscated Code Contest)。

C 的去向(Whither C?)

到 20 世纪 80 年代初,C 已是 Unix 小型机世界的主导语言,随后扩展到个人计算机和大型机。软件公司喜欢用 C 写文字处理、电子表格、编译器等产品——因为 C 程序紧凑高效、易修改、易适配新机型。

90 年代起,很多大项目转向 C++(在 C 之上嫁接面向对象编程,object-oriented programming)。C++ 几乎是 C 的超集,所以学好 C 也顺便学了大半个 C++

尽管有 C++、Java 等新秀,C 依然是软件行业的核心技能:嵌入式系统(汽车、相机、DVD 机里的微处理器)、科学计算、Linux 开发,处处有 C。

计算机在做什么(What Computers Do)

学 C 之前,先花两分钟理解计算机的基本构造:

  • CPU(中央处理器):干大部分计算活的部件;
  • RAM(随机存取内存):工作台,临时存放程序和文件;
  • 永久存储(如硬盘):断电也不丢的仓库;
  • 外设(键盘、鼠标、显示器):你与计算机交流的通道。

CPU 的”人生”极其简单:从内存取一条指令 → 执行 → 再取下一条 → 执行……(GHz 级 CPU 每秒能这样干约十亿次)。CPU 有几个自己的小抽屉——寄存器(register),其中一个存放”下一条指令的内存地址”。

两个关键事实:

  1. 计算机里的一切都是数字。字符有数字编码,指令也有数字编码;
  2. 程序最终必须以 CPU 能懂的**机器语言(machine language)**表达。

想给计算机下命令,就得用机器语言写一张精确到步骤的清单。连”把两个数相加”都要拆成:

  1. 把内存位置 2000 的数复制到寄存器 1;
  2. 把内存位置 2004 的数复制到寄存器 2;
  3. 寄存器 2 加到寄存器 1,结果留在寄存器 1;
  4. 把寄存器 1 的内容复制到内存位置 2008。

而且每条指令还得用数字代码表示!幸好,机器语言编程的黄金时代早就过去了。

高级语言与编译器(High-level Languages and Compilers)

C 这样的**高级编程语言(high-level language)**让编程轻松许多:

  • 不用数字代码写指令;
  • 指令更接近人的思维方式。加两个数,你只需要写:
total = mine + yours;

但计算机看不懂这行字,于是**编译器(compiler)**登场:它把高级语言程序翻译成计算机需要的机器语言指令。你负责高层思考,编译器负责烦琐细节。

编译器还带来一个巨大的好处:每种 CPU 有自己的机器语言,但每种机器都可以配一个对应的编译器。同一个 C 程序,配上不同编译器,就能在不同机器上运行——这就是”一次编写、到处编译”的可移植性来源。

使用 C 的七个步骤(Using C: Seven Steps)

图:编程的七个步骤:定义目标 → 设计程序 → 编写代码 → 编译 → 运行 → 测试和调试 → 维护和修改

第 1 步:定义程序目标

想清楚程序要做什么:需要什么信息、要做什么计算、要汇报什么结果。这一步用一般术语思考,别想具体语言。

第 2 步:设计程序

决定怎么实现:用户界面什么样?程序如何组织?数据如何表示、用什么方法处理?好的数据表示方式能让后面的编程轻松一大截。这一步依然别碰具体代码。

第 3 步:编写代码

把设计翻译成 C 语言。用文本编辑器创建源代码文件(source code file)。书中的示例长这样(现在看不懂没关系,第 02 章就懂了):

#include <stdio.h>
int main(void)
{
    int dogs;
 
    printf("How many dogs do you have?\n");
    scanf("%d", &dogs);
    printf("So you have %d dog(s)!\n", dogs);
 
    return 0;
}

这一步别忘了写注释(comment)——把解释直接嵌进源代码里。

第 4 步:编译

编译器把源代码变成可执行代码(executable code),即计算机的母语——机器语言。它还会顺带把 C 库(library)里的标准例程(如 printf()scanf())纳入最终程序(准确说干这活的是链接器,下一节细讲)。编译器同时检查你的程序是不是合法的 C——发现错误就报告,并且不生成可执行文件。学会看懂编译器的报错,本身就是一项要练的技能。

第 5 步:运行程序

在 Unix/Linux 控制台下输入可执行文件名即可运行;在 IDE(集成开发环境)里点菜单或按键运行;也可以直接双击文件图标。

第 6 步:测试和调试

程序能跑,不代表跑得对。程序里的错误叫bug,找错改错的过程叫调试(debugging)。犯错是学习的自然组成部分——随着你水平提高,你的错误也会变得更强大、更隐蔽。出错的可能包括:设计错误、好想法实现错了、没考虑奇怪的输入、C 用法不对、打字错误、括号放错位置……

第 7 步:维护和修改

程序被用起来之后,你会想改它:修小 bug、加新功能、适配新系统。文档清晰 + 设计良好的程序,维护起来才不痛苦。

记忆口诀:先规划,后编码

原书特别提醒:初学者最爱跳过第 1、2 步直接写代码。程序简单时脑子能装下全过程,可程序一长,凭感觉写出来的就是丑陋、低效、难懂的代码。任务越大越复杂,规划就越重要——动笔前先用纸笔写下目标和设计,省下的时间是真金白银。

编程机制(Programming Mechanics)

源代码文件

用 C 写的程序保存在文本文件里,文件名要以 .c 结尾。文件名分两部分:点号前是基本名(basename),点号后是扩展名(extension)。例如 budget.c 里,budget 是基本名,c 是扩展名。注意 Unix 区分大小写:budget.cBUDGET.c 是三个不同文件,而 BUDGET.C 不是合法的 C 源文件名(大写 C 不行)。

目标文件、可执行文件与库

C 把源代码变成可运行程序分两步:编译(compiling)链接(linking)

编译器先把源代码翻译成目标代码文件(object code file,简称 object file)。目标文件虽然已是机器语言,但还不能运行,缺两样东西:

  1. 启动代码(startup code):程序与操作系统之间的接口。同样的硬件,DOS 和 Linux 处理程序的方式不同,启动代码就不同;
  2. 库例程代码printf() 之类的标准函数代码存在**库文件(library)**里,目标文件里只有”我要用 printf()“这句话,没有 printf 的真身。

图:编译器与链接器——源代码经编译器生成目标代码,链接器把目标代码、启动代码和库代码组合成可执行文件

**链接器(linker)**把三样东西合并成一个可执行文件:你的目标代码 + 系统的启动代码 + 用到的库代码(只提取你用到的那些函数)。

一句话总结:目标文件和可执行文件都是机器语言,区别在于目标文件只含你自己代码的翻译,可执行文件还包含库例程和启动代码

Unix 系统上的流程

Unix 不自带编辑器,用 emacs、vi 等通用编辑器写一个 inform.c

#include <stdio.h>
int main(void)
{
    printf("A .c is used to end a C program filename.\n");
    return 0;
}

编译命令:

cc inform.c

几秒后提示符返回,编译完成。用 ls 一看,多了个 a.out——这就是可执行文件。运行它:

a.out

输出:

A .c is used to end a C program filename.

图:在 Unix 上准备 C 程序的完整流程:编辑 inform.c → cc 编译 → 生成 a.out 并运行

细节:cc 会生成与源文件同名的目标文件 inform.o,但单文件程序链接完成后它会被删掉;想保留 a.out 就要改名,否则下次编译会被覆盖。

Linux 与 Windows

Linux 上用的是 GNU 的 gcc 编译器,流程与 Unix 基本一致(通常还把 cc 设为 gcc 的别名)。

Windows 不自带 C 编译器,一般装集成开发环境(IDE)——自带编辑器、菜单化编译运行、出错时能定位到出问题的行。新建项目时选择控制台(Console)类型的项目,才能跑本书这类命令行程序。

新手坑:IDE 窗口一闪而过

在 Windows IDE 里运行程序,输出窗口经常在程序结束的瞬间自动关闭,什么都看不见。解决办法:在 return 语句前加一行 getchar();,它会让程序等你按一次 Enter 键再退出。如果程序此前刚读过你的输入(比如让你输入体重),可能需要写两次 getchar()

源文件必须是文本文件

无论什么系统,源代码文件必须是纯文本文件,不能是字处理软件的专有格式(后者塞满了字体、排版信息)。用记事本等文本编辑器没问题;用字处理器的话,要”另存为”纯文本,且留意有的软件会自动加 .txt 扩展名——要改回 .c

语言标准(Language Standards)

K&R C

C 诞生之初没有官方标准,Kernighan 和 Ritchie 1978 年的《The C Programming Language》第一版成了公认标准,史称 K&R C 或经典 C。但它只定义了语言、没定义库,于是 Unix 自带的库成了事实标准。

第一个 ANSI/ISO 标准(C89/C90)

1983 年 ANSI 成立 X3J11 委员会,1989 年通过新标准,同时定义了语言和标准 C 库;ISO 在 1990 年采纳了实质相同的标准。这套标准因此被称为 C89(ANSI 批准年)或 C90(ISO 批准年)。

委员会有一组很有趣的指导原则,被称为”保持 C 的精神”:

  • 信任程序员(Trust the programmer);
  • 不阻止程序员做需要做的事;
  • 保持语言小而简单;
  • 一种操作只提供一种方法;
  • 追求速度,哪怕牺牲一点可移植性。

C99 标准

1994 年开始修订,成果是 C99。三大目标:

  1. 国际化:支持国际字符集;
  2. 修正缺陷:把实际中已被验证的做法编入标准(比如适配 64 位处理器);
  3. 提高计算能力:改进科学与工程关键数值计算的适用性。

同时保持保守:最小化与 C90 和 C++ 的不兼容,让 C 保持精炼——用委员会的话说,“让 C++ 去做那个庞大而有野心的语言吧”。当时的多数编译器并未完全实现 C99 的全部特性,有些特性还需要修改编译器设置才能开启。

最佳实践: sticking to the standard

写 C 程序时坚持标准写法(如 int main(void) + return 0;),不要用某些编译器私放的写法(如 void main())。标准写法保证你的程序从一台机器搬到另一台、从一个编译器换到另一个时都不出问题——这正是 C 可移植性红利的兑现方式。

本章小结

  • C 是为程序员设计的、强大而简洁的编译型语言,优点是高效、可移植、灵活,代价是需要程序员自律;
  • 计算机只懂机器语言,编译器负责把 C 源代码翻译成机器语言,链接器把你的目标代码、启动代码和库代码组装成可执行文件;
  • 编程七步走:定目标 → 设计 → 写代码 → 编译 → 运行 → 测试调试 → 维护修改;
  • C 标准经历了 K&R C → C89/C90 → C99 的演进。

新手坑:只动手不动脑

又要强调一次:不要跳过”定义目标”和”设计程序”直接写代码。这个坑在第 1 步时摔一跤最便宜——现在养成纸笔规划的习惯,等程序长到几百行再摔,代价就是几小时的迷茫和返工。