欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++和Java从编译到运行的过程区别

程序员文章站 2022-05-25 12:23:24
以下内容纯属臆测,没有科学依据,也不想(没空)翻看权威资料。 一、C++编译和运行过程 1、C++每个编译单元整体上看都是各种声明和定义 C++编译单元就是指每个cpp文件,...

以下内容纯属臆测,没有科学依据,也不想(没空)翻看权威资料。

一、C++编译和运行过程

1、C++每个编译单元整体上看都是各种声明和定义

C++编译单元就是指每个cpp文件,整体上看(全局的东西,函数内部不算,类定义内部不算)无非就是变量(包括类的实例也算变量)、函数或者类的声明和定义。其中变量占用内存空间,存放在运行时的“全局区”,这个内存空间的数据一般是可变的,可以随时被修改;函数(体)不占用内存空间(本质上也占用,因其编译完后变成一堆永远不变的纯用来运行的代码或称指令,而且占用空间较少,哪怕很大的程序编译完后纯代码也不大,所以讨论时常常认为其不占用空间),存放在“代码区”,其内容永不被改变;类(体)没有专门的存放区,因为在运行时根本不存在类了(纯个人愚见),就像基本类型int一样,任何类型在运行期都不起作用了,运行时本质(或者说全部的工作)就是“从某块内存读数据进入cpu,或者把cpu的数据存放入某块内存,或者cpu内部进行运算”,任何类型只在编译期有用,被编译器用来进行错误检查等,防止不同类型的变量混杂使用,防止出现一些非常难查找的异常,所以说类体只在编译时有用,被编译器使用,把函数内用到的类的成员翻译成一个个带有复姓的长名字,如类名.成员名,其中碰到new 类名时,就翻译成调用类的构造函数(对运行期来说就是一个地址而已),总之类的成员(非静态,静态的东西本质就是全局的)不可能出现在全局定义的地方,也即不可能存放在全局区,当然可以在全局的地方定义一个类的实例,编译器对待他和对待普通变量没什么区别,只不过占用的内存空间稍微大了些而已。

2、符号表的作用(编译和链接过程)

先看编译过程,C++编译器使用符号表,运行时就不用符号表了。符号表简单理解为具有三列的表格(符号名、类型、内容),只有变量、函数和类能进入符号表中,其中类型列对于变量来说是描述该符号怎么分配内存和初始化,类型列对于类来说就是类里面含有什么成员,怎么进行成员初始化,内容列对于变量来说就是变量存放的具体数据(包括对象变量也是),内容列对于函数来说就是函数代码体的首地址,内容列对于类来说是空的,不用的。C++编译每个文件时,第一步从整体上扫描,无非就是声明和定义,把所有定义的东西都装进那个.cpp文件对应的符号表,就产生了符号表;第二步进行局部扫描,也称错误检查,首先进行符号检查,就是扫描函数体和类体,碰到不认识的符号,先看该符号有没有在本cpp文件声明,如果声明了就把当前符号记录成问号(如 普通变量x和对象变量stu翻译如下,x = 15翻译成x? = 15,stu.no =10 0翻译成stu?.第三个内存单元 = 100),如果符号没事先声明,也不在本cpp的符号表里头,那就报错;然后对函数体和类体进行语法错误检查,看看有没有错误的地方;第三步是进行翻译成机器语言,例如把函数名翻译成地址,变量名也翻译成地址等;其中最重要的就是可能把一行的文本代码翻译成很多行的机器代码,例如碰到函数体内使用到new一个类则查找构造函数然后翻译成一堆对对象成员的分配内存指令,碰到使用对象成员变量时翻译成从对象变量首地址开始下移多少个位置才取出具体的数据;另外要注意碰到new的地方都依据符号表的类型列翻译成具体的分配内存和初始化的指令(汇编有具体的指令集,不仅是new,变量定义语句也如此翻译)。总之经过上面三步过程,编译就算完了,其最终结果就是产生了一个符号表和一个.obj文件,其中符号表里都是该cpp文件中定义的全局的东西(变量定义、函数定义或者类定义),obj文件都是函数体(特别注意只有函数体,没有类体,类体中的函数本质也是函数体,只不过具有复姓而已,类体的成员变量此时无用了,已经都翻译成某个对象成员便宜多少位地址形式的东西了,其本质就看成对象变量),该函数体就是所谓的指令,其中函数体里面可能有一堆的含有问号的符号。

再看链接过程,链接时依然用到符号表,而且强烈依赖符号表,链接时第一步先扫描每个cpp文件对应的obj文件,碰到含问号的符号时就在所有的符号表里查找,找到后把符号对应的内容代替问号;第二步链接器把所有的obj文件链接在一起形成一个大的obj(在内存中,这里不考虑链接库),把所有的符号表中变量的内容连在一起形成一个大的表(在内存中,函数和类都不用了);第三步是生存exe文件,具体过程是先划分出数据区和代码区(exe只有这两部分),其中数据区存放符号表中定义的各种全局的变量,代码区存放大obj的内容(即所有函数体的集合,注意只有函数体,即所有指令)。

3、运行过程

以后运行exe时,操作系统都是先根据exe的数据部分分配出全局区(包括常量区),然后根据代码部分分配代码区,然后系统自己分配栈区和堆区,就可以开始运行了。

二、如何理解静态联编和动态联编中的静和动?

所谓静态联编就是指exe运行前(也称编译期)会起作用的语句,动态联编实在运行期(main运行后开始算起)会起作用的语句。例如变量定义语句分两种,全局变量就是编译器就起作用的语句,因为全局变量在生成的exe文件中已经被描述成全局的东西了,exe文件运行成进程的开始就会先产生全局区,然后给全局表里分配内存,也就可以看出全局变量定义语句在真正运行前(main函数运行前)就起作用了;而对于函数内的局部变量,则属于运行前起作用的语句。因此我们说函数重载都是静态联编,因为重载在编译时就确定是哪个函数了,也即编译时就翻译成具体的确定了的函数地址了;而运行时的多态如Father *p=new children(); p.fun(); 中的fun函数运行的是子类的函数,原因是p.fun语句在编译器仅仅进行语法错误检查,根本就没有真正运行(函数体里的语句除了静态变量定义外,全部都在运行期才起作用),而在运行期才真正起作用,所以是动态联编。

三、声明性语句、定义性语句和运行性语句

你会发现除了函数体内部的语句外,都是声明性语句或定义性语句,所以我把整个代码分成这三类,其中函数体内部除了静态变量定义外,都是运行性语句,而且运行性语句只会在函数体内部出现(包括类的成员函数体)。这里说的运行性语句是指main函数开始后真正起作用的语句。需要注意的是函数里面出现的new一个对象或者定义一个变量的语句早在编译器就依据符号表的类型列翻译成一堆分配内存和初始化指令了(汇编有具体的指令集),但是编译器仅仅是把其翻译成一堆机器指令,并明确了怎么分配和初始化(任何变量包括类一定都是在编译器就明确怎么分配内存和初始化),并没有真正在内存中分配内存,真正分配内存实在man运行后,运行到该代码时才起作用的,所以我也常常认为函数体内的变量定义性语句(包括new)是“半运行性语句”,因为好像汇编期间其也起了一点作用(虽然只是起到明确怎么干,但没有真正干)。

四、分析类在编译器和运行期的作用

类在编译器作用就看成是个模板,他本身自己不起任何作用,有用的是依据他建立的一个个对象的成员,编译完成后,就完全不需要类了,而且编译完成后的代码里面没有类,所有的对象都翻译成一个变量的首地址,对象的成员都翻译成相对于首地址的偏移地址。尤其是运行期起作用的仅仅是函数体内的代码,类就更不可能用到了,类的成员函数也早翻译成复姓函数,跟类没有关系,就跟普通函数一模一样。所以类本质就是个模板,用来方便组织数据和行为,没有他也一样能做到,只不过有了它能使人不用考虑具体组织细节,而且更方便人们用面向对象思维思考问题,所以类是面向对象思维的一个实现工具,没有类,一样可以用别的方法实现面向对象思维。

五、java编译和运行过程

1、java编译和运行

Java编译期和C++完全不一样,他比C++简单得多,也不会使用C++中的用于连接多个obj的符号表(填充多个obj中的问号符号)。java不存在链接,只有编译,而且编译也仅仅是把文本代码翻译成字节码,其中最需要注意的就是,碰到import语句时,会把下面对于的类的前面加上包名,形成类的全名形式。

Java运行时先运行含有main函数的那个class字节码文件,碰到第一次使用的类时,Java虚拟机的类加载器才去加载那个类到内存中运行。

C++可不是这样,C++运行时早就没有类的概念了,所有的东西都变成和普通变量和普通函数没什么区别的东西了,而且类函数早就变成普通函数进入exe的代码区了,所以从代码量来看,C++中的类的代码即使没运行到跟C++类有任何关联的代码,C++类的代码也依然占用内存(例如exe的代码区就包括了在类函数体里定义的指令)。当然我们这里讨论的东西不包括C++的动态链接库,实际上C++的动态链接库使用了别的技术(从行为上来看动态链接库更像java中的类加载器动态加载类的技术)。

2、Java类加载器作用(怎么查找类)

Java类加载器(虚拟机类加载器)在两个阶段分别起作用,在编译阶段或者运行阶段,第一次碰到使用某个类时(注意不是import,import只是说明把一些符号加上复姓而已),例如new一个类实例,或者声明一个类实例等等,类加载器起作用,把该类加载进入内存,具体过程是根据父包名.子包名.类名,把其中的点号都变成斜杠,然后形成一个子目录,附加到classpath父目录后面,然后根据这个目录查找到类文件并加载到内存中,一旦一个类已经加载到内存中,以后再用到该类时就不重复查找和加载,直接使用内存中的类就可以了。

这也解释了为什么.java文件中定义的每个类(包括内部类)都会生成一个以该类名作为文件名的.class文件。这是因为便于类加载器查找到这个类的具体定义,否则想想看,如果文件名不和类名一致,类加载器定位都类所在目录后,由于不知道类在哪个文件中,就需要把所有的文件都打开然后在每个文件中查找,效率肯定非常低下。

而且之所以Java规定一个.java文件中最多只能有一个public类,并且一旦含有public类,则该.java文件名必须和public类名一致(包括大小写)是因为,只有public的类才会对其他包输出并且很可能被多次用到(被多个其他包的类文件用到),java类加载器每编译一个java文件都会把含有public的类事先放到内存中存放起来,以后在编译别的java文件时(注意编译主要就是明确变量怎么分配内存和初始化而且一定会明确和进行错误检查),如果用到则直接从内存中取出,提高了编译速度,而那些非public的类文件由于一般只被本包中的类文件用到一次或很少的几次(例如一个非public的类被本包中的多个输出类用到),所以编译器编译到这个类时不用事先把其放到内存,以后用的的话按需加载,这样就大大提高了编译效率。

六、include和import的区别

有了上面的知识就容易分析两者的区别了,include作用是在原地展开,import的作用仅仅是起到复姓的作用。c++中碰到include就会把头文件内容原地展开,等于代码长度增加了。java中碰到import则表示下面的代码碰到某个符号时,在起前面加上复姓,变成包.类名的形式。我认为import根本没有起到目录查找的作用,因为import使得每个类都有复姓的形式,以后第一次加载该类的时候通过复姓在目录中查找就行了,所以import语句时根本就没有查找,只是在第一次加载类的时候才查找。

七、怎么理解类(本质)

无论是c++还是java,理解类时都把他看成一个类型,定义一个类时是产生了一个模板,通过类实例化时是产生了一堆成员,只不过这些成员在内存上是紧挨着的并且都具有复姓。总之,类就可以看成具有复姓的一堆成员而已。

八、C++动态链接库

至于静态链接库,就理解为完全和cpp无任何区别,只不过提前把cpp编译成二级制代码而已。

至于动态链接库,本质就是一堆共享的变量和函数(注意没有类,动态链接库中到处的类本质是导出加了复姓的变量和函数而已)。要输出整个的类,对类使用_declspec(_dllexpot);要输出类的成员函数,则对该函数使用_declspec(_dllexport)。其中导出类就相当于在类的所有成员前面加上_declspec(_dllexport)。所以本质上根本没有导出类,导出的都是成员,即使是实例化一个类实例,也是使用了导出的构造方法而已。

所以可以推测动态链接库实现方法大致如下:在源文件中用的变量或函数或者类实例都在源文件中留下了编译成特定地址的符号,而在dll库中含有相同的编译成特定地址的符号,运行时碰到该地址就知道在dll的那个位置运行即可。

有关c++动态链接库导出类的详细过程参加文章:https://blog.csdn.net/clever101/article/details/3034743

九、Java、C#与C++的区别(主要体现在运行时对类的处理上)

Java和C#都有运行时的类加载器,当运行时第一次用到某个类时,类加载器就会把该类加载进内存,所以使用起来非常简单,类甚至也可以看成一个具体的对象或变量,对其进行处理,但是C++运行时根本就不存在类了,所以无法直接处理类,所以根本不存在反射机制等。

另外Java和C#在第一次加载类进内存时还是有区别的,在Java中类存在一个个的.class字节码文件中,并且加载器根据类名的复姓查找到位置并加载该类。而在C#中,没对每个类生产单个类文件(这是C#先进之处,避免产生大量的文件),而是把多个类防止一个dll文件中(此dll文件称为程序集),使用时在编译器需要提前把用到的程序集引入到工程中(我猜测就是在生成的exe文件中写上用了哪些程序集),编译时碰到不认识的符号就从引入的哪些程序集里查找,一旦找到就把对于的符号写成“程序集名.命名空间名.符号名”的形式,以后在运行期间第一次碰到“程序集名.命名空间名.符号名”时就根据程序集名字在工作目录中或System32目录中动态加载该程序集进入内存,并从程序集中找到类信息供类加载器使用。总之,Java是通过“命名空间名.类名”来动态加载类(Java的包名就完全等价于命名空间名,因为包名唯一作用就是复姓作用),而C#是通过“程序集名.命名空间名.符号名”来动态加载类(就好像java中的一部分类打包成一个程序集并起个模块名,然后根据模块名.命名空间名.类名访问一样,这样加个层级可更好的分门别类的管理大量的类)。

最后补充一点,C#中的using 命名空间名和Java的import作用是完全一模一样的,都是仅仅起到复姓的作用,没有其他任何作用,不要想复杂了。只不过import ss是在代表以下代码中出现的类名全部替换成ss,而using ss是代表下面代码不认识的符号前面自动加上ss作为姓。