计算机语言基础教程(施工中)

来源:互联网 发布:mac新建的文件夹在哪里 编辑:程序博客网 时间:2024/04/29 21:08

========== 前方注意,施工现场 05/20/16 ==========


1. 序言

  现在计算机与网络的应用已经非常普遍了,我觉得这个时代里,人人都会熟练操作计算机应该不是什么奇怪的事了。不过经常还是能见到身边的人说,编程什么的太难了搞不懂,隐隐之中我突然有种让全民脱离不会编程的时代的使命感,于是这堆文字就出现了。(什么鬼
  我承认现在已经有许多计算机语言的教程了,比如C语言里著名的谭书等等,多到烂大街。而且我这一个从未在IT界待过的渣渣,突然跑出来说要做教程也是找打脸。不过打脸就打脸吧,好歹我也见过几种语言,用过那么几年,而且这个教程的读者应该还是啥编程都没学过的新手,不是闯了几年的老鸟,只要能让人轻松掌握编程,不会坑人就行了。(什么鬼x2
  因为自己不是老手,文中肯定存在纰漏甚至不懂装懂的地方,如果有大神路过,欢迎拍砖指正。(被一砖拍死)

  言归正传,在这篇教程里,我将会从编程中会遇到的最基础的问题开始,也就几乎是零基础开始,讲解计算机语言的使用方法。我的表述会尽量追求通俗易懂,因此会损失一定的严谨。但我也不打算把教程写成一个手把手教学,不会像什么零基础英语速成一样,先给一堆实用句子,到了最后再说什么发音语法句型。我会按照逻辑的基础到复杂、底层到上层的顺序来安排内容,虽然初期会感觉知识量很大,但知识之间是相互联系的,不会突然出现一个莫名其妙的概念,也不会出现“诶这个问题好像还没有提到”的遗漏。
  因为计算机语言这个体系涉及到很多专有的基础知识,学习门槛不可避免地会稍高。我会把这些知识一步一步地罗列出来,但并不意味着它们是必须马上掌握的。如果觉得整页的文字太过乏味,可以先记下它想讲的概念是什么,到后面需要用到的时候再回头来看,说不定会更容易理解。当然有些基础内容还是没法跳过的,但我会说得很详细,慢慢看不着急。
  在教程的开始,我不会使用任何一种现存的计算机语言进行讲解,我将使用自然语言或者伪代码做基本表述,而后再拓展到具体语言。也就是说,如果你希望速成某种语言,那么这篇教程不适合你。但我相信,在学完本教程之后,你将可以自由使用教程中详细讲解过的语言,并且你将有能力迅速掌握其他计算机语言。
  本教程的重点在于计算机语言的思想与原理,本着授人以鱼不如授人以渔的宗旨,相信各位会喜欢的。
  那么,下面开始。


2. 一些基本问题

  在学习任何知识之前,最好先弄清楚一些最基本的问题,在这里,比如:

2.1. 计算机语言是什么?有什么用?

  计算机语言是人类与计算机交流的工具,我们利用它可以获得并控制计算机的一切信息。简单的比如让计算机显示一段消息、播放一段音乐,复杂的比如视频聊天、制作一个游戏,更复杂的比如控制其他设备、让计算机像人一样进行交流,这一切都可以用计算机语言表达,也必须用它来表达。
  目前所说的“计算机语言”,通常指人类向计算机(而非计算机向人类)传达信息时所用的语言。

2.2. 计算机语言与我们平时使用的语言有什么区别?

  我们平时交流用的语言即自然语言。自然语言的词汇量极大,结构灵活多变,我们之所以能够理解自然语言,是因为小时候不断地积累、训练。而计算机虽然构造复杂,但毕竟是一堆死的电子器件,难以像人类一样自由使用自然语言。当然我并不是说计算机不可能使用自然语言,有朝一日,自然语言也将成为计算机的通用语言,但是,不是现在。
  因为计算机语言是供人类使用的,所以计算机语言的表达方式都在不断地模仿自然语言,虽然,它们与我们常见的对话依然相差甚远。

2.3. 计算机语言都是什么样的?

  在自然语言里,我们可以表达各种各样的东西,其自由度几乎是无限的。但计算机语言做不到,它往往只是做到了自然语言的某几个方面。而对这些方面的侧重程度不同,对应的计算机语言也就不同。这就是为什么计算机语言种类多如牛毛的原因。
  那么有哪些值得注意的方面呢?对于计算机,我们注重它的功能,即,它能帮我们做什么。所以容易想到从语言的功能上来分类。
  自然语言里常见的功能有这么三个:
  陈述句。它表明一件事的存在,仅此而已。比如:

被水淹没,不知所措。
        ——陈茂蓬教授

  疑问句。它提出一个问题,并且希望问题得到解答。比如:

一起比赛考零分了谁是第一名耶?
        ——德国Boy

  祈使句。它提出一个要求,并希望这个要求得到实现。比如:

乖乖站好。
        ——比利·海灵顿

  计算机语言通常也是由它们构成的。
  除去功能的不同,在计算机语言中,还需要考虑具体语言的使用环境,即,语言是在什么场合下使用的,它重点关注的内容是什么。
  根据使用环境的不同,计算机语言又可以分成很多类。举个很简单的例子,在简单的代数中,我们会关注四则运算法则,关注一些简便运算;而在更复杂的代数中,我们会关注符号间的关系,关注算式的变换,虽然不可避免地需要用到简单代数的内容,但我们并不关注它的细节。很显然,简单代数与复杂代数所表达的内容是完全不一样的,也可以说,它们是两种语言。

2.4. 语言种类繁多,应该如何选择学习?

  每种语言都有自己胜任、并且无法用其他语言取代的领域。选择的时候应根据自己的爱好或需求来确定语言的针对领域。
  当然,这时候往往是一脸懵逼:这些语言我都没见过,我哪知道它的针对范围?
  其实很多计算机语言的样子看起来是差不多的,只要学会了一门,那么所在的这一类语言都能触类旁通地学会;在明白了基本原理之后,再开始接触那些与众不同的地方。从外表上看,它们大致可以分成以下几类:
  1、基于命令的语言。这类语言主要描述了一系列指令,指示计算机应当按什么顺序执行什么命令。这是最常见的一类语言,例如汇编、C/C++、Java、PHP、MATLAB等等都属于这一类。
  2、基于描述的语言。这类语言主要是表达一种数据结构,常用于数据处理与文字排版,例如HTML。
  3、基于函数的语言。这种语言相当于第一种的抽象发展,这类语言不重视命令的执行过程,它注重想要的功能,以及如何把功能组装起来,例如LISP。
  4、基于逻辑的语言。这类语言描述了逻辑系统的命题与规则,常用于专用系统,例如Prolog。
  现在说学计算机语言,往往就是指第一类,当然我这里也只会讲第一类。
  第一类语言的基本原理都是一样的,即给计算机下命令,比如说先把浏览器关掉,然后显示一只Doge,紧接着下面显示一句“233”,最后播放一段金馆长的声音。这种语言告诉计算机先干什么,再干什么。
  下面我将以这类语言的基础为中心展开。

2.5. 掌握一门语言之后,就能随心所欲地使用了吗?

  并不能。
  就跟我们中学写作文是一个道理,懂得字怎么写,懂得句子的语法是怎么样的,然而并不知道一个东西要怎么叙述,憋半天憋不出几个字。单掌握了语言还不够,还需要掌握如何把自己的想法表达出来。在作文里,这叫做表达方式,在计算机语言里,这被称为算法
  算法的学习与语言的学习同等重要。在学习初期,我们可能更关注语言的使用,但在后期甚至是以后的使用中,算法将一直是最重要的问题。顾名思义,算法的内容基于数学,依赖于逻辑思维,所以数学没学好的同学先做好心理准备吧。
  除了算法,其实还有类似表现手法啊修辞手法啊之类的知识需要掌握,但那时后后期的事情了。

  好了,基本介绍先到这里,下面正式开始教学吧。


3. 面向过程的语言

  面向过程的语言,指的就是将欲执行的命令按顺序下达给计算机的语言,告诉计算机先干什么,再干什么,这个命令序列被称为一个过程,或者程序。
  在2.3中曾经提到,一门语言的结构,依赖于它的使用环境。因此在讲解计算机语言之前,我们先来看一下他们共同的基础环境:冯诺依曼计算机。

3.1. 冯诺依曼计算机

  计算机,顾名思义,就是拿来做计算的。从简单的四则运算,到实现复杂的弹道计算,计算机不断地更新发展以胜任更高要求的计算。却被愚蠢的人类拿来打游戏。从电子计算机诞生到现在,有一种计算机的结构一直被普遍使用,那就是冯诺依曼体系结构。
  对于一台计算机需要由什么组成,冯老爷子大概是这么想的:
  1、首先得要有个运算器吧,不能做运算的话还叫什么计算机。运算器也不能太复杂,能做四则运算和一些不可或缺的算数就够了。往复杂了想还有微积分啊集合论啊什么的太多了做不完,只做出基本运算,那些复杂的组合一下就出来了;
  2、还需要一个流程控制器,总不能做完一个运算就卡住了吧。有哪些算式还要算,要按什么顺序算,流程控制器来解决;
  3、不行,缺一个储存数据的设备。道理很简单,谁算数不用打草稿的?中间过程需要找个地方来记录,除非是一路死算到底;
  4、等等,它还不知道我想算什么,我需要个输入设备,比如说拿个键盘把算式打给它;
  5、想到了输入,那也得有输出吧,还要个屏幕显示计算结果,要不然天知道它算出个什么鬼。
  完。计算机就诞生了。喜大普奔。
  画出图来就像这样:
  冯诺依曼体系结构基本示意图
  键盘是最常见的输入设备,显示器是最常见的输出设备。常见的存储器有很多种,但其中跟基本设备联系最紧密的,还是内存。
  综上所述,运算器、控制器、存储器以及输入输出就是计算机的核心内容,也是我们使用计算机语言时需要考虑的部分。
  其实吧,如果把控制器和存储器拿掉,剩下的就是一台小学时候天天玩的计算器;正因为有了这两个部件,计算才得以自动化,简单的计算器才有了质的改变。
  简单来说,运算器就是做运算的,至于它怎么做运算,与我们没什么关系;同样输入输出也是这样,我们只要知道计算机收到了我们的数据,或者我们能看到计算机计算的结果,就够了,其他细节与我们无关。但剩下的控制器与存储器,还需要稍微再熟悉一下。

3.1.1. 程序的基本流程

  程序其实就是一系列计算。按照正常人类的思维,这些计算的正常顺序就是从头算到尾,理所当然。所以控制器也就被设计成从头开始,一条一条地向后执行各个命令。
  比如现在计算一个算式:
  

1/(2+3)=?

  计算过程就是,先算出 2+3=5,然后计算1/5=0.2,完毕。结果就是0.2。直观点的话就像下图:

Created with Raphaël 2.1.0开始计算2+3=5计算1/5=0.2结束

  似乎没什么问题。每一步都有着固定方法,井井有条,照这样子算下去就对了。
  那我们再来看这个算式:
  

|1|=?

  第一步先算……呃。这个算式怎么不按常理出牌。
  绝对值运算,并不是一个按先后顺序就能完成的计算。虽然可以通过先平方再开方的方式得到结果,但是我相信没有谁会这么算的。|3.1415926...|来个平方试试?我反正不干。
  只要是个正常人,对绝对值运算都是这么处理的:数字如果带负号,那就去掉负号,否则就什么都不做,原来是多少那就是多少。
  那么要把这个计算任务交给计算机,应该怎么描述它呢?
  不需要多想。既然计算机是被设计出来造福人类的,那么我们是怎么想的,就让计算机怎么做,不要再让我们费神去安排一个别扭的计算过程。如果按顺序计算不能满足我们的需求,那就让它不按固定顺序计算,教它学会判断
  于是这个算式的计算过程就成了这样:
  
111

  嗯,好了。
  ……诶不对等等,两个结果都是1的话,还要判断做毛线啊,直接说结果是1不就得了?
  没有想的那么简单。算|1|什么的,人一眼就能看出来。还有很多你看多少眼都看不出来的算式,比如说加个未知数进去的:
  
|x|=?

  这种算式就没有固定答案了,只有固定的计算过程:
  
x<0xx

  或者洋气一点,用英文来表达:
if  or  x<0,answer is x ;else,answer is  x .

  至于x是多少,我也不知道,x可以是12450也可以是-233,所以必须要一个判断过程,才能得到|x|的正确答案。直观的过程图是这样的:

Created with Raphaël 2.1.0开始x < 0 ?结果为 -x结束结果为 xyesno

  那么提到了未知数x,是不是意味着计算机能帮我们化简个代数式解个方程什么的?哇,数学题不用愁了。
  然而并不是。至于原因,看看存储器就明白了。

3.1.2. 计算机存储器的结构

  存储器就是张草稿纸,哪里有空打哪里。
  可能很多人都听说过了,计算机运算用的是二进制数blablabla。然后还有计算机的电子器件利用的是逻辑的“是”与“否”两个值blablabla。
  这些都不重要。重要的是你要知道,计算机是一坨电子器件,它只认数字,其他的什么都不认识。而存储器的作用,就是记录这些数字。
  为了形象起见,想象一下,在草稿纸存储器里,这些数字是怎么排列的呢?
  当然不会像这样:
  什么鬼草稿
  这草稿没谁看得懂……
  在计算机存储器里,数据(也就是数字)都是非常有序地排列的。是多有序呢?就像这样:
  数据的有序排列
  存储器里的数据都是像这样有顺序地、从头到尾像一条线一样地排列着,这样的储存结构被称为线性储存。数据就这样堆满了存储器。
  至于刚刚所说的未知数x,并不是真的一个概念上的未知数,而是对上面这些数的代指,比如x表示第一个数233,或者第二个数666,可以理解为给某个数取个名字。你事先做好约定,之后使用的时候,告诉计算机x,它就知道,哦,你说的是第二个数。用个符号去表示,总比说什么“第四个数加第六个数除以第一个数等于几啊”要好听得多。
  另外还有一件非常重要的事情没有说。
  存储器不仅可以记下某个数字,它还可以把某个数擦掉,然后记下一个新的数字。虽然我们打草稿的时候习惯用笔一划然后找个空位置继续写。
  这意味着它里面的内容不是固定的,而是非常灵活的。我们解一个复杂方程,可能要用好几张草稿纸,而对计算机来说,这张草稿纸可以写了擦,擦了写,算来算去也就用了一张纸。所以在这个过程中,虽然x代表的是纸上某个固定位置,但这个位置上的数是可以变化的;现在说的x可能是12,但是一系列计算之后,x的值可能就变成801了。
  也正因为这点,x又被称为变量,区别于无法更改的“固定量”。以后说到变量,指的就是存储器里的某个位置,或是这个位置上的数字是多少。那具体是哪个位置呢?这个没有什么关系,这么大一块地方,想放哪里就放哪里,我们只需要明白它在这块存储器里就行了,剩下的琐碎工作交给计算机自己去处理。
  总而言之,储存器就是一个很长的数字序列,如果我们需要用到某个数字,告诉计算机“第几个数”或者直接用变量符号表示就行了。

3.2. 基本语句

  好了,做了这么多铺垫,是时候讲讲语言的内容了。或者你觉得知识太多踹不过气来?那么缓缓再说也行。

3.2.1. 简单的命令过程

  我们先从最简单的内容开始:对计算机下命令。那么计算机能听懂哪些命令呢?
  上面说到了,计算机由5个部分组成,于是我们能操纵的内容无非就是这些:
  运算器:做基本计算。加减乘除取余数,等等等等;
  控制器:如果不去动它就是按顺序走。你可以让它随时做个判断,或者干脆不按顺序跳着走;
  存储器:记下一个数,或者使用一个数;
  输入/输出:就如名字所说。
  ……你问我不按顺序跳着走是个什么鬼?那是个看起来很屌但是实际上并没有什么卵用的东西,特殊情况下能用得到,等等再说。
  这样看起来似乎很难理解,那么来看看这个例子吧,它用到了所有的命令:

伪代码#1:

a ← Number from keyboardb ← Number from keyboardx ← a - bIF x < 0    y ← -xELSE    y ← xScreen ← y

  一眼看去,雾草这什么鬼。
  这段程序所做的事情就是,从键盘获得两个数ab,然后计算|ab|。看起来命令有一大坨,但拆开来看的话,思路其实非常的清晰:

a ← Number from keyboardb ← Number from keyboard

  Keyboard就是键盘,Number from keyboard就是用键盘打出的数字,ab是内存里的两个变量。这两句命令的意思就是,从键盘那里获得数字,然后放进变量ab中。
  从输入设备那里获得数据,往往是程序的第一步,因为一个程序通常不是为了解决某一特定计算问题,而是为了处理某一类计算问题,这一类问题中包含了未知量,只有在实际计算时才能确定它们的值。譬如我们可以设计一段程序用以计算|14|,也可以写一段程序来计算|10007|,但是通常来说,我们只会设计|ab|的程序,一是因为这样可以多次利用程序,二是因为使用符号会让问题变得抽象,可以让我们在设计程序的时候专注于问题中的逻辑关系而不被具体数值干扰。这是一个好的习惯,嘛你以后就会懂了。

x ← a - b

  这句话说的就是,计算ab的值,然后把结果放进变量x中。这里的x就是用来当草稿的,一个中间过程。
  把一个数放进某个变量中,这个过程又被称为赋值,以后可能会听到“把什么东西赋给xxx”,指的其实就是把那个值放进变量xxx中。

IF x < 0    y ← -xELSE    y ← x

  还记得上面计算绝对值的例子吗?这里所做的事情是完全一样的:如果x<0,就把x的值放进变量y中,否则就把x的值放进y中。y就是|ab|的最终结果。
  如果读得比较快,你可能还会问,为什么每次都要把结果放进变量中?因为运算器只负责每一次的计算,不负责记录,如果没有把结果放进变量中,那么这次的计算结果就没了,谁也不知道结果是什么,相当于做了一次无用功。或者,你也可以直接把结果输出。

Screen ← y

  Screen指的是屏幕。这句话的意思就是,把变量y的值显示到屏幕上。虽然上面说了,但这里再强调一下:不要忘了把计算的结果输出出来,否则真的不知道它算了个什么鬼。
  好了,拆解完毕,上面那一坨程序其实就是这几句话组成的。虽然我把计算机的命令列举出来了,但实际上大多数时间不会去注意它们,因为实际使用时都是用到上面那样的语句,这些语句已经把所有的命令都用到了。所以我们来进一步熟悉熟悉语句吧。

3.2.2. 更多的命令过程

  神马?你觉得|ab|什么的心算都不用一秒?好好好,我们来算点难的。

1+2+3+4+...+100=?

  这个问题比较实际,生活中能碰的到。那么问题来了,这种计算怎么让计算机解决?
  纳尼?你告诉我用(n+1)n2就能算出来?快别闹咱们按常理出牌。
  按照最简单的计算方法,要得到结果,就是从1开始,一个个累加,算到100为止。于是程序如下:

S1 ← 1S2 ← S1 + 2S3 ← S2 + 3....S99 ← S98 + 99S100 ← S99 + 100Screen ← S100

  好了……好个屁啊!说好的计算机是造福人类的呢,为毛要手写100行命令啊!
  呃,人的工作量似乎大了点,因为要写的东西太多了。很显然,每条语句的内容都是很相近的,不过就是Sn=Sn1+n重复计算了100次。于是又一个重要的概念诞生了:重复执行,或称为循环
  利用循环的概念,这个程序可以简化成这样:

伪代码#2:

S(0) ← 0FOR n FROM 1 TO 100    S(n) ← S(n-1) + nScreen ← S(100)

  第二句话的意思是,n从1开始,每次累加1,直到n=100,对于每个n,都执行一遍下面的语句。而第三句话说的就是,在每个累加过程中,都计算一次求和。这样的循环,既方便书写,也容易理解。
  这个循环过程展开来就等价于下面的语句:

S(1) ← S(0) + 1S(2) ← S(1) + 2....S(100) ← S(99) + 100

  同样都是一步步的,从1开始累加,直到100。
  在这里,这些语句看上去可能会与数学上的概念混淆。注意,命令语句与数学上的方程组有很大的区别。Sn=Sn1+n是一个数学关系,它代表着量与量之间的联系;而SnSn1+n是一条命令语句,它只意味着一个计算过程,即计算出Sn1+n的值,然后赋值给变量Sn,这只是一个有顺序的机械的过程,这个过程结束之后,各个量之间不存在任何关联。
  在数学上,你可以写出这样的方程组:

S3S1S2=S2+3=1=S1+2
  无论顺序如何,方程联立后总能得到正确的解。
  但你绝能写出这样的计算机程序:
S2S1S1+21
  计算机收到这段指令后,将按顺序执行第一句和第二句。在这里,S1的定义被放在后面,在计算第一句的时候,由于还没有对S1赋值,计算机将无法计算出S1+2的结果,这会导致严重的后果。因此,在设计程序的时候,设计者必须要清楚在一个计算任务中需要先干什么,再干什么,并且要明确所有的量都在掌控之中,绝不能出现对未知量的使用
  注意到了吗?在伪代码#2中,我特意在第一行给S0定义了数值。这是为了在接下来的循环中,当n=1时,确保被使用到的S0不会是个未知的量。实际上,这就是对循环初始条件的处理。

3.2.3. 变量的基本使用

  好了,现在已经学会了让计算机流水作业,学会了让它自行判断,也学会了减少人工操作的方法,剩下的事情就是再补补漏,再推广应用应用了。
  然而我是不会让你这么轻松的。(Doge
  还记得我在讲存储器的时候强调了什么吗?不记得的话再回去看看。
  对,变量的内容是可以改变的。这也是计算机的计算能力凌驾于手工计算之上的原因之一。
  在伪代码#1中,除去ab,我们用到了xy两个临时量(或称为中间变量);而在伪代码#2中,中间变量就更多了,从S0S99,共100个最后都用不上的变量。这不仅浪费了存储器的空间,随着记号的增加,这更会给我们对程序的记忆和管理造成影响。对应的解决办法也很简单,那就是适当地重复利用变量;或者为了方便记忆,给变量取个好听的名字。
  稍加改写,上面的程序可以写成这样:

伪代码#3

a ← Number from keyboardb ← Number from keyboardx ← a - bIF x < 0    x ← -xScreen ← x

  xx,很多初学者都对这句话一脸懵逼。但我相信,我已经重复了很多遍“计算机是按顺序计算的”,如果你还是一脸懵逼,一定是因为你没认真看。
  如果非要我解释……好吧。把变量x的数值从内存中取出来,然后给它加上负号,最后把这个计算结果赋值给x。就这样。
  整个程序的运行过程大概像是:

程序流程 每一行执行后 a ← Number from keyboard a=2,b=?,x=? b ← Number from keyboard a=2,b=5,x=? x ← a - b a=2,b=5,x=3 IF x < 0 x=3 x ← -x x=3 Screen ← x ...

  
  至于原来的

ELSE    y ← x

  改写一下就成了

ELSE    x ← x

  呃……把x的值取出来再放进x里,怎么看都是多此一举,所以就不需要加上这段了。
  看看这段新的程序,是不是感觉更简单更容易理解呢?
  嗯还有累加求和的程序,改写一下就成了这样:

伪代码#4

Result ← 0FOR n FROM 1 TO 100    Result ← Result + nScreen ← Result

  这变量就更少了,而且变量的含义也是一目了然。
  它的运行原理同上,程序流程大概像这样:

程序流程 每一行执行后 Result ← 0 Result=0 FOR n FROM 1 TO 100 Result=0,n=1 Result ← Result + n Result=1,n=1 FOR n FROM 1 TO 100 Result=1,n=2 Result ← Result + n Result=3,n=2 FOR n FROM 1 TO 100 Result=3,n=3 Result ← Result + n Result=6,n=3 …… ... FOR n FROM 1 TO 100 Result=4950,n=100 Result ← Result + n Result=5050,n=100 Screen ← Result ...

  
  这种做法,第一眼看上去可能难以接受,但实际上它是最常用也是最好用的方法。

3.2.4. 一些简单算法

  哟西,学完了上面的内容,你就已经可以设计程序来完成大多数计算任务了。一下说了这么多内容,想必一时也难以接受,现在就让我们来喘(zuo)口(dao)气(ti)休(lian)息(xi)一下。
  在2.5中我曾讲到过,语言是要搭配着表达方式使用的,也就是算法。计算机语言搭配上算法,才能真正解决问题。这里列举一些简单的算法,通过理解算法的思路,你会更容易掌握语言使用的。
  用得最多的算法都是一些不怎么动脑子都能明白的东西,因为使用它们理所当然,所以也就没有名称。但是有时候为了指明是哪个算法,它们也就有了一些奇奇怪怪的名字。一般来讲,有固定名称的算法都是一些经过数学推导、经过复杂证明的世界级著名算法。我这里用的名字也会是奇奇怪怪的、并不“标准”的,只要能意会就行,不要在意细节。

3.2.4.1 最值查找算法

  计算任务:由用户输入N个数,求这N个数中的最大值。
  解决思路:在一堆数里找最值,人眼是怎么找的,计算机就怎么找。方法当然就是一个个数看过去,然后每次记下看到的最大的那个数,所有的数都看过之后,记下的那个数肯定就是所有里面最大的了。
  算法设计:
  按流程来思考,假设自己是计算机,上面的想法要怎么实施。
  首先我们需要知道用户的N到底是多少,然后从键盘那里读这N个数。上面已经知道了如何读一个数并存进一个变量中,那么现在有N个数,很显然就是来个循环读取它们。
  知道是哪些数之后,接下来就是一个个比较了。比较N个数,显然还是要来个循环处理。从开头两个数开始,比较两个数的大小,并且把更大的那个数记下来。然后往后就是比较这个更大的数与后面的数的大小,或者说,比比先前所有数中最大的数,和后面的某个数,谁更大,把最大的数记下来,重复这个过程直到结尾,最后剩下的肯定就是所求的最大值。再把记下的这个数输出就行了。
  程序设计:
  
伪代码#5:

//获得个数NN ← Number from keyboard//获得N个数FOR i FROM 1 TO N    a(i) ← Number from keyboard//开始比较,用max来记录最大的那个数//首先我们看到了第一个数,把这个数放进max中//你可以理解为“到此为止看过的最大的数就是第一个数”//也可以理解为“循环之前是要确保初始条件的”max ← a(1)//接下来是循环,把所有数都比较一遍(除了第一个不用再比较了)FOR i FROM 2 TO N    //如果现有的max比a(i)还要小,那么a(i)就可能是最大值    //否则max可能是最大值,不做改变即可    IF max < a(i)        max ← a(i)Screen ← max

  上面的伪代码中使用了注释,也就是在代码中任意位置都可以写的说明。这里的注释以符号“//”开头。
  注释可以告诉读者下面的程序到底在做什么,方便了理解;但另一方面,注释与程序混在一起,有时反而会造成阅读困难。当然写注释其实是个好习惯,只不过初学的时候容易看糊涂。如果你能够理解程序的思路,那么下面这个无注释版本可能适合你:

伪代码#5(无注释版):

N ← Number from keyboardFOR i FROM 1 TO N    a(i) ← Number from keyboardmax ← a(1)FOR i FROM 2 TO N    IF max < a(i)        max ← a(i)Screen ← max

  如果觉得程序还是有点难理解的话,可以动手算算它是怎么运行计算的,看到直观的过程也许你就懂了。
  What?你懒得动手算?信不信我欧拉欧拉你(╬ ̄皿 ̄)=o)) ̄#)3 ̄)
  算了,既然你诚心诚意地发问了,我就大发慈悲地告诉你,为了防止世……咳。
  除去开头读取N和那N个数,后面的比较过程如下(数字是我自己假定的):

数列 比较状态 变量 7,25,2,10,66,13 第1个数 max=a1=7 7,25,2,10,66,13 max=7<25 max=a2=25 7,25,2,10,66,13 max=252 max=25 7,25,2,10,66,13 max=2510 max=25 7,25,2,10,66,13 max=25<66 max=a5=66 7,25,2,10,66,13 max=6613 max=66 7,25,2,10,66,13 完毕 max=66

  
  因为这个算法是逐个比较、逐个查看所有数据的,所以算是个线性查找。
  记得以前有人这样问过我:“为什么比较的时候不能写成a<b<c<d这样的形式?”我相信,现在的你不会问出这种问题来。我们比较的都是变量,换言之,比较的都是数值,变量仅仅是某个数的代指。对计算机来说,a<b<c<d2<1<5<3没什么区别。2<1<5<3是个什么鬼不等式?我也不知道,更别提计算机了。或许计算机能够判断1<2<3之类的不等式是正确的,然而这并没有什么用。
  关于变量比较的详细说明,参见后面的课程。
  总的来说,思路很简单,如果还是有点懵,那可能是我太啰嗦了。这样吧,我们来看一个流程非常非常短的算法。

3.2.4.2 数值互换算法

  计算任务:给定ab的值,要求互换它们的数值。例如,给定a=1,b=2时,要求计算结果为a=2,b=1
  对于互换部分的计算,曾经有小伙伴想都没想就给了我这段程序:

a ← bb ← a

  对不起我不知道该用什么表情才好……说之前能先动手试试这程序算的是什么鬼吗……

计算步骤 变量(计算后) 给定ab的值 a=1,b=2 ab a=2,b=2 ba a=2,b=2

  变量的值可以变化确实是好事,但是它们每次只能记住一个数啊。这样赋值,直接就把原来的数字给擦没了。
  那么怎么办呢?最简单的方法就是,既然直接赋值会把原来的值给擦掉,那么就在赋值之前先把它保存起来:

伪代码#6:

t ← aa ← bb ← t

  先准备好一个新的变量t,用来保存a的值,然后a被怎么擦都没事了,b赋值给a后,再把保存好的a赋给b,完毕。……诶等等还有输入输出没加上去,嘛算了我只是做个示范而已。(偷懒)
  这个例子可以让我们深刻地感受到数学变量与计算机变量有多大的区别。
  或者你非得用数学方法来完成这个任务,那么,是在下输了。

3.2.4.3 穷举算法

  上面那个算法果然还是太短了。绝大多数的算法都利用了各种循环和判断,单是一路计算手写的语句,只能是简单得不能再简单的任务。你觉得循环跟判断组合在一起太复杂了?那没事,现在我们来学一个无脑算法,这就是算法中的万金油,实用方法中的BUG——穷举。
  计算任务:猜猜我的年龄?我的年龄与3相除的余数是1,与5的余数是2,与7的余数是1,计算这个年龄是多少。
  解决思路:我反正不会列出一堆方程然后拼了命地找规律。计算机的一个巨大优势就是,它算数算得贼快。所以呢,对这种问题的一个万能办法就是,对所有可能的答案,一个个去试,试出哪个符合条件那就是哪个,反正又不是我去算
  算法设计:
  对于年龄,一般就是1~100了吧。当然你要是告诉我127也是问题的答案,放心我绝对不会打死你的。所以就做个循环,把这个范围的数字都验证一下,看看哪个符合题目要求。
  “把某个范围的数按顺序全部循环计算一遍”这个过程,又被称为遍历
  程序设计:

伪代码#7:

min ← 1max ← 100FOR i FROM min TO max    IF (i MOD 3 = 1) AND (i MOD 5 = 2) AND (i MOD 7 = 1)        Screen ← i

  这里的mod表示算余数。这个程序……不需要多说了吧,就是对算法设计的一一对应。
  至于为什么开头要多个变量minmax,这是个习惯问题。因为这样写的话,万一,遇到什么情况需要改变范围什么的,直接改动初始的赋值就行了,不需要去程序里找那些数字,同时这样也方便理解。

  但其实呢,我对这个程序还不是很满意。因为,万一算出好几个结果来了,造成误会就不好了。嘛虽然这里确实只有一个答案,但是为了更普遍的问题,我还是希望得到一个答案之后,就不再继续计算。但是上面说到过的语句似乎并不能做到这一点……为了实现它,有两个新的方法可以采用:使用更广义的循环,或是直接打破程序的正常流程。

3.2.5 条件循环

  上面提到的循环,是从一个值累加到另一个值的简单循环。现在我们来看一种更普遍的循环结构。
  循环,也就是重复执行。它可以被理解为,对某些命令,一共要做多少次;同时也可以理解为,在某个范围内,或者说在某个条件内,重复执行命令。比如说,变量i从1到100累加循环,也可以理解成,当1i100时重复执行命令,同时每次执行时i累加1。

FOR i FROM a TO b    do something...

  这个循环可以等价地改写为:

i ← aWHILE i <= b    do something...    i ← i + 1

  其中while语句的意思就是,当后面的条件满足时才执行下面的语句。由于这个广义的循环每次只是做个条件判断,所以原来那个循环中涉及的初始条件与变量累加需要自己来完成,但这也增加了灵活度。
  举个例子,WHILE i < a, do something的执行流程像是这样的:

条件 是否成立 执行的指令 i<a ? do somthing... i<a ? do somthing... … … … i<a ? 离开循环,执行之后的语句

  它的流程图是这样的:

Created with Raphaël 2.1.0开始条件满足?重复的指令...结束yesno

  上面那个等价的循环就是这样的流程:
  
For循环等价流程图
  
  在这种循环结构中,重复执行的条件是可以任意给定的,脱离了原来的只能从一个数累加到另一个数的模式。利用这种循环,我们可以很轻松地把原有的简单循环改成这样:

伪代码#8:每次累加2的计数循环

i ← aWHILE i <= b    do something...    i ← i + 2

伪代码#9:从大到小计数的循环

i ← bWHILE i >= a    do something...    i ← i - 1

伪代码#10:累加求和超过100即停止的循环

i ← 1s ← 0WHILE s < 100    s ← s + i    i ← i + 1

  以及上一小节中,想要实现的“得到答案就结束”的方法:
伪代码#11:按条件搜索的循环

min ← 1i ← minWHILE NOT((i MOD 3 = 1) AND (i MOD 5 = 2) AND (i MOD 7 = 1))    i ← i + 1

  一旦循环条件不满足了,循环即终止。或者另一种理解方法:只要循环条件满足,过程就会一直进行下去。
  为了区别这些循环,通常用它们的语句标志来称呼,比如这里的条件循环也可以被称为While循环,之前的计数循环也被称为For循环
  有时候为了简便,会强行创造出一些多余结构。比如对于伪代码#11中的WHILE NOT(...),完全可以创造出一个UNTIL ...循环,意为“直到条件满足时才结束的循环”。直到判断条件成立了,搜索结束,给出答案,这刚好迎合我们的本意。当然这两种循环仅仅是书写上不一样,其内容是完全等价的。这种创造物被称为语法糖,优点自然就是方便书写和理解,而缺点也是显而易见的:如果不清楚语法糖的原型,使用时极有可能带来疏漏;语法糖是可以任意创造的,当语言中包含大量语法糖的时候,你就不得不花精力去记忆和理解它们。在这里,我不会讲解那些可有可无的语法糖,只会把精力放在最常用、最基础的结构上。譬如For循环,虽然可以说是While循环的一个语法糖,但它确实非常常见,也符合正常人的思维。

3.2.6 打破正常流程

  除了用条件循环来实现上上小节提出的问题,还有一种更为简单粗暴的方案。
  在学习条件循环的时候你也许隐隐约约已经注意到了,循环结构的运行流程,并不是老老实实的一路向下的,正如我在一开始就提到过的一样,程序可以“不按顺序跳着走”。当一次循环结束之后,程序会跳到循环的开始,判断循环条件,然后决定是继续循环还是跳出循环。
  我们可以把这个跳跃的过程单独拿出来使用,即跳转。计算机遇到跳转语句之后,就会把程序直接跳到你所指定的地方。比如:

x ← 1GOTO Displayx ← 2Display:Screen ← x

  这段程序中,Display是一个标号,代表一个位置,没有什么特别含义,名字也可以随便取。程序先执行x1,然后就碰到了跳转语句。跳转语句GOTO Display指示程序应当立即跳转到标号Display所在位置继续执行,所以中间的x2就没有被执行,最后屏幕上显示的数字就是1
  用跳转来解决上面的问题,程序就是这样的:

伪代码#12:

min ← 1max ← 100FOR i FROM min TO max    IF (i MOD 3 = 1) AND (i MOD 5 = 2) AND (i MOD 7 = 1)        GOTO DispDisp:Screen ← i

  它的意义也是一目了然的:遇到符合条件的数之后,就直接跳出循环。
  你可能会觉得,卧槽,这个好啊,这么一来程序想怎么控制就怎么控制了啊。
  如果你真这么想,你会被同行打死的……为什么呢?你看看这段程序就懂了。

Code1: x ← 1GOTO Code5Code2: x ← 2GOTO Code6Code3: x ← 3GOTO Code2Code4: x ← 4GOTO Code6Code5: x ← 5GOTO Code3Code6: Screen ← x

  问:这段程序的结果是多少?
  对没错,答案是2。但是我相信,这种程序你也不会想再看到第二遍。大量存在GOTO语句的程序,总是混乱而难以理解的,因为它不存在“判断”“循环”这样易于接受的结构。
  另外程序的跳转也是有一定限制的,它不能违反我们的一般常识。比如说,你可以提前结束一个判断,跳转到判断语句的外面,但你绝不能从外面跳转到一个判断的内部,因为避开条件而执行内部命令没有任何意义;你可以中断一个循环,跳转到循环外面,但是你不能从外部跳转到循环内部,因为没有初始化的循环是混乱而无法理解的。也许存在一些方法迫使计算机做出违反常识的流程,但它的结果必然是无法预测的。
  所以我一开始就说了,GOTO语句看起来很屌,但实际上并没有什么用。只有在一些迫于无奈的时候,或者是某种语言本身根本就没有提供IF和循环的时候,才会使用这句最基层最灵活的方法。
  GOTO语句过于灵活,难以控制,但是在一个情况下确实会被大量使用,那就是伪代码#12中所用到的,在特定条件下中断循环。想要打破程序的正常流程,但是又要小心不能被GOTO的强大灵活性弄坏了整个结构,于是人们造出了两个语法糖,它们只拥有GOTO的极小部分能力,那就是BREAKCONTINUE
  BREAK也就是“突破”“打断”的意思,计算机遇到它之后,就会跳出当前循环,执行循环外的下一条语句。例如:

伪代码#13:

min ← 1max ← 100FOR i FROM min TO max    IF (i MOD 3 = 1) AND (i MOD 5 = 2) AND (i MOD 7 = 1)        BREAKScreen ← i

  这段程序的功能和结构与伪代码#12是完全一致的。跳出循环后,遇到的语句就是Screen ← i
  结合上一小节的While循环解法,这个坑爹的问题总算是圆满解决了,这两种程序就是使用最多的方案。
  好的,现在我们来……诶等等还有个CONTINUE忘讲了。
  CONTINUE的意思就是“继续”,它的功能是“跳过当次循环”,计算机遇到它之后,会忽略掉它下面的语句,跳转到循环的开头处,开始一轮新的循环。例如:

FOR i FROM 1 TO 10    IF i MOD 3 = 0        CONTINUE    Screen ← i

  当i是3的倍数时,计算机就会遇到CONTINUE,此时计算机不会执行下面的输出语句,而是跳回到循环开头FOR语句那里,将i累加之后继续循环。
  CONTINUE语句常用于排除循环中不符合执行条件的过程。
  
  *实际上,For循环和While循环跟以下的形式是等价的:
  
*伪代码:For循环的等价形式

i ← aStartOfLoop:IF i <= b    do something...    i ← i + 1    GOTO StartOfLoop

*伪代码:While循环的等价形式

StartOfLoop:IF condition ?    do something...    GOTO StartOfLoop

3.2.7 结构的嵌套

  到现在为止,除去流水式的程序,我们已经学习了IF判断、FOR循环、WHILE循环3种结构。利用这些结构,已经没有什么任务是做不到的了。让我来把它们玩得复杂一点。(观众:我撤了(别
  先来个实用的问题:在屏幕上显示九九乘法表。
  九九乘法表中,每个结果都是两个数相乘,而这两个数都是从1到9变化的。很显然完成这个任务需要用到循环,同时也很显然,一个循环不够用了。
  我们把过程捋一捋:把两个数称为aba是1的时候,b要从1到9循环一遍做一遍乘法;a是2的时候,b又要从1到9循环一次……也就是说,a要做一次大循环,而对于循环过程内部,每一个a都有一次b的循环,这就是,循环嵌套

伪代码#14:循环嵌套

FOR a FROM 1 TO 9    FOR b FROM 1 TO 9        Screen ← a * b

  直接把伪代码写出来似乎更容易理解。
  视情况的不同,循环可以有更复杂的嵌套,N层都行,但是为了容易理解,一般不会有3层以上的嵌套。

  理解了嵌套的概念之后,我们来看个更难一点的问题。
  计算任务:找出100以内的所有质数。
  解决思路:
  质数,定义就是只能被1和它本身整除的正整数(1除外不是质数)。那么在计算机上,如何判断一个数是不是质数呢,或者说如何判断一个数能被哪些数整除呢?没有什么好方法,于是我掏出了万能算法——穷举。
  比如说对于数15,除去1和它自己,从2开始一直到14,每个数都去尝试算算除法,如果所有的数都不能整除,那么它就是个质数;但在这里,算到3的时候,15/3=5能整除,哦,不用再算了,15不是质数。
  知道了如何判断一个数是否为质数,现在问题要我们算100以内的所有质数,那太好办了,刚刚的过程外面再套一层2到100的循环就行了,对每个数都判断一下是不是质数。
  算法设计:
  使用两层循环,第1层循环用来遍历100以内的数(除了1),第2层循环用来逐个计算除法,验证第1层循环内的数是否能被其他数整除,如果到最后都不能被整除,那么说明是质数,向屏幕输出这个数,否则就不是质数,终止判断过程。
  程序设计:

伪代码#15:寻找质数(试除法)

//灵活起见,用变量来代替固定范围range ← 100//第一层循环,遍历范围内所有数FOR i FROM 2 TO range    //第二层循环,判断遍历的i是否存在其他因子    FOR j FROM 2 TO i        //如果i能被比它小的数整除(即余数为零)        //那么就不用继续比较了,i一定不是质数        //注意BREAK只能中断第二层这1层循环,不是直接跳到最外面        IF i MOD j = 0            BREAK    //这里是j的循环以外    //如果到最后j循环到了i的大小,说明中途没有中断出来    //也就是说,i没有被整除,是质数    IF i = j        Screen ← i

  简洁的无注释版如下:

伪代码#15:寻找质数(试除法,无注释版)

range ← 100FOR i FROM 2 TO range    FOR j FROM 2 TO i        IF i MOD j = 0            BREAK    IF i = j        Screen ← i

  虽然注释中提到了,这里我还要再说明一下,在循环嵌套时,BREAK只会打破当前的循环,比如上面的代码中,在j变量的循环中遇到BREAK,计算机会跳转到与当前循环结构并列的下一条语句,也就是IF i = j。如果你在i循环中放一个BREAK,那么就是直接跳到程序结束了。
  也许你会对这个程序设计有些疑问,比如说为什么j是循环到i而不是i1,为什么非要做i=j这种违和的判断。如果非要这么问的话,下面这个程序也许会好些。

伪代码#15(试除法,带标记版)

Yes ← 1No ← 0range ← 100FOR i FROM 2 TO range    isPrime ← Yes    FOR j FROM 2 TO (i-1)        IF i MOD j = 0            isPrime ← No            BREAK    IF isPrime = Yes        Screen ← i

  好像又更长了。
  在这里,我定义了一个标记变量isPrime,顾名思义,意思就是“它是一个质数吗?”。这个标记的值可以是Yes或者是No,当然既然是变量那它们肯定就是个数,具体数值可以任意定义。
  使用这个标记之后,看起来是不是更容易理解了呢?
  也许你又会问了,循环第一句的isPrime ← Yes是干吗的?它是个变量的初始化,防止isPrime在未被赋值时就被使用。你可以试着把这句话去掉,或者改成isPrime ← No,动手走一走流程,想想程序会受到什么影响。
  
  与循环嵌套类似,条件判断也能嵌套。这个没什么好说的,和日常生活所碰到的一样,不过就是把“如果怎么样,那就怎么样”进化成了“如果怎么样,再看看如果那么样,那就怎么样”。
  举个例子,如果要从两个数a,b中找出最大的一个数,一个简单的IF就行了:

IF a > b    max ← aELSE    max ← b

  但如果要从三个数a,b,c中找出最大的那个数,因为数量太少了用不着循环,简单来说这个过程可以写成这样:

伪代码#16:IF嵌套求最大值

IF a > b    IF a > c        max ← a    ELSE        max ← cELSE    IF b > c        max ← b    ELSE        max ← c

  这段程序也没什么好说明的,就是简单地比较两次然后找出最大值。理不清的话,代几个数进去算算就明白了。

3.2.8 条件选择结构

  条件判断语句的嵌套形式中,有一种堆叠的情况常被用到,那就是选择结构:通过一系列判断,挑选出正确的情况。
  例如一个分段函数:

f(x)=x+2x22x1x+4,x<1,1x<1,1x<2,2x

  当x的值确定后,为了得到正确的函数值,我们需要判断x的所在区间。这里有4个区间,那么就需要做至少3次判断。程序如下:

x ← Number from keyboardIF x < -1    f = x + 2ELSE    IF (-1 <= x) AND (x < 1)        f = x * x    ELSE        IF (1 <= x) AND (x < 2)            f = 2 * x - 1        ELSE            f = -x + 4Screen ← f

  在这里,最后一个条件不做判断也行,因为,如果上面3个条件都不满足的话,那就必然是在第4个区间里了。
  ……但是这程序看起来这么那么别扭。一层堆一层,条件多了那是要上天啊。
  所以这种堆叠通常是写成这样的:

伪代码#17:IF…ELSE IF选择结构

x ← Number from keyboardIF x < -1    f = x + 2ELSE IF (-1 <= x) AND (x < 1)    f = x * xELSE IF (1 <= x) AND (x < 2)    f = 2 * x - 1ELSE    f = -x + 4Screen ← f

  这段程序与上面的程序其实是完全一样的,只不过少了几个换行和空格,但是看起来就一目了然了。这显然就是一个逐个判断选择的过程,第一个条件不满足,就判断下一个条件,直到找到了满足的条件,然后运行它内部的语句,最后离开整个IF结构。如果搞不清嵌套关系,请看上面;如果搞不清逻辑关系,请看这里。当然,一般来说只能见得到伪代码#17的样子。
  另外,有一种条件选择经常被用到,那就是等量关系。例如对于一份菜单,选择了第一项应该怎么样,选择了第二项又应该怎么样,这里的选择条件就是简单的相等。为了简化这种常见情况,人们又发明了一个语法糖:SWITCH。它的结构如下:

伪代码#18:SWITCH选择结构

SWITCH 变量    CASE 数值1:        do something...    CASE 数值2:        do something...    ...    DEFUALT:        do something else...

  它的意义就是,对于给定的变量,在下面的CASE列表中找到一项相等的数值,然后运行对应的语句;如果找不到相等的数值,那么就运行DEFUALT下面的语句。视情况而定,DEFUALT可有可无。
  比如这个例子:

x ← Number from keyboardSWITCH x    CASE 1:        Screen ← "小笼包"    CASE 2:        Screen ← "叉烧包"    CASE 3:        Screen ← "奶黄芝麻豆沙包"    CASE 4:        Screen ← "大肉包"    DEFUALT:        Screen ← "...你倒是选一种啊"

  我饿了。
  什么?你问我为什么出现了数字以外的东西?嘛,计算机只认识数字倒没有说错,至于具体怎么回事,嗯……
  其实到此为止程序的流程也没有什么好说的了,常见的过程和结构就是这些了。在遇到问题的时候,一般来说,就是寻找算法,把问题分解,然后用上面这些内容去一一对应。
  好了,我们现在就先抛开流程,来研究研究上面这个新问题吧。

3.3. 数据的类型与计算

  在前文中,我说过计算机是只认识数字的,而且用到的例子中也只有数字的计算。这个说法并没有错,但是容易令人混淆真相:计算机只能操作数字,并不意味着我们就只能使用数字。我们可以利用各种方法,创造出想要表达的新内容。
  但在说明那些新内容之前,我需要详细讲解一下对数字的使用。在计算机内,一个数字的储存、使用和计算,虽然在上面的例子中看起来只是对变量符号的简单操作,实际上它的真面目要复杂得多。我会把它在逻辑上的、最基本的样子展现给你,你将完全看清计算机是如何操纵数据的。

3.3.1 内存的抽象结构

  不要听到“抽象”两个字就头疼,如果把它的真实结构搬出来的话,相信我,你会想狗带的。
  内存是最常用的存储器之一,程序运行时,所有的变量都放在它那里。所以要了解变量的操作,我们就来看看内存的结构。
  跟前文所说的一样,内存里的数据是连续的、按顺序、像是一条线排列着的(即线性储存);但跟前面说好的不一样,你还是需要了解一下二进制是个什么鬼。因为,内存里的数据真的是按二进制存放的,而且,数据的分类、安排、总量也极大地受到了它的影响。

3.3.1.1 二进制数

  二进制数不是什么复杂的东西,只不过不合常识罢了。
  我们通常用到的数,是十进制的,也就是说,个位数只有从09一共十个数字,我们从0数到9之后,满十进一,十位数增加1,个位数回到零。十进制只是我们的习惯用法,实际上任何一个数都可以用N进制来表示的。
  根据这个等价说明,我们先来看看,比如8进制。8进制的个位数只有07,满8进一,所以从0数到7之后,在十进制里对应的是8,而在8进制里对应的是10。注意,它们只是写法不同,实际上指的是同一个数。同理的,8进制的11完全等价于十进制的9
  回到我们熟悉的用法上。一个十进制数1234,这串数字表示的含义其实是

(1234)10 =1×1000+2×100+3×10+4×1=1×103+2×102+3×101+4×100

  同样的,对于任意N进制,含义是类似的:
(abcd)N=a×N3+b×N2+c×N1+d×N0

  比如刚刚提到的,(11)8=1×81+1×80=9。因为展开后的计算都是按照平常的十进制来描述的,所以计算结果也就是十进制的对应数。或者换句话说,把数位展开并求和计算,就是N进制到十进制的转化方法。至于十进制数如何转化到N进制上,有兴趣的话请参考附录。
  现在可以来看看二进制数了。二进制,也就是个位数只有01的表示法,满2进一,比如:
(1001)2(111000010)2=1×23+0×22+0×21+1×20=1×23+1×20=9=1×28+1×27+1×26+1×21=450

  可以看出,二进制与其他进制相比,没有什么特殊的地方,除了它的数字不是0就是1,而且它特别长。
  实际使用时,为了贴近二进制,又不想写这么长的数字串,人们常常使用16进制来表示数字。16进制的个位数分别是0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,对应十进制里的015
  之所以说16进制贴近二进制,是因为16进制的1位数刚好相当于二进制的4位数,(F)16=(1111)2,相当于一种缩写。以后要凸显二进制的特性时,都是用16进制数来简化表达。

3.3.1.2 数据的基本单元

  内存中的数据全都是二进制的,连起来看就是这样的:1100100101...0010111101......
  而一个存储器的容量是固定的,所以无论它有没有被使用,它都被二进制数位填满了,不存在既不是0也不是1的地方,更不存在所谓“空白”的地方,只不过没有被使用的部分是无价值数据而已。就像这样:
这里写图片描述
  如果真的把储存的数值就这么直接化为二进制放进去,管理起来那就要了命了,长度有短有长暂且不说,分界线根本找不到。
  对于数据的储存和划分,公认的方案是这样的:
  一个二进制数0或者1称为一个数Bit);把整个存储器,每8个二进制数位归为一个基本单元,这个单元称为字节Byte)。任何数据占用的空间都必须是基本单元的整数倍。
  一个字节有8个二进制数位,也就是说它可以表示0000000011111111也就是0255这个范围的任意数字。超出这个范围的数需要更多位来表示,一个字节装不下。
  但是单单字节这个概念无法满足日常的使用,因为平时涉及到的数字,比255大的多了去了。为了表示8位以上的数,人们常常把2个字节、4个字节作为整体来记录数据,即16位数与32位数。别问我为什么没有3个字节的24位数。
  2字节又称为一个Word),4字节又称为一个双字Double Word或缩写为DWord)。
  有时候能听到“这块内存是4GB的”“这块硬盘是2TB的”,说的就是存储器里能存放多少个字节。顺便一提,各个常用单位的换算如下:

1TB=1024GB=10242MB=10243KB=10244Byte=10244×8Bit

  考虑到存储器的容量是有限的,我们有必要来谈一谈数字的划分。为了表示一个数,由于不确定它到底是多少,到底是什么范围的数,可以给它准备1字节,4字节,或者N字节,甚至是几百几万字节。但是,存储器遭不住这么折腾,如果对每个数都划分出超大的储存空间,那么用不了几个数,存储器就满了。
  为了节约存储器的空间,也为了日后数据的传输,我们必须要为不同的场合准备不同的数字长度,或者说,数字类型。最常用的就是上面提到的,字节、字与双字。对于很小的数,应该使用字节;对于更大的数,应该使用字或者双字。双字可以表示02321)=4294967296的数字,对于大多数情况,这个范围够用了。当然,如果你觉得你的存储器足够大,或者用到的数据量很小,无视数据类型的划分,一概使用字或者双字,这完全是可以的。
  说完了正整数,还有负数和小数忘了讲了。
  负数和小数用二进制怎么表示?呃……这个有点复杂,你只需要知道它们确实能够表示出来就行了。如果非要纠结细节,好吧,附录欢迎你。
  负数的表示方法与正整数只有一点点差别,它的类型也是一样的:字节、字和双字。表示范围嘛,1字节的话可以表示128127的任意数,大概就是负的原来的一半到正的一半的感觉,字和双字也是类似的。
  至于小数,它的表示方法有很大的不同,所以我们给它另外规定了一个类型,叫浮点数。根据占用的位数不同,它又分为单精度Single)和双精度Double),使用的长度分别跟双字(32位)和四字(64位)相等。
  相比“浮点数”这个称呼,整数(正整数和负数)又被称为定点数
  上面几种类型可以归纳成下表:

数据种类 数据类型 占用字节 表示范围 整数 Byte 1(8位) 0255128127 Word 2(16位) 0655353276832767 Double Word 4(32位) 023212312311 浮点数 单精度 4(32位) ±(1.4×10453.4×1038) 双精度 8(64位) ±(4.9×103241.7×10308)

  其实一般不会去记具体范围,用的时候感觉数字不会太离谱就行了。

3.3.1.3 内存中的变量

  了解了内存数据的划分之后,我们进一步来看看变量在内存中的真实情况。不同于上面的示意图,上面的图只是对数据按“每个”来划分的,而考虑按字节划分,考虑数据类型之后,实际的储存情况是这样的:
这里写图片描述
  在这里强调一点:在使用变量时,除了要跟计算机约定好变量所在的位置,还要说清楚变量占用的空间,或者说,要明确变量的类型。因为无论更多或是更少的字节被使用,计算的结果肯定都是错误的。
  对于约定一个变量的类型,我采用以下的伪代码来表示:

DEFINE variable AS DWORD

  不同于之前的语句,这句话不是一条命令,而是一条陈述,它只是告诉计算机,有一个变量叫做variable,这个变量的类型是双字(DWORD)。这条语句又被称为变量定义。既然不是命令,那么它也就跟程序的流程无关,只要保证计算机在使用变量之前能看到这句说明,你可以把它放在程序的任何位置。(但习惯上,变量定义是放在程序开头的。)
  举个例子:

伪代码#19:含有变量定义的程序

DEFINE a AS DWORDDEFINE b AS DWORDa ← Number from keyboardb ← Number from keyboardDEFINE s AS DWORDs ← a + bScreen ← s

  也许你注意到了,上面的变量定义中,只说明了名字和类型,没有约定它的位置。
  变量的所在位置又称为内存地址,或简称为地址,它是以字节为单位计数的。比如上图中,第一个变量的地址是2,第二个变量的地址是6,第三个变量的地址是10
  当地址也确定之后,一切的操作就可以机械地完成了。在使用变量的时候,计算机按照给定的地址去查看内存,然后根据约定类型的长度,把变量的数据取出来;写入变量的时候也是类似,先找到内存中的对应地址,然后把变量长度范围的数据改写为新数据。这就是计算机操作变量时所作的工作。
  但是变量地址对于我们来说,是个无所谓的东西,地址是多少都照样使用。在低级的语言中,每个变量的地址都需要手动规定,这无疑是一项麻烦而又无意义的工作。因此在更高级的语言中,语言的内容变得抽象,我们看不到也用不到对地址的处理,这个工作由计算机自行完成。
  可能你会觉得,变量的类型也是个无所谓的东西,反正就是个数字,具体是什么类型,要占用多少内存,让计算机自己处理就好了。这个想法并不错,在一部分高级语言中,变量确实是不区分类型的,甚至无需做变量定义。这种语言被称为弱类型语言,相对的,需要定义类型的语言为强类型语言。弱类型语言的甜头,想必你已经尝到了:在前面的伪代码中,你只需要往变量里塞东西,用的时候取出来就行了,其余细节的都无需考虑,设计者可以一心一意去处理逻辑问题。而风险与利益同在:弱类型会降低计算机的计算速度,而且混淆的类型容易让设计者产生误解。“有什么好误解的?”你一定这么想吧。当然,不产生误解仅仅是在变量全是数字的情况下,如果有其他类型,问题就麻烦了。

3.3.2 字符类型

  有了一台功能强大的计算机,我们当然不希望只是一大坨数字满眼飞。最起码的,文字信息就得有吧。但是上面也说过了,内存里的数据全是二进制数,根本没有其他类型插手的余地。怎么办呢?那就用数字来代表字符。
  考虑平常使用的26个英文字母,算上大小写,不过才52个。再算上标点符号数学符号等等乱七八糟的东西,这些字符加起来也就百来个。于是一个非常简单的字符表示方案诞生了:用一个字节去表示一个字符,0255中每一个数字对应一个确定的字符,具体关系如下:
标准ASCII对照表
  可以看出,127个位置就已经绰绰有余了,不仅包括了常用字符,还容纳了许多控制标志。为了利用这一个字节里剩下的位置,人们又增加了一些对应关系:
扩展ASCII对照表
  这两张表的对应关系被称为美国标准信息交换代码(American Standard Code for Information Interchange,简称ASCII),是现今最通用的单字节编码系统。它包括了键盘上能按出来的所有字符,还包括了一些奇奇怪怪的东西,对于我们来说,32127编号(16进制的207F)的字符,也就是ASCII打印字符就足够使用了。
  当然计算机是认识这张表的,计算机通过这张表也认识了字符这个概念。所以只需要告诉计算机,这里有个变量是字符类型(Character,简写为Char):

DEFINE variable AS CHAR

  计算机就会自动地通过ASCII对照表,把变量里的数字当做字符来处理。例如,给一个字符变量赋值数字65,或者给它赋值字母A,对计算机来说是完全一样的。注意,字符1不等于数字1,它等于数字49。再注意,字符类型的变量只需要1个字节。
  ……啥?中文汉字怎么表示?汉字数量太多了,一个字节是装不下的。ASCII不能表示汉字,表示汉字的方案有GBK、Unicode等等编码系统,它们都是多字节编码的。咳,跑题了。
  根据字符的编码表,我们就可以方便地使用各个字符了。那么就到此为止了吗?并没有。现在我们只知道单个字符怎么使用,然而真正用到的远不止一个字符,我们看到的往往是一大堆文本。

3.3.3 数组与字符串

  数组,就是把数字拼成一组,它不是什么新类型。
  早在认识循环的时候,我们就用过数组了。在对数列an=n求和的时候,循环中的每一个Sn都是一个相互不同的变量,把100个不同的Sn并排在一起,就成了数组。在这里,S就是数组的名字,n就是序号,或者叫下标,每一个Sn被称为一个元素
  那么数组有什么用呢?数组可以让我们通过序号来控制数据,对于等待计算的一大堆数据,我们可以用一个符号来表示它们这个整体,然后通过序号来批量处理它们,比起创建一坨变量,这是再好不过的方法了。一个简单明了的例子就是3.2.4.1小节中的程序,无论是在输入时,还是在比较时,数组都是无可代替的选择。
  既然这一章节讲的是数据类型,那么这里就有必要说说数组在内存里的样子。
  数组的储存是线性的,或者说,跟它的名字一样,在内存里,真的就是把一组数并排在一起,如图所示。
数组的储存结构
  图中两个数组是这样定义的:

DEFINE a[5] AS BYTEDEFINE b[7] AS WORD

  这两句话的含义应该不需要再解释了,图已经说明了一切。为了防止括号的意义被混淆,表示数组和下标的时候,下标用方括号而不是圆括号围住。例如,数组a的第4个元素写成a[4]
  值得注意的是,从底层的角度来看,一个数组在内存中的占用空间是确定的。也就是说,数组在定义的时候就确定了元素的数量,在之后的使用中,绝不能操作这个范围以外的数据。比如你定义了数组a有5个元素,那就不要去尝试读取或者写入a[6]。尝试这个操作往往会引起错误,因为定义范围以外的内存,可能正好是其他某个变量的空间。这种错误的尝试被称为下标越界
  在一些语言中,计算机只会记录数组变量的起始地址(或称为首地址,例如上图中数组a的首地址为3),不会保存数组的元素个数。计算机只保证,你定义时候的N单元空间不会被其他变量占用,这之后就不会再去管它到底有多少单元了。也就是说,定义数组的时候,计算机会告诉你,“好了,这里有块空间足够大,你拿去用”,至于你问它“我的数组是多大的?”它会告诉你,“不关我的事”。这意味着下标越界将会是很常见的疏忽。为了避免它,你应该手动用一个变量记录数组的大小。
  但有些高级语言对数组做了些手脚,使得你可以随心所欲地使用任意下标的元素。
  
  除了在批量处理上很有用,数组的另一个常见用法是表达字符串
  字符串其实就是字符类型的数组,表达方式可能不同,但两者一毛一样。比如这串数据:
字符串内存示意图
  可以定义为:

DEFINE string[15] AS CHARstring[1] ← 'Y'string[2] ← 'o'string[3] ← 'u'...string[15] ← '?'

  或者

DEFINE string[15] AS CHARstring ← "You look at me?"

  或者再干脆一点

DEFINE string[] AS CHAR ← "You look at me?"

  第一种定义方法是手动一个元素一个元素赋值。一般不会这么写,累死活该
  第二和第三种方法是把手写的字符串整体赋值给一个字符型数组。区别于单个字符,字符用单引号围住表示,而字符串用双引号围住表示。第二和第三种写法的唯一区别是,前者你需要自己数到底有几个字符,而后者你把数数的任务扔给计算机了。注意,自己数的时候,一定要确保数组的容量足够大,否则赋值的字符串会强行占用数组以外的内存,这个错误称为内存溢出
  注意你不能把定义写成这样:

DEFINE string[] AS CHARstring ← "You look at me?"

  要记住,数组在被定义的时候必须要有一个确定的大小,因为正如之前所强调的,在约定变量的时候,计算机不仅要准备好一个地址,还要准备好足够大的空间。不告诉大小,计算机也不知道怎么确定这个变量。
  但在你定义了N个元素之后,蛋疼的是,计算机还是不会记录元素个数。为了记录字符串的长度,而又不希望另创一个变量,有两种常用方法可以采用:
  一是把字符数组的第一个元素当成数字使用,用它来记录字符串的长度,而字符串的数据从第二个元素开始;
Pascal字符串格式
  二是在字符串的末尾补一个数字0,因为字符串里的字符都是ASCII码中32以后的,虽然偶尔会出现32之前的控制符号,但不会用到0这个编码,所以用数字0来表示字符串的结束,注意是0而不是0
C字符串格式
  无论哪种记录格式,都多了一个字节的占用空间。所以使用这类字符串的时候,一定要注意定义字符数组时,元素个数是比字符数多1个的。

  至此,计算机内的基本变量类型就讲完了。对,只有这么一点:整数、浮点数、字符、字符串,没有什么其他类型是天天用得上而又必不可缺的了。现在我们来看一个本应是放在整篇教程开头的程序:

伪代码#20:第一个程序

Screen ← "Hello World!"

  我想,你现在应该能理解它是什么意思、怎么运行的了。

3.3.4 常量

  区别于变量,对于内存中那些不会被改写的数据,我们称之为常量
  常量分为两种,一种是我们在程序里写着的,讲道理,是不可能改变的量,比如:

82 ← 666'!' ← '?'

  这两条语句是不可理喻的,因为一个数字是不可能被赋值为另一个数字的,它们是常量。同样的,手写的字符串也是这种常量。
  你可以写:

Screen ← "What happened!?"Screen ← "Ah HAHAHAHA"[5]

  ("????"[n]的意思和数组里的一样,指字符串的第n个字符。)
  但是没人理解它们是什么鬼:

"OMG" ← "F@CK""Shxt!"[3] ← 'i'

  字符串里写着是什么,那就是什么,不允许被改写。这就是为什么上一小节里大费周章地介绍字符数组,我们需要把字符串放进字符数组里,数组才是变量,可以被改动。
  还有另一种常量。这种常量和变量没有任何区别,只不过在使用时,我们不会去改动它,同时也规定不能去改动它。这种常量带来的好处是,它可以告诉别人,“我是个事先给定的数值,不是中间变量”,而且强制规定不能改变,能够确保它不会被误用。
  例如前文中那个1+2+...+100=?的问题,在那之后学了一大坨新知识以后,程序可以写成这样:

伪代码#21:数列求和的完全体

DEFINE min AS CONST DWORD ← 1DEFINE max AS CONST DWORD ← 100DEFINE i AS DWORDDEFINE s AS DWORD ← 0FOR i FROM min TO max    s ← s + iScreen ← s

  其中的CONST是英文constant的缩写,即“常量”。
  你没看错,这程序就是需要这么麻烦。看起来很罗嗦,但仔细想想又没法去掉任何一句。实际上,这段程序只需稍加修改,就能在计算机上真正运行起来,而不只是在这里空谈了。
  定义中的CONST是可有可无的,它丝毫不影响程序的运行。但为了程序的可读性,为了表达的准确性,还是养成习惯加上去吧。

3.3.5 基本类型的转换

  不同类型的变量相互转换,这是一个常有的需求,比如:

DEFINE a AS BYTE ← 233DEFINE b AS DWORDb ← a

  一个很小的数,放进一个大容量的变量里,合情合理。
  因为所有数据的原本面貌都是二进制数字,所以在一定程度上,各个变量之间都有转换关系。这些转换关系首先要满足一点,那就是小容量的变量都能够转换成大容量的变量,但反过来就不一定了。这里的容量,不仅指内存中的占用空间,也指变量类型所能表示的数的范围。
  简单的转换关系如下:

CharByteWordDWordSingleDouble
  箭头指示了类型可以往什么方向转换。如果要将箭头反过来,情况就有些复杂了:
  在类型的表示范围的允许内,双精度可以转换为单精度,否则会出错;
  在类型的表示范围的允许内,浮点数可以转换为整数,转换后小数部分被去掉,只保留整数部分,若整数部分过大,转换会出错;
  在类型的表示范围的允许内,整数之间可以逆着箭头方向转换,否则会出错。
  如果以后遇到新类型,转换的时候考虑数据是否在表示范围内就行了。
  在运算中,如果出现不同类型的变量混合运算,那么计算机通常是这样处理的:先把小容量的变量转换为大容量类型,然后再进行计算。例如:

伪代码#22:一个简单的类型转换

DEFINE a AS BYTE ← 1DEFINE b AS SINGLE ← 1.5DEFINE c AS WORDc ← a + b

  最后的运算流程是这样的:
  a + b中出现的最大容量的类型是SINGLE,所以先把a转换为SINGLE类型,转换后a = 1.0
  接着计算加法,1.0 + 1.5,计算结果为SINGLE类型的数,值为2.5
  由于cWORD类型,所以先将计算结果转换为WORD型,将2.5的小数部分扔掉(不是四舍五入),得到2
  将最后结果2赋值给c,完毕。

3.3.6 数值的运算

  讲完了数据类型,我们来看看各种数据可以有哪些计算。
  对数字的运算,大体可以分为以下4种:
  基础算术运算。这类运算包括了数字的加法、减法、乘法、除法、求余、负号,没了。

a ← 1 + 2      //a = 3b ← 3 - 2      //b = 1c ← 3 * 4      //c = 12d ← 5 / 2      //d = 2.5e ← 13 MOD 5   //e = 3f ← -e         //f = -3

  关系运算。这类运算用于比较两个值的关系,它的计算结果只有两个:True)或者False)。在不同的语言中,这两个结果有不同的具体数值,但通常来说,用0来表示“否”,用0以外的任意数字表示“是”。这类运算包括:大于、小于、大于等于、小于等于、等于、不等于,没了。

a ← 1 > 2      //a = Falseb ← 3 < 2      //b = Falsec ← 4 >= 3     //c = Trued ← 5 <= 2     //d = Falsee ← 6 = 6      //e = Trued ← 9 != 2     //d = True

  逻辑运算。这类运算用于逻辑命题的计算,包括:逻辑与、逻辑或、逻辑非,没了。

a ← True AND False    //a = Falseb ← True OR False     //b = Truec ← NOT True          //c = False

  位运算。这类运算针对二进制数而设计,可以按二进制数位来操作数字。它们包括:位与、位或、位异或、取反、左移、右移,没了。

a ← 0110 & 0011    //a = 0010b ← 0110 | 0011    //b = 0111c ← 0110 ^ 0011    //c = 0101d ← ~0110          //d = 1001e ← 000110 << 2    //e = 011000f ← 011000 >> 3    //f = 000011

  (为直观起见,参与位运算的数以二进制形式书写)
  前四条运算是在每一位上做逻辑运算的,逐位运算得出结果。


========== 施工重地,请勿靠近 05/20/16 ==========

(优先级)

字符串与输入输出

(常见算法)
(输入输出格式)

指针*

0 0
原创粉丝点击