程序起源 - 第一章

这篇文章并不是真的介绍程序真正的起源过程!! 仅仅只是一部类小说的入门教程…

计算机的本质

计算机的本质是什么呢? 顾名思义, 当然是一个计算工具了. 不过得益于计算机的可扩展设计, 他所能做的不仅仅只是像一个计算器那样进行简单的计算, 而是在程序语言的基础上干更多的事情, 从而支撑起这个日渐丰富的信息化世界.

程序语言

最初的计算机确实是用来进行一些计算工作的, 比如计算导弹的弹道轨迹等. 但是计算机刚发明的时候, 没有键盘, 甚至没有屏幕, 因此人们只能使用穿孔卡带, 对照着机器指令表一条一条的把指令输入进去, 用0和1来计算. 当计算机技术发展到一定程度的时候, 人们就迫切的需要设计一种更高级的方式, 从机器语言之中解脱出来, 提高生产效率. 这个时候, 汇编语言成为了一个跳板. 人人们通过翻译的方式, 把一些指令包装成了一些易于记忆的单词, 写完程序之后让编译器根据指令表把源代码翻译成指令, 产生最终的程序.

但是这样远远不够, 特别是对于一些只想潜心研究数学物理而不想把时间浪费在编写繁杂的汇编指令上的科学家们.

雏形

首先, 人们想到的, 是让计算机可以执行一些简单的计算指令, 例如:

1
2
2 + 3
(3 / 2) + 5 * (3 + 4)

这种简单的算式.

这种类似于计算器的程序, 可以通过一些算法很轻易的实现.

不过为了执行的方便, 制定了一些规定:

1, 不同对象之间必须以明确的运算符号相分割, 不允许出现 2(3 + 4)这种情况, 虽然几乎所有学过数学的人都默认2和括号之间是乘法运算关系.
2, 括号内的内容被优先执行, 表达式基本符合先加减后乘除的基本运算法则, 但是在优先度相同的内容之间尽量从左往右执行, 但不保证一定是这样.

于是根据这几个小规定以及简单的计算需求, 人们研究出来一种简单的计算器程序, 用户每输入一个式子, 程序就会进行计算并将结果呈现在屏幕上. 人们把这种可以被正确执行的, 完整的式子称作一条合法的表达式语句.

变量

但是, 这个程序很快就不那么令人满意了. 人们有时候需要计算一个很长的过程啊, 把这个过程写到同一行未免有点难为人, 如果前面的计算结果恰好要在后米纳的计算过程中使用, 记不住也是一件很难受的事情.

在发明计算机的时候, 为了让计算机能够读取程序并执行, 设计者在计算机里面留了一个叫做内存的玩意儿, 这个东西可以储存程序的指令, 是不是也可以储存临时计算结果呢?

答案是当然可以. 懒不愧是科学发展的第一驱动力, 人们决定对这个计算器程序进行一些升级, 往里面加入一些能够储存临时结果的东西, 也就是变量. 变量这个东西其实很常见了, y = f(x)这种式子在数学课本上到处都是, 如果要进行数学研究, 怎么能少了变量这种东西呢?

在设计变量的时候人们碰到了一些问题, 在计算机里面, 所有的数据都是用0和1来存储的, 那么我们怎么知道这一大堆0和1, 表示什么数呢? 如果是整数, 自然好表示, 如果是小数呢? 首先, 由于只能存储0和1, 所以你是没有办法表示小数点的. 人们设计了两套系统, 一套用来处理整数, 一套用来处理小数. 人们并不想费脑筋去编写程序推断你的变量究竟是小数还是整数, 索性规定:

1, 要先声明变量并指明变量的类型, 才能够使用它.

声明一个变量, 只需要使用 <变量类型> <变量名称>的方式就可以了. 例如int x, 会声明一个整数类型的x变量以供使用.

于是你就可以这样在计算器程序里面写 (假设’->’后面的是用户的输入)

1
2
3
4
5
6
7
8
-> int x
声明了整数类型的 x
-> x = 2 + 5
计算 2 + 5, 结果为 7, 储存在 x 里面.
-> int y
声明了整数类型的 y
-> y = 3 + x
读取 x 的值为 7, 计算3 + 7, 结果为 10, 储存在 y 里面.

人们在编写好的计算器程序里面提供了这样几种类型:

int : integer的缩写, 意思是整数. 比较小的整数都可以储存在int类型里面.这个比较小, 最大的界限是-2147483648 ~ 2147483647, 但是大多数程序员是记不住这个数字的, 索性记着10位数以下的都用int类型.

long : 比较大的整数, 当你发现int类型不好用的时候, 可以尝试把类型声明改成long.

long long : 最大的整数类型, 范围是-9223372036854775808 ~ 9223372036854775807, 但是也没有人记得住. 如果long不足以招架你的计算结果, 是时候请出long long了, 如果long long也不足以解决你的问题, 你可能就要借助一些编程手段而不是埋怨编译器不好了.

float : 单精度浮点数, 说的直白一点就是普通小数. 但是精度不高.

double : 双精度浮点数, 占用空间比float类型大了一倍, 精度比较高, 但是用来计算整数的时候还是有所问题, 比如会在计算过程中把中间结果 2 储存成 1.999999999999999, 所以应当避免用浮点数类型进行整数运算.

char : 为了处理一些文字信息, 人们又引入了字符类型. 用八个二进制数位来表示一个字母. 一个字符既可以用单引号括起来表示 'a', 也可以用数值表示 97.

unsigned : unsigned表示无符号类型, 可以放在各个整数类型前面当前缀, 这样可以省下来表示负数的部分, 数值的表示范围扩大了一倍.

这样, 拥有了变量的计算器, 变得更加的强大了.

分隔符

但是呢, 人们有时候并不想一边输入一边查看计算结果. 如果我要进行科学演示怎么办? 或者说, 如果我想让我的代码能够读取输入然后进行一系列的计算怎么办呢? 有人提出来, 可以把表达式按顺序来写在一个文本文件里, 计算器通过读取这个文件来一条一条的执行. 这确实是个好办法, 但是怎么告诉计算器, 输入的一大堆东西, 从头开始读多长算是一条完整的指令呢? 有人提议说用换行来区分. 这样做确实可以, 但是如果表达式很长应该怎么办呀… 我们为了方便阅读, 有时候挺想把一大长句的表达式分成几行来阅读…而且, 如果以换行来区分的话, 写起来格式必须中规中矩的, 限制好多. 最终开发者决定, 使用分号来当不同表达式语句之间的分隔符, 并且做出了一个保证, 即当计算器遇到分号的时候, 它会保证分号之前的所有计算任务和表达式全部都执行完毕, 否则就不会继续往下执行.

据此, 人们就可以随心所欲的写代码了. 只要符合规定, 哪怕是乱成下面这样的代码, 也一样可以执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
x;
int y;


x = (2 + 3
/4 * 5 (3-45)
);
int __ = 8;

x=__=x*
(00)|__-__-__-
(__)| __-__-
x||__-__-
x|| x||x;

这些看起来奇奇怪怪的表达式都是合法的.

函数与作用域

函数在数学里面是一个很重要的概念, 数学家们表示计算器很好用, 但是如果能写像 y = f(x) 这种程序就更好了. 于是人们又开始了长时间的研究…… 如何表示一个函数呢? 自然是沿用数学的习惯啦, 这样就可以最大限度的让计算机为数学服务而不用另学一套规则. 可是, 如果我想定义: f(x) = 2 * x + 1, 应该怎么去定义呢? 直接这么写确实是一种办法, 但是我们还需要给函数加上数据类型, 否则的话计算机不知道该怎么去处理函数里面的变量. 经过改进后, 人们可以通过: f(int x) = 2 * x + 1的格式来定义一个函数. 这么做还有一个漏洞, 我们还需要定义f(x)的计算结果的类型, 于是最终成品就是: int f(int x) = 2 * x + 1, 而使用的时候就可以写: y = f(2);来计算x = 2f(x)的值, 这么做确实令数学家们更加的高兴了.

而且从这个时候, 计算器就不能仅仅是对程序语句进行顺序的执行了, 跳转重用的概念也出现在了舞台上. 例如下面的代码:

1
2
3
int f(int x) = 2 * x + 1;
int y = f(2);
int z = f(3);

计算器按照顺序读取到第一行函数定义的时候, 因为只是个定义, 并没有实际的x让他去计算, 所以计算器跳过了这一部分. 读取到第二行的时候, 需要计算f(2)的值, 于是计算器就需要跳转到第一行, 将x赋值为2, 再执行2 * x + 1的计算, 最终将计算结果返回并赋值给y, 然后跳转到第三行, 发现要计算f(3)的值, 于是再次跳转到第一行, 计算完毕后跳回第三行并给z赋值. 两次执行f(x), 实际上就是对f(x)的定义进行了重复使用, 也就是重用.

但在开发者按照这个思路设计计算器程序的时候来了个大问题, 等号已经被定义成赋值的符号了啊, x=2就是把x赋值为2, 如果这么设计的话, 开发者就需要弄一个智能的等号, 判断当前语境, 如果是变量就执行赋值的操作, 如果是函数, 就执行定义函数的操作… 可是当时的计算机哪有这么多计算资源呢? 设计这样一个智能的等号不仅浪费计算机的资源, 延长工作时间, 而且计算器设计起来会很复杂, 前两次升级已经有很多程序员猝死了, 还是饶过我们吧QAQ.

还有一个更加棘手的问题, 比如按照这个规则写出来的程序:

1
2
3
4
5
int x;
int f(int x) = 2 * x + 1;
x = 5;
int y = f(x);
int z = f(2);

那么函数定义里面那个x, 和参与了y的计算的那个x, 是不是同一个x呢? 当给z赋值时, f(2)里面的x实际上是等于2的, 那么计算之后, x究竟是5还是2呢? 这种令人迷惑的代码对头发实在是不怎么友好, 而且太为难程序开发者了..

于是开发者们提出来一个叫做代码块和作用域的概念.

我们可以用大括号{}来包含若干条表达式语句, 这些表达式语句共同组成了一个代码块, 在这个代码块内声明的变量只在这一对大括号括起来的若干条表达式中存在. 我们就称这对大括号所包含的表达式语句为这些变量的作用域.

这样一来我们可以用代码块来把代码分隔开, 定义函数的时候就可以写:

1
2
3
4
5
6
7
8
int f(int x)
{
2 * x + 1;
}

int x = 5;
int y = f(x);
int z = f(2);

由于代码块把x和外部的语句隔离开了, 所以在执行f(x)的时候, 里面的x的值如何改变, 并不会影响到外面的x.

这样的版本看起来已经很完善了, 但是开发者们总觉得, 既然提出来了代码块这个概念, 那么应该可以干更多的事情啊, 比如说把一连串更加复杂的表达式封装到一个函数里? 例如:

1
2
3
4
5
6
int f(int x)
{
int y = 9;
y = x * y + 9;
//这个函数应该返回什么结果?
}

函数定义写到一半戛然而止, 这个函数的最终计算结果是什么呢?

于是一个叫做return的机制被提了出来, 可以发明一个叫做return的语句, return后面所跟的数值就是函数最终的执行结果. 同时规定, 函数遇到return的时候就会返回return后面的数值, 同时立即返回调用函数的那个部分, 函数里面处于return后面的语句统统不执行. 于是我们可以这么写:

1
2
3
4
5
6
int f(int x)
{
int y = 9;
y = x * y + 9;
return y;
}

这样的函数定义越来越完善了, 同时, 这种灵活的代码块设计也让函数做更多的事情, 让代码看起来更有序.

操作系统的横空出世

随着计算机技术的发展, 计算机拥有了键盘和屏幕, 甚至鼠标. 人们也需要分享计算机的计算资源, 在一台计算机上运行不同的任务. 只有一个计算器肯定不行啊, 这样的话相当于是浪费了宝贵的计算机资源. 所以人们开发了一个叫做操作系统的软件, 本质上是一个调控平台, 其他的所有软件都运行在操作系统的平台上, 并听从操作系统的调遣. 操作系统按照时间, 让每一个程序都能在一段时间里面运行上一小会儿, 从而保证多个计算任务可以同时进行. 但是由于计算机的普及, 垃圾程序员开始变得多了起来, 具体表现就是许多人会写一些不符合规定的代码, 让计算器程序执行到一半的时候出一些没办法识别的错误. 而操作系统没办法识别这些错误, 也没有办法结束这些程序, 就只能干等着, 浪费了资源又增加了系统的不稳定性. 于是开发者们苦思冥想, 如何才能在不增加现有代码复杂度的情况下保证计算器可以正确运行呢?

最终, 得益于代码块函数的概念, 人们想出来了一种简单的方法. 那就是, 将所有的计算表达式全部函数化! 计算器读取文件时, 从一个叫做main的函数开始执行, 在main函数的结尾有一个return 0;, 由于main函数是整个代码的主体, 不会有外部代码会写int x = main(1);这种表达式的, 所以计算器会把这个0返回给操作系统, 操作系统收到0之后就明白了, 这个任务已经成功的完成了. 如果计算器遇到了不符合规定的代码怎么办呢? 这个时候计算器会立即停止执行, 并返回一个非0的数字给操作系统, 表示这个代码有bug, 没能计算成功. 经过几个版本的迭代开发, 计算器和操作系统都变的更加智能并且可以识别一些常见的错误了, 这个时候操作系统就可以准备一张对照表, 计算器遇到某些类型的错误的时候可以按照这个表来返回一个非0数字, 来表示程序具体出了什么错误. 这样以来代码的编写者就可以通过查看系统日志的方式来看自己的程序出了什么错误, 从而加快排除bug的速度, 提成检查程序的能力.

根据这种设计, 开发者们规定, 所有在函数外部的过程性表达式全部都是非法的, 所有的计算过程必须写在函数定义的一个代码块内, main函数的定义就是主计算过程, 在任何函数里都可以调用外部的函数, 只要被调用的这个函数的定义在之前已经写出来了就行.

这个时候, 程序语言的发展已经有了一个最基本的框架了.

1
2
3
4
5
6
7
8
9
10
11
12
13
int f(int x)
{
int y = x + 7;
int z = x * (y + 3);
return x + z;
}

int main() {
int x = f(3);
int y = f(x);

return 0;
}

这就是一份合法的代码了. 由于计算器每读取一条指令就会计算出结果显示在屏幕上, 所以我们就可以从屏幕上看到所有的结果.

预告

由于语言的快速发展和复杂化, 原有的计算器变的越來越庞大, 在每个操作系统上都安装一个臃肿的计算器程序逐渐变的困难. 人们开始想把这些表达式组成的代码直接翻译成机器指令包装成程序, 从而脱离了计算器也一样可以执行. 于是人们发明了一个新的软件, 同时又增加了一些新的功能, 让程序可以在屏幕上输出特定的文字和数值, 而把计算过程隐藏起来……

# 相关文章
  1.程序起源 - 第二章
评论

:D 一言句子获取中...