Skip to content

1 课程介绍与编程环境配置

负责人:@ichn-hu

首先恭喜大家获得了复旦的录取,也欢迎大家来到复旦大学腾飞书院。为了丰富大家的暑期生活(手动斜眼)以及让大家为即将到来的大学生活做好准备,我们特意准备了一系列先导课程。

在这部分课程中,我们将从 0 开始教你用 C 语言进行简单的程序设计。有些同学可能以前接触过程序设计的概念,也写过一些程序,而有些同学可能完全不了解。 为此,我们将从最基本的原理开始讲起,并制作了展示具体的操作的视频,帮助大家上手。此外我们也会收集大家遇到的问题,并及时更新到课程主页上。 希望我们的努力能为程序设计“祛魅”,让大家可以更轻松地接受程序设计的概念和思维模式,并可以亲手写出一些简单的程序实现自己的想法。

什么是程序设计?

我们如今的生活已经离不开程序。无论是你手机上的 APP、电脑上的 office 软件还是各种各样的网站,它们的背后都是由程序驱动的。 你的每一次点击、每一条朋友圈、每一条消息,都会在你看不到的程序的世界里引起一轮协奏。而这个程序世界的宏伟,你恐怕难以想象。

所谓的程序,就是一系列明确的、可以被计算机理解的语句组成的文本。现在有很多编程游戏,比如有一个叫 lightbot 的(手机上可以下载安装玩玩哦~)。 在这个游戏里面,你可以操作一个机器人,用给定的基本操作(比如前进、转向、重复等),组合出一套"程序",从而让机器人行动起来达成游戏目标。

lightbot 编程游戏
lightbot 编程游戏

自上个世纪 40 年代以来,计算机科学和技术得到了迅速的发展,而编程则是操作计算机完成运算的必须的手段。早期的计算机编程和操作上述机器人的编程非常相似,计算机的指令集就是机器人可以完成的操作,而程序员的工作就是用这些基本指令组合出有意义的指令序列,然后计算机就可以非常快速地执行这些指令,从而可以计算出一些远远超出人的计算能力的结果(如导弹轨迹,早期计算机的发展与曼哈顿计划关系紧密1)。

随着计算机的发展,程序本身的形式也经历了进化。

最早的时候,受制于电子技术,计算机是一个占据很多间房子的庞然大物,而程序的逻辑需要通过拔插电线组合出来(那个时候其实还没有指令的概念,即没有 stored-program),这时的编程是一个十分需要细心和耐心的工作(当然现在的编程也需要)的工作,因此早期的程序员都是女性(感兴趣可以读读 Programming the ENIAC 还有 Untold History of AI: Invisible Women Programmed America's First Electronic Computer)。

最早的计算机 ENIAC 和最早的一批程序员

因为拔插电线改变计算机的内部逻辑是一件非常低效率的事情,所以后来出现了 punched card 作为程序的载体,而程序就是卡片上的洞。 计算机可以读取卡片上的洞的排列形式,从而执行这些洞所表达的计算。

https://en.wikipedia.org/wiki/Computer_programming_in_the_punched_card_era

举个例子来说,假设卡片上一行有 4 个可以打孔的位置,打孔表示 0,不打孔表示 1,那么我们可以得到 $2^4=16$ 种表示(0000,0001,0010…… 等等),对于一台可以读取这种卡片的计算机,每个表示都可以是一个指令或者输入数据。

为了解释这样的打孔编程可以作出何种有益的计算,我简单介绍一个计算机的抽象。这个计算机内部有一个存储器,这个存储器每个位置上都可以存储一个四位的二进制数据,从左往右我们从 0 开始给这些位置标号(假设这个存储器足够长)。同时我们还有一个指针 i,指向存储器中的一个位置。此外我们还有一个寄存器 R,你可以理解成是一个特殊的存储器,它只可以存储一个四位数据。在计算机刚刚启动的时候,存储器上每个位置的值都是 0000,R 也是 0000,而且 i 等于 0,即指向存储器的第一个位置。我们用 $S_i$ 表示第 i 个位置上的数据,下表给出了每个表示所代表的指令的意义

表示 计算机内的操作
0000 指针 i 向右移动,即 $i = i + 1$
0001 指针 i 向左移动,即 $i = i - 1$,如果 i 已经是 0 了,计算机直接崩溃重启
0010 把当前指针指向的存储器位置上的数据复制到 R 中,即 $R=S_i$
0011 把 R 中的数据复制到指针指向的存储器位置上,即 $S_i=R$
0100 计算 R 和 $S_i$ 的和,结果放到 R 之中,即 $R = R + S_i$
0101 计算 R 和 $S_i$ 的差,结果放到 R 之中,即 $R = R - S_i$
0110 R 中数据的值增加 1,即 $R = R + 1$
0111 R 中数据的值减少 1,即 $R = R - 1$
1000 如果 R 的值是 0,则回退 $S_i$ 条指令重新开始执行

这样,卡片上的洞就被赋予了计算的意义。这里我们用上述操作,做一个简单计算,从 0 加到 3 等于多少?可以写出下述代码

0000  # 指针向右移动,到编号为 1 的存储器
0110  # R = R + 1,那么 R = 1(初始时是 0)
0011  # 把 R 写到 S1,那么 S1 = 1
0000  # 指针再右移动,到编号为 2 的存储器
0110  # R = R + 1,那么 R = 2
0011  # 把 R 写到 S1,那么 S1 = 2
0000  # 指针再右移动,到编号为 3 的存储器
0110  # R = R + 1,那么 R = 2
0011  # 把 R 写到 S1,那么 S1 = 3
0001  # 指针向左回退 3 次,到 i = 0
0001
0001
0010  # R = S0,即 R = 0
0000  # 指针向右移动一个位置
0100  # R = R + S1
0000  # 继续移动
0100  # R = R + S2
0000  # 继续移动
0100  # R = R + S3

执行完之后,R 中的值就是 $0 + 1 + 2 + 3$。

这个例子或许意义不够明显(受限于我们对计算机抽象的简化,而且加太多了会“溢出”,记得四位最多表示 16 个状态,四位存储不下大于 15 的值),但它展示了计算机的计算原理。现代计算机的处理器(CPU)里,指令更加丰富,有更多的寄存器,存储器空间也更大,但本质上和我们的小计算机模型没有差别。如果你想知道更多,介绍一本书《编码:隐匿在计算机软硬件背后的语言》,可以自行购买(电子版网上也可以找到,这里就有一份)。

每种 CPU 都有自己的指令集,就像上面给出的表中展示的那样,指令集本质上就是由 01 构成的二进制表示。你或许知道,桌面电脑(desktop computer,可以理解为家用台式电脑、一般的笔记本电脑)上最常用的 CPU 使用的是 x86 指令集,现在大部分智能手机使用的是 ARM 指令集。

使用最基本的指令进行编程并不是直接写 01 二进制(虽然并不是不行,只是太麻烦以至于十分困难),而是使用汇编语言(assembly language)进行。汇编语言使用更容易记忆的方式来写指令,比如上面的 0000 可以用 inc i 代替,0011 可以用 mov R, Si 来表示,这样写出来的程序比 01 串更加具有可读性,对程序员来说也更加友好。

lightbot 编程游戏
比如说这里就是 x86 汇编写出来的计算复数相加的程序,注意在计算机世界中,我们更常用 16 进制来表示数(因为比一大串 01 更加简练)。最左边一列表示每一条指令在程序文件中的位置,我们可以不用管,中间一列就是指令的 16 进制表示,这一列才是 CPU 可以识别、执行的,而最右边一列才是由程序员写的汇编程序。可以想象,如果编程是让程序员直接写中间那一列是多么残忍啊!

汇编程序写出来之后,并不能直接被 CPU 识别执行,因此需要经过一个简单的翻译过程,把汇编指令翻译成二进制指令,这个翻译过程就叫做编译(compile)。而这个翻译过程,也是可以通过程序来自动实现的(所有程序员都是懒的,一个有益的思路是,能由计算机自动完成的事情就写个程序来做~),完成这个翻译过程的软件,通常叫做编译器2

不过写汇编仍然不是一个简单的事情,一方面你需要记忆非常多的指令,另一方面针对一个 CPU 指令集写出来的汇编程序往往只能在这种 CPU 上执行,我们称为迁移性太差(portability),而且如果写任何程序都需要对计算机底层有如此深厚的理解,那么我们也不会在先导课程中教大家写程序了3

好消息是,在先导课程中我们将教你用一个名为 C 语言的编程语言来写程序,这比用汇编写程序要容易太多了——这也是为什么人们会创造 C 语言。因为写汇编很痛苦,在上个世纪 70 年代,贝尔实验室的 Dennis Ritchie 创造了 C 语言。

一句话总结 C 语言和汇编语言对比的优势:C 语言提供了更高的抽象层级(abstraction level)。 比如说,CPU 中有限的寄存器被抽象成了可以由程序员自由使用的变量,而对存储器的操作也可以通过由变量表示的指针更方便地完成,同时 C 语言也能更直观地表达条件分支、循环等结构(这些我们都会在随后的课程中教会大家,所以如果你不知道是什么意思也没关系)。更重要的是,C 语言也提供了和操作系统交互的一些程序库(library,可以理解为已经写好,可以让程序员随意调用的程序),从而可以写出一些更好玩的程序,耐心看到文章最后,给大家展示一个,可以在你们自己的电脑上运行哦~。

类似的,C 语言写的程序并不能直接由计算机直接执行,而是要通过编译的过程,把 C 语言程序翻译成二进制程序。而 C 语言编译器作为一种专门的软件,并不是所有的系统上都自带,如果你使用的是 Windows 操作系统,那么你需要自己安装,我们会在随后给出一个安装教程;如果你使用的操作系统是 MacOS,那么你的系统上自带一个名为 clang 的编译器,可以免去安装;如果你使用 linux 操作系统,那么大概率你应该已经知道要怎么做了。

C 语言编译器本身也是一个程序,他把你写的 C 语言程序翻译成计算机能执行的二进制指令,这件事情能行得通,有以下前提条件

  1. 你的程序本质上是一个文本文件,你需要使用某种工具去编辑这个文件的内容(即“编程”这个过程),这个工具没有任何限制,可以是记事本(Windows)也可以是 xcode(MaxOS)、VSCode(全平台)或者 CLion(全平台) 等专业编程工具。无论工具是什么,编程的人是你,你要做的就是把程序文本写入到程序文件中去。
  2. 你需要“调用”编译器去编译你的程序。最简单的方法就是用命令行(command line)去指挥编译器,指定一个程序文本文件,让编译器去编译它!
  3. 你写的程序得是合法的。合法的意思是,符合 C 语言的语法和规范,这正是我们要教你的。如果程序不合法,那么编译器会向你抱怨(complain),形式是警告(warning)或者错误(error)。这个时候不要感到害怕和无助,先仔细读一读编译器输出的内容,这样可以排除掉非常多粗心造成的错误。

lightbot 编程游戏
如果上述事情你都做对了,那么你就能看到编译器给你点的赞!(如果是用 IDE 的话可以见到,而命令行不会,因为命令行的逻辑是如果没有错误则不会有任何输出)

编译之后输出的二进制程序,如果是在 Windows 平台,双击这个输出文件即可执行,而如果是 MacOS/Linux 则需要继续用命令行。

看到这里,如果你仍然不知道具体要怎么做也不用担心,接下来我们将实际操作一番。

C 程序设计的基本概念

如何配置编程环境

所谓配置编程环境就是在你的操作系统上安装 C 的编译器以及程序编辑工具,我们将对不同系统给出不同的方案。

Windows

使用 Windows 的同学看过来!

Dev C++

教材上给出的 Dev C++ 是一个轻量级的集成开发环境(IDE,Integrated Development Environment),顾名思义,它里面集成了一个 TDM-GCC 编译器。安装 Dev C++ 是在 Windows 上获得 C 语言编译器的最方便的方法之一,而且 Dev C++ 本身也可以作为代码编辑工具,对于入门来说比较友好。

除了在教材给出的 sourceforge 上下载(非常慢),我们在国内服务器上也维护了一个镜像,点击这里光速下载!

除了 Dev C++,Code::Blocks 也是一个非常推荐的初学者友好的 IDE(下载更快),如果你想用一个比 Dev C++ 更新的 IDE,点这里下载。

大概需要做以下几件事情

  • 下载 Dev C++ 或者 Code::Blocks
  • 双击下载的文件,按照指示完成安装,记住安装的路径
  • 找到编译器安装的路径
  • 把编译器的路径添加到环境变量之中(从而可以在命令行调用编译器)
  • 愉快地写一个简单的程序
  • 使用 CMD 调用编译器,编译文件
  • 运行输出的结果

你的第一个程序

FAQ

整理、列出常见问题和解决方案


  1. 互联网史——曼哈顿计划、亚里士多德和原子弹 https://zhuanlan.zhihu.com/p/76149568 

  2. 这里对于汇编语言来说,其实编译的过程本身叫做 assemble 而不是 compile,但道理是一样的 

  3. 不过了解这些底层原理对学会写程序是有益的,而且很有趣哇~