社会进行曲——浅谈计算机语言的发展

    技术2022-05-11  20

    社会进行曲——浅谈计算机语言的发展

    一.  原始社会 机器语言

    人类刚刚诞生,社会只是雏形。生产力极度低下,人类却享有最多的自由。

    1946年,冯诺依曼的第一台现代计算机诞生时的情形像极了原始社会。

    “程序员”(我不知道这个名称是否辱没了他们)是精通电子技术的专家,他们通过设计复杂的电路板来完成各种计算工作,而计算机也仅仅用来满足最根本的需要——军事(由此可看出人类的本性)。“程序员”是计算机的主人,他们能操纵所有的硬件资源,指挥那个笨拙的庞然大物完成种种不可思议的任务。

    语言的诞生

    IO的发展促使了语言的诞生。穿孔纸带的产生使程序员不必具有太多的电子技术知识,他们只需要懂得计算机的语言(指令),就能与他们交流。计算机所能听懂的语言是由01组成的一长串奇怪的数字,这就是 机器语言。程序员辛苦的在纸带上打孔,向计算机发号施令。

    让我们推测一下下面这个简单的程序用机器语言来写应该是什么样子(既然是推测,那么大多数的细节都可能是错的,所以我一直认为做一个历史学家应该是个好主意)。

    Void main()

    {

        int I, J, K;  

        I = 1;

        J = 1;

        K = I + J;

    };

    请原谅我将这种古老文字翻译成了中文,因为我想没有人会对它感兴趣的:

    将内存位置为40000的一个机器字(如果是32位计算机的话)置为1

    将内存位置为50000的一个机器字(如果是32位计算机的话)置为1

    将内     将内存位置为4000050000的两个机器字相加,并将结果置给内存位置为60000 的机器字。

    因为程序员可以控制一切硬件资源,所以他们可以指定任意内存位置来使用(只要硬件允许),所以上述的40000等也可以是其他的数字。

    前辈程序员就这样过着自由但简朴的生活。

    社会持续进步,IO继续发展。新型的IO设备,如磁带、键盘等的出现使穿孔纸带被丢进了垃圾堆。但程序员们依然平等、自由。

    原始社会的崩溃

        特权阶级的出现标志着原始社会行将崩溃,贵族就是操作系统。贵族垄断了部分特权,程序员们再不能像以前那样操纵所有的硬件资源了,相当多的资源由操作系统接管,程序员们失去了部分自由,却换来了开发效率的提高。

    让我们再考虑一下,上面那个小程序用机器语言实现应该是什么样子的:

    版本1

    将位置为40000的一个机器字(如果是32位计算机的话)置为1

    将位置为50000的一个机器字(如果是32位计算机的话)置为1

    将位置为4000050000的两个机器字相加,并将结果置给位置为60000的机器字。

    看起来,版本1似乎与以前的实现几乎相同,但实际上,这里的40000等数字已经不是实际的内存位置了,操作系统来负责选用实际的内存位置,也就是说,它将负责将程序(进程)地址映射为 实际的物理地址。

    版本2

    将栈指针(pStack)向下移动12个字节;

    将内存位置为pStack – 12 的一个机器字置为1

    将内存位置为pStack – 8 的一个机器字置为1

    将上述两个位置的机器字相加,并赋给内存位置为pStack – 4的机器字。

    版本2的实现假定硬件中存在栈指针寄存器,幸运的是,我们最熟悉的x86系列CPU满足这一点。

    栈则是非常重要的一个概念,我们将在以后讨论这一点。

    不管你喜欢与否,程序员们最自由的时代过去了。在这个伟大的时代里,每个程序员都通晓一切,他们清楚的知道自己的程序是怎样运行的,很少有迷忙与焦虑,他们是计算机的主人。但这个时代已经一去不复返了,

    机器语言,这个最根本的语言也被丢给了编译程序,程序员们再也不用记忆那些难懂的01字串了!

    奴隶社会  汇编语言

    正因为机器语言十分难记,人们逐渐使用 助记符号来代替01字串,如ADDMOVE等等。他们只是对机器语言的简单的替换,但仅仅这样仍算是一个巨大的进步,汇编的出现大大提高了程序员的开发效率。

    计算机当然看不懂汇编语言,编译程序负责将汇编语句翻译成机器语言。

    奴隶社会后期——宏汇编

    早期的汇编语句与机器语言语句之间是一一对应的关系。但这种机械的平均主义显然成了社会发展的障碍。能者多劳,宏汇编语句一句可能抵的上好几句的机器语言。这种抽象提高了程序员的工作效率,但却使他们与机器越来越远。随着社会的进步,人类正逐步失去自由。

    人的本性是贪婪的,为获得更高的效率,程序员们很愿意付出自由的代价,人们正呼唤着更加抽象的语言,可以屏蔽掉所有的机器细节,社会正酝酿着革命。

      封建社会 高级语言的诞生

    FORTRAN的出现标志着一个新的时代的到来,人们从此得以从复杂的机器细节中抽身,人们再不用管内存、寄存器等琐碎的事情了,这当然是计算机历史上的一次伟大的革命。

    封建社会早期:goto横行的时代

            高级语言出现依始,goto由于他的灵活和高效成了程序员们竞相追捧的工具,有关goto的种种复杂诡异的技巧在程序员之间传诵。Goto甚至成了衡量程序员水平的标尺。

            程序员们虽然失去了控制硬件资源的自由,却在高级语言的使用上不受任何限制。他们可以使用任意的风格,诡异的技巧,写出除他们之外(大多数情况下包括他们)谁也看不懂的程序。

        封建社会后期:结构化程序设计

            当大多数人还沉浸在goto带给他们的自由与荣耀时,不世出的天才人物Edsger Dijkstan(此君还是信号量的发明人,却不幸于数月之前架鹤西去)却敏锐的发现了goto所带来的种种问题。这位荷兰传奇科学家发表了他的著名论文《goto 有害论》,轰动了整个计算机世界。Edsger Dijkstan指出,goto是导致程序复杂、混乱、难以理解的罪魁祸首,它还使效率难以度量,程序难以维护。

            通过众人的努力,人们总结出一套行之有效的程序设计方法,称为结构化程序设计。程序员不能再随心所欲的编码了,goto成了程序员们避之不及的毒药。又一次,人类为了社会的进步,付出了自由的代价。

    高级语言编译浅谈

    在这里,我不想谈编译原理(我也不懂J),我只想根据自己理解,谈几个我认为比较重要的问题(以c/c++语言为例)。

    1.  内存分配策略——栈和堆

    栈和堆本来是不容易混淆的,但怪就怪在有些书上将栈称为“堆栈”,凭空给人们带来许多疑惑。

        说白了,栈和堆都是 进程空间内的一段虚存(将被映射到物理内存,所以也可说是内存中的一段空间),所不同的是,计算机对栈给予了更多支持。

                    还记得栈指针寄存器吗,当程序调用内存时,栈指针将指向栈顶,存贮空间的分配就是通过移动栈指针来完成的(见版本2)。那么,哪些变量将在栈中分配空间呢:

    l   函数参数

    例如:

     int Add(int I, int J)

    {

    int K = I + J;

    return K;

    };

     

    当此函数被调用时(a= Add(1, 2)),栈指针向下移动8个字节,参数 I(1)J(2)将被压栈,当函数返回时,栈指针向上移动,空间自动被释放。

    l   局部非静态变量(自动变量)

    仍以上面函数为例,变量K的存储空间也是在栈中分配的,当此函数被调用时,参数首先被压栈,然后,栈指针继续向下移动4个字节,给K分配空间,IJ相加的结果首先被赋给K,然后,再将K的值赋给调用者的变量a。这时,函数就要退出,栈指针相上移动12个字节,释放掉所有的空间。正因为局部非静态变量变量空间的分配是由编译器在编译期间就确定,所以又被称为自动变量。

    既然栈用起来这么方便,那么要 来干什么呢?显然,如果我们在程序运行之前(编译时)就能知道要申请多少空间,栈就足够了,但在复杂的应用中,人们很难提前知道自己要申请多少空间——例如在空管系统中,只有通过外部信号才能知道究竟有多少架飞机来。

    我们可以把 看成存储空间的仓库,当我们需要存储空间时就从仓库中领取,不使用这段空间时就把它还回去(这是一个相当复杂的算法)。领取和归还的过程是程序执行时决定的(所谓的动态决定),编译时无法确定存储空间的位置。

    c++语言中通过关键字new来动态申请空间(c语言中使用函数malloc),如:

    int * pInt = new int;

             将在堆中分配一个int型数(注意,指针pInt的存储空间在栈中)。 

                  堆中分配的存储空间是在运行时确定的,编译器无法自动的清除掉这些存储空间,所以必须由程序员负责清除,c++中使用关键字deletec中使用函数free),如:

                   delete pInt;

                  由于存储空间是有限的资源,如果我们不及时释放就会带来种种问题,这种错误被称作内存泄漏。

    注意:一些语言如Java中,堆中的空间也能自动释放。实际上Java是通过实现一个“垃圾收集器”在运行期间实现这一点的。

    到这里,栈和堆的问题基本已经谈完了,但既然前面已经谈了局部非静态变量,不妨了解一下静态变量和全局变量。

    如下例:

    int Sum(int I)

    {

        static sum = 0;

        sum += I;

        return sum;

    }

     函数中的变量sum就是静态变量,静态变量的存储空间并不是分配在栈中。它被

        分配在一个特殊的位置,称作静态存储区。我们不需要知道 静态存储区究竟在什么地方,我们只需要明白,静态存储区内分配的存储空间只有在程序退出时才会被释放。所以,函数中的变量sum就具有了记忆功能,如:

    第一次调用此函数时  a = Sum(1);  a = 1;

    第二次调用此函数时  b = Sum(2);  b = 3.

    全局变量就是定义在所有函数之外的变量,它可以被所有的函数使用。当然,它也被存放在 静态存储区。

    2.       编译与链接

    l       声明与定义

    要想区分编译与链接,就先要明白声明与定义的区别。

    Bruce Eckel的经典名著《c++编程思想》2.1节中对此有清楚的解释,我在这里就引用他的讲解。

    “声明”向计算机介绍名字,说明这个名字是什么意思。而“定义”真正为这个名字分配存储空间。

    记住,c/c++中任何变量只能被定义一次!

             所以,在头文件中定义变量不是一个好习惯,因为一旦此头文件被多个其他文件包含,链接期间就会多重定义错误(奇怪的是,在一些老式的c编译器中这样做居然不会产生错误,但无论如何,请不要这样做)。

             关键字extern 表示现在我们只是声明一个变量(函数),但对函数来说extern是可选的,不带函数体的函数自动被看成一个声明。

    l       编译与链接

    编译与链接是两个阶段,编译的单元是文件,也就是说,一次只能编译一个文件,编译的结果是二进制的目标文件。而在链接阶段,各个目标文件被“链接”在一起形成可执行文件(或动态连接库等)。

    编译阶段允许文件中存在 声明过但没有定义的 变量或函数,变量名或函数名被存放在目标文件中,在链接阶段编译器将综合所有的目标文件,找到所有变量和函数的定义。

    例如:

    文件main.c如下:

    void  NotExit(int i);

    void main()

    {

        NotExit(1);

    }

     

    cc main.c 能够通过编译,却会在链接阶段报错:

       NotExitunresolved function.

    假如文件 NoExit.c如下:

      void NotExit(int i)

    {

        return;

    }

     

    cc main.c NotExit.c 就能通过编译和链接。

    同样,倘若存在多个NotExit函数的定义,程序也将不能通过链接。

    但遗憾的是,如果文件NotExit.c中函数的定义如下:

     void NotExit(double i)

         {

        return;

    }

    cc main.c NotExit.c 竟能通过编译和链接(如果cc调用的是c语言编译器的话)。但是,如果是c++编译器,上述程序不能通过链接。

    那么,c++是怎样做到这一点的呢? 这要从函数重载谈起:

    重载使我们能够定义具有相同名称的函数(只要他们的参数不同),猛一看,这将给 链接 阶段带来混乱,如果函数名相同的话,链接器如何知道应该调用哪一个函数呢?答案就在函数名中,c++编译器将改变函数的内部名称!

    举个最简单的例子,

       void  NotExit(int i)在内部将被命名为 NotExit_int;而void NotExit(double i)将被命名为 NotExit_double

       extern “C” 的产生:由于编译器将在内部改变函数的名称,而不幸的是并不存在一个标准来规范这种行为,所以不同的编译器产生的函数名是不同的。所以,如果想要使用其他 c++编译器产生的库,将变得十分困难。为了解决这个问题,人们想出了extern “C”。这个符号表示,要以c编译器的方式产生函数,也就是说,编译器不能改变函数名称。但这样一来,也就意味着此函数不能拥有c++函数的好处:不能重载、也不能成为成员函数。

    四  资本主义社会 面向对象的语言

    历史的车轮不可阻挡,结构化程序设计没有风光太久,就不得不将风头让给了新兴贵族——面向对象。

    自第一个成功的面向对象语言smalltalk问世以后,人们纷纷搭乘oo快车,种种新兴的面相对象语言不断出现,而一些古老的语言也不甘寂寞,为自己披上了oo外衣。最成功的面向对象语言有:c++Java等。

    c++是对c的扩展。c是封建社会中最有影响力的语言,它以他的灵活、高效闻名于世。c++继承了c的优点,奇迹般的在没有损失太多效率的情况下支持了所有的oo特征(Bjarne Stroustrup真是个天才)。但是,正因为c++有太多的传统需要传承,它身上有太多非oo的特点,所以它并不是一个“纯”的面向对象语言。而且,c++支持了很多oo中很有争议的特征,如:多重继承、虚继承(如果你不想跟自己过不去,就别使用他们)等,使c++成为一种非常复杂的语言。有人甚至认为,c++的规模之大,有甚于Ada(就我看来,Ada真是一种复杂的语言,它的强类型检查使习惯自由的我感觉像是带上了镣铐)。

    相比之下Java就“纯洁”多了。Java脱胎于c++却摒弃c++中许多不符合oo规范的特征,并努力使自己简单。Java并不仅仅是一种程序设计语言,它还代表了一种潮流,Java程序拥有一个统一的运行环境——Java虚拟机,所以它可以轻易的跨越平台,成为各大厂商的新宠。与c++相比,Java最大的缺点就是运行效率低,但随着Java本身和硬件的持续发展这个缺点越来越不明显,而开发效率的显著提高使大批c++程序员转投Java阵营。有人甚至认为,Java将会像高级语言取代汇编语言那样取代c++语言。

     面向对象若干问题浅谈

    1.  面向对象三大基石

    封装、继承、多态

    2.  关于代码重用

    面向对象出现依始,大量类库被设计使用,相比于传统的函数库,他们更加容易使用,人们认为找到了一个良好的代码重用机制,继承的作用被过分夸大。但是,许多年过去了,人们发现 代码重用 仍然是很困难的任务。

    人们进行反思,提出了一些新的观点:

    l  慎用继承,多用组合

    l  关于 实现继承 接口继承

       实现继承就是传统的继承方式,子类不仅继承基类的接口(成员函数)还继承基类的实现。

       接口继承指 子类仅仅继承基类的 接口,但不继承基类的实现。为方便理解我举一个c++的例子:

       class  IAdd

         {

                  virtual int add(int I, int J) = 0;

         }

           class Add: public IAdd

    {

        int addint I, int J

    {

    return I+J;

    }

    }

    作为基类的提供者仅仅提供一个接口的描述,具体的实现由实现厂商来完成,不同厂商的实现由于使用同样的接口,可以互换。

    如果使用标准的接口的话,作为软件的开发者就可以在市场上选购他所需要的实现,此时,软件将可以像搭积木一样被搭建起来,这就是所谓的“组件编程”。

    当前,新技术、新思想、新名词层出不穷,令人眼花缭乱。各种技术领域越来越走向分化,程序员们距离底层实现越来越远,不懂的领域越来越多,也越来越感到焦虑和迷忙,他们已经由计算机的主人变成了他的奴隶。

    总之,这是一个纷繁复杂的时代,这是一个人类彻底失去自由的时代,这是一个令程序员迷忙焦虑的时代,不管你喜欢与否,我们现在正处在这一伟大的时代中。

      共产主义社会  ??语言

    这是一个理想的社会,人类将迈向 自由王国,获得最大的自由。但这一时代的来临还遥遥无期……

     


    最新回复(0)