跳至内容
Topos Uranos

摘要

本课构成课程的第一个单元,阐述课程的完整计划。它说明课程的宗旨,即程序设计本身,而不是某一种语言;解释为什么用 C++ 这门不隐藏机器的语言来讲授程序设计的基础,而不选用一门学习曲线更平缓的语言;指出课程面向哪些学生、只假定很少的已有知识、通向哪些课程;陈述七项总体学习目标以及服务于每一项目标的单元;描述八个内容单元及其所含的课;估计课程所需的总学习量、它在各单元间的分配、建议的学习节奏和学分;解释课的编排方式;最后,就如何学习本课程、如何使用人工智能助手以及如何进行版本控制提出建议。

学习目标

  1. 描述课程八个单元的顺序,并解释每个单元为后续单元提供了什么。
  2. 陈述课程的七项总体学习目标,并识别哪些单元服务于每一项目标。
  3. 用一个例子解释,为什么本课程用 C++ 讲授程序设计基础,而不用一门隐藏类型和内存的语言。
  4. 根据各单元的估计学时,计算课程或其中一部分所需的学习量,并据此规划个人的学习节奏。

宗旨

本课程教人从零开始编写正确的程序:把用文字表述的问题转化为一串精确的指令,选择如何表示数据,掌控所写内容的执行顺序,并把代码组织成接口清晰的函数。若如此,它的对象就不是一门语言,而是程序设计本身:构思算法,表示数据,分解问题,并对所写程序的正确性和代价进行推理。因此,本课程名为“程序设计基础”;也因此,C++ 的每一种构造都与它所解决的问题以及对它进行推理的方法一同呈现,而绝不以语法目录的形式出现。

由此可见,C++ 是本课程的工具,而不是它的目的。然而,工具对用它学到的东西并非无关紧要,而选择这一件乍看最难的工具是经过考虑的;后面一节将说明理由。不过,值得先指出它最明显的结果:每个单元的总结课都含有一个固定的节“适用于任何语言的内容”,它把学到的东西与其在 C++ 中的形式分开,并说明在其他语言中如何表达,或者有什么不同。由此可知,学完本课程的学生不仅会用 C++ 编写程序,还能在任何语言中认出换了外衣的同样的思想。

为什么是 C++,而不是更友好的语言

有些语言的学习曲线更平缓,例如 Python,用它们只需几行就能写出第一个有用的程序,不必声明类型,不必编译,也不必过问数据存放在哪里。如果目的是尽快写出能运行的程序,那条路就是正途。而本课程的目的在于理解程序在做什么,为此宜选用一门不隐藏机器的语言。C++ 迫使人看清六件事,而更省心的语言则默默地把它们处理掉了:

  • 类型。 每个数据都带着一个类型声明,类型规定它能取哪些值、占多少空间、允许哪些运算;在执行任何东西之前,编译器就会拒绝不相符的内容。
  • 编译。 编写、编译、链接和运行是不同的时刻,每个错误都属于其中之一;知道它属于哪一个,就已经改正了一半。
  • 内存。 变量是一个有名字的存储单元,具有生存期和作用域;引用是同一个存储单元的另一个名字,而指向已不存在的存储单元的引用是一个真正的错误。
  • 复制的代价。 把一个向量传给函数可能会把它整个复制一遍;在按值传递和按引用传递之间作出选择,是编程者的决定,而不是机器的决定。
  • 数的极限。 32 位整数不超过 2 147 483 6472\,147\,483\,647,浮点数也无法精确保存 0.10.1。
  • 未定义行为。 标准未加定义的情形(越出数组的边界、有符号整数溢出、读取未初始化的变量)不会产生有保证的错误,而可能产生任何结果;因此,本课程总是开启警告和检测器(sanitizer)进行编译。

下面的例子以小见大地说明了这一点。程序在一个 int 类型的变量中计算从 11 到 1515 的阶乘。在整数可以无限增长的 Python 中,同样的计算会毫无异议地打印出 13!=6 227 020 80013! = 6\,227\,020\,800;而在 C++ 中,12!12! 与 1313 的乘积放不进一个 int,本课程编译所有示例时都启用的未定义行为检测器会让程序恰好停在出错的那一行:

C++ 程序
#include <print>​int main(){    int factorial{1};    for (int n{1}; n <= 15; ++n) {        factorial = factorial * n;   // 乘积放得进 int 吗?        std::println("{}! = {}", n, factorial);    }}
输出
1! = 12! = 23! = 64! = 245! = 1206! = 7207! = 50408! = 403209! = 36288010! = 362880011! = 3991680012! = 479001600main.cpp:7:31: runtime error: signed integer overflow: 479001600 * 13 cannot be represented in type 'int'

程序以代码 1 结束。

若如此,这门语言迫使人提出一个另一门语言本会隐藏起来的问题:所选的类型能容纳多大的数,容纳不下时该怎么办。两种回答都谈不上抽象意义上的孰优孰劣;重要的是知道这个问题存在。整数及其表示 一课回答了它,每节总结课中关于未定义行为的那一节也会回到它。

由此产生一种不对称,这正是作此选择的深层理由。用 C++ 学习基础的人,之后会明白更省心的语言替他做了什么:垃圾回收器、无限大的整数、下标检查、类型的隐式转换。反之,先用其中某种语言学习的人,看不见这些层次,日后才会被它们绊倒,那时一个真实的程序会因语言替他隐藏的某个原因而出错。因此,本课程选择的道路起步更难,却更具可迁移性:在这里学到的东西在任何语言中都适用,而在一门隐藏这些东西的语言中学到的,在这里却未必适用。

然而,C++ 是一门庞大的语言,一门讲基础的课程必须抵制把它全部讲授的诱惑。这第一门课程只讲结构化程序设计(数据、表达式、流程控制、序列、文本和函数),采用 C++23 标准及其现代写法:用 std::println 输出,用 std::array 和 std::vector 表示序列,用 std::string 处理文本,采用范围(ranges)形式的库算法。需要以此为基础的内容留给 C++ 程序设计 II:指针和动态内存、带成员函数的类、自己编写的模板、匿名函数和异常。

学习对象与预备知识

本课程面向从未编写过程序的人,不论来自哪个培养方案,从一年级起即可学习:需要在学习数据结构和系统方面的课程之前打下坚实基础的计算机专业学生,需要用计算机计算而又不盲目信赖它的工科和理科学生,以及想看到自己的算法变成可以运行的程序的数学专业学生。

这是一门 1 级课程,没有先修要求,内容自成体系。它只假定两件事:学校里的算术,包括整数除法及其余数;以及计算机的基本操作(新建文件夹、保存文件、打开程序)。它所用到的二进制表示、分情况推理、条件的逻辑或归纳法,都在需要的地方讲授:二进制在讲整数的课中;德摩根律在讲关系与逻辑的课中;归纳法在讲递归的课中。因此,各课中出现的指向本站其他课程的链接是深入的延伸,而不是先修要求。

通向何处

由上可知本课程在本站中的位置。它没有先修要求,是两个培养方案“信息学与计算”和“工程学”的目标课程,因为在这两个方案中,程序设计既是工具,也是对象。因此,它宜作为这两个方案最先修读的课程之一,与 LaTeX 学术写作 和 数理逻辑 I:命题逻辑 一起学习。

它有一门互补课程,可以在它之前、与它同时或在它之后学习。数理逻辑 I:命题逻辑 把这里作为实践来使用的东西作为理论来研究:第 3 单元的复合条件、真值表和德摩根律,以及证明一个过程正确的思想,这一思想在本课程中表现为循环的不变式和对递归的归纳。代数与几何 I 则证明本课程所编程实现的关于数的结论:欧几里得除法、辗转相除法、进位制和秦九韶算法(霍纳法则)。而 LaTeX 学术写作 在它的 团队协作写作 一课中讲授用 Git 进行版本控制,本课程建议从第一个程序起就使用它。

然而,本课程有意把需要以它为基础的内容留在外面。它预定的后续课程是 C++ 程序设计 II,将深入讨论指针和动态内存、类与对象、模板、匿名函数、异常以及标准库。因此,本课程不是一本 C++ 手册的第一部分,而是一门完整的基础课程,可以到此为止;而继续学习的人,将在第 II 门课程中看到同样的规范被用于规模大到再也离不开它的程序。

总体学习目标

以下目标描述完成本课程的学生应当能够做到的事。每一项都表述为可观察的表现,以便用具体任务加以检验;每一节内容课又各自说明自己的目标,它们服务于一项或多项总体目标,而结课的一课则展示每项目标在哪些单元和课中得到训练。七项目标都有编号,以便引用。

  1. 目标 1。 用程序解决问题:规定输入什么、输出什么,设计算法,手工执行它,并编写、编译、运行和调试它,同时解读编译器的消息和警告。
  2. 目标 2。 为每个数据选择合适的类型,并对其表示的极限进行推理:整数的取值范围、溢出、浮点数的舍入以及文本的编码。
  3. 目标 3。 构造正确的表达式,了解优先级、类型转换、副作用以及标准未加定义的情形。
  4. 目标 4。 用正确且会终止的判断和循环设计流程控制,并用不变式论证其正确性。
  5. 目标 5。 把问题分解为接口清晰的函数,选择参数的传递方式,并用递归解决问题。
  6. 目标 6。 用基本算法和标准库表示并处理复合数据(序列、表格、记录、文本和文件),并估计其代价。
  7. 目标 7。 把较大的程序组织到多个文件中,用前置条件和后置条件对其加以规约,系统地测试它们,并论证其正确性。

五项目标各有自己的单元:目标 2 对应数据与类型单元;目标 3 对应表达式单元;目标 4 对应流程控制单元;目标 5 对应函数单元;目标 6 对应复合数据单元,并延续到文本与文件单元,在那里目标 2 随着 UTF-8 编码再次出现。另外两项目标则贯穿整门课程。目标 1 以第一个单元为主,它讲授规约、编写、编译和运行的工作循环,还有最后一个单元中讲调试的那一课;但它在所有单元中都得到练习,因为本课程的每个程序都要编译、运行和修正。目标 7 以最后一个单元,即更大的程序单元为主;但它始于第一个单元的风格指南,延续于第二个单元的常量,在第五个单元的契约中成形(此后每个函数都伴有契约),也在第四和第六个单元的不变式中成形。由此可知,这两项目标的达成不能归于某一个单元,宜在整门课程中持续培养。

内容

本课程分为十个单元:本介绍、八个内容单元和一个总结。每个内容单元以一节导言课开始,介绍单元中的各课、它所假定的已有知识以及它要解决的问题,并以入门测评开篇;以一节总结课结束,它重建本单元的概念链,把适用于任何语言的内容与其在 C++ 中的形式分开,汇总本单元的未定义行为,在结业测评中重测入门测评考查的技能,并提出比各课题目难度更高的挑战题。

第 0 单元 课程介绍

由本课构成,确定课程的宗旨、语言的选择、总体目标、内容、学习量和设计。

第 1 单元 入门

它的目标是把学生从用文字表述的问题,带到一个能编译、能运行、能读取数据并写出结果的程序,并让学生知道途中每个工具做了什么。它从没有代码开始,因为算法先于程序;单元结束时,学生对 C++ 所知仍然不多,但已经知道怎样用它工作。

  1. C++ 入门:导言。
  2. 从问题到算法:输入与输出的规格说明、编号步骤、手工跟踪、顺序、选择和重复。
  3. 程序、编译器与 C++:预处理、编译和链接;编译型语言与解释型语言的对比;语言的标准。
  4. 在 Windows、macOS 和 Linux 上安装环境:三份完整的指南、编译器、Visual Studio Code 及其 C++ 扩展、验证以及常见问题。
  5. 编译、运行与配置:本课程的编译选项、错误和警告、检测器、编辑器的生成任务以及控制台的编码。
  6. 程序的解剖:包含指令、main、语句、块、注释以及本课程的风格指南。
  7. 输入、输出与错误:std::cin、std::println 与格式说明、不符合预期的输入以及错误的分类。
  8. C++ 入门:总结与挑战。

第 2 单元 数据与类型

它的目标是让学生明白:每个数据都带着一个类型存放在内存中,类型规定它能取哪些值、占多少空间、允许哪些运算;程序中的数不是数学中的数,而是有极限的表示。

  1. 数据与类型:导言。
  2. 变量与内存:声明与花括号初始化、赋值、状态的跟踪、名字以及未初始化的变量。
  3. 整数及其表示:二进制和十六进制、每种类型的取值范围、二进制补码以及有符号和无符号的溢出。
  4. 浮点数:为什么 0.10.1 不精确、累积的舍入、带容差的比较、无穷大和非数(NaN)。
  5. 字符、布尔值、类型转换与常量:字符的编码值、隐式转换和显式转换、拒绝丢失信息的转换,以及 const、constexpr 和 auto。
  6. 数据与类型:总结与挑战。

第 3 单元 表达式与运算符

它的目标是教会学生构造其值可以确定地预测的表达式:哪个运算先做,结果是什么类型,表达式有什么副作用,哪些表达式没有意义。本单元还没有判断和循环:条件只是求值并打印出来。

  1. 表达式与运算符:导言。
  2. 算术运算:负数的商和余数、表达式树、优先级、结合性以及结果的类型。
  3. 关系与逻辑:复合条件、用德摩根律对其取反、短路求值以及 1 < x < 5 这一错误。
  4. 赋值、副作用与未定义行为:表达式的值和作用、自增、求值顺序,以及标准未加定义的情形的目录。
  5. 位与数学库:位运算符、移位、掩码、<cmath> 以及 <numbers> 中的常量。
  6. 表达式与运算符:总结与挑战。

第 4 单元 流程控制

它的目标是教会学生用判断和循环掌控执行顺序,并对其正确性进行推理:每种情形都得到处理,每个循环都做它该做的事,并且会终止。它兑现了课程第一课的预告:判断和重复,如今写成代码并得到验证。

  1. 流程控制:导言。
  2. 判断:串联和嵌套的 if 与 else、互斥且穷尽的情形、配合 enum class 的 switch 及其陷阱。
  3. 循环:while、do 和 for,循环的跟踪,各种形式之间的转换以及嵌套循环。
  4. 设计正确的循环:计数器、累加器、最大值和哨兵这几种模式,不变式,终止性,以及与调试器的初次接触。
  5. 数值算法:素性检验、辗转相除法、进制转换、秦九韶算法和二分法,每一种都附有论证。
  6. 流程控制:总结与挑战。

第 5 单元 函数

它的目标是教会学生把问题分解为有名字的部分,每一部分都有接口和契约,并用递归解决问题。它把第 4 单元的长程序变成由一个个可以逐一验证的部分组成的程序。

  1. 函数:导言。
  2. 函数:声明、定义和调用,调用栈,前置条件和后置条件,以及不返回值的函数。
  3. 参数与作用域:按值传递、按引用传递和按常量引用传递,用结构化绑定得到多个结果,变量的作用域和生存期。
  4. 用函数进行设计:逐步求精、先接口后函数体、“一个函数,一项任务”的规则以及重载。
  5. 递归:基本情形和递归情形,用归纳法证明正确性和终止性,栈以及递归的代价。
  6. 函数:总结与挑战。

第 6 单元 复合数据

它的目标是教会学生表示数据的集合,并用基本算法处理它们,这些算法既有手写的,也有取自库的,同时估计其代价。它从孤立的数据走向集合,从“对不对?”的问题走向“代价多大?”的问题。

  1. 复合数据:导言。
  2. 数组:std::array,用下标遍历和范围 for 遍历,不越界的下标,以及作为对照的 C 风格数组。
  3. 向量:std::vector,读取长度未知的一串数据,累积、过滤和变换,大小和容量。
  4. 算法的代价:主导操作的计数、增长阶以及时间的测量。
  5. 查找与排序:线性查找和二分查找,选择排序和插入排序及其不变式,以及库中的算法。
  6. 表格:二维表格,按行和按列遍历,转置、乘积,以及存放在扁平向量中的表格。
  7. 记录:struct、指派初始化器、作为参数的记录以及按某个字段排序。
  8. 复合数据:总结与挑战。

第 7 单元 文本与文件

它的目标是教会学生把文本当作数据来处理,并让程序在读写文件时不会因格式错误的输入而崩溃。单元结束时,学生能够编写处理真实数据的程序,连同其中的带调字母和错误一起处理。

  1. 文本与文件:导言。
  2. 字符串:std::string 及其操作、npos、std::string_view 视图,以及作为对照的 C 风格字符串。
  3. 文本不只是 ASCII:UTF-8 编码、字节与字符,以及会破坏文本的操作。
  4. 正确读取输入:std::getline、流的状态、遇到无效数据后的恢复,以及用 std::from_chars 进行校验。
  5. 文本文件:打开、逐行读取、写入、追加,以及处理逗号分隔值文件。
  6. 文本与文件:总结与挑战。

第 8 单元 更大的程序

它的目标是教会学生组织、验证和调试那些一个文件已经装不下、脑子也记不住的程序。它让第一课开启的循环闭合:规约、构建、验证;它的总结课以一个综合整门课程的结课项目作结。

  1. 更大的程序:导言。
  2. 多个文件与自建库:头文件和实现、头文件保护、单一定义规则以及分离编译。
  3. 程序测试:从规格说明出发的测试用例、自动化测试、assert 和 static_assert。
  4. 程序调试:Visual Studio Code 的调试器、断点、调用栈,以及定位错误的系统化策略。
  5. 更大的程序:总结与挑战。

第 9 单元 课程总结

由 C++ 程序设计 I:最终回顾 一课构成,它重温七项总体目标,用一幅图展示每项目标在哪些单元和课中得到训练,按目标汇总各次结业测评的首次作答,并指出学习接下来的方向。

学习量与学分

一门课程的学习量不以讲授学时来衡量,异步课程中并没有讲授学时,而以学生的全部工作来衡量:阅读、动手输入并编译示例、解题和复习。估算在本站所有课程中采用同一方法:按每分钟一百词计算每节课正文(包括示例代码)的阅读时间,每道例题二十分钟,每道练习题三十五分钟,每道挑战题四十五分钟,每次入门测评或结业测评十二分钟,再加上十分之一的复习余量。

结果

本课程各课合计有 105105 道例题、105105 道练习题和 5252 道挑战题,另有十六次单元测评和七十道由网站即时检查的简短练习。按单元估计的学习量(取整到小时)如下:

  • 第 0 单元,课程介绍:11 小时。
  • 第 1 单元,入门:2828 小时。
  • 第 2 单元,数据与类型:2222 小时。
  • 第 3 单元,表达式与运算符:2222 小时。
  • 第 4 单元,流程控制:2222 小时。
  • 第 5 单元,函数:2222 小时。
  • 第 6 单元,复合数据:3030 小时。
  • 第 7 单元,文本与文件:2121 小时。
  • 第 8 单元,更大的程序:2020 小时。
  • 第 9 单元,课程总结:不到 11 小时。

合计约为学生的 188188 小时学习。这个数字应当如实看待:它是针对一位输入并编译全部示例、解出全部例题、练习题和挑战题的学生的估计。已经用别的语言编过程序的人,在前几个单元会少用不少时间;从未安装过程序、也从未读过错误消息的人可能需要更多时间,尤其是在第 1 单元,它与复合数据单元一样,也是篇幅最长的单元。

然而,这个数字是名义上的:课程的学分由它确定,它假定阅读速度恒定,每道挑战题用四分之三小时。在用真实学生测量之前,宜给它配上一个现实的区间。如果阅读速度只有假定速度的一半(每个示例都亲手输入、编译并加以改动以观察变化时就是如此),每道挑战题又需要七十五分钟而不是四十五分钟,同一方法会得出约 250250 小时。因此,预期的学习量介于约 188188 小时与约 250250 小时之间。上限是一个审慎的界,而不是测量结果;等到知道学习本课程的人实际所用的时间,这个区间会加以修正,在此之前,学分按名义数字计算。

建议节奏

为了给节奏命名,本计划采用与本站其他课程计划相同的标准:全日制学习相当于一个 6060 学分的学年,约 16501650 小时,分布在约四十周中,即每周约 4040 小时;半日制为其一半,即每周约 2020 小时。由此得出推荐的节奏为一个十六周的学期,每周约学习 1212 小时,略少于全日制的三分之一;按现实区间计算,则为每周 1212 至 1616 小时。这个学期可以同时修读 数理逻辑 I:命题逻辑,它的真值表和证明的思想可以在本课程的条件和不变式中辨认出来。

强化节奏为每周约 2020 小时,即半日制,可在九到十周内学完本课程。较宽松的节奏为每周约 66 小时,要用两个学期,两部分几乎各占一半:第 0 至第 4 单元,从算法到流程控制,约需 9595 小时;第 5 至第 9 单元,从函数到结课项目,约需 9393 小时。这样的划分是自然的:学完第 4 单元时,学生已经能在单个 main 中写出完整的程序,后半部分则教他如何组织这些程序。

学分

本站的学分衡量一门课程要求学习者付出的全部学习量(阅读、解题、挑战题和复习),而不是课堂学时。一个学分相当于约 27.527.5 小时的学习,一年的全日制学习相当于 6060 学分,即约 16501650 小时;这与上一小节为各种学习节奏命名时所用的标准相同。

然而,把估计的 188188 小时除以 27.527.5,得到 6.8,取整为 77。若允许每学分在 2525 至 3030 小时的学习之间浮动,本课程的学分数 CC 介于 6.3 与 7.5 之间:

(18830)≤C≤(18825)\left( \frac{188}{30} \right) \leq C \leq \left( \frac{188}{25} \right)

因此,本课程为 77 学分,略多于全日制一年的九分之一。这个数字并非疏忽:课程要讲授基础,就需要多长就有多长,并没有为了凑成一个整数学分而删减。

学分的用处是实际的:借助学分,可以比较本站各门课程的学习负担,并决定同时修读几门课程才不会超出可用的时间。例如,以半日制学习一个十六周的学期,即约有 320320 小时的人,可以修读合计约 1212 学分的课程;若同一学期全日制学习,则约 2323 学分。

这些学分是本站为规划学习而采用的自有计量:它们不对应任何官方学分体系,也不意味着任何机构的学术认可。

课程设计

学习形式

本课程是异步、在线、自定进度的:没有日期、没有直播课,也没有截止期限,每位学生按自己的准备和时间所允许的速度前进。本课程将以本站的十四种语言提供。程序中的标识符使用英文,在所有版本中都相同;而注释和程序显示的消息则使用页面的语言,因此每个版本都以各自的输出进行编译和运行。各课都注明最好先学过哪些课,因此推荐的顺序是明确的。

一节课的结构

所有内容课都遵循同样的结构。它们以一段摘要和一份自身学习目标的清单开头,这些目标都表述为可观察的表现。接着是讲解,其核心是程序。本课程的每个完整程序,在构建网站之前,都按 C++23 标准、开启全部警告并启用内存错误和未定义行为的检测器进行了编译和运行;代码下方发布的是它的真实输出,或者在所讲的是错误或警告时,是编译器的真实消息,或者在程序发生未定义行为时,是检测器的真实报告。若如此,本课程中没有一份输出是手写的:网站在构建时会检查每个程序是否都保存了其当前版本的输出,只要有一个缺少,网站就不会发布。另一些图形则把正文所推理的内容逐步演示出来:带有名字和值的存储单元、调用栈、循环的执行过程、一个数的各个位。

每节课的结尾是三道例题,附有逐步的解答,以及三道练习题,每道都有一条折叠的提示,然后是答案,有时还附有一份详细解答,最好在写出自己的解答之后再打开。题目要求的是一个想法,而绝不是机械地重复某个示例,并且只使用到该课为止讲过的内容。此外,每节课还包括两道由网站即时检查的简短练习:一道是预测某个程序的确切输出,另一道是补全或改正一段代码。应当了解这种检查能做什么、不能做什么:网站并不编译学生的代码,而是用模式检查关键部分是否存在、所要找的错误是否已经消失。因此,在网站上通过的练习,仍须在自己的计算机上编译和运行,那才是唯一的最终裁判。

导言课和总结课有自己的结构。导言介绍单元、其中各课和贯穿它们的线索、它所假定的已有知识以及它要解决的问题,并包含入门测评。总结重建单元的概念链,标明每一环在哪一课中确立以及各自用到什么;它包含两个固定的节“适用于任何语言的内容”和“本单元的未定义行为”、结业测评以及六道挑战题(最后一个单元为十道,其结课项目分为若干阶段),每道都有提示、答案,并在推理值得时附有一份折叠的详细解答。

语音朗读

每节课都可以用本站的语音朗读功能来听,它使用设备自带的语音:正文按书写原样朗读,其中引用的代码会被有意义地读出,代码示例会被提示出来,但不会逐字符朗读。这样就可以离开屏幕复习讲解内容;而程序则始终需要编辑器。

评估

本课程不评分:它的评估是形成性的,依靠自我测评。练习题可以用来检验每节课目标的达成情况,挑战题可以用来检验每个单元目标的达成情况;在所有这些题目中,最终的裁判都是编译器及其警告和检测器,因为一个编译时没有警告、通过了自己的测试用例、也没有惊动任何检测器的程序,是无可争辩的证据,尽管正如讲测试的那一课所说,它并不是证明。

此外还有单元测评。入门测评位于导言课中,是一份简短的自我测评,用于衡量起点;结业测评位于总结课中、挑战题之前,用于衡量所达到的水平,并与入门测评比较。两者都直接在浏览器中批改:每个答案都会得到解释,每个典型错误都会得到指明它的评语,每道题都会指向值得复习的那一课。它们都不评分、不发证书,结果只保存在学生的浏览器中;对所获技能的认证将来会在另一个平台上进行。最终回顾把每次结业测评的首次作答汇总为按总体目标划分的概况。

人工智能助手

如今已有人工智能助手,能在几秒钟内写出本课程几乎任何一道题的解答。有必要清楚地说明它们应处的位置,其理由与支撑其余一切的理由相同:本课程训练的不是生产程序,而是一种能力,即把一个问题想透直到把它变成正确程序的能力。若如此,把练习交给助手,就等于取消了练习,正如在尝试之前就看答案会把一道题变成阅读。得到的程序可能是正确的;然而提交它的人并没有学会写它,等到助手出错时也不会写,而那恰恰是最需要这种能力的时候。

由此得出的是一条使用规则,而不是一条禁令。助手可以用来解释看不懂的错误消息,提出没有想到的测试用例,把自己已经完成的解答与另一种解答作比较,或者从另一个角度阐明讲解中的某个想法。它不能用来写练习、题目或挑战题。在所有情况下,都适用一条对一切他人代码都成立的原则,不论代码来自助手、论坛还是同学:先阅读、测试并理解,然后才接受。助手可能写出能编译、看起来正确,却发生未定义行为或使用了语言旧写法的代码;学完本课程的学生能够识别这一点,而把课程交给别人代劳的学生则不能。

如何学习本课程

本课程是按顺序学习而设计的。每个单元都以前面的单元为前提:表达式用第二单元的类型来写,循环用第三单元的表达式来写,函数封装第四单元的循环,复合数据则用函数来处理;跳过一节课,迟早都得回头去学。

主要的规则只有一条:每个示例都要亲手输入并编译。程序设计不是靠阅读学会的,而是靠编写、编译并阅读编译器的回应学会的。因此,学生按照 在 Windows、macOS 和 Linux 上安装环境 一课安装自己的环境,并在自己的 Visual Studio Code 中重现每个程序,使用 编译、运行与配置 一课教人设定的选项;使用无法安装程序的计算机的人,可以暂时使用在线编译器,安装那一课对此有所介绍。示例最好亲手输入而不是复制,因为手能记住眼睛略过的东西;最好在运行每个程序之前先预测其输出,因为预测与实际结果之间的差别恰恰是还需要学的东西;最好把编译器的每条警告都当作值得回答的问题来读,而不是当作可以容忍的噪声。程序能运行之后,最好再故意改动它(删掉一个分号、换一个类型、越出一个向量的边界),读一读编译器或检测器说了什么,因为每个故意引发的错误,日后都会被认出来。

每个单元都有两个检查点。入门测评位于导言课中,是起点:最好在学习该单元之前、不查阅任何资料作答,并记下各个错误所指向的课。结业测评位于总结课中、挑战题之前,是对照:学完各课后作答,同样不查阅资料,并与入门测评比较。两者都以首次作答为准,比较的是它,汇总进最终回顾概况的也是它。做练习题和挑战题时,最好在尝试已经穷尽时才打开提示,最后才打开答案以作核对。在编译出自己的解答之前就看过答案的题目,不算解出来了,只算读过了。

版本控制

最后,最好从第一个程序起就用版本控制系统保存课程中的工作。Git 记录工作文件夹的每一个状态,可以回到其中任何一个,并显示两个状态之间改变了什么;对编程者来说,这意味着可以放心地做实验,不怕丢掉原本能运行的东西,还能通过比较版本找到引入错误的那次改动,这就把讲调试的那一课所教的二分排查用到了程序的历史上。本课程不讲授 Git,因为本站的另一门课程已经讲过:LaTeX 学术写作课程的 团队协作写作 一课解释了最基本的工作流程、哪些文件需要保存而哪些需要排除,以及如何解决冲突。在那里适用于文档源文件的道理,同样适用于程序的源文件;只需把要排除的 LaTeX 辅助文件换成编译会重新生成的可执行文件和目标文件即可。