曾传新
首先我们纠正一下题目中一个小小的误区,不是C和C++不能跨平台,应该是说是C/C++源代码在编译后生成的
.exe
文件不能跨平台,源代码和可执行文件要区分开来。想要搞明白这个问题,我们先得了解一下源代码是怎么变成程序的。
四个过程:预处理——编译——汇编——可执行文件
当我们编写完代码后,源代码会经过上述的四个环节,最终变成常见的可执行文件。
预处理阶段(hello.i):在源代码中会有头文件,一些宏,注释等。预处理的目的就是将头文件展开,宏文件代换,去掉注释等,对代码进行一些初步的处理
编译阶段(hello.s):这一阶段主要是检查语法上的错误,比如内存有没有溢出,指针有没有指错对象,然后生成可汇编文件。
汇编阶段(hello.o):计算机是不认识代码的,所以需要将汇编代码转换成0和1组成的机器码
链接(a.out):链接有两种情况:静态和动态。静态库和应用程序编译在一起,在任何情况下都能运行;而动态库是动态链接,文件生效时才会调用。最终生成一个可执行文件。
编译器的作用
我们不要把编译器想的太厉害,觉得编译器是万能的。实际上编译器就像是一个翻译,负责把高级语言转变成机器能看懂的低级语言,翻译过程就是上述的四个过程。但是其中有一点需要格外注意。那就是不同的公司使用的指令集不同。输出程序的格式和CPU使用的指令集有关,比如X86,arm,还有MIPS等等, 由于设计思路的差异,所以不同平台上编译生成后的可执行文件格式是不一样的,可能在ubantu里能运行的C程序,放到windows下就会报错。
类比一下java,为什么说java可以跨平台,是因为java内置了一个虚拟机,程序都从虚拟机中跑的,所以有人说“java不仅是一种语言,更是一个平台”。
综上所述,C/C++的一些基础性代码是可以跨平台的(可能会受API影响),是生成的可执行文件不能跨平台,C/C++不自带编译器,不同平台下的编译器存在差异。
(都看到最后了,麻烦点个赞和关注吧,谢谢~)
爱思考的奥特曼
先来讨论一下C&C++语言的执行过程,从而搞清楚为什么C&C++语言不能跨平台。
我们分阶段来讨论(如图4所示):
- 预处理阶段。预处理器(cpp)来把 代码中开头的行进行展开, 比如头文件,宏等内容,修改最初的C文件。
- 编译阶段。编译器(ccl)将修改后的C文件,翻译成了 另一文本文件,,这就是我们所说的汇编程序了。
- 汇编阶段。汇编器(as)将翻译成机器语言指令。 把这些命令打包成一种叫做可重定位目标程序(relocatable object program)的格式,此时的输出格式就是了。这其实就是二进制文件了。
- 链接阶段。编译过程最后还有一个链接阶段(程序调用了 函数),最后的输出结果还是和上一步类似,都是直接二进制文件。
比如 计算 1+1,两个 数据1都 使用 来表示,而 加操作,放在cpu中,可以是 (这个是胡乱写的),这个二进制代表的加操作能被计算机识别。而因为这个加操作对于cpu来说,编码的格式是固定的。所以可以直接一个助记符来表示,这样科学家们写程序就方便多了,而这就是汇编程序的由来。因为汇编程序完成之后,可以再有一个专门的程序(就是要上文中所说的汇编器)来把编写的汇编程序编译成0和1.这样计算机也可以识别了,而汇编语言本身也方便了程序的编写和阅读。
编写汇编比直接编写二进制方便高效了太多。但是 随着计算任务的复杂,程序的规模越来越庞大,使用汇编程序也很累啊,那么是否有更简单的方式呢?所以科学家们发明了高级语言(比如 ,等),在编写程序的时候,使用C语言等编写,然后再使用 编译器将C语言程序翻译成汇编程序,汇编程序再使用汇编器编译成0和1,这样,cpu能识别的东西没有变化,但是对于编写程序的人,确实方便了很多。
通过以上的描述,我们就知道了高级语言的大概由来。也明白了我们所编写的各种高级语言,到了最后,其实都是转化为二进制执行。
而直接二进制格式的程序,我们称之为本地机器码(native code)。而类似那些 之类的 助记符,以及汇编的编写格式或标准,我们称之为 指令集。
但是问题的关键来了。不同公司所生产的 cpu芯片。他们所使用的指令集不同啊, 这种芯片设计的事情,没有国际统一的标准,甚至像intel所代表的复杂指令集,和arm为代表的精简指令集,还有PowerPC、MIPS等指令集,它们指令集的设计思路就是不一样的。
所以为什么说C&C++语言不能实现跨平台运行,就是因为它编译出来 输出文件的格式,只适用于某种特定的CPU,而其他CPU不认识啊。
鼹鼠科技
程序的可移植性,本质上与实现它的编程语言无关。需要区分两个概念:编程语言和语言的实现。
编程语言
我们通常说的C/C++、Java、Python、JavaScript等编程语言,其实是指语法和语义上的一种规范。这种规范,定义了编写程序时必须要遵循的一种书写规则,只有符合规则才能被编译器正确识别和理解。
语言的实现
语言的实现,通常指的就是编译器了。对于同一种语言,不同的编译器可以有完全不同的实现方式。
以Python语言为例,就有很多种大相径庭的编译器实现。
- CPython - 解释执行。
- PyPy - JIT执行。
- Jython - 把Python源码编译成Java字节码,然后再Java虚拟机里执行。
C/C++也是一样,有很多种不同的实现,不过目前主流的实现,全都是编译成二进制的机器码执行的。但是,其实网络上也有很多C/C++的解释器,就是直接把C/C++的源码解释执行的。
我目前正在开发中的一个编译器项目,就支持C语言的绝大部分语法,目前的实现,也是解释执行的。
了解了编程语言和语言的实现后,再来说说程序的可移植性。
程序的可移植性
程序的可移植性,通俗来讲,就是你在一个环境上编写出来的代码,是不是能够很方便的把它放到另外一个环境上运行。这里的环境,包括软件环境和硬件环境。
- 软件环境:通常指操作系统,以及程序所依赖的运行时环境,如系统支持库等。
- 硬件环境:指程序运行的目标硬件,包括CPU、芯片组、外设等。
但从编程语言本身来说,理论上,任何高级编程语言都是可移植的,只不过实现可移植性的方式有所不同。
编写一次,到处编译
对于C/C++,目前几乎所有通用的实现都是直接把源码编成二进制目标代码。对于这种实现方式,要想进行移植,就必须要保证在不同的软件环境和硬件环境下,都要实现相应的编译器。比如要有Windows+x86的编译器、Linux+x86的编译器、Linux+ARM的编译器等等。也就是编写一次,到处编译。
所以,所谓的C/C++程序不能跨平台,是指被编译成二进制的可执行文件不能跨平台,而不是C/C++语言本身不能跨平台。
编译一次,到处运行
而Java则选择了不同的实现方式。Java典型的实现是,把Java源码编译成字节码,然后把字节码放到虚拟机中进行执行。
对这种方式,要实现可移植性,则必须要保证,在每种不同的软件环境和硬件环境中,都必须要实现相应的Java虚拟机。
比如要有Windows+x86的Java虚拟机,Linux+x86的Java虚拟机、Linux+ARM的Java虚拟机等。
这些不同的虚拟机,都遵循相同的Java字节码的规范。因此,只要把Java源代码编译成标准的Java字节码,就可以在所有的这些虚拟机中运行。
对编译器、OS内核、性能调优、虚拟化等技术感兴趣的朋友,欢迎关注!