早期的编程发展——从接线板到面向对象
在之前讲CPU的例子中的程序,是一个被存储于内存中的、被设计好的指令集合。这里我们展开来讲讲程序。
早期的编程方式
最早可编程的机器是雅卡尔织布机,通过设计穿孔纸片以纺织出不同图案的布。
之后出现了读取答题卡的汇总机(美国人口普查),不过这种只能执行固定功能的机器不属于计算机,答题卡也不是程序,而是数据。
之后出现了功能类似CPU的机器,能够执行四则运算以及一些简单的指令。其上面有很多接口用于出入信号或数据,通过不同的接线方式能够实现不同的功能。在这种机器的基础上,发展出了专门聚焦于控制插线的部位,叫做 控制面板(control panel),因其用于操作线路,也被称为 插线板(plug boards)。插线板是可拆卸的,这意味着人们可以将插线板独立于机器之外,对插线板进行设计,再接入机器,以实现特定功能。插线板的设计实际上就是编程,插线板是程序的承载体,就像内存。
但是插线板上的线路会变得十分复杂,而且将其接入机器也十分耗时。世界上第一台电子计算机需要三个星期的时间才能更换一个插线板。
好在,随着内存的发展,人们开始将程序存入内存。能在内存里存储程序的计算机被统称为 存储程序计算机(stored-program computers)。而且内存只要足够,不仅可以存储程序,也可以存储伴随着程序的数据。程序和数据被存储在同一个地方的设计,叫做 冯诺依曼架构。
冯诺依曼架构下的计算机,标志性地,CPU/ALU + 数据寄存器(如RAM)+ 指令寄存器(IR)+ 指令地址寄存器(IAR)。现代计算机使用的就是冯诺依曼架构。
CPU部分举的例子就是一个冯诺依曼计算机。
而数据和程序写入内存——或者说,编程——的方式,在1980年之前,主要使用的是穿孔纸卡(和答题卡类似);除此之外还有一种常见的方式叫 面板编程(panel programming),其改进了插线板的方式,将插线改为使用开关和指示灯,这样方便了操作,也节省了成本(如省去了昂贵的外围设备——穿孔卡读取器等)。
发展到这,所谓的编程成了直接往内存里输二进制码,依旧属于硬件层面的编程。这样的编程是十分麻烦且痛苦的
编程语言发展史
之前讲CPU的时候举了个例子,那个例子里面的RAM就有一个程序,即一连串设计好的8bit二进制数,且其形式为“操作码+数据”,而我们的CPU能通过特定的组合电路直接理解数字里面的操作码和地址。
这种计算机能直接理解的特定的二进制数——确切地说,其所属的语言——叫做 机器语言 或者 机器码(machine code),而且计算机只能理解这种语言。这是一种二进制语言。
从之前的叙述能知道,早期的编程必须得用机器码。
上面提到的直接将二进制数写入内存来编程用到的即是机器码。
对程序/代码的高层次描述,被称为 伪代码(pseudo-code)。
显然机器码很难用,于是有人想着给每个操作码分配一个名字来方便记忆,毕竟比起背数字,还是背单词更简单。这些名字叫做 助记符(mnemonics)。从“操作码+数据”变为“助记符+数据”,从用0
和1
写代码到可以直接写LOAD_A 14
这样的代码,就好些很多了。其实就是建立了一个映射。
实现这个映射的二进制程序叫做 汇编器(assembler)。它是用汇编语言写的,可以读取输入的文字指令,自动转换为二进制指令,也就是机器码。
一个细节
之前的
jump
指令后面直接跟着所需跳转到的指令的位置,且这个位置是固定的,但是如果我们更新了几条指令,那么jump
所指的指令可能会变化。显然更新程序会很困难。但是有了汇编器之后,我们能够使用自动跳转功能——我们可以一种标签,标签可以指向某条指令,汇编器会根据标签自动找到对应的地址,这样程序员就可以专心于编程,不用管这里的底层细节。
这之后,就来到了编程语言的黄金时代,涌现了大量的编程语言,包括我们所熟知的C, C++, Java, Python, 等等。
编译基础-语句与函数
规定句子结构的一系列规则叫 语法(syntax)。任何语言都有语法。语法构建出 语句(statement)。
常见的语句包括:
- 赋值语句(assignment statement)
- 条件语句(conditional statement)
为了隐藏复杂度,我们可以将常用的代码打包为 函数(function),有时也被叫做 方法(method) 或 子程序(subroutine)。
算法入门&数据结构
解决问题的具体步骤就是 算法(algorithm)。
记载最多的算法之一是排序(sort),这个在FDS里面学吐了。
而提到数据,相信用过excel的同学都知道,将信息格式化能够大大方便函数的套用,节省操作。写程序也是如此,于是就有了研究如何存储数据的学说——数据结构。主要的数据结构有数组,结构体(struct),矩阵(即二维数组),链表,树,图,队列,栈,堆。在合适的地方需要使用合适的数据结构。
数据结构具体内容见FDS笔记
图灵机与可判定性问题
讲到计算机,实在是绕不开计算机科学之父——阿兰·马蒂森·图灵。他上大学时专注于一个很有意思的问题,叫 可判定性问题,即“是否存在一种算法,输入正式逻辑语句,输出准确的‘是'或’否‘答案"。例如问”是否有一个数大于所有的数“。如果存在这种算法,那么对于数学的研究相当有用。简单来说,就是问能不能通过计算解决一切问题。
针对这个问题,图灵提出了一种假想的计算机——确切来说是一种计算模型——叫做 图灵机(Turing Machine)。图灵机相当简单,所以应用也很广泛。简单来说,图灵机是一台理论计算设备,有一个无限长的纸带子,这个纸带可以存储符号,图灵机可以读取和写入纸带上的符号;除了纸带,还有一个状态变量用于保存当前状态(state);当然还有一组规则,规定各种符号对应的操作,例如写一个符号,或改变状态,或将读写头移动一格,等等
例如,下图为一个计算0之前有基数个1还是偶数个1的图灵机规则。
图灵证明了这个简单的假想机器在给予足够的时间和内存后,可以执行任何计算,即这是一台通用计算机。也就是说,这东西给予足够的规则、状态和纸带,可以创造任何东西。即便效率可能较低,但是就可计算和不可计算而言,没有什么比图灵机更强大。一切可计算问题都能计算,这样的虚拟机或者编程语言——或者说任何拥有这种特性的东西——被叫做 图灵完备(Turing Complete) 的。目前几乎所有计算机都是图灵完备的。《我的世界》里面的基于红石的系统也是图灵完备的。
还有个关于图灵机的 停机问题 ——是否有算法能在不执行的情况下验证某个纸带的输入是否能够让图灵机有停止的时候,还是会无限执行下去。图灵证明这问题不能用图灵机解决,因为这玩意儿不能用计算解决——计算机的能力是有极限。这回答了可判定性问题。
关于”智能”
智能是指计算机拥有与人类一样的智力。图灵认为,如果计算机能欺骗人类相信它是人类,这计算机才算智能的。这成了智能测试的基础——“图灵测试(Turing Test)”
软件工程
软件的代码相当多,例如微软的office约有4000万代码。开发大型程序的需求形成了软件工程这一学科。显然对于大的程序,仅仅将代码拆分成多个函数再分别去实现,还是相当困难的。
我们可以将函数打包成 层级(hierarchy),将相关代码放在一起,打包成 对象(object),这两词是什么意思呢?
对象
对象是一种模块,包含的是多个相关的函数。例如,汽车车载软件包含多个和定速巡航相关的函数,例如设定速度的、控制加速减速的、停止定速巡航模式的,显然这些函数是相关的,那么我们可以将这些函数包装为一个“定速巡航对象”。
除了这个对象,我们可能还有“点火对象”、“散热器对象”等等和引擎有关的对象,那么我们可以用一个更大的对象“引擎对象”——或者说,父对象(parent object)——来囊括这些小对象——或者说,子对象(children object),而整个汽车的软件可以被包装为“汽车对象”,此时“引擎对象”又成了相对于“汽车对象”的子对象。
也就是说,对象就像一个包装容器,可以装入其它对象,抑或是函数和变量。
把函数打包成对象进行编程的思想叫做 面向对象编程(object oriented programming)。面向对象编程的核心是隐藏复杂度,选择性地公布功能。十分适合做大型项目。
分级思想与模块化思想在计算机里很常见,讲硬件时就是晶体管打包成逻辑门,逻辑门打包成ALU、RAM等电子元件,电子元件打包成CPU,CPU打包成计算机。
团队协作
不同的人员专注开发不同的对象,但是这些对象是要组装在一起的,但大家都只参与了特定的对象开发,对其它的对象不太了解,这样会阻碍组装。于是我们需要 文档(documentation) 来告诉别人我们写的代码到底是什么东西,此外也可以直接提供 程序编程接口(application programming interface/API)。
API控制一个对象里面哪些函数和数据允许外部访问,哪些仅供内部使用。面向对象的编程语言(OO language)可以直接指定函数是public
还是private
来设置权限,例如C++,C#,Obejctive-C,Python,Java,等等。
其它
有专门用于写代码的工具,其里面集成了很多功能,被叫做 集成开发环境(integrated development environment/IDE),比如Dev C++,VScode,idea,VIM,等等。
写代码记得写文档(documenting),就是写一个README,也记得注释(comment)一下。
文档可以提高代码的 复用性(code reuse),就是方便cv(复制粘贴)。在别人代码的基础上进行有用的修改,测试(check out/debug)好后,然后放回去,这叫 提交(commit)。代码的测试可以统称为 质量保证测试(quality assurance/QA)。
关于程序的版本命名
alpha版本是最早的,错误最多,用于内部测试;beta版本属于早期的,可供外部测试的。
多个人写代码可能会有冲突,比如两个人改了同一个地方,或改错了。所以我们设置一个项目有 主版本(master/main),大家要进行修改需要创建自己的 分支(branch) 而不是直接改master。而且得益于 源代码控制,我们如果写错了可以 回滚(rolled back)到代码之前的样子,也可以知道谁谁谁在什么时候改了代码的什么地方。
Bug
千万千万要记得写注释和文档,千万千万。