这一章在干嘛?
开门见山:用一个完整的小程序把「写 C 代码 → 翻译成机器能执行的文件 → 运行看结果」整条链路走一遍,对 C 的样子建立第一印象。细节不懂没关系,后面每一章都会把对应的部分展开讲透。
1.1 解剖一个完整的小程序
下面这个程序做一件很朴素的事:从标准输入读文本,把每一行中指定列范围的内容提取出来打印。列号从 0 开始,成对给出,以一个负数结束。比如输入 3 7 -1,就提取每行的第 3~7 列:
#include <stdio.h>
#include <string.h>
#define MAX_COLS 20 /* 最多处理的列对数 */
#define MAX_INPUT 1000 /* 每行输入的最大长度 */
/* 读列对:成对存入 cols,遇到负数或读满停止,返回对数 */
static int read_columns(int cols[], int max_cols)
{
int n = 0, v;
while (n < max_cols && scanf("%d", &v) == 1 && v >= 0) {
cols[n++] = v; /* 起点 */
if (n < max_cols && scanf("%d", &v) == 1 && v >= 0)
cols[n++] = v; /* 终点 */
else
break; /* 凑不满一对:结束 */
}
return n / 2;
}
int main(void)
{
int cols[MAX_COLS];
char line[MAX_INPUT];
int pairs = read_columns(cols, MAX_COLS);
int start, end, i;
while (fgets(line, MAX_INPUT, stdin) != NULL) {
line[strcspn(line, "\n")] = '\0'; /* 去掉行尾换行 */
for (i = 0; i < pairs; i++) {
start = cols[2 * i];
end = cols[2 * i + 1];
printf("%.*s\n", end - start + 1, line + start);
}
putchar('\n');
}
return 0;
}
逐块看它都用了什么:
| 代码片段 | 涉及的知识点 |
|---|---|
| include <stdio.h> | 预处理指令:把标准库的函数声明抄进来(第 14 章) |
| define MAX_COLS 20 | 符号常量:给「魔法数字」起名字(第 14 章) |
| int cols[], char line[] | 数组:一串同类型数据住在连续内存里(第 8 章) |
| line + start | 指针运算:数组名退化成指针再偏移(第 6、8 章) |
| printf(”%.*s\n”, …) | 格式化 I/O:域宽、精度那些 % 开头的暗号(第 15 章) |
| static / return | 函数与存储类型(第 3、7 章) |
为什么第一课不是语法细节?
因为 C 的知识点是一张网:指针连着数组、数组连着函数参数、函数连着声明。先见过「整体长什么样」,再逐个击破,比从孤立语法片段学起效率高得多。
1.2 从源码到运行:翻译的完整流水线
计算机只认机器码,C 源文件必须经过一条流水线变成可执行文件:
- 预处理:纯文本替换——#include 抄头文件、#define 做替换、条件编译裁代码;
- 编译:把预处理后的文本翻译成汇编/目标码,同时做语法检查;
- 链接:把你写的目标文件和库的目标文件拼在一起,补齐「外部符号」的地址(比如 printf 到底在哪儿)。
日常一条命令 gcc main.c -o prog 其实把这几步全做了。分步执行可以用 -E(只预处理)、-c(只编译到目标文件)观察中间产物。
链接期错误的典型长相:
undefined reference to 'foo'——编译器见过 foo 的声明所以不报错,但链接器找不到它的定义。九成是忘了写实现或忘了链上对应的目标文件。
1.3 给初学者的三条上手建议
① 警告要当错误看。 C 编译器非常宽容,很多「能跑」的程序其实埋着雷。把 -Wall -Wextra 开成习惯,警告清零再谈运行。
② 每个知识点配一个最小实验。 学到指针运算就写五行代码验证 *(p+1) 到底等于什么;sizeof 一个怀疑的类型就 printf("%zu\n", sizeof(int))。机器永远比记忆可靠。
③ 出错先想内存。 段错误、乱码、诡异崩溃,大多源于越界、未初始化、悬空指针。养成习惯:声明变量就初始化,用数组先问长度。
本章通关标准:
1. C 从源文件到可执行文件要经过哪几个阶段?
预处理(文本替换:头文件、宏、条件编译)→ 编译(翻译为目标文件)→ 链接(合并目标文件与库、解析外部符号)。gcc 一条命令实际串了三步,可用 -E / -c 分步观察。
2. 编译能过但链接报 undefined reference,问题出在哪?
声明只告诉编译器「有这么个函数」,定义才产生真正的代码。链接器找不到定义就会报这个错——检查是否漏写实现、漏链目标文件。
3. 为什么第一课建议读完整程序而不是背语法?
C 的知识是一张互相引用的网(指针→数组→函数参数→声明)。先建立整体印象,后面各章展开时才有挂靠点,比孤立记忆语法片段高效。