第1章:初识 C 语言
学习 C 的第一步,是建立一条完整的认识链:先明确要解决的问题,再用 C 描述处理方法,经过编译和链接生成程序,最后通过运行与测试确认结果。
这一章的重点是理解 C 的定位和程序的形成过程。具体语法可以在后续学习中逐步补齐。
1. C 是一门怎样的语言
1.1 从解决实际问题出发
C 语言诞生于 20 世纪 70 年代初的贝尔实验室,由 Dennis Ritchie 在 B 语言的基础上发展而来,与 UNIX 操作系统的开发关系密切。
它的设计目标偏重实用:让程序员既能用较容易理解的方式组织程序,又能有效利用计算机的硬件资源。
因此,理解 C 可以抓住三个关键词:
- 抽象:用变量、表达式和函数描述任务,不必逐条编写机器指令。
- 控制:能够较细致地控制内存、数据表示和底层操作。
- 责任:程序员需要主动保证操作合理,语言不会替你拦住所有错误。
1.2 主要优势与代价
| 特点 | 具体含义 | 学习时应形成的认识 |
|---|---|---|
| 适合结构化设计 | 可以把大问题拆成小任务,再通过函数组织起来 | 先考虑任务如何分解,再写代码 |
| 高效 | 便于生成紧凑的代码,并控制运行时间和内存使用 | 高效依赖算法、实现和编译器,不能只看语言名称 |
| 可移植 | 遵循标准、少依赖特定平台的源代码,较容易移到其他系统 | 移植通常需要在目标平台重新编译 |
| 功能灵活 | 既能编写一般应用,也能参与系统和硬件相关开发 | 灵活性使它适用面广,也增加了出错空间 |
| 库支持丰富 | 可以调用已有函数完成常用任务 | 语言本身与库要配合理解 |
| 表达紧凑 | 运算符丰富,较少的代码就能表达较多操作 | 应优先保证清楚易读,避免刻意写得晦涩 |
尤其需要留意指针和内存相关操作。它们赋予程序员较强的控制能力,也可能带来难以定位的错误。
控制能力越强,对设计、检查和调试的要求也越高。
1.3 典型应用方向
C 的特性使它适合以下任务:
- 操作系统及相关基础软件。
- 嵌入式设备和微控制器程序。
- 编译器、解释器以及其他语言的运行支持。
- 科学与工程计算。
- 对性能和资源占用有明确要求的应用。
C 与 C++ 有共同的历史和许多相似语法,但学习时仍应区分两种语言的规则,使用 C 模式编译 C 程序。
2. 计算机为什么需要编译器
2.1 程序运行涉及哪些部件
| 部件 | 主要作用 | 可以怎样理解 |
|---|---|---|
| CPU | 执行指令,完成运算和控制 | 实际执行任务的处理单元 |
| 寄存器 | 保存 CPU 当前处理的数据、指令或地址等信息 | CPU 内部容量很小的工作区 |
| RAM(内存) | 保存运行中使用的程序和数据 | 程序执行时的工作空间,通常断电后内容消失 |
| 硬盘、固态硬盘等存储设备 | 长期保存文件 | 程序和数据的持久存放位置 |
| 输入、输出设备 | 接收输入并呈现结果 | 键盘、屏幕等人与程序交互的通道 |
程序保存在磁盘上,与程序正在运行,是两种不同状态。运行时,操作系统会把所需内容装入内存,并安排执行。
2.2 CPU 执行的是具体指令
可以先把 CPU 的工作过程简化为:
取得指令 → 理解指令 → 执行操作 → 继续取得指令
它能执行的指令构成指令集。这些指令通常十分具体,例如移动数据、执行算术运算、比较数值或跳转。
在程序员看来,两数相加只需写成:
total = left + right;
在机器层面,它可能需要取出数据、完成加法、保存结果等多条指令。实际采用什么指令,由目标机器和编译器等因素决定。
数据和指令在计算机中都以二进制形式表示。CPU 能直接执行的是机器指令,不能直接执行 C 源文件中的文字。
2.3 三种表达层次
| 层次 | 表达方式 | 与硬件的关系 |
|---|---|---|
| 机器语言 | 用机器指令编码表示操作 | 直接对应特定指令集 |
| 汇编语言 | 用助记符等形式表达机器指令 | 仍与特定体系结构紧密相关 |
| C 等高级语言 | 用变量、表达式、函数等表达处理逻辑 | 可以把同一套逻辑交给不同目标平台的工具处理 |
编译器承担翻译工作,使程序员可以集中表达处理逻辑。 不同目标平台需要与之匹配的编译工具。
3. 从源代码到可执行程序
3.1 先分清三类文件
| 文件 | 常见名称 | 包含什么 | 能否直接运行 |
|---|---|---|---|
| 源代码文件 | hello.c |
程序员编写的 C 文本 | 通常不能 |
| 目标代码文件 | hello.o、hello.obj |
编译得到的机器代码及链接所需信息 | 通常不能 |
| 可执行文件 | hello、hello.exe |
链接形成、供操作系统加载执行的程序 | 在兼容的运行环境中可以 |
源文件应保存为纯文本。把文件名从 .txt 改成 .c,只改变名称,并不等于完成编译。
3.2 编译与链接各做什么
先用两大阶段理解生成程序的过程:
源代码 hello.c
│
│ 编译过程
▼
目标文件 hello.o ───────┐
其他目标文件 ──────────┤
所需库与启动支持 ──────┤
│ 链接
▼
可执行程序
编译器负责检查和翻译源代码,生成目标代码。若出现阻止编译完成的错误,需要修改后重新编译。
链接器负责连接各部分代码,解析跨文件和库函数的引用,形成可执行程序。
这里的“编译过程”是概括说法;进一步细分还会涉及预处理、编译和汇编等环节。入门时先把“翻译代码”和“连接代码”这两件事分清。
3.3 为什么目标文件还不能直接运行
目标文件里虽然已有机器代码,却通常还存在尚未解决的引用,并缺少构成完整程序所需的其他部分。
例如,程序调用了 printf(),并不意味着编译器已经把它的实现写进该目标文件。链接阶段还需要处理程序对库函数的依赖。
启动支持则负责程序进入正式执行前所需的运行环境准备,以及与系统约定的衔接。
对于库,还要避免一种过度简化的理解:生成可执行文件不代表所有库实现都会完整复制进去。 静态链接可以把所需代码纳入程序;动态链接则可能保留依赖,在加载或运行时使用相应的共享库。
3.4 分开编译的意义
当程序由多个源文件组成时,可以先分别生成目标文件,再统一链接。
这样做有两个直接好处:
- 便于按功能划分模块,分别开发和检查。
- 某个源文件改变后,通常只需重新编译受影响的部分,再重新链接。
因此,目标文件是支持模块化构建的中间成果。
4. 可移植性与语言标准
4.1 可移植主要指源代码
可移植性关注的是:把程序转到另一个系统时,需要付出多少修改和适配工作。
例如,同一份遵循标准的计算代码,可以交给两个平台各自的编译器,生成不同的机器代码。
同一份 C 源代码
/ \
平台 A 的编译工具 平台 B 的编译工具
↓ ↓
平台 A 的程序 平台 B 的程序
不要把源代码可移植理解成“一个可执行文件能在所有计算机上直接运行”。可执行程序还受到 CPU 架构、操作系统、文件格式和运行库等条件限制。
以下因素都会增加移植工作:
- 使用某个操作系统独有的接口。
- 直接操作特定硬件。
- 使用编译器专有扩展。
- 假定不同平台的数据类型大小等细节完全一致。
4.2 标准为什么重要
如果不同编译器对同一段代码采用互不兼容的规则,程序就难以移植。语言标准为不同实现提供共同的约定。
需要区分三层概念:
- 语言标准:约定语法、语义和标准库接口等规则。
- C 实现:编译器、库及相关运行支持对这些规则的具体实现。
- 编译选项:决定本次构建采用哪套规则、是否启用扩展等。
4.3 本章涉及的标准演进
| 阶段 | 需要掌握的重点 |
|---|---|
| K&R C | 正式标准出现前的重要语言描述,常称经典 C |
| C89 / C90 | ANSI 于 1989 年发布、ISO 于 1990 年采纳的同一套基础标准,规范语言和标准库 |
| C99 | 在保持 C 风格的基础上,增强国际化、数值计算等支持,并修补实际使用中的不足 |
| C11 | 继续适应新技术和安全需求,引入并发相关支持,并把部分特性设为可选 |
这些名称描述的是本章涉及的演进阶段。学习具体特性时,应留意它属于哪一版标准,以及所用实现是否支持。
标准化也不意味着抹平所有硬件差异。C 会为实现保留一些选择空间,以适应不同机器。
5. 编程的七个步骤
编写代码只是整个过程中的一部分。真正的起点是把问题说明白。
| 步骤 | 核心问题 | 应形成的成果 |
|---|---|---|
| 1. 定义目标 | 程序要做什么?输入和输出是什么? | 明确的任务描述与约束 |
| 2. 设计程序 | 用什么数据表示信息?怎样处理?如何与用户交互? | 处理流程、数据方案和模块划分 |
| 3. 编写代码 | 怎样用 C 表达设计? | 源文件及必要注释 |
| 4. 编译 | 代码能否正确翻译并完成链接? | 可执行程序,或需要处理的诊断信息 |
| 5. 运行 | 程序在目标环境中如何表现? | 实际执行结果 |
| 6. 测试和调试 | 结果是否符合目标?错误在哪里? | 测试结论与修正后的程序 |
| 7. 维护和修改 | 如何修复问题、增加功能或适配环境? | 持续改进的程序 |
5.1 目标与设计有什么不同
目标说明“做成什么样”,设计说明“准备怎样做”。
例如,目标是“把输入的长度换算成另一种单位并显示结果”;设计则要决定使用什么数据类型、怎样读取输入、采用哪个公式,以及如何处理不合法的内容。
先用自然语言和流程描述问题,有助于把需求判断与语法细节分开。
5.2 测试与调试有什么不同
- 测试:选择输入和场景,检查实际结果是否符合预期。
- 调试:发现异常后,定位原因并修复。
“编译成功”“能运行”和“结果正确”是三个不同层次。例如,把换算公式写反,代码仍可能通过编译并正常结束,只是答案错误。
测试应覆盖普通情况、边界情况和不合法输入;修复问题后,还需要重新验证。
5.3 七个步骤会反复往返
设计不合适时,要修改设计;测试暴露需求遗漏时,也可能需要重新定义目标。这些步骤构成迭代过程。
清楚的命名、合理的模块划分和说明意图的注释,可以减少后续修改时重新理解程序的成本。
6. 建立一个最小的编译运行流程
下面用一个小程序把前面的概念串起来。先关注操作流程,不必立即掌握全部语法。
6.1 保存源代码
把以下内容保存到纯文本文件 hello.c:
#include <stdio.h>
int main(void)
{
printf("Ready to learn C.\n");
return 0;
}
暂时只需知道:
<stdio.h>提供标准输入、输出相关声明。main是这种普通命令行程序的入口函数。printf用于输出,字符串中的\n表示换行。return 0;表示程序正常结束。
6.2 一次完成构建
在已提供 cc 命令的 macOS 或类 UNIX 命令行环境中,进入源文件所在目录后运行:
cc -std=c11 -Wall -Wextra hello.c -o hello
./hello
预期输出:
Ready to learn C.
| 命令部分 | 含义 |
|---|---|
cc |
调用系统配置的 C 编译驱动程序 |
-std=c11 |
按 C11 模式处理代码 |
-Wall -Wextra |
启用一组常用警告,帮助发现可疑写法 |
hello.c |
输入源文件 |
-o hello |
指定输出文件名 |
./hello |
执行当前目录中的 hello |
一条构建命令可以自动调用编译、链接等工具。命令数量与内部阶段数量并不相同。
6.3 分两步观察目标文件
cc -std=c11 -Wall -Wextra -c hello.c -o hello.o
cc hello.o -o hello
./hello
第一条命令中的 -c 表示生成目标文件后停止,不执行链接。第二条命令再通过编译驱动程序调用链接器,生成可执行文件。
这就对应了:
hello.c → hello.o → hello
6.4 工具名称与开发环境
GCC 和 Clang 都能处理 C 程序;cc 在不同环境中可能指向不同工具,可以用 cc --version 查看。
IDE 把编辑、构建、运行和调试等功能集中到一个界面中。使用 IDE 时,也应理解这些操作对应的阶段。入门示例通常适合创建控制台或命令行项目,并确认源文件按 C 语言处理。
如果程序结束后窗口立即关闭,可以从已有终端运行它,便于观察输出。具体菜单和按钮会随工具而变,构建与执行的基本机制是一致的。
7. 用一次单位换算练习程序设计
以“输入英寸,输出厘米”为例,先完成目标定义和设计,不急着写 C 代码。
7.1 明确目标
- 输入:一个用英寸表示的长度,允许包含小数。
- 处理:使用
厘米数 = 英寸数 × 2.54换算。 - 输出:换算结果,并明确标出单位。
- 约束:本例把长度限定为非负数;非数字或负数输入需要给出错误提示。
“长度必须非负”属于需求约束,应在设计前明确下来。
7.2 设计处理流程
开始
↓
提示输入长度,并说明输入单位是英寸
↓
读取输入
↓
输入是否为有效的非负数?
├─ 否 → 显示错误提示 → 结束
└─ 是 → 乘以 2.54 → 显示厘米结果 → 结束
数据表示应能容纳小数,避免把 1.5 英寸截成整数。输出也应事先约定精度,例如保留两位小数。
7.3 先写出测试预期
| 输入(英寸) | 预期表现 | 检查目的 |
|---|---|---|
1 |
输出 2.54 厘米 |
基本换算 |
0 |
输出 0.00 厘米 |
零值边界 |
1.5 |
输出 3.81 厘米 |
小数处理 |
100 |
输出 254.00 厘米 |
普通较大数值 |
-1 |
提示长度不能为负 | 需求约束 |
abc |
提示输入无效 | 非数字输入 |
先写测试预期,可以在实现之前发现需求中的模糊之处。
8. 学习时应采用的方式
采用逐步深入的方法:先理解一个概念的用途,在小程序中使用,再回头学习细节。函数、循环、字符串等内容都适合这样反复接触。
阅读示例时,可以采用以下顺序:
- 先预测程序会做什么。
- 亲手输入并编译运行。
- 对照预测解释结果。
- 每次只改动一个地方,观察变化。
- 遇到诊断信息时,尝试理解其原因。
阅读命令和交互示例时,要分清程序输出、用户输入和表示按键的说明。占位名称需要替换成实际值;表示按键的文字也不一定需要原样输入。

