第二章 编程初步这一章我们主要来讲计算机编程。之所以我选择这个主题作为讲计算机入门的第二章,是因为很多计算机用户还不会编程,而编程却是让计算机发挥其功效的很重要的基础技术。并且,编程也不是很高深的内容,适合大多数年轻的计算机用户学习。本章所讲的计算机编程是比较简单的内容,主要使用 GWBASIC 这个小巧的程序开发环境。它比较适合入门者。当然,从实际上说,可能 TrueBASIC 更加适合初学者,因为它是 BASIC 语言的发明人设计编写的,但是因为现在 TrueBASIC 比较难找,并且也没有十分好的版本,所以我拿传统 BASIC 的代表者 GWBASIC 来讲。 GWBASIC 简介GWBASIC 是微软公司在 1983 年以后开发出来的一个 BASIC 解释器和开发环境。微软公司,即美国 Redmond(西雅图附近)的微软公司,当时由比尔·盖茨作总裁。所谓 GW,有人说是是 Gates, William,就是威廉姆·盖茨,也就是比尔·盖茨。当然实际上微软公司要表达的是 Graphics Workstation,就是“图形工作站”。那个时候微芯片计算机还普遍在 286 的时代,计算速度还不高,所以没有十分复杂的操作系统。普遍的家用计算机更是没有什么高的要求,所以在那些计算机上,普遍安装了 BASIC。所谓 BASIC,就是 Beginners' All Purpose Symbolic Instruction Code,翻译成中文就是“初学者万能指令式代码”。这里我说到了“解释器”,它的英文是“interpreter”。这是什么意思呢?先让我们来了解一下计算机程序的概念。我们人类,写出一些代码,用来规定计算机按照一定规则运算。这一套规则就是计算机程序。所谓解释器是指用户编写的程序在运行的时候并不是直接在计算机的处理器上运行,而是经过该程序的解释,在该程序的流程中运行,也就是,把用户编写的程序“解释”为它自己的流程。这个解释器本身,包括它的解释功能和它解释出来的流程,则在处理器上直接运行(这是一般情况;也有多层解释的情况,但最底层的解释器在处理器上直接运行)。所谓开发环境,就是用来开发编写程序的一套环境。 使用 GWBASIC 来完成简单计算GWBASIC 的一大特点是很快就可以让用户上手。这是因为它设计得很简易。当然也有和它一样容易上手的 Python 脚本语言的开发环境,但是因为我还不熟悉它,所以无法为大家讲述 Python。现在,让我们来讲 GWBASIC 最让初学者感到开心的功能:不用写程序,直接可以当计算器用。 来,让我们启动 GWBASIC。如果你用 DOS,先切换到 GWBASIC 所在的目录,然后往命令行上打“GWBASIC”并回车就可以了。不过,呵呵,现在还有只用 DOS 的人吗?你如果用 Windows 也不妨,用资源管理器找到 GWBASIC 程序,用鼠标双击就可以了(注:2008年以来,64位Windows日渐普及,这种类型的Windows不能运行GWBASIC。此时,可以使用DosBox来运行GWBASIC,或采用其他虚拟机环境)。此时你会看见一个屏幕,如下图。
GW-BASIC 3.23
(C) Copyright Microsoft 1983,1984,1985,1986,1987,1988 60300 Bytes free Ok 1LIST 2RUN← 3LOAD" 4SAVE" 5CONT← 6,"LPT1 7TRON← 8TROFF← 9KEY 0SCREEN 然后,让我们来试试看它的计算功能。先让它来计算一个简单的算式:2 + 3 = 5。我们输入“? 2 + 3”然后回车。屏幕上将显示计算结果 5,并换行显示新的“Ok”。
? 2 + 3
5 Ok 我们刚才试的这个算式还很简单,实际上多数可以用计算器算的式子都可以用 GWBASIC 来计算。GWBASIC 支持的计算除了四则运算以外,还有 sine,cosine,tangent 等三角函数,乘方,开平方根,对数等等。现在让我们看看 GWBASIC 的乘除运算。输入“? 2 * 3 / 9”然后回车。
? 2 * 3 / 9
.6666667 Ok 显示出的小数位数只有 7 位,比常见计算器的位数要少。不过以后如果真的有必要,可以在 GWBASIC 中使用位数达 14 位的小数。试试看输入 ? 2# * 3 / 9 并回车。其中 # 号是表示双精度浮点数。需要注意的是,GWBASIC 中的计算,如同科学计算器一样,是先乘除后加减的。比如说“2 + 4 / 2”的结果是 4。当然,不用说的是,四则运算是从左到右的。 再让我们来看 sine 等运算。在写它们的算式时,要使用括号,这是与使用一般计算器不同的地方。另外,所有的角的大小都是以弧度记的,而不是以角度记的。所以 90 度角要写成 0.5 * 3.141593 的形式。这里 3.141593 是圆周率的近似值。角度与弧度之间的转换公式是:弧度 = 角度 / 180 * PI。注意 PI 在实际使用时要用近似值代替。
? sin(0.5 * 3.14)
.9999997 Ok 做完了上面的事,我们也许想退出 GWBASIC 了。要退出 GWBASIC,只要输入“system”并回车就可以了。 编写我们的第一个小程序编写程序,是使用计算机的方法之一。为什么说是方法,这是因为使用计算机可以做许多事情,比如上网、编辑文档、写程序等等。而写程序则是使用计算机的最直接方法之一。我们所使用的软件可能没有某些功能,这个时候就可以通过写程序来让计算机来帮我们实现我们的思想。 使用 GWBASIC 编程序,可以实现一些比较简单的功能。并不是它不能实现复杂的功能,而是 GWBASIC 程序必须写得非常复杂,才能实现复杂的功能。相比之下,其他的一些语言,比如 C++ 等,就可以不必将程序写得太复杂就能实现比较复杂的功能。但是 GWBASIC 正是因为简单,所以易学。所以我这里讲如何用 GWBASIC 来写程序,作为入门来说还是挺合适的。 我们的第一个小程序,不复杂。不过还是让我们一步一步来写,每写一步就看看它的运行结果。这样可以更好地明白计算机程序是怎样工作的。 先输入“10 print "Hello! Welcome to the first program."”然后回车。输入的时候注意不要把我这句子里用的引号与程序中的引号搞混了。这里“print”的作用与前面我讲计算功能的时候用的句子开头的“?”(问号)的作用是一样的,表示显示。
10 print "Hello! Welcome to the first program."
你会发现在输入这句话之后没有出现任何结果,也没有出现“Ok”的字样。为什么呢?仔细观察一下,会发现这句话是以一个数字开头的。以数字开头,后面跟一个空格(或者多个空格),再跟随语句,这是在告诉 GWBASIC,把这条语句保存为以开头这个数字为行号的语句。这个说起来有些拗口,也不是很好理解。 行号,指的是什么呢?就是一行语句的代号。在一个 GWBASIC 程序里,每条语句都有一个行号。同一行号只能有一句语句。在程序执行的时候,GWBASIC 根据行号从小到大来执行程序。行号与行号之间不必要紧接。比如说,一个程序有两条语句,它们的行号分别是 10 与 20,那么 GWBASIC 在执行它的时候就会先执行行号为 10 的语句,然后再执行行号为 20 的语句。 而输入“10 print "Hello! Welcome to the first program."”并回车以后,GWBASIC 则自动地将它记录为程序的行号为 10 的语句。你可能会有疑问:在输入这句语句之前有没有程序的语句呢?我可以跟你说:没有。在你刚刚运行 GWBASIC 的时候,没有任何一个程序在里面,所以是空的。你加进去的这个行号为 10 的语句是第一个程序语句。 同样的道理,你可以接着打 20 再打行号为 20 的语句。为什么要打 10、20 而不是 1、2 这个道理以后再讲。我先要讲的是,如果你接着打的语句的行号不是 20,而是 10,那会怎么样?因为一个行号只能放一条语句,因此结果就是把你原来打进去的行号为 10 的语句擦掉。 好,现在我们看看如何查看我们已经输入的程序。要查看已经输入的程序,方法很简单,只要输入“list”命令并回车就可以了。
10 print "Hello! Welcome to the first program."
list 10 print "Hello! Welcome to the first program." Ok 接下来,我要说明一个问题。通常人们写 BASIC 程序的时候,行号都是十的倍数。这是因为有时候要修改程序,想在程序中插入一些语句。这个时候如果行号都是紧挨着的,就无法在当中插入了。所以为了方便修改程序,行号都是 10 的倍数。另外,写 BASIC 程序有一定的特殊性:最好事先打打草稿,因为行号与 BASIC 程序的关系非常密切。还有,就算一开始行号都是 10 的倍数,但是经过修改,还有可能有一些行会紧挨在一起。这个时候,可以使用 GWBASIC 提供的一个命令“renum”。使用这个命令,可以让程序重新以 10 的倍数编号。但要注意,renum 执行过后,行号就变掉了,所以最好再 list 一下看看变成什么样了,心里有点数。 现在先来看看我们的第一句程序运行的结果。输入“run"命令并回车,应该出现如下的结果:
run
Hello! Welcome to the first program. Ok 这第一句话很简单。它的功能就是输出一行字“Hello! Welcome to the first program.”。 继续输入以下语句:
20 input "Please enter your name: ", username$
30 if username$ <> "" then 60 40 print "You didn't enter your name. Please try again." 50 goto 20 60 print "Hello, "; username$; "! You are my guest." 运行一下。此时这个程序并不是一运行就退出,而是等在“Please enter your name:”这个提示信息这里。你可以输入“John”,或者你自己的英文名字并回车。如果输入“John”,那么运行结果如下:
run
Hello! Welcome to the first program. Please enter your name: John Hello, John! You are my guest. Ok 再运行一次,试试看不输入任何东西就直接回车。结果会如何呢?
run
Hello! Welcome to the first program. Please enter your name: You didn't enter your name. Please try again. Please enter your name: 此时你会看到,它发现你没有输入名字,就显示“You didn't enter your name. Please try again.”,然后再次提示你输入名字。 如果只输入一个逗号,再回车,你可以试试看。结果并未出现“You didn't enter your name”这样的信息,而是出现了“?Redo from start”这样的信息。
Please enter your name: ,
?Redo from start Please enter your name: 这是为什么呢?我们的程序里面并没有写过这样一句话。其实这个信息是 GWBASIC 显示的,而没有经过我们程序的处理。之所以 GWBASIC 会显示这样一个信息,是因为我们在程序里给 GWBASIC 一个 input 命令。而 input 命令的格式是可以接受多个值的,每个值之前用逗号分开,即形如“值1,值2,值3”这样的形式,或者“John,May,Tom”这样的形式。我们光输入一个逗号,等于输入了一个分隔符,表示我们给 GWBASIC 两个输入值(因为如果只有一个输入值的话是没有必要用逗号分隔的),但程序里的 input 语句只有一个目标 username$,所以 GWBASIC 认为我们输入得太多了,显示“?Redo from start”,让我们重新来一次。 现在我来详细解释一下这段程序。 10 print "Hello! Welcome to the first program." 20 input "Please enter your name: ", username$ 30 if username$ <> "" then 60 40 print "You didn't enter your name. Please try again." 50 goto 20 60 print "Hello, "; username$; "! You are my guest."
为了进一步说明变量的作用,我举一个例子:在 GWBASIC 中,你可以打这样一个语句: let username$ = "Lincoln Yu" 这个时候,如果再打“print username$”,结果就与 print "Lincoln Yu" 执行的结果一样了。 变量的用处有很多,因为它是用来保存程序中间数据的手段之一。程序在运行时可能有多个中间数据,用变量来保存是很合适的。
所谓字符串,就是一连串的字符。比如在双引号里打 Jack,就成了 "Jack",这就是一个字符串,它里面有 4 个字符。而空字符串,就是字符串里没有字符,字符数为 0。BASIC 语言里用字符串可以表示文字。如果不加双引号,就不是字符串,而叫变量名。比如直接打 Jack 就表示以 Jack 为名称的变量。
程序的流程刚才演示的程序,已经有些不简单了。它不是一顺边地往下走,走到底就结束。它会在代码中转来转去。其中 if ... then 60 和 goto 20 这两句就会让它转到行号为 60 的地方或者行号为 20 的地方。 这种跳转,让程序不再是一顺边向下走,我们于是就把整个程序流转的规则称为“流程”。 流程隐含在程序里面,它起着指导程序的走向的作用。由 if ... then 语句表示的跳转,叫做有条件跳转,因为只有在 if 语句的条件满足时,它才会跳转。由 goto 语句表示的跳转,叫做无条件跳转,因为它不需要满足任何条件,只要代码执行到这句 goto 语句,就必须跳转。 为了理解流程,在阅读代码的时候,你可以假想有一个指针,指着现在执行到哪一行。当程序一开始运行的时候,指针指向行号最小的语句开始执行。之后,可能一顺边向下执行,可能遇到 if 语句而要跳转,可能遇到 goto 语句而必须跳转,等等。当最后一行执行完后,程序就结束了。 正因为 if 语句的判断条件中往往有变量,因此变量在程序中起的作用不仅仅是参与计算,也能对跳转产生决定作用。 比如前面那个程序中,username$ 这个变量就起着决定跳转的功能。当它是空串(即 "")时,程序不跳转;否则就跳转。 同时,变量还可能受外部条件影响。所谓外部条件,就是指除了程序本身以外的条件,比如用户的输入等等。前面程序中的 username$ 就是通过用户输入而被赋值的。在这种情况下,由于输入有多种可能性,因此必须考虑到这多种可能性,才能读懂整个程序。 现在,你可以用我写的这些知识来读前面那个程序。读下来的理解大致就是:先显示欢迎辞。然后提示用户输入名字。输入的名字如果不是空串,那么就显示“你好,某某!你是我的客人。”然后结束。如果输入的名字是空串,那么就显示“你没有输入你的名字。请再试一次。”,再让用户重新输入名字,直到输入的名字不是空串为止。 通过对流程和变量的运用,我们可以写出更复杂一些的程序。其中一个就是计算质数。
10 A = 2
以上是一个简单的计算质数的程序。我们分类来看这些语句。
现在我们来讲这个计算质数的算法。A 和 B 都是从 2 开始。A 从 2 开始是因为最小的质数是 2。B 从 2 开始是为了寻找不能整除 A 的除 1 以外的最小整数——因为要除掉 1 所以从 2 开始。我们测试所有比 A 小的 B,并判断是否有 B 可以整除 A。(注:除法里面,A 除 B 是 B/A,A 除以 B 是 A/B,提请注意。)如果有 B 可以整除 A,那么说明 A 不是质数,否则如果比 A 小的除 1 以外的整数没有可以整除 A 的,那就说明 A 是质数。 从程序来看,在 B 的那个循环(从行号 40 到 70)里面,一次次判断,如果遇到能整除的情况,就跳转到 90,跳过了 PRINT 语句,就不会显示 A。否则,如果通过 40 的判断,遇到 B 大于等于 A 的情况正常退出循环,则显示 A。 计算质数的程序有好多种。这里介绍的这个是比较慢的。可以试着把 一般程序的流程常见的可以归为三种:顺序、分支(即如果 A,那么 B,否则 C)和循环。为了提倡使用这三种标准的流程,就有了结构化编程。结构化编程中,除了跳出多层循环或跳过一大块代码(用于错误处理)之外,一般不用 GOTO 语句,而使用结构化的分支、循环语句。GWBASIC 没有提供完全结构化的这种语句,而 QBASIC 中,有相应的结构化语句:IF ... THEN ... ELSE ... END IF、DO WHILE ... ... LOOP。尽管如此,GWBASIC 仍旧能够实现等效的功能。下面我们来看一下 GWBASIC 中的流程控制语句。 IF 语句:
一般用法:
IF 条件 THEN 行号 当条件成立时跳转到行号。否则继续往下执行。
举例:
IF 1 = 1 THEN 20 如果 1 = 1 则跳转到 20
较完整的用法:
IF 条件 THEN 行号或语句 ELSE 行号或语句 当条件成立时,如果 THEN 后面是行号,则跳转到 THEN 后面的行号,如果 THEN 后面是语句,则执行 THEN 后面的语句。当条件不成立时则去执行 ELSE 的内容。
注意 THEN 和 ELSE 后面的语句可以是带冒号的多个语句的组合。
举例:
IF 1 < 2 THEN PRINT "yes" : PRINT "right" ELSE PRINT "no" 如果 1 < 2 则显示 yes 回车,再显示 right 再回车(PRINT 默认是带回车的)。
GOTO 语句:
GOTO 行号 直接跳转到行号。即:无条件跳转。
PRINT 语句:
PRINT 内容列表 内容列表包括:基本内置数据类型的数据(整数、整数变量、字符串、字符串变量、浮点数、浮点数变量)、逗号、分号。其中逗号和分号可以用来连接各个数据,如 PRINT "Hello", 1, 2 等等。分号连接的时候,除了数字之外数据之间是不加空格的,在数字后面则会加一个空格。逗号连接的时候,是对齐到一定的列数再输出的,一般是对齐到下一个 14 个字符的起始位置,即第 1、15、29、43、57 列等位置(71 列不用,太短了)。注意如果 PRINT 语句的最后一项不是逗号也不是分号的话,就会回车,即 PRINT "A" 再 PRINT "B",则这 A 和 B 分两行显示;如果加上一个分号或逗号,就会依着分号或逗号的连接方式继续显示下去。
赋值语句:
目标变量 = 表达式 目标变量是赋值的目标,如 A = 1 则将 A 中的值设为 1。表达式可以看作一个算式,在赋值时表示将运算的结果给左边的变量,比如 1 * 2 就是一个表达式。它的结果是 2。在赋值时,右边的结果没有算出来之前变量的值是不会改变的,因此在 A 为 0 的情况下执行 A = A + 1,在计算表达式 A + 1 的时候,A 是不会被改变的,因此它的结果是 A + 1 = 0 + 1 = 1,然后 A 才会被赋以 1 这个值。
第二个质数计算程序我们前面的质数计算程序的速度比较慢,或者说效率比较差,此时我们可以考虑修改它,以得到第二个质数计算程序。在我们做这个修改之前,先加一些语句显示程序运行的时间,以便我们观察。
10 T0 = TIMER
这样,在程序运行完毕之后,就会显示一个时间值。见下图的 .21875。这个就是程序运行的秒数(精确到 0.06 秒左右)。
953 , 967 , 971 , 977 , 983 , 991 , 997 , .21875
然后我们来加一个改进:本来 B 循环到 A - 1 之后才结束,现在我们让它循环到根号 A 就结束。这样做的原因是:如果 A 不是质数,而 B 是它的因数,那么 A / B 这个数也是 A 的因数,而 B 和 A / B 之间必有一个是小于等于 A 的平方根的。反之,如果 B 循环到 A 的平方根都没有显示 A 不是质数,那么 A 就肯定是质数。
10 T0 = TIMER
见上面的程序,我们引入了 SQRTA 这个变量,用它来保存 INT(SQR(A + 0.00001)) 的值。其中 SQR 函数是用于计算 A 的平方根的。INT 函数是用于计算向下取整的。为了避免浮点数运算的不精确性带来问题,加上 0.00001。因为,假如我们计算 SQR(A) 的结果是 1.999999,那么此时加上 0.00001 以后就变成了 2.000009,向下取整就成了 2。一般单精度浮点数的精确位数有七位。其实 0.00001 在此处更可以用 0.5 取代,这样就不需要知道单精度浮点数有多少精确位数的知识了。之所以可以用 0.5,是因为对 2 以上的 A 值,它的平方根的向下取整结果一定小于或等于 A - 1。 40 SQRTA = INT(SQR(A) + 0.5) B 的那个循环条件也变了,变成了 B < SQRTA。另外,我们把计算 1000 以内的质数变成了计算 10000 以内的质数,以便更明显地看到程序速度的提高。 第三个质数计算程序再进一步来想想,能不能利用已有的质数来筛选新的质数呢?这样就可以少访问许多合数了。比如,如果有一个合数 C 能整除 A,那么 C 的任意一个质因数都比 C 小,只要拿这个质因数来测试 A 即可。因此合数完全可以不必用来测试了。 为了要记录已产生的质数,我们需要一个数组。幸运的是,BASIC 是支持数组的。使用数组时,先用 DIM 语句定义数组的元素个数。DIM PRIMES(2000) 表示定义名为 PRIMES 的数组,长度为 2000。此时 PRIMES 就是数组名而不是一般的变量名了。 1 到 10000 之间的质数个数的确是不超过 2000 个的,应该是 1229 个,因此我们现在就定数组的长度为 2000。此外,为了速度考虑,我们还是要把小于或等于根号 A 这个条件用在程序里。我们还使用 PRMTOP 这个变量来表示 PRIMES 数组下一个能放进去的位置是多少;一开始是 0,注意 BASIC 里面 DIM 出来的数组,下标从 0 开始,一直到定义时指定的数字如 2000,所有这些下标都是可以用的。即 PRIMES(0) 是第 1 个变量,PRIMES(2000) 是第 2001 个变量。
10 T0 = TIMER
这个程序是迄今为止我们写出来的最快的程序。可惜非常大的大数,比如 15 位的数,要分解这样的数的质因数,用这个程序还是不行,太慢太慢了…… 编程初步就讲到这里,有什么不懂的问题,请问老师并参考相关书籍。 |