三、C++编译
前面给大家演示了如何从写C++代码到编译代码再到执行代码的全过程。这个过程中非常重要的编译环节,被我们一个按钮或者一个ctrl+F7快捷键就给带过了。其实这个环节非常重要,如果你非常了解这个环节,你开发源代码就会更加自信和清醒,而不是迷迷糊糊,摸棱两可……
下图直观描述了从源代码到计算机执行完毕的各个大环节:
内容实在多,图上没位置了,这里就继续说明上图。上图的1234是几个大步骤。1是我们的开发环境,你的代码就是在这个平台上书写、调试、编译、执行。。。等等
上图2是我们在1中开发的代码。这4行代码的功能就是把一个数字3存储到内存。但是A是我们人类能看懂的英文文字、数字3、一些符号,比如分号、小括号、花括号等等。那cpu可是看不懂这些呀,所以我们要把A编译成D2,因为cpu可以看懂D2,cpu就可以执行了D2了,执行完毕就可以完成我们人类的任务:写个3到内存。本部分重点讲3这个过程,就是如何编译的。
那我们再讲点题外的东西,以便我们对整体流程有一个认识。那就是后面的4,4说的就是cpu是如何执行D2的。我们说了D2是可执行二进制指令,也就是机器码(machine code),这些机器码先按照字节为单位加载到内存,就是E,当然E会很长很长,上图我就画了2个内存单位。加载完毕后就逐个单元地送到CPU。CPU只是一些电路(当然这个电路会及其复杂,cpu又称芯片,要不怎么芯片被卡脖子呢),它功能其实非常简单,它只会从内存读取数据、往内存中写入数据。当内存的这些指令数据送到CPU后,有用这些指令0101,就是高电平低电平的意思,那这些不同的特殊组合的高低电平流就会引起cpu中的晶体管打开或关闭,也就是引起不同的电路,不同的电路又会产生不同的输出结果,不同的输出结果指的是不同的高低电平的输出,也就是有序的0101序列的输出,而输出就是输出到内存。而内存上不同序列的0101又和人类认识的符号相对应。于是输出就又能映射成人类理解的东西,比如屏幕上的一行字,比如声卡输出的一段音频等等人类的东西。
具体到我们上图的例子,cpu输出的结果就是在内存的某个内存单元里面存了个3。那cpu具体是如何执行F的呢?我们看4.1,4.1中的H就是部分机器码,都是0101,我们看不懂,没关系,我先把这0101转化为16进制,就是J,J我们还看不懂,那我们把J在转化成K(这个过程叫优化,比如全部是0的就是空行的意思,也就是啥也不用做,所以可以删除了。关于优化环节我们后面还要单独拿出来演示),K还是看不懂,那我们把K再转化为L和M。L和M我们人类认识吧,这就是汇编语言,至少里面mov是move的意思,是我们人类的tocken,呵呵。。其中L的意思启动和结束的意思,仅仅是M这行代码是我们人类的任务:mov一个数字3到内存的后面表示的那个内存地址里面。这才是我们人类任务执行完毕的整个流程。
看似已经洋洋洒洒写了很多了,其实这个流程也是只是个大概。上图中每个箭头都表示从这一步到下一步的意思,其实每步还有很多很多的细节。本小节,我们只把步骤3——编译,拿出来展开讲解。
1、明确几个概念
我们的文本代码.cpp需要转化成.exe可执行程序,cpu才可以执行。
从.cpp到.exe的过程,我们就笼统的叫做编译,其实这个过程是需要经过下面3个子过程的:
(1)预处理(Preprocess):从.cpp变成.i文件。是处理一些预处理语句。
(2)编译(compile):从.i文件到.obj文件。这一步是编译的核心。这一步还要分很多个子环节,也是本部分的重点。其实如果你源文件没有引用其他文件或库之类的,就像上图的示例中的源代码,啥也没有,那其实到这步结束,生成.obj文件就已经是二进制可执行文件了,cpu就可以执行了。但事实上,我们不会写上例中那么无聊的代码,至少我们要写一个在屏幕上打印个hello world啥的。那在屏幕上打印东西都已经不是那么简单了,此时你写的源代码就需要引入头文件啥的,那此时生成的.obj文件就没法放cpu上执行,因为.obj文件里面有引用其他文件的代码,而且其他文件代码cpu也不知道,所以就没法执行。所以此时你还得进行链接link。尤其是我们在实际项目开发过程中,我们不仅会有很多引用,我们还会有很多.cpp文件。compile只是对每个.cpp文件分别进行编译,分别生成每个文件的.obj文件。那此时就非常有必要再有个链接器把我们这些所有的.obj文件都链接起来。
(3)链接(Link):从.obj文件到.exe文件。为什么要链接,其实上面已经说得非常清楚了。一般情况下我们的源代码编译都是要进行链接的,链接完毕后就是.exe文件,cpu就可以执行了。
所以,平时我们经常说的编译其实是预处理+编译+链接,其实就是build,build=preprocess+compile+link。如果你的源代码简单到无聊,那你可能就不需要link了,但一定得预处理和编译才能执行。
- 此外,这里还得梳理几个概念:
(1)编译器是把我们每个.cpp文件都分别编译成独立的.obj文件,所以在这个过程中每个.cpp文件都是一个翻译单元(translation unit),一个翻译单元生成一个obj文件。
在C++中,文件翻译单元是指一个.cpp文件以及它包含的所有头文件组合到一起形成的单独编译单元。每个.cpp文件在编译时都会生成一个翻译单元。例如,假设你有一个名为main.cpp的源文件和一个名为utils.h的头文件。main.cpp包含#include "utils.h"。在编译main.cpp时,utils.h中的所有内容也会被插入到main.cpp中,形成一个翻译单元,然后这个翻译单元会被编译成可执行文件或对象或库代码。
在这个例子中,main.cpp和utils.h组合在一起形成了一个翻译单元,utils.cpp是另一个翻译单元。在编译时,每个.cpp文件都会独立编译成一个对象文件或库或可执行程序。最后,链接器会将这些对象文件和库文件和可执行文件合并成最终的一个可执行文件。
(2)C++是不关心文件名的,不像Java语法,你的类名和你的文件名得一样、你得文件夹层次要和你的package一样。但是C++不一样:在于C++中,文件只是提供给编译器源代码的一种方式。文件名只负责告诉编译器你给它的是什么类型的文件、以及这种类型文件编译器应该如何处理它。比如你给编译器一个.cpp的文件,编译器就把这个文件当作C++文件进行处理;当你给编译器一个.C或者.H文件,编译器会把.C文件当成C语言文件进行处理,而不是当作C++文件进行处理,同时把.H文件当作头文件进行处理。这些都是默认的约定。当然你也可以更改,比如你给编译器一个.abcdef文件,那你就得再告诉编译器这种文件你按照C++文件进行处理,也是可以的。
(3)文件是指一组相关数据的有序集合,这个数据集的名称叫做文件名。例如源程序文件(.cpp)、目标文件(.obj)、可执行文件(.exe)、库文件(头文件)等。文件通常是驻留在外部介质(如磁盘等)上的,在使用时才调入内存中来,这就是为什么对文件操作时需要打开和关闭的原因。
在C++语言中,文件是一个流的概念。头文件fstream定义了三个类型:
ifstream:从一个给定文件读取数据;
ofstream:向一个给定文件写入数据;
fstream:读写给定文件。
类型的操作和cin、cout一样,可以用IO操作符(<<和>>)来读写文件,也可以用getline函数读取一整行数据。
(4)在 C++ 程序中,符号(例如变量或函数名称)可以在其范围内进行任意次数的声明。 但是,一个符号只能被定义一次。 这就是“单一定义规则”(ODR)。
这些概念是我们理解后面的基础,目前我能想到的就是这么多。下面我们就展开编译的3个子过程,详细聊:
2、预处理(Preprocess):
编译器中进行预处理操作(Preprocess)的是预处理器(Preprocessor), 也是第一个上场的程序。预处理器(Preprocessor)是对C++程序源代码进行简单替换和增删的一个操作。
预处理不对程序的源代码进行解析,仅仅是替换或删除某些代码块而已。所以预处理完毕后文件(.i文件)还是一个字符串文件,就是还是都是英文单词和数字那样的字符文件。
那么预处理是如何进行增删替换的呢?当然就是人为给它定义一些规则,符号规则就增删替换。
那也所以,预处理的工作流程就是:逐行遍历.cpp文件中的所有代码行,遇到预处理语句时,根据规则增删。
那么哪些是预处理语句?哪些是要替换的?用谁替换?哪些又是删除的?
上面是预处理原理,下面我们看看预处理的实操过程:
从上图可以看出我们的.cpp文件只有1k,但是build完毕后竟然有几十k,原因就是编译器进行了预处理。
我们对比一下有预处理语句的cpp文件和无预处理语句的cpp文件,编译后的差别:
那么预处理到底是不是简单的替换和删除呢?看下面例子:
为啥Math.cpp编译后没大多少啊?因为仅仅是从EndBrace.h文件中复制了一个后花括号而已。就是我前面说的,预处理仅仅是复制粘贴一些文本而已。这里就是把.h文件中的字符}复制到Math.cpp中的include那行位置上而已,或者说就是替换了include那行而已。那到底是不是你说的啊?我们看看生成的预处理文件.i文件就能证明了:
可见是不是就是仅仅是用头文件中的}替换了源文件中include那行代码,就是一个删除并替换的动作。下面我们再看看其他情况下的.i文件是不是也是按照前面说的规则生成的:
可见,在预处理过程中主要就是,处理源代码中的预处理语句:引入头文件(替换)、去除注释(删除)、处理所有的条件编译指令(该删除的删除,不该的留下),宏的替换(替换),添加行号(添加),保留所有的编译器指令(添加)。
以上就是预处理,下面我们看更核心部分:
3、编译(Compile)
待续。。。。