摘要
本课开启本课程的第一单元,这一单元专讲用 C++ 编程的入门。本课说明这一单元的用途:把从未编过程序的人,从一个用日常语言陈述的问题,带到一个能够编译、运行、读取数据并写出结果的程序,同时清楚途中每一种工具做了什么;介绍本单元的六节课和贯穿它们的线索,从用中文写出的算法,一直到错误的分类;说明课程假定已有的那一点知识,即学校里的算术、按书面指示办事的习惯以及文件和文件夹的操作;提出本单元要解决的问题,即如何把一个会用手算的计算,变成一台机器能够不出错、不知疲倦地执行的过程;最后指出它通向何处,即后面的各单元和“C++ 程序设计 II”课程。
学习目标
- 描述本单元内容的顺序,并说明每节课为其后的那节课提供了什么。
- 认识本课程假定的预备知识(整数和小数的运算、求商和余数、按书面指示办事、把文件保存到指定的文件夹),必要时加以复习。
- 结合一个例子,说明本单元教人走过的道路:从题目陈述到规格说明,从规格说明到算法,再从算法到能编译、能运行的程序。
- 认识学完本单元时应当会做什么,并把它与后面各单元将要学习的内容区分开。
入门测评
在学习本单元之前,先测一测自己的起点是有益的。这项测评是自我测评:不计分,唯一的目的是为学习指引方向。由于这是本课程的第一单元,所以没有关于前面单元的题目;取而代之的是,前三道题检查课程假定已有的知识:整数除法的商和余数、一个过程中各步骤的顺序,以及文件的名称、扩展名和所在文件夹。其后的七道题按顺序涉及本单元六节课的核心思想。从未编过程序的人不必指望答对,这也无妨:每个答案,无论对错,都会解释要点,并指出讲解这一要点的那节课。同样的能力会在单元末尾的总结课中再次测量。
开始本单元之前,先测一测你的起点。这是一次简短的自我测评:不计分,结果只保存在你的浏览器中。
第 1 题来自前面的单元把 47 张纸分给 6 个人,每人分得同样多的整张,不够再分一整轮的纸留下。每人得到几张,留下几张?
这是带小数的除法,不是整数除法:纸不能撕开,而题目问的是剩下几张。把商向上取整需要并不存在的纸;被除数和除数都是正数时,整数除法的余数从不为负。把余数和商的小数部分混为一谈:余数以张数计,而不是以一轮的几分之几计。这是整数除法:商是完整轮次的最大数目,余数是剩下的部分,它总是小于除数。本课程从第一个程序起就要用到它,在 C++ 中它有专门的运算符。复习: 从问题到算法
第 2 题来自前面的单元用电子邮件发送一个附件有四个步骤:A,填写收件人的地址;B,点击“发送”;C,新建一封邮件;D,添加附件。哪种顺序是恰当的?
列举的顺序不是执行的顺序:照此执行,邮件在存在之前就发出去了。结果取决于顺序:发送之后才添加的附件不会送达。发出的邮件既没有收件人也没有附件;之后的步骤已经改变不了它。有些步骤依赖于另一些步骤(不能给没有新建的邮件添加附件,也不能填写已经发出的邮件),有些则不然。认出这些依赖关系,是写算法首先要做的事。复习: 从问题到算法
第 3 题来自前面的单元一个文件保存在
C:\Users\ana\programas\suma.cpp。它的名称是什么,扩展名是什么,在哪个文件夹里?把包含文件的文件夹和文件本身混为一谈:名称是路径的最后一个组成部分。扩展名是名称的一部分;有些系统默认把它隐藏起来,而本课程要求把它显示出来,因为suma.cpp和suma.exe是截然不同的文件。C:是路径开始的驱动器,不是扩展名。路径从根开始依次列出直到该文件的各级文件夹;扩展名,即名称中最后一个点之后的部分,说明文件内容的种类:.cpp表示 C++ 源代码。要编译它,终端必须位于那个文件夹中,或者接收完整路径。复习: 编译、运行与配置
第 4 题一份食谱写道:“如果面团是干的,就加一勺水并揉面;重复,直到面团不再干。”面团恰好需要三勺水才不再干。一共要问几次面团是不是干的?
没有算上回答为“否”的那次提问,而这次提问也是要做的:没有它,谁也不知道该停止加水了。把过程的文字和它的执行混为一谈:条件只写了一次,但每一轮都要判断。数的是勺与勺之间的间隔,不是提问的次数。在每一个先问再做的重复中,条件判断的次数比循环体执行的次数多一次:最后一次提问正是允许退出的那一次。一边数一边手工执行这个过程,就能毫不含糊地看出这一点。复习: 从问题到算法
第 5 题下面哪一条指示,任何人或者一台机器都能以完全相同的方式执行?
取决于执行者的口味:两个人会以不同的方式执行,而机器没有口味。没有说重复几次,也没有说什么叫合适:终止条件无法检验。没有确定这些数:“大”不是一个数量,而且除数可能是零。它说明做什么运算、用什么数据、结果放在哪里;没有任何东西取决于执行者的判断。这种确定性是算法的三项要求之一。复习: 从问题到算法
第 6 题一个程序编译时没有错误也没有警告,用许多数据运行都正常,而用某一个特定的数据运行时,它停了下来,并给出消息“division by zero”。错误是在什么时刻产生的?
编译器翻译文本,而不执行它,也不知道用户将会输入什么数据。链接器把程序的各部分组装起来;它不做计算,也看不到数据。换用其他数据,同一个可执行文件就能正常工作:缺陷在于程序对这个数据所做的事,而不在于工具。只取决于程序文本的问题,在编译或链接时就会被发现;取决于数据的问题,只有在运行时才会暴露。因此,一个程序能够编译,并不因此就没有错误。复习: 程序、编译器与 C++
第 7 题已经安装了 Visual Studio Code 及其 C++ 扩展,可是在终端中输入
g++ --version时,终端回答说不认识g++。最可能的原因是什么?扩展不包含编译器,也不影响终端能找到什么。命令g++ --version不编译任何程序:它只询问版本,而终端甚至没有找到可以询问的对象。编辑器无关紧要:缺少的是编译器,换哪个编辑器都一样。编辑器本身不编译:它调用一个独立的编译器,这个编译器必须已经安装,并且能被终端找到。C++ 扩展帮助编写代码,但并不附带编译器。第 8 题编译器输出一条以
warning:开头的消息,尽管如此,它还是生成了可执行文件。这意味着什么?那将是错误(error:),那样就不会有可执行文件。用试过的数据能运行,并不能证明它对其他数据也行;被忽略的警告,往往就是明天的错误。可执行文件是正常的;可疑的是程序在所指出的位置上做的事。错误使人得不到可执行文件;警告允许得到它,但指出了可疑的代码。许多警告揭示的是真正的缺陷,所以本课程要求程序编译时没有任何警告。复习: 编译、运行与配置
第 9 题一个 C++ 程序从哪里开始执行?
第一行通常是一条#include指令,它在编译之前处理,并不执行。决定入口点的不是在文件中的顺序,而是名字main。入口点的名字由标准规定,不能在编译命令中选择。main是入口点:系统启动程序时调用它,它结束时程序也就结束。没有main的程序能够编译,但不能链接。复习: 程序的解剖
第 10 题一个程序声明了
int age{};,执行std::cin >> age;来读取年龄,而用户输入的是abc。会发生什么?它不会停止:它像读取成功一样继续运行,所以这个缺陷很危险。int变量只能保存整数;字母既不会转换成数,也不会被保存。读取不会重试:它失败一次,就让流处于错误状态。读取整数时遇到字母就会失败;标准规定,变量得到 0,而且在错误得到处理之前,后续的读取什么也不做。如果程序不检查读取是否成功,它就会在没有任何信号的情况下写出错误的结论。复习: 输入、输出与错误
本单元的内容
本单元有六节课,它们按顺序走过从一个问题到一个能正常工作的程序的道路。每节课都以在它之前的那节课为前提,而第一节课不以语言的任何知识为前提。
- 从问题到算法。课程从没有代码的地方开始,因为算法先于程序。这节课教人用输入、输出以及两者之间的关系(前置条件和后置条件)来规约一个问题;用编号步骤毫不含糊地描述算法;借助跟踪表手工执行算法;在算法中认出顺序、选择和重复;并借助一个每一轮都减小的量,论证一个重复会终止。
- 程序、编译器与 C++。为机器写的算法就是程序,而 C++ 程序并不是按写下的样子执行的:预处理器、编译器和链接器把它转换成可执行文件。这节课依次讲解这三个阶段,区分编译时就能发现的问题和只有运行时才会显现的问题,比较编译型和解释型两种模式,并按时间梳理语言的各版标准,直到本课程使用的 C++23,以及编译器正在实现中的 C++26。
- 在 Windows、macOS 和 Linux 上安装环境。三份完整的指南,每个系统一份,用来安装一个支持 C++23 的编译器和编辑器 Visual Studio Code 及其 C++ 扩展,用一个检验程序确认一切正常,并解决常见问题:终端找不到编译器、版本过旧,以及没有指定所需的标准。
- 编译、运行与配置。编辑、保存、编译、运行、阅读这一工作循环,既在终端中进行,也在编辑器中进行,使用本课程的编译命令。这节课区分阻止生成可执行文件的错误和指出可疑代码的警告;启用检测器(sanitizer),即内存错误检测器 AddressSanitizer 和未定义行为检测器 UndefinedBehaviorSanitizer,它们在程序运行时对它进行监视;并解释为什么输出会显示奇怪的符号而不是汉字。
- 程序的解剖。把最小的程序一件一件拆开:包含指令和头文件,函数
main和退出码,命名空间std,语句、块和注释。这节课教人从第一条消息读起,借助编译器的消息改正结构错误,并确定本课程的风格指南。 - 输入、输出与错误。程序与外界交流:用
std::print和std::println输出,借助格式说明规定宽度和小数位数,并用std::cin读取。这节课展示输入不符合预期格式时会发生什么,以及如何检查读取,最后按照错误出现的时刻对错误分类,以此结束本单元:编译错误、链接错误、运行时错误和逻辑错误。
把它们串起来的线索就是程序本身。第一节课构思它,第二节课说明它被转换成什么,第三节课安装转换它的工具,第四节课教人使用这些工具并倾听它们说什么,第五节课解剖它,第六节课让它与使用它的人对话。如果是这样,那么到单元结束时,学生对 C++ 所知仍然很少,但知道怎样用它工作,而这正是课程其余部分所假定的。
假定已有的知识
本课程不假定任何编程经验。不过,它假定了三种技能,如果入门测评显示出某些疑问,现在就值得复习一下。
第一种是学校里的算术:整数和小数的运算,遵守运算顺序(除非有括号,先乘除后加减),尤其是求整数除法的商和余数。最后这一点比看上去更重要。把 47 张纸分给 6 个人,每人 7 张,剩下 5 张,写成 ,其中余数 小于除数 。C++ 有一个求商的运算符和一个求余数的运算符,而大量整数计算(把秒换算成小时、把一个数拆成各位数字、判断一个数是否为偶数)都归结为它们。想要仔细研究整数除法的人,可以在“代数与几何 I:数、函数与平面几何”课程中找到它及其证明;这里只要会算就够了。
第二种是严格按照书面指示办事的习惯,以及识别一条指示何时不足以让人知道该做什么的能力。这不是一种知识,而是一种态度:读到“加盐适量”,就能注意到这条指示取决于执行它的人。本单元的第一节课把这种态度变成方法。
第三种是计算机的基本操作:新建一个文件夹,用自己选定的名字把文件保存在其中,认出名字中的扩展名(.cpp、.txt、.exe),并知道每个文件在哪个文件夹里。最好从现在起就把系统设置为显示扩展名,因为有些系统(例如 Windows)默认会隐藏它们:源代码 suma.cpp 和可执行文件 suma.exe 是截然不同的文件,把它们弄混是一些荒唐错误的根源。本单元也会教人使用终端,所以不必事先就会;只要愿意用输入命令代替点击按钮就够了。最后,安装程序需要能够在自己的计算机上安装;对做不到的人,讲安装的那节课给出了一个在线的替代办法。
本课程不假定懂英语。编译器的消息是英文的,而本课程教人逐个字段地阅读它们,因此其中要紧的内容(文件、行、缺陷的类别)不必精通英语也能看懂。本课程也不假定超出学校水平的数学:例子用到的是除法、平均数、乘法表、日期和时间。
本单元要解决的问题
一个会用手算的计算,还不是程序。两者之间的距离,正是本单元教人走过的,而一个简单的问题可以让我们量出这段距离。
一个秒表把每项活动的时长记录为秒数,例如 7384,而我们想把它写成小时、分钟和秒的常见形式,其中分钟和秒总是两位数:2:03:04。用手算,这是一连串的除法:,由此得到 2 小时和 184 秒;,由此得到 3 分钟和 4 秒。对一个时长,一分钟就能算完。可是对一份记录中的一千个时长,手算既漫长又容易出错,这项工作正适合交给机器来执行。
然而,机器无法接收上面的解释。它接收的是用一种编程语言写成的文本,而要写出这段文本,必须先解决手算时没有明说的三个问题。第一个是规格说明:输入什么(一个非负的整数秒数),输出什么(三个整数,即小时、分钟和秒),以及什么关系把它们联系起来:若 为总秒数,则 ,其中 且 。第二个是算法:对于任何合法的输入,都能产生这一输出的精确步骤。
- 取值为 除以 的商。
- 取值为除以 的商(被除数是 除以 的余数)。
- 取值为 除以 的余数。
- 写出 、冒号、两位数字的 、冒号和两位数字的 。
第三个是用 C++ 写出算法,采用语言所要求的确切形式,并用合适的工具把它翻译成可执行文件。下面的程序做的就是这件事。其中有许多东西还没有讲解:变量(第 2.1 课),给出整数除法的商和余数的运算符 / 和 %(第 3.1 课),用 std::cin 读取以及格式说明(第 1.6 课);这个程序中的格式说明 {:02} 把一个数写成两位、在左边补零,它是那节课所讲的宽度的一种变体,第 3.1 课也会用到它。目前只要能在其中认出算法的四个步骤就够了。
#include <iostream>#include <print>int main(){ int total{}; // 变量将在第 2.1 课讲解 std::print("时长(秒):"); std::cin >> total; const int hours{total / 3600}; const int minutes{total % 3600 / 60}; const int seconds{total % 60}; std::println("{} s = {}:{:02}:{:02}", total, hours, minutes, seconds);}7384时长(秒):7384 s = 2:03:04第 9 至 11 行是步骤 1 至 3,第 12 行是步骤 4。结果与手算的结果一致并非巧合,而是每个程序都应当经受的检验:先用一个答案已知的数据来试它。
程序解决了问题,但本单元也教人不要轻信它。如果用户输入 abc 而不是一个数,读取就会失败,total 保持为零,而程序毫无报警信号地写出时长为 0:00:00:这是一个看起来像真的假结论。如果输入一个负的时长,规格说明没有规定应当发生什么,结果也就毫无意义。如果写成 total / 360,而不是 total / 3600,程序照样编译、运行,给出错误的结果,而没有任何工具发出警告。这些缺陷各属不同类别的错误,在不同的时刻被发现,要用不同的方法寻找;本单元的最后一节课对它们进行分类。由此可知,本单元要解决的问题不仅是写出这个程序,而且是知道它为什么能工作,在什么数据下会失效,以及哪种工具会(或不会)对每个缺陷发出警告。
因此,学完本单元时,学生将能够编写、编译和运行这一类程序:少量的输入数据,按顺序进行的若干计算,以及带格式写出的若干结果。他们还不能在 C++ 中做判断或重复执行语句(虽然在用中文写出的算法中可以),也还不能有根据地为每个数据选择类型;这些是后面各单元要带来的。
通向何处
本单元是整个课程的门槛。在这里未加解释就使用的东西,以后会按以下顺序加以解释。第 2 单元“数据与类型”讲解变量以及变量如何保存在内存中,整数的界限(以及为什么一个正确的计算也可能溢出),带小数的数的舍入,以及类型之间的转换。第 3 单元“表达式与运算符”讲解 C++ 的算术,包括负数的商和余数、条件的逻辑,以及标准在表达式中未作定义的地方。第 4 单元“流程控制”把第一节课用中文写出的判断和重复带到 C++ 中,并教人论证一个循环是正确的、会终止的。第 5 单元把程序分解为函数;第 6 单元引入复合数据(数组、向量、表格和记录),以及查找和排序的算法及其代价;第 7 单元讨论文本、稳健的输入读取和文件;第 8 单元讨论多文件程序、系统的测试和调试器。
学完本课程之后,“C++ 程序设计 II”接着讲本课程有意留在外面的内容:指针和动态内存,带方法的类,自己编写的模板,匿名函数和异常。解决本课程的问题不需要这些主题中的任何一个,而把它们推后,可以让人专注于在任何语言中都适用的东西:规约问题,设计算法,清楚地写出算法,并检验它是正确的。本单元的总结课正会归纳这一区分,即什么是 C++ 特有的,什么可以带到任何其他语言中去。
已完成
登录账户以保存您的进度