C 程序设计语言全书学习地图

以 K&R 第二版官方八章为骨架,从教程程序、类型表达式、控制流和函数结构走到指针、结构体、标准 I/O 与 UNIX 接口,并用“读、跑、改、解释”闭环掌握 ANSI C 核心。

学习目标

  • 能描述 K&R 第二版八个正式章节的依赖关系,并把本站 10 个页面映射到原书核心单元
  • 能编译、运行并修改三个 ANSI C 基线程序,解释 main、值传递、EOF 与外部作用域的边界
  • 能回答:为什么“样例在本机输出正确”不能证明 C 程序正确,阅读 K&R 时应怎样识别实现定义、未指定与未定义行为?

从一个完整程序开始

K&R 不是先给出一张语法元素清单,再让读者等几十页才运行程序。第一章是 :先让你看到输入如何流入、状态如何变化、结果如何输出,再追问每个表达式为什么合法。

最小起点应当明确入口类型与返回状态:

#include <stdio.h>
 
int main(void)
{
    puts("hello, world");
    return 0;
}

cc -std=c90 -Wall -Wextra -Wpedantic hello.c 编译。int main(void) 表示不接收参数,return 0 把成功状态交给宿主环境。K&R 第二版面向 1988 年的 ANSI C;本站会同时指出后来标准的常用写法,但不会把 // 注释、变长数组或 C11 原子类型倒灌成原书规则。

八章不是五个模糊板块

官方 InformIT 样章目录列出八个正式章节:教程式导论、类型/运算符/表达式、控制流、函数与程序结构、指针与数组、结构、输入输出、UNIX 系统接口。附录 A 是参考手册,附录 B 汇总标准库,附录 C 总结版本变化;它们适合查证,不应替代正文的例子与推理。

本站把第五章拆成“指针与数组”和“指针运算与字符串”两页,并增加学习地图与总复习,所以页面数是 10;这不表示原书突然多了两章。每个页面都必须能回指官方单元,整书覆盖率也按这八个单元核算。

第一层:教程、类型与表达式

原书第一章通过温度换算、字符计数、单词计数、数组和函数把语言拼成整体。下面的温度表同时展示 、整数算术和 for 循环:

#include <stdio.h>
 
#define LOWER 0
#define UPPER 300
#define STEP  20
 
int main(void)
{
    int fahr;
    for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
        printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
    return 0;
}

第二章再解释这段代码里的类型宽度、常量、算术转换、关系/逻辑运算、位运算、赋值表达式、条件表达式与优先级。关键不是背表,而是区分 f() + g()+ 决定如何组合结果,却通常不决定先调用 f 还是 g

第二层:控制流与程序结构

第三章把语句块、if-elseswitchwhilefordo-whilebreakcontinuegoto 放进同一控制流模型。读者应能把每条边写成“条件为真去哪里、状态在哪修改”,并识别循环不变量,而不是只会把 for 机械改写成 while

第四章讨论函数、非整型返回值、外部变量、作用域规则、头文件、staticregister、块结构、初始化、递归与预处理器。C 的参数只有 。接口声明进入头文件,单一定义留在一个源文件,内部实现可用文件作用域 static 隐藏。

第三层:指针与数据组织

第五章把地址、函数参数、数组、指针算术、字符串、指针数组、多维数组、命令行参数、函数指针和复杂声明串起来。数组在大多数表达式中转换为首元素指针,但数组对象与指针变量不是同一种类型;sizeof arraysizeof pointer 的差异正来自这里。

第六章从结构体基础走到结构体函数、结构体数组、结构体指针、自引用结构、表查找、typedef、联合和位域。链表节点之所以能递归连接,是因为结构体可以包含“指向同类型结构体”的指针,却不能直接包含一个无限大小的同类型对象。

C 程序设计语言(K&R)· 全书地图从基本语法到底层接口——五个板块构成 C 语言完整视图C 语言概览设计哲学学习路径类型与运算符基本类型运算符类型转换控制流与函数分支循环函数定义作用域指针与数组指针算术字符串命令行参数结构体与IO结构体联合标准I/OUNIX接口类型奠基、控制流组织逻辑、指针操作内存、结构体与IO连接系统
K&R 全书五个板块:概览引路,类型与运算符奠基,控制流与函数组织逻辑,指针与数组操作内存,结构体与IO连接操作系统。

第四层:标准 I/O 与 UNIX 边界

第七章的 FILE *、格式化输出/输入、变参、文件访问、stderrexit 与行 I/O 属于标准库接口。第八章再下沉到 UNIX 的文件描述符、read/writeopen/closelseek、目录遍历和存储分配器。前者是可移植 C 库,后者是操作系统接口;不能因为常在同一系统可用,就把 POSIX 函数说成 ISO C 标准。

字符循环必须用 int 保存 getchar() 的返回值,因为它既要容纳所有 unsigned char 值,还要容纳负值宏 EOF

#include <stdio.h>
 
int main(void)
{
    int c;
    while ((c = getchar()) != EOF)
        if (putchar(c) == EOF)
            return 1;
    return ferror(stdin) ? 1 : 0;
}

若把 c 写成 char,某些实现会无法区分字节值与 EOF,或者永远匹配不到 EOF。这类问题说明 C 程序不能只问“本机跑没跑通”,还要问接口的完整值域和标准契约。

怎样读每个 K&R 示例

先预测,再运行;先开严格告警,再做单点修改。每次实验都记录标准模式、编译器版本、输入、输出和诊断,尤其不要用一次观测为未定义行为编故事。

“实现定义”意味着实现必须选择并记录一种行为,例如某些整数类型宽度;“未指定”意味着标准允许若干选择且不要求记录,例如一些求值次序; 则没有可依赖结果,例如越界访问、数据竞争或有符号整数溢出。

猜一猜:同一段含未定义行为的代码,在 -O0 连跑一百次都输出相同结果,能否证明它在 -O2 或另一台机器也成立?答案是否定的;稳定观测不是语言保证。

在现代工具链复现历史示例

K&R 第二版记录的是 ANSI C 成形期的语言与库。复现实验应保留原例要证明的机制,却不能无说明地保留今天已经危险或失效的接口。每个示例先在 -std=c90 -Wall -Wextra -Wpedantic 下确认原书语法边界,再在项目采用的 C17/C23 模式下重编译并比较诊断;编译器、标准库、目标架构和优化级别都写进实验记录。

原书中若出现 gets,现代练习应改用带容量的 fgets,并解释 gets 因无法限制写入长度而在 C11 被删除。旧式函数定义、隐式函数声明或省略返回类型的代码要明确标为历史形式,再给出带原型的现代等价写法;不能为了“忠实”而教读者关闭告警。

测试输入至少覆盖空输入、只有换行、最大合法长度、超长行、文件结束和 I/O 错误。涉及整数时同时检查零、边界值、负数与转换后的范围;涉及数组和指针时画出对象边界与生存期。最后用 AddressSanitizer、UndefinedBehaviorSanitizer 等动态工具补充本次执行证据,但与 TSan 一样,零报告只说明当前覆盖没有发现问题。

把“原书代码”“标准规则”“现代安全改写”并排记录,才能既尊重历史语境,又避免把当年的接口限制误当成今天仍推荐的工程实践。

学完后的交付标准

读完本站每一页,不以“看懂了”验收,而要留下四类证据:一份能在目标标准下无警告构建的最小程序;一组正常、边界和失败输入;一段对对象生存期、值域、作用域或 I/O 契约的解释;一次单点变更前后的预测与结果。

最终你应能从 int main(void) 一路解释到 read 为什么可能短读、结构体为什么需要自引用指针、函数参数为什么仍是值传递,以及标准库与 UNIX 接口的边界。附录用于查语法、库函数和版本变化;正文示例负责建立推理能力。

小结

  • K&R 第二版有八个正式章节,本站 10 页是教学拆分,不改变原书单元
  • 第一章先用完整程序建立整体直觉,第二至四章收紧表达式、控制流与程序结构规则
  • 第五、六章用指针和结构体组织内存中的关系,第七、八章连接标准库与 UNIX
  • ANSI C、后续 ISO C 与编译器扩展必须分层标注,默认编译通过不等于属于原书标准
  • 学习闭环是读契约、严格构建、单点修改、规则解释;一次正确输出不能证明无未定义行为

练习

问题 1 把温度换算程序中的 5.0 / 9.0 改成 5 / 9。先预测输出,再解释结果与赋值目标为 double 为什么无关。

问题 2 为什么字符复制程序用 int c 而不是 char cEOF 是文件中的一个特殊字节吗?

问题 3 为一个三文件程序规划 counter.hcounter.cmain.c:函数原型、共享类型、外部对象声明、对象定义和私有辅助函数各放哪里?

名词解释

本章出现的专业名词,用大白话再讲一遍。

教程式导论

先通过逐步扩展的完整程序建立语言整体,再在后续章节精化每条规则。K&R 第一章覆盖变量、循环、字符 I/O、数组、函数、值传递、字符数组和外部作用域。

符号常量

#define 建立的预处理器名字,在翻译早期执行记号替换。它不是带类型的 C 对象;复杂替换文本应加括号,并优先评估 const 对象或函数是否更合适。

运算符优先级

决定表达式如何分组的语法规则,不等同于子表达式实际求值顺序。拿不准时加括号可以明确分组,但不能凭括号虚构标准未保证的跨操作数顺序。

求值顺序

多个子表达式在抽象执行中的先后约束。若标准未规定先后,程序不能依赖编译器某次碰巧采用的顺序。

值传递

函数调用把实参值复制给形参。传指针时复制地址值,函数可通过该地址访问调用者对象,但形参本身仍是独立副本。

未定义行为

程序违反标准要求后,标准不再约束执行结果。一次或多次看似正确的输出不能建立可移植语义,必须消除根因。

资料与写作方式声明

本章以C 程序设计语言(K&R)权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…