语言的设计发展是从低级向高级发展的过程。汇编放一边不说,可以看看C是如何转换为汇编代码,我仅仅是对原理性东西做概要解释,所以有些地方可能不够准确。C语言我可以将程序内容,分为三个部分:变量、结构、函数。
变量一般分为函数内变量,函数外变量,这两者之间是有区别的,函数外变量,最终将被存放在一个全局堆空间,而函数内变量往往通过栈方式来构建出来,或者说是动态存在于栈空间。对于函数外变量与函数内变量的存在模型的说明,与汇编语言没有任何关系,汇编语言是对具体动作的指示,而将变量通过什么方式存储、消除是语言背后模型设计的问题,这个又有点类似虚拟机模型的讨论了,不跑题,不瞎扯了。
那结构是什么,本质上什么也不是,如果没有这个结构的变量,那么这个结构真的屁也不是,如果定义了这个结构的变量,那么就有说的了,这个变量会在生成代码的过程中有所特殊处理,特殊在几个地方,一个是会为这个对象分配一个内存块来存储这个结构的变量,另外还有清楚结构内的数据组织,数据对齐方式,这样在处理A.p这样的子数据时才可以准确的定位到相应的内存位置上。
函数就太普通不过了,不过函数在生成汇编代码的过程中需要考虑一个问题,参数如何传递?最简单的方法指定一个内存区,将需要传递的参数按某种形式放入这块内存中,被调用函数然后通过对这块内存访问读取传递的参数,理论上这个是可行的,实际中也和这个过程差不多,只是有所区别,在介绍具体的区别时,我觉得先介绍下这样做的不合理性,首先,如果多个函数在多个线程中运行,那这个内存块如何维护,其次这个内存块多大合适,最后,这个内存块的地址如何通知被调用函数,通过寄存器安全吗?以上的问题就是实际使用模型有所区别的改造目标,最终实际上采用的方案非常简单,非常优秀,说了大家都觉得自己也能想到这么做,但我真的认为这个不是大多数人能想到的模型,实际中通过栈的方式将参数按某种顺序压入,被调用函数然后按某种方式访问栈中数据,这样内存非常独立,另外用到多大就产生多大,调用完成后即刻释放。太漂亮的模型了,优美,简洁,这才是真的多一分嫌多,少一分嫌少。
刚刚分析了变量、结构、函数之间的关系,以及他们运行模型相关的一些知识,那么他们怎么就转换为机器码的?如果还问这样的问题,看来还是没有完全透彻理解我上边论述内容的深刻含义,那就直白点,简单的解释下这个问题,首先,将C代码转换为一种更容易理解的形式,本质上就是转换为语言语法规则的表达层面,一般也叫语法树,这样做一方面容易理解编写内容的具体含义,另一方面也是对编写是否存在错误的一种检测过程,然后,在语法树的基础上,根据运行模型,例如堆栈模型,生成具体的代码。这里与实际过程有出入,但本质上就是这样的过程。
既然都说到这里了,那顺便再一起说说什么是线程?线程实际上与结构类似,是一个背后黑手,有点让人痒痒的,但有找不到实际证据。它是怎么个东西呢?首先,我们在代码中,说启动一个线程,往往是告诉操作系统,我要在本进程中启动一个并行执行的过程(本来启动一个进程也可以,但进程被设计的不相互操作,还要考虑进程间通信,所以才有了线程这样的并行执行过程),然后,操作系统就分配一个线程结构在内核中,这个结构记录了寄存器信息,栈信息,从这样来看,线程本质上就是寄存器与栈信息的一个结构体,只是操作系统能够根据这个结构体,给CPU分配一个时间,让其在这个基础上运行,如此而已,从实际中,如何来感知就是这样的过程,我们可以在代码调试过程中,查看同一个函数在不同线程中参数的地址就清楚参数被压栈到哪个地址了,这就说明每个线程都是单独的栈空间,从这个道理上来说,一个应用程序可以分配的线程,理论上与两个因素有关,一个是你预期线程的最大栈空间,另一个是主线程预期的最大栈空间。如果预期过大,可能会浪费,导致启动新线程失败,预期过小,在函数调用过程中,栈空间由于增长到了其它线程的栈空间,会导致程序错乱,更麻烦。
差点忘了说了,C语言中还有一个问题值得说下,那就是函数重载,在C语言中容许存在同一个函数名称,但参数不同,这个为什么值得说下,因为它说明了一个很重要的问题,函数如何映射到汇编代码的函数符号,如果仅仅通过函数名称来标识,那肯定不行,到时候根本无法跳转到正确的地址上,那怎么办?其实简单,我们不就是为了让这个函数名称唯一吗,既然在C语言中通过参数类型类唯一标识的,那么我们就将参数标识记录到函数的符号中,例如原来函数声明如: int FA(int p); 最终生成的函数符号可能是FA_i,这个在实际中,肯定不是的,但原理肯定是,这个如何验证?可以通过编写一个动态链接库,然后通过一个dll依赖检查程序,查看dll导出函数名称,这样就知道这个函数被映射为什么符号名称了。还有个题外话,有些时候我们在链接过程中会碰到说,某个符号不能被找到,大多数原因是链接库没有指定,还有些原因是链接库导出函数的命名规则与我们当前编译器的函数命名规则不一样造成,还有,如果被调用函数的参数压栈方式与我们当前编译器的函数压栈方式不同,那程序运行也是很危险的。
C的问题就先告一段落,接下来我们对C++进行分析,C++与C相比,有两个重大的区别,一个是类函数,另一个是多态。
类函数问题的解决很简单,刚刚开始学习面向对象时,可能会疑惑,这个类函数是不是每个类对象中都有一个,这个是我们最直接的想法,但实际中比这个要优美、漂亮多了,每个类函数都被转换为函数,通过在函数符号前加入类信息(可能还有命名空间,原理类似,不做具体分析了),这样就先解决了,函数如何被唯一标识的问题,另一个需要解决的问题是,类对象调用是,对象如何能够被这个函数感知到,因为这个函数还要使用这个类对象的数据,可以将类函数默认做一点小小的改动,就是都增加一个参数,这个参数就是累对象指针,这个也要看具体的编译器,有些可能直接通过寄存器进行的传递,但目的是一个,但不同方式下编译器编译的结果就很难互相调用。
多态问题的解决,其实也很简单,在类对象构造函数中,建一个多态函数表,将表的指针存放在类对象的第一个数据中,这样在访问一个多态函数时,直接通过类对象的第一个指针数据获取到多态函数表,然后根据函数在多态函数表中的顺序,对此函数进行访问即可。
联系客服