转码记录 Vol.02 | 25 Winter:校内三门 + 自学四门,这学期开始有了层次感
上学期(24 Fall)用三门课完成热身。这学期开始不一样了:校内三门课 + 自学四门课同步推进,工作量直接翻倍。CS 61A、CS 61B、CS 61C、Nand2Tetris——技术知识开始有了层次感,从 NAND 门到高级语言的完 整链路第一次在脑子里贯通。
上学期(24 Fall)用三门课完成了热身——离散数学建立了形式化推理的思维方式,微积分稳住数学基础,EECS 1012 给了第一次写出可运行程序的感受。
但那个学期结束时,有一种感觉:框架有了,还很空。知道命题逻辑是什么,能写简单的 JavaScript,但还停留在"知道是什么"而不是"理解为什么"的阶段。
这学期开始不一样了。校内三门课 + 自学四门课同步推进——其中 CS 61A 实际在 24 Fall 期间并行完成,放在这里一起说。工作量直接翻倍,但收获也是。
24 Fall 同期自学:CS 61A – Structure and Interpretation of Computer Programs(UC Berkeley)
这门课用 Python 和 Scheme 讲,但重点不是语言语法,而是计算的基本思想:抽象、递归、解释器、高阶函数。
对我影响最大的是"函数是一等公民"这个概念——函数可以作为参数传入另一个函数,可以作为返回值返回。这和命令式编程(先做 A 再做 B)是不同的范式。Lambda、闭包、尾递归优化,在这门课里都有系统讲解,而不是在某个框架文档的角落里顺带提一句。
课程还要求用 Python 实现一个简化的 Scheme 解释器。解释器的工作是:源代码字符串 → 词法分析(Tokenizer)→ 语法分析(Parser)→ 求值(Eval)。每一步都自己实现。做完这个之后,对后来 Nand2Tetris 里实现编译器的过程有了直接的认知基础——两者的结构几乎一样,只是输出目标不同。
校内课程
EECS 1022 – Programming for Mobile Computing
正式课程名是移动计算编程,用 Java 和 Android 环境做实际的移动应用开发,成绩 A+。
难点不在语法,在于从脚本思维切换到面向对象思维。
JavaScript 里写逻辑,函数是主角,数据跟着函数走。Java + Android 的世界里,首先要问的是:这个功能属于哪个类?这个类有什么状态?状态由谁管理?Activity 的生命周期(onCreate → onStart → onResume → onPause → onStop → onDestroy)是 Android 的基本框架,每个阶段对应不同的资源分配和回收策略。在写代码之前,必须先想清楚对象的结构。
Android 开发引入了事件驱动编程的完整模型:用户点击按钮,触发 OnClickListener,回调函数执行。这个模式和上学期 EECS 1012 里 JavaScript 的事件处理是同一套思想,但现在用类和接口来描述,结构更明确,边界更清晰。
课程 Lab 最后要做一个完整的 Android App,从 UI 设计到数据逻辑到设备传感器接入。第一次真正体会到"一个可以装进手机的程序是怎么从代码变出来的"。
MATH 1090 – Introduction to Logic for Computer Science
这门课的前置是 EECS 1019(离散数学),把上学期学的命题逻辑推向了更系统的形式化方向,成绩 A。
EECS 1019 讲的是命题逻辑的基本概念——真值表、逻辑连接词、证明方法。MATH 1090 在此基础上引入谓词逻辑(Predicate Logic),以及程序规约和验证的应用。
谓词逻辑的关键升级是引入了量词(∀ 和 ∃)和变量。∀x ∈ ℤ, P(x) 这类语句允许对一类对象统一描述性质,而不只是处理具体的真/假值。这让"描述系统行为"变得可能:可以用形式化语言说清楚一个函数"对所有合法输入都返回正确结果"是什么意思,而不只是靠测试用例来近似。
这门课最有实用价值的部分是前置条件(Precondition)和后置条件(Postcondition):
- 前置条件:函数调用前,输入必须满足的约束
- 后置条件:函数执行后,输出和状态必须满足的约束
这套语言让"正确性"从模糊的直觉变成了可以被形式化描述的东西。后来在 EECS 2101(数据结构)里,每个 ADT 操作都要用这套语言来规约;在 EECS 3311(软件设计)里,这套方法会进一步系统化。这学期学的是基础,那些课里用的是应用。
MATH 1014 – Applied Calculus II
接续上学期的 MATH 1013,这学期完成微积分下册,成绩 A。
内容:积分技巧(分部积分、换元、三角代换)、级数(比值法、根值法、积分判别法)、多元函数入门(偏导数、梯度)。
级数收敛判别值得单独记一句:判断级数收敛与否用的是渐近估算思维,和算法分析里判断复杂度是否可接受用的是同一套工具。现在看起来是数学课,后来在 EECS 3101 算法分析里会用到。
自学课程
CS 61B – Data Structures(UC Berkeley)
系统学习基础数据结构和算法:链表、树(BST、AVL、Red-Black Tree)、哈希表、图(BFS / DFS / Dijkstra / Prim)、排序(MergeSort / QuickSort / HeapSort),以及对应的时间空间复杂度分析。
Berkeley 的这门课有完整的 Project 体系,Gitlet 项目是其中难度最高的:用 Java 实现一个简化版 Git,支持 init、add、commit、branch、checkout、merge 命令。
核心设计挑战:
提交历史是 DAG(有向无环图),不是线性序列。 每个 commit 节点有指向父节点的引用,branch 名只是一个指针,指向某个具体的 commit;HEAD 是指向当前 branch 的指针。 只有在脑子里建立这个图结构,checkout 和 merge 的行为才能讲得通。
merge 的逻辑依赖 LCA(最近公共祖先)。 当两个 branch 要合并时,系统需要找到它们提交历史的分叉点。这是图遍历算法在实际工程问题里的直接应用——BFS 从两个节点出发,找到第一个共同访问到的节点。
持久化靠内容寻址存储。 Git 的底层是一个文件系统:每个 blob 和 commit 对象用 SHA-1 哈希值命名,存在 .gitlet/objects/ 目录下。Gitlet 用 Java 对象序列化实现这个机制。做完这个项目之后,git log --graph 里的那张图不再是黑盒。
Nand2Tetris – Build a Modern Computer from First Principles(Coursera)
Part I 是从 NAND 门开始搭数字电路,一路到 CPU 和内存(硬件层)。这学期的重点是 Part II:软件栈。
完整的翻译链路:
Jack 源代码(高级语言)
↓ Jack 编译器(Tokenizer + Code Generator)
VM 中间代码(基于栈的虚拟机语言)
↓ VM 翻译器
Hack 汇编语言
↓ 汇编器(Part I 已实现)
二进制机器码
VM 翻译器:VM 语言用栈机器模型——所有运算通过压栈和弹栈完成,没有寄存器的概念。VM 翻译器的工作是把这些栈操作映射到 Hack 汇编的寄存器操作上,同时处理函数调用约定(调用帧的创建、局部变量分配、返回地址保存)。
Jack 编译器:分两步。Tokenizer 把源代码字符串切成 token 序列(关键字、符号、标识符、整数常量、字符串常量);Code Generator 按语法规则递归处理 token 序列,生成对应的 VM 代码。递归下降解析(recursive descent parsing)是实现的核心结构,每种语法结构对应一个递归函数。
这门课完成之后,对"高级语言到机器指令"的翻译过程有了完整的、可以逐行对应的认知,而不是一个模糊的"编译器负责这件事"。
CS 61C – Great Ideas in Computer Architecture(UC Berkeley)
这门课从 C 语言出发,一路向下到 RISC-V 汇编、CPU 微架构、内存层级、并行计算。是这学期最重要的自学内容,也是下学期 EECS 2021(计算机组织)的直接前置。
C 语言和内存模型:C 没有垃圾回收。局部变量在栈上分配,malloc 的内存在堆上(必须手动 free),全局变量在静态段。C 的指针直接操作内存地址——理解内存布局,才能理解为什么空指针解引用会崩溃,为什么数组越界的后果是不确定的(而不是报一个整洁的 IndexOutOfBoundsException)。
RISC-V 汇编:没有变量,没有类型,只有寄存器(32 个通用寄存器)和内存地址。函数调用靠约定(参数放 a0-a7,返回地址存 ra)。写了几个 RISC-V 程序之后,开始能直接把 C 代码在脑子里翻译成汇编结构:一个 for 循环是什么,一个函数调用是什么。
CPU 流水线:现代 CPU 将一条指令拆成 IF(取指)→ ID(译码)→ EX(执行)→ MEM(访存)→ WB(写回)五个阶段并行运行。数据冒险(后续指令依赖前面指令还没写回的结果)靠转发(forwarding)或流水线暂停(stall)解决;控制冒险(分支跳转方向未知)靠分支预测处理。
内存层级:寄存器 → L1 缓存 → L2 缓存 → 主存 → 磁盘,每级延迟差一到两个数量级。程序的访存模式(时间局部性和空间局部性)决定缓存命中率,进而决定实际性能。经典例子:矩阵乘法两种循环顺序(ijk vs ikj),算法复杂度相同,但访存模式不同,性能可以差几倍。这个例子之后在 OS 课和数据库课里都会再次出现。
