自学编程,从此开始

上第2学堂,听有趣的编程课

课文: 《从机器指令到高级语言》 (点击查看完整内容:视频+评测+讨论+……)

作者:第2学堂

知其然,知其所以然。学习C++,应该懂它的“三观”。

课文题图

 

第4节 从机器指令到高级语言

1 什么是计算机程序?

计算机程序是一组指令(及指令参数)的组合,这组指令依据既定的逻辑控制计算机的运行。

1.1 什么是指令

让我们来想象一个游戏。

游戏中有两个人,其中一个双眼用布蒙上。另一个人是你。你不哑,他不聋。场地中混乱地摆上许多啤酒瓶(称为“雷区”)。游戏任务是由你发号施令,指挥被蒙眼者从场地的一端穿行到另一端,并且不能碰倒啤酒瓶。

现在,你就会明白什么叫“指令”。指令就是一套符号,这套符号的含义,你懂,他也懂。

你会根据现场情况,向他发出类似这样的指令:“向前2.5步、向左1步,向后1.5步,向左0.5步,向前4步,停!”。

“向前”,这就是指令。“2.5步”,这就是指令所需要的参数。在不需要具体区分时,我们也往往将“指令和指令的参数”有时通称为“指令”,有时通称为“数据”。

不同的处理器往往会有自己的一套指令(称为指令集)。如果把锤子当作一个处理器,它的指令应该是“锤”。剪刀则是“剪”。换成汽车呢?如果你是初学者,正好,你的师傅坐在副座上,你就有幸听到相对复杂的指令了:“左转!右右右!踩离合!油门!减档!刹车!停!!滚!!!”。最后一个指令很明确不属于汽车的指令集。

1.2 指令兼容

对于计算机而言,不同的“处理器”类型——不同产家生产的处理器,甚至同一产家生产的不同版本的处理器,往往都会有不同的指令集合。为了商业利益,有些厂家间就会进行“联衡”,相互之间保持尽量大的兼容,当然也各有留了一手特定指令。典型的如Intel和AMD两家CPU产商。当然也会因为厂商策略,产品定位等不同,而无法实现兼容的指令集合,比如当前智能手机使用的的ARM-CPU,就和桌面PC机CPU的指令不兼容。

我们学习C++的推荐环境,是桌面PC,包括笔记本,所采用的CPU基本使用Intel或AMD等厂商的生产的CPU,这一类指令集称为“80x86 CPU 汇编指令”。

1.3 程序=指令的逻辑组合

这是我们给出的第二个回答“计算机程序是什么”的表达:“计算机程序是一组指令(与其所需的参数),这组指令依据既定的逻辑控制计算机的运行。”

在这个定义中,有三个重要的概念。其中,我们谈到了程序中的“指令”,但是我们还没有谈到“组合”及“逻辑”。

继续前面的“雷区安全穿越”游戏。

理论上,如果场地不变、酒瓶摆放位置不变、参与人不变,那么作为指挥者,你完全可以把第一次的指挥过程记录在案,形成一套“指令的组合”。

img

看,这就是程序!一组共六个的指令(及其所需数据)的组合。六步之间的组合逻辑又是什么?就是要帮助你成功地走出“雷区”的逻辑。这个最终目标,通常我们称为“业务需求”。

指令组合的结构又是什么呢?首先,指令和指令之间有次序关系,游戏者必须先执行完第1步,再做第2步……乱着来这个程序就完全失效了。从本例上看,就是要顺序执行,但有时会碰上更复杂的雷区,就有可能用上更复杂的结构,比如“重复”。发指令者会在某一种这么说“请重复前面两步三次”。在计算机程序中它叫“循环结构”。

在以后的学习过程中,很多时候我们认为程序就是指令;同样很多时候,我们会觉得程序就是逻辑。

1.4 从程序到软件

还有很多的时候,我们也不区分“程序”和“软件”二者。也许前者更趋于抽象,而后者趋于具体。比如我们在写那些表达我们的思想逻辑时,我们喜欢说“写程序”;而当程序完成,可以待价而沽时,我们称它为软件产品。

2. 什么是编程语言

程序是按照一定逻辑组合的一组指令。游戏中双方使用的是自然语言交流指令。如果游戏双方是聋哑人,那么用嘴巴说的那套指令就玩不转了——计算机聋不聋不好说,但当我们要对计算机下达指令,人类这一套得天独厚有悠久历史的自然语言,玩不转了,怎么办呢?

解决这一问题所要做的第一件事就是:制定“机器语言”——机器有了语言,我们就可以和它亲切地交流……

“等等!”突然有个同学没举手就站起来要求发言:“机器,没有生命的东西!小猫小狗有语言倒可以接受,机器也有语言,还要我们去学习,这亵渎我作为人类的尊严!我要退学!”。

得解开这个结解,不然自尊心强的同学心生学习障碍。

首先,语言其实不仅仅代表有声音的内容。英语、汉语等,说出来是口头语言,写在纸上是文字语言。也都有对应的哑语,一组用手势来表达的语言符号。

对应到编程这个目的,我们是需要一组用来表达计算机指令的符号,有了符号,我们就可以组合它们,以表达我们人类思维的种种逻辑,当然,组合并不是完全自由的,需要遵循特定的语法。不直接使用人类的语言,不是人类的语言太贫乏,而是因为人类语言太丰富、特别是人类语言的符号太多、语法太复杂,导致笨笨的机器无法理解。所以我们必须同样通过人类的头脑,来制定一套相对简单语言作为机器语言。

原来机器语言也是人类制定的,并且还比较简单,这样一来自尊心的问题解决了,但新的问题又来了:“既然比较简单,为什么还需要学习呢?并且听说还挺难学的?”。

原因有很多,排在第一个的是:这里的“简单”是专门为机器定制的,适合机器阅读理解的语言,对人类来说反倒很难。比如,一开始科学家设计计算机时,都是按人类的习惯采用“十进制数”来表达数据,但要设计能够理解十进制数的的电路逻辑很难搞,后来“老冯”提出了二进制。那么我们来对比一下:十进数的789和大小一致的二进制数1100010101,如果你需要理解后面那个数,是不是需要好好学习?

再者,学习编程并不是仅仅学习编程语言自身,我们往往希望通过编程来让计算机帮助人类解决某些复杂问题。编程语言再怎么复杂,要搞懂它的语法可能一年就够了,相比学习汉语或英语确实还算简单的——但是,如何用简单的工具去解决复杂的问题,这个过程本身是复杂的,是需要学习的。

结论是计算机语言必须学习,我们就从最原始的机器语言开始。

2.1 机器语言

机器语言要简单什么程度,才能让计算机“一看”就明白呢?机器又是怎么“看”它的语言呢?

直至今天我们所用的计算机都叫做“电子计算机”,因为它们的重要组成基础,是电子元器件。以我超级贫溃的电子知识,我能讲出常见的电子元器件有“电阻”、“电容”;其中的电阻,我记得中学物理课上学习过:“当电压一定时,通过导体的电流与它的电阻成反比”。就这些了。

头好大!又有同学夹起书要走了,别急,学习编程并不太需要什么复杂的电子电路的知识,不过,同学们今天带算盘了吗?

中国古人发明算盘,算盘分为上下两个区,其中上区的一颗珠表示5,而下区则一颗珠表示1。对于算珠来说,基本状态有两种:“上”和“下”。所以,当我们使用算盘时,一颗珠子拨上拨下表示5或1,身为中国人,我表示这事很简单。但发明电子计算机的先哲们(包括冯·诺依曼同志)他们找到了更简单的状态:那就是适用于电子计算机的基本符号是“通电”或“断电”。

为了国家荣誉,穿越时空的我大胆地向年轻的“小冯”提出质疑:“这样设计不是浪费电子元件的强大的表达能力吗!为什么不考虑用电阻或电压值来表达呢?比如,1伏表示数字1,2伏表示数字2,3伏表示数字3……”

冯同学这么回答:“贵国的老祖宗设计算盘时,为什么不设计一种‘不上不下’的状态呢”?

“好吧,看来虽然科学家是有祖国的,但科学无国界。各位能够从我国老祖宗发明的算盘身上得到启发,并发扬光大,造福全人类,我表示祝福和感谢。好好干!未来世界每个人都会有一台计算机,你们信吗?”

算盘珠子如果存在“不上不下”的状态,就会非常容易搞出错误。想想你家的灯泡,给它220伏它亮着,给它221伏或219伏,它也是差不多亮,你认得出来吗?拿仪表量都会有误差范围呢。所以,想要让电子元件精确表达状态,彻底“断电”和“通电”是最容易做到的。

就这样愉快地决定了,这一刻冯·诺依曼在笑,发明算盘的无名祖宗也在笑,咦,连发明太极的伏羲老祖也咧着嘴,这是为什么呢!

2.2 机器语言的字母

科学家用“0”和“1”,来表达“通电”或“断电”。不过它们仅仅是计算机语言的“基本符号”。英语中有26个字母,单词由26个字母组成,然后再由单词组成语句。我们也可以机器语言有,并且仅有两个字母,那就是“通电”和“断电”,为了表达更简捷一点,我们今后就说成是“0”和“1”(至于0是表示通电还是断电,我们不去关心了)。

哈哈,没想到吧,刚才谁吓唬俺们说机器语言不好学?我看它至少比英语容易13倍。让我们现学现用一下。你有男/女朋友吗?首先你们碰个面,一起约定一些“机器指令”的表示方法,比如:

0000 :你;0001 :我;0010 :老的;0011 :地方、场所;0100 :相见;0101 :想念;0111 :很、非常、那是相当的;1000 :今天;1001 :晚上;1011 :七点钟;1111 : 亲爱的。

今后,你们可以用自定义的计算机机器语言来交流了。比如,这是一封信:

“1111 0001 0111 0101 0000 1000 1001 1011 0010 0011 0100”

2.3 二进制(基础)

因为长10个指头,所以人类采用十进制。因为电子元件有两个稳定状态(通电、断电),所以电子计算机采用二进制。

十进制组合0到9十个数字以表达所有的数字,二进制使用“0”和“1”就可以表达所有数字。算术老师说 “逢十进一” ,所以我们知道9+1=10。二进制的世界中,0+1=1,然后再往加1,即1+1=10。由此可知,十进制中的“2”,在二进制中里必须写成“10”,并且记得将它读成“壹零”。

用这种方法,就可以用0和1来表达所有数字。事实上计算机中所有数据最终都是由0和1表达:一首MP3,一张相片,一封电子邮件,一个应用程序,全是用0和1表达的。所以如果有一大串1和0,到底它是表达什么呢?这和这个数据的上下文有关。这和我们的现实世界是一个道理,比如“250610”这串数值,可以解释成“二十五万零六百一十”,也可能是山东省某地的邮政编码,甚至可以认为它是一段简谱。

那么,用只有0和1这两个“字母”的机器语言来编程的话,当然也就是满纸的0和1了——最早的程序确实是写在纸上的,只不过比我们想象的要有趣:纸是长长的纸带,而0或1则用画圈圈表示,一圈两圈三圈,有圈的地方估计是表示1?然后交给负责打孔的助手,有圈的地方打上一个孔。当程序需要被执行时,就将纸带塞给机器。机器要实现读懂这些孔,确实不复杂,比如可以用光照检测,透光表示1,不透表示0,就这样电子线路读懂了0和1。

2.4 汇编语言

还记得我们用二进制写的那份情书吗?

“1111 0001 0111 0101 0000 1000 1001 1011 0010 0011 0100”。

我保守认为用这样的语言写情书,于增进双方爱意方面帮助不大,倒是有利内容保密和提高人类记忆力。

纯粹的机器语言实在太难记难识,先哲们立即想到要为它们制定一些助记符。因为计算机是西方人发明的(并不是唯一原因),所以助记符就是一些简短英文字母组合,这些助记符及相应的语法规则,就称为“汇编语言”。

比如,我们要实现这样一段功能:已知b等于1;c等于2;计算b + c值,并将该值赋给a 。用二进制机器语言来表达,基本是如下内容:

0001010  01010101  11000100
00000011  01010101  11000000
10001001  01010101  11001000

换成汇编语言记录,则如下:

mov edx,[ebp-0x3c]
add edx,[ebp-0x40]
mov [ebp-0x38],edx

汇编语言比机器语言稍稍“人性化”了一点,但仍然不好记忆,二者没有本质的区别,仅是一种简单的翻译,很多时候,我们把汇编语言与机器语言等同视之。

视频中演示的网站,可实现将C++代码编译为汇编或机器语言,请尝试:https://gcc.godbolt.org

2.5 高级编程语言

机器语言(或汇编语言,下同)对人难读、难写、难记,对机器却易读、易处理,运行效率高,占用内存少——而当时计算机的存储器昂贵,处理器功能有限,因此使用机器语言基本是必选项。幸好那时候的程序员,基本就是我们口中的科学家叔叔阿姨们,他们坚持下来了,坚持用机器语言干了好些事,其中很重要的一件事是什么呢?。

有了机器语言,硬件性能也慢慢在长进,先哲们开始正儿八经考虑人类的感受了。是得有一些“高级”点的语言,让人类写起程序来,不那么累。

这类高级语言当然是要在字面语义及语法方面,都要比机器语言更符合人类的思维习惯。尽管只是往这个方向走出一小步,它们再不是“机器语言”了,因为机器读不懂。怎么办?方法也简单,先用机器语言写一个“翻译”程序,负责将高级语言先翻译成机器语言。

负责将高级语言”翻译“成汇编或机器语言的程序,可以称为”翻译器“,或者“编译器”,不管名字如何,计算机史上最原始的第一个翻译程序,本身当然是用汇编语言写的,使用汇编语言本就难,使用汇编语言来写一个将高级语言翻译成汇编语言的程序, 难上加难。当初(50、60年前)那些写这些程序的人,基本上都是大拿,不,简直他们就是科学家。有同学担心了,有一天先哲们都驾鹤西去,那编译器不是没人能改进它了?

别担心,有了第一版编译器,新的程序员大可以用高级语言写一版新的编译器的代码,然后用旧的编译器编译出新的编译器的目标程序,这个过程可以一直继续下去……你有没有一种新的担心?

旧的编译器,编译出新的编译器,这让我想到:机器人会不会根据它当前所掌握的智能,自主制造出比当前的它智能的升级版本,然后新版本机器人学习更多知识,再继续制造一个更智能的版本。终于有一天机器人“聪明”意识到,世界应该由它们来统治?

3.编程思维(人类思维模式在编程上的折射)

“面向过程”和“面向对象”是两种广泛应用的编程思维。并且正好是我们所要学习的C++语言可用上的两个主要思维。

3.1 面向过程

日常生活中要完成一件大事,往往先得先把它分解成多件小事,即:把一个相对大的办事过程,拆分出存在时序关系的多个小过程。

比如:一位家族主妇想做道菜,可以将做菜的事分成:备菜、炒菜、上桌等过程。其中备菜过程,又可以细分出买菜、洗菜、切菜等等。对应到编程语言,通常用“功能函数/function”代表过程,也有的语言更是直接对应到“过程/procedure”,所以面向过程被提前为“PO”,即“Procedure Oriented”。

3.2 面向对象

面向过程的思路很直观,但是当一件事情更庞大、更复杂时,就难以在一开始就有条理地梳理出到底需要多少过程。比如,你被选为总负责人,规划一届奥运会工作,这时理智你要怎么做呢?简单地试图将这件事从大到小分到很细,然后开始推进,成功概率不大啊。

“面向对象”的设计思路是,先考虑这件事情中,需有些对象,这些对象又如何分类。比如当奥运会负责人,你得想到得有负责管钱的,于是要找财务类的人才;得有搞开幕式的,于是要找文艺导演类的人才;得有负责场地的,于是要找建筑类人才;得有负责安全的,于是找安全类的人才;得有管体育的啊,于是找体育类人才……

每一类的人才对外需要提供什么具体功能,你需要去定义,然后再找到符合的人,一个不够找两个或更多,光有人显然也不够,还有要各种物品等等,所有这些人、物都称为“对象”。对象和对象之间当然会有各种关系,谁管理谁?谁配合谁……等一切梳理得差不多了(这里的“差不多”没有贬意,因为你可能真的无法完全理清),事情的舞台就交给这睦“对象”们运转起来……

抽象归纳一下:面对复杂问题时,我们可以先关注问题需要涉及哪些事物,并对事物进行分类,即定义事物应该具有的功能,再梳理这些事物之间的关系。这样的解决问题的思路,就叫做“面向对象”,称为“OO”,即“Object Oriented”。

3.3 二者关系

“面向过程”和“面向对象”两个思路并不冲突,你在分类或梳理关系的过程,自然而然是一个分大类,再分小类的过程,而对象在执行某个很具体的功能时,必然也是一个由大到小化解事情的过程。

4.从C 到 C++

C语言在人与机器两极中,往人这一头迈出非常优雅的一小步。这是C语言自身的一小步,也是编程语言史上的一大步。因为历史机缘,也因为语言自身优秀,C是一众“面向过程”语言中的王者,已经成为许多重要基础软件的主要编程语言,比如操作系统,比如编译器,还比如用它来写其它语言。

"世界上只有1类高级编程语言。"

这是C粉们吹嘘C语言地位的话:世界上只有1类高级编程语言,一类是C语言,还有一类是C语言写成的语言。

在符合“人类”思维这一端,C语言以“面向过程”为思路,同时提供清晰、简单的语法规则。它语法规则直接影响到几个重要的后来者,如:Object C、C++、Java、C#、D语言等等。

在接近“机器”特征这一端——比如当我们需写硬件设备的驱动时——C语言甚至被称为“中级”语言。原因在于它非常优秀地反映了机器,尤其是“内部存储器”的特征。因此它很能够又快又好地被编译成汇编、机器语言。因此它很快在许多机器上,代替了汇编语言,成为操作系统、编译器的首先的编程语言。

尽管我们吹过牛,说许多高级语言都由C语言写成(这当然不全是事实),但这其中与C语言之间最有延续、兼容关系的,当数C++。或许从名字上就可见一斑。最初C++甚至被叫做“C with class”,即“带类的C语言”。这里的“class/类”,就是前面介绍面向对象概念时提出的的“事物分类”的“类”。没错,C++是一门支持“面向对象”思想的编程语言。

编程语言的发展,从“低级”向“高级”不断发展。“低级”指的是“机器”这一端;而“高级”指的是“人类”这一端。这中间有两个非常重要的原因。一个当然是机器的硬件性能越来越好了,撑得起将高级语言转换到机器语言的代价,另一个则是人类寄希望于计算机程序帮助解决的问题,越来越复杂了。问题越复杂,解决问题的逻辑就越复杂。尽管我危言耸听过“机器人要取代人类”,但毕竟那还没有发生,所以解决问题的逻辑还是需要人来写成“指令清单”(程序),所以人类一直改进编程语言向人类自身的思维模式靠近,就很好理解了。

人类需要计算机帮助解决的问题有什么?上到太空飞翔的,下到海底潜伏的,中到你手上拿的手机,还都离不开计算机程序。更典型的如:财务人士希望用软件管账、人事专员希望用软件管档、厂长希望用软件管理生产;老师希望用软件管成绩,还有前面提到的开办奥运会……这些事情可能不需要用到复杂的数学知识,但它们都会涉及许多事物(对象),对象分成许多类型,类型之间、对象之间关系很啰嗦。尽管C语言是写操作系统、写编译器、写其它语言的首选,但在处理这些人间事,需要很高级的程序员,这世上没有这么多高级的程序员,所以只能由高级的程序员写一些高级的编程语言以供不是那么高级的程序员来编程。

很多人说C++语言又难学又难用,我只赞同前半部分,其实C++语言学会以后,至少比C语言易用。

如果说C语言成为IT世界地下100米基础的奠基者,那么C++语言就是IT世界地面所有高楼、公路、桥梁的建筑者。全世界用于写电子文档的主要办公软件,比如微软的Office,或我当前正在写书的金山WPS,或者跨平台的OpenOffice,是用C++写的。写文档如此,播放音乐的软件也基本是C++写成。著名的图像处理软件PhotoShop是C++写的。你上网用的许多浏览器也是、你在电脑上聊天用的QQ或MSN也是……

我不知道您为什么要学习编程,更不知道您为什么要学习C++。不过是时候交待一件事情:选择C++作为我面向编程初学者的第一教学语言,不是我只懂或只擅长C++,而是因为我一直认为,过往五十年和未来至少再十年间,编程一直是一件必须在“人”和“机器”间取得平衡的事,所以找一个有着对应的位置的编程语言起步,是明智的。

我们必然通过C++学习和实践“面向对象”的编程思想。然而,C++是一门集大成者的语言,不仅支持“面向对象”、也支持 “基于对象”、“面向过程”和“泛型”的编程思想。从这一点同样流行的Java或C#相比,C++不算是纯正的“面向对象”的编程语言,但这正是C++所追求的,也是我所推崇的:“你不可能用一种思想,解决所有问题”——宣称用一种思想,就可以解决世间问题的,那不是编程语言,那是……算了,你懂的。

5.诸事诸物当有出处

学习的过程中,人人会犯错,但并非人人都懂得故意犯错。记住,敢于犯错才会学习。

#include <iostream>
using namespace std;
int main()
{
    cout << "你好,C++的世界" << endl;
}

将以上代码的第一行 “#include ” 直接删除,或在行首加上 "//" 转为注释。 再尝试运行(编译),将得到以下编译错误:

error: 'cout' was not declared in this scope。

(英语不好的同学注意了:以上内容中, declared 、scope 这两单词如果不懂,一定请马上就查询字典加以了解,它们是C++编程错误消息的高频词,记下它们性价比很高。)

这错误是在埋怨:亲,cout 是什么鬼?在当前范围内,我找不到它的出处啊!

恢复对的文件包含,改为删除 “using namespace std”一行,上述错误会再次出现。

“namespace”读作“名字空间”。意思就是“名字”起作用的“空间”。比如初中部和高中部都有一个同学叫丁小明,那么在初中部,要提到高中那位丁小明,就得加上前缀:“高中的丁小明”。名字空间是用来预防各类声明中原本很容易发生的重名。“cout”和"endl"在iostream中的声明,其实都位于 “std”名字空间之下( “std”正是英文 单词 “standard / 标准”的意思); 对二者的使用,更加严谨的称呼方法是 “std::cout”和“std::endl”,其中的 “::” 可以理解为“的”字。因此,输出问候那一行,严谨的写法是:

    std::cout << "你好,C++的世界" << std::endl;

此时“std::”类似于符号名称的前缀。写大程序时,通常我们推荐这种严谨的写法,如果不想一直写前缀,就得恢复刚刚我们注释掉“using namespace std”,它相当于告诉大家,后面遇到找不到符号,就假设它们是属于“std”名字空间下的再找找……

本小节更多内容,请查看视频。