论道-计算机语言的前世今生

2021-05-19 09:29

阅读:620

不知道是第几次困惑于C++的复杂性。不错,相比于其他语言,C++是很不好理解,而且一不小心,开发效率极慢。原因是?内存管理?C语言很容易就精通;面向对象?Java、Python等上手快开发速度也快;template,Haskell的泛型更好;C++的主要组成部分,分开来学习,都不复杂,但是一旦强行将它们拧在一起,自然就造成莫名的喜感。特别是,在提供高抽象的同时,还要费神劳力照顾内存,就很容易精神分裂。同时,C++完全灵活开放的语法组合,多姿多彩无穷无尽的选择,让人无所适从,很容易就迷失在茴香豆茴字的考究上,既要性能,又要类型安全,还要弹性(静态及动态),最后只好放弃开发效率。

几十年下来,C++日渐式微,做了艰巨的努力,却只换来如此吃力不讨好的惨淡收场,语言越来越复杂,开发效率越来越让人不满意。平心而论,C++上面在C上面做的任何一点改进,都有存在的价值,更何况改动是如此巨大,但是有时候反而很多人愿意选择C而非C++,而且开发效率甚至可能比C++还快,代码也更好维护。不管怎么样,让我们来考察一下C++在C上面所做的改进,这种改进是实实在在的,确实是带来了新的思路。一切还是要先从机器语言上开始说起,这是整个故事发展的起源。

大家都知道,上古时代,猿猴用机器代码写程序,而且好像还是01二进制编码的表现方式。这些01代码精确表达了计算机CPU所要做的每一个动作(每一条指令)从内存的某个地址中读数到寄存器中,寄存器中又再做那些事情,动作完了之后,又将运算结果送回内存,程序计数器跳转到那个位置,所有这些,每一个细微的动作(指令),都在01代码中体现得淋漓尽致。至于cpu怎么执行指令,可以参考《编码的奥秘》,很好的计算机硬件入门书。今时今日,最终CPU面临着的程序其实也是01的形式,只不过是猿猴语言已经跟这种形式,实在扯不上什么关系。这里旧事重提,想表达的意思是,不管猿语多高级,最终落实到CPU上执行的时候,必须还是精确无比的01代码,实实在在表达了CPU每一步的动作,半点含糊不得,没有一丝丝歧义的地方。表面上面向对象的代码,好像很智能,GC也多体贴,包括函数式程序,多么不食人间烟火的Monad,最终落实到CPU的时候,还不是要老老实实地一步一个脚印地一个不落地执行每一个细微动作。

无疑,用01写代码太无趣了。没多久,猿猴马上就改进了策略,引入助记符。也即是针对CPU上每一个指令的编号,就用英文单词或缩写来代替,至于立即数,也可以是十进制数。这也就是汇编语言了。这些汇编语言的程序代码,要执行时,先用一个简单的翻译器转换为机器代码再执行。翻译器真的很简单,不过就是通过查表法,将助记符替换为01代码,外加将十进制数字转换为二进制,翻译器用机器代码写的,因为只能用机器代码来实现。相比于原来的机器代码,翻译器实在并没有做多余的事情,意思还是那些意思,指令步骤还是那些指令步骤,一个都不能少,汇编代码与机器代码一一对应。翻译器的出现,虽然原理简单工作简单,但是减轻了猿猴记忆上的工作量,简直极大解放生产力。猿猴用汇编语言愉快地生产代码,不知道过了几多春秋寒暑。

接着就是C语言的伟大时代,面向过程,结构化编程。通过顺序执行,条件语句,循环语句,函数调用这些语法要素;又提供基本类型,数组,允许用户自定义结构体;与此同时,又保留了直接操作内存的手段,也就是指针。有了这些玩意,猿猴终于可以开始用人话与机器对话了,表达的粒度更大了。虽然相比于汇编语言与机器语言直接的一一对应,C语言与汇编语言的对应关系没那么明显,自然,这种不明显,意味着编译器要做更多的工作,也意味着一份C语言的代码,其对应的汇编语言要长了很多。但是,C语言与汇编语言,某种意义上,依然是一一对应,只不过粒度更大,以语句为单位,而不是指令。一条C语言的语句,意味着好几条汇编语言。稍加训练,猿猴马上就可以在大脑中建立这种对应关系,看到一段C语言代码,马上就可以条件反射这段代码在机器中的内存布局,以及CPU将如何执行这些C语言代码。

显然,C语言代码,某种程度上讲,具备所见即所得的特点。这种特点,说好听,就是C语言代码很清晰,很精确地表达了机器的全部动作,让猿猴有掌控一切了如指掌的虚幻快感。说句不好听就是,C语言本质上还是面向机器的语言,别看它有点像是说人话的意思。猿猴不能在其上玩稍微高级一点的动作,必须清清楚楚地写明每一步的操作,就算是重复类似的工作,如果函数没法提炼,那就没戏了。当然,可以用预处理来一定程度上减轻低抽象的痛苦。在C里面,每一个名字就代表了一个确切的意思,或者是唯一的函数或者是唯一的变量又或者是唯一的宏,不可能有那么一点智能的意思,没有函数重载没有函数重写等玩意。因此,C语言中,代码还没写多久,马上就要面临给函数起名字的纠结。

C语言编译器内部相比于简单的汇编翻译器,自然要做更多的事情。但是,它的内核还是很机械,很无趣,不做哪怕一点点多余的事情,相比于汇编语言,不过就是可以表达的粒度大了一点点,一点都不智能。不智能的意思是,它没有函数重载,没有函数重写,数据类型所蕴含的无穷潜力,在C这里仅仅用于定义内存布局,简单的类型检查。猿猴在C语言里面难以用类型玩出啥新奇的花样。代码复用的手段只有函数以及预处理,不过就是将连续操作的一段语句包装成更大粒度的东西。由于缺乏高级的抽象机制,用C语言实在没办法搞应用框架这种高大上的玩意,实在是语法很不友好,抽象粒度太细,抽象手段太单一。C语言这种语法简单内涵单薄的猿语,只有lognjmp和达夫设备还算有点点小惊喜,要精通还不是就手到擒来,再容易不过。

只有深刻感受到C语言的局限,功能残缺,才能对C++的各种改进有一点点感觉。啰里啰嗦铺垫了一大堆,好不容易终于轮到大C++粉墨登场了,只可惜本文的篇幅过长,这就打住。

表面上看,C++不过是比C多了很多语法糖,当然,每一条语法糖,都代表一种新的抽象手法,表示写优雅的代码又多了一种选择。

比如说,析构函数,用以当对象的生命周期结束时将被调用。析构函数的调用时间与函数调用的即时调用就很不一样,应该可以感受出来这种时间差异区别的明显。C语言中没有任何办法做析构函数这样延后执行的手段,除了手工显式的在作用域结束之前调用函数,就别无他法了。而大C++就大不一样,只要对象存在析构函数,只要定义对象变量,只要变量要死了,其析构函数就被调用。而且,当有多个不同类型变量聚在一起,各自都有析构函数,编译器就会很体贴的按照栈式顺序执行这些对象的析构函数,这些函数调用的动作,从代码字面上看不出来,但是C++的语义就规定了这样一系列的动作必须发生。而C语言的话,就要求猿猴明确地调用这些析构函数,而且栈式顺序也续保持一致。这么说,是否可以感受到C++编译器相对于C语言更智能一些了。C语言的所见即所得的意思,就是一切要做的事情,猿猴必须给我一一写出来,否则,本编译器绝不多做一点点多余的事情。重复自动化的明确工作,机器来做远比人类可靠,因为机器是不会错的。就拿析构函数调用这件事情来说,猿猴可能就漏了调用代码,又或者调用顺序就错了。

又比如说,虚函数,虚函数表,就可以将多个不同的函数打包在一起,这样子,在模板方法中的几个关键点上,同样的虚函数名称调用下,子类就各自做不同的动作系列。当然,虚函数不仅仅用于模板方法。虚函数相比于函数调用,又是全新的抽象手段。对于面向对象语言来说很自然的语法,C语法要费老大劲才能达到同样的效果,定义虚函数表结构,创建虚函数表变量,初始化虚函数表内容,每次创建对象时,设置正确的虚函数表指针。然后,调用虚函数时,还要通过索引找到虚函数指针,再将对象地址还有其他参数传递给虚函数。鉴于虚函数的使用,对于C语言来说,这么麻烦,猿猴一般都不会在代码中轻易使用。猿猴每次使用虚函数,C++编译器就暗地里要做这么多的事情。而程序字面上,代码上的虚函数调用也不再能够明确指明其调用的具体是哪一个函数。

C++的构造函数、函数重载、操作符重载、隐式类型转换、异常等等,那个不是为了让编译器多做一点事情,自动化产生代码。如果你承认机器的自动化生产代码就是好,就是妙,更何况这种自动化生产代码行为,全在猿猴的掌控之中。那应该可以欣然接受C++要比C不要好太多的结论。但是,具体到代码的二进制复用以及内存布局上,却一塌糊涂,特别是多继承,孰优孰劣,有点不好取舍了。

如果仅仅是这样,那也没什么,相比于C,C++只是多了一些语法糖,多了一些抽象手段,在输出优雅代码时多了一些更好的选择,而且由于C++的自由随意,这些语法糖可以随心所欲组合,搞出来很多花样,但是充其量,还是停留在与CPU对话的猿语层面。就算只是这样,其实已经有很多猿猴开始跟随不上C++的脚步了,很多猿猴,就算是class这个概念,也玩不好。可是后来,C++搞出了template这个关键字,而且还图灵完备。而template又可以横跨切入到面向对象(多继承),预处理,内存操作,全局对象等,这些家伙,本来每一个都不是省油的灯,就给人带来了很大的困惑。这种困惑,虽然反映到代码上的理解比较吃力,但是,更麻烦的是,猿猴摸不准template对整个语言抽象度的提升究竟作用有多大,而答案是,这种提升非常大,以至于C++的代码表达能力,几乎无所不能。(待续)

 

转自知乎文章:恒虚之境


评论


亲,登录后才可以留言!