前言
我们平常用C/C++语言写代码的时候,运行只是靠编译器,点一下运行按钮就会出现我们代码运行的结果,那我们的代码究竟是怎么得到最终结果的呢?还是非常值得我们去了解与学习的。
一、翻译过程
> 预处理
预处理功能主要包括宏定义,文件包含,条件编译,去注释等。
1、宏定义、文件包含、去注释
我们现在写一个实例代码,让大家更好的了解
上述代码可以让我们很好的验证预处理的功能。
我们正常编译时,会直接给出一个含有最终结果的可执行文件,但我们想要查看预处理之后的总体代码应该怎么看呢?
执行命令:
g++ -E 文件名 -o 可执行文件名(在预处理这里,后缀名一定要是.i)
选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。 选项“-o”是指目标文件,“.i”文件为已经过预处理的C原始程序。
此时生成了一个含有预处理结果的文件,我们手动命名为了text.exe,我们打开:
左边是我们原来写的代码文件text.cc,右边是经过预处理之后的文件text.i,我们通过对比可以发现这两个文件里面的内容差的不是一点半点,这一万七千多行最下面才和我们源代码有点相似,那上面多出来的那么多行就是我们的头文件的展开,是头文件里面的内容,预处理会把我们的有文件里面的内容拷贝进来,这也就是预处理的作用之一,最下面我们也可以看出,我们的M已经被替换成了100,也就是宏的替换,注释也经过预处理之后就不见了,预处理的功能也由此可见。
2、头文件的所在路径
我们在text.cc文件的最开始可以看到,预处理阶段,会按照路径找到头文件,并将它拷贝进来
我们可以进入照这个路径查看头文件:
我们可以看到我们常用的iostream、algorithm等头文件都在这里,这是因为我们在安装gcc/g++时都会首先把你C/C++中所需要的头文件安装下来,以防之后用的时候找不到。
3、条件编译
我以一个实例来讲述条件编译,就拿我们Xshell举例。Xsehll有学生版、企业版、专业版等版本,我当前使用的就是免费的学生版,每个版本的功能都各有不同,专业版和企业版的功能肯定要比学生版本的功能多。
那在这里有一个疑问,在维护Xshell代码时,我们需要维护三份来对应不同的版本吗?
其实只需要维护一份,就可以适应三个版本,这里写一个代码帮助大家理解。
我们在vim里面写入这样一串代码:
从上到下对应Xshell的学生版、企业版、专业版,功能依次增加。我们运行这串代码:
我们会发现,只运行了专业版部分的代码,我们在文件全局区域加上学生版条件v1的宏定义,比如:
运行之后我们会发现:
这次又只运行了学生版的代码。
所以我们可以通过一个这样的方式来进行一个代码的裁剪、功能的保留。
但我想要修改的话,每次打开真的太麻烦,所以我们在这里有一个比较方便的操作,我们不用打开文件,再编译代码时在命令行加上一段小命令即可。称为命令行宏定义。
命令:
gcc -D <条件> <文件名>
注意!我们需要先把源文件中的宏定义去除.
举例:
> 编译
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查 无误后,gcc 把代码翻译成汇编语言。
用户可以使用“-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
命令:
gcc/g++ -S <预处理后的文件名,后缀为.i>
执行命令后后生成一个后缀为.s的文件。
我们这里还是以预处理阶段用到的一个c++文件演示:
打开编译完的文件:
我们可以看到已经转化成了汇编语言。
> 汇编
汇编的作用是生成生成机器可识别代码,也就是二进制。
读者在此可使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了
命令:
gcc/g++ -c <编译后的文件名,后缀为.s>
实例:
我们打开汇编完的文件:
我是看不懂......
> 链接 (重要)
最后链接步骤,会生成我们的可执行文件或者库文件
执行命令:
gcc/g++ <汇编后生成的文件,后缀为.o>
执行这条命令就会生成我们最终的可执行文件,没有进行自定义命名的话,文件名默认就是a.out。
我们也知道,计算机认识二进制语言,我们在汇编过程中已经将语言转化为了二进制语言,可是为生成的为什么不是可执行文件呢?为什么还需要链接这个步骤呢?
1、链接是什么?
链接是我们的程序与库链接的过程。是我们不曾知道的第三方库。也就是语言的标准库。
标准库里包含了我们所需要的头文件,需要调用的函数等等,每个语言都有自己的一个标准库,就比如我们printf打印,我们写出来就可以用,但是在底层需要去库里调用这个函数的。
我们可以通过指令:
ldd a.out
查看我们该程序所用到的标准库: 其中,libc.so.6=>/lib64.libc.so.6(0x00007ff05c32e000)就是我们的默认动态链接库。
就好比我们学校为我们提供图书馆,我们进入图书馆去读完一本书,读完再回去。待会作一个更未为详细的讲解。
2、怎么去链接?
在我们Linux下的库分两种库,分别是动态库和静态库:
- 动态库文件后缀:.so
- 静态库文件后缀:.a
在与动态库或者静态库分别进行链接时,分别对应动态链接与静态链接两种方式。
- 静态库是指编译链接时,把库文件的代码全部加入(拷贝)到可执行文件中,因此生成的文件比较大,但在运行时也 就不再需要库文件了。
- 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时 链接文件加载库(直接去库里面去执行),这样可以节省系统的开销。如前面所述的 libc.so.6 就是动态 库。gcc 在编译时默认使用动态库。
动态库和动态链接的优缺点:
1、不能丢失,一旦丢失,所有涉及到该库的程序都无法执行
2、节省资源
静态库与静态链接的优缺点:
1、一旦形成,与库无关
2、浪费资源。
如果我们想调用静态库,我们可以执行命令:
gcc -o <自定义生成文件名> <编译文件> -static -std=c99
生成可执行文件:
我们对比一下动静态链接生成的文件大小,我们再生成一个使用动态链接生成的可执行文件,默认名字为a.out。
我们可以看到,大小差了200倍!
使用静态库的时候,Linux默认是没有的,需要我们手动下载,需要执行命令:
sudo yum install -y glibc-static libstdc++-static
静态链接的应用场景:
如果你编译的程序具有非常大的跨平台性的话,以后可能会部署在很多机器上,这时候你就可以静态链接,把你的二进制代码直接部署在其它机器上,这样我们的机器不依赖动态库,不用做过多的环境监测。
二、历史原因
我们会有这样的疑问:为什么不能一步直接生成可执行文件呢?为什么要经过这么多麻烦的步骤?
>编程历史
1、二进制编程
我们知道,在六七十年的时候,还没有C语言的时候,人们编程用的都是二进制编程,就是我们听过的打孔纸带:
穿孔纸带是早期计算机的储存介质,它将程序和数据转换二进制数码:带孔为1,无孔为0,经过光电输入机将数据输入计算机。所以当时的二进制编程是不需要编译器的!!!
只不过这样太麻烦,当时会编程的都可以被称为科学家。后来人们为了提升效率,写出了汇编语言。
2、汇编语言
汇编语言, 即第二代计算机语言,用一些容易理解和记忆的缩写单词来代替一些特定的指令,例如:用"ADD"代表加法操作指令,"SUB"代表减法操作指令,以及"INC"代表增加1,"DEC"代表减去1,"MOV"代表变量传递等等,通过这种方法,人们很容易去阅读已经完成的程序或者理解程序正在执行的功能,对现有程序的bug修复以及运营维护都变得更加简单方便。
那汇编语言需不需要编译器呢?答案是肯定需要的!
因为计算机的硬件不认识字母符号,这时候就需要一个专门的程序把这些字符变成计算机能够识别的二进制数或机器语言。因为汇编语言只是将机器语言做了简单编译,所以并没有根本上解决机器语言的特定性。
3、C语言
也就是我们现在经常使用的语言,在此我就不多介绍,在这里我想问的是,那我们C语言的编译又是什么样子的呢?是直接转化为二进制语言吗?
>编译器的自举
在汇编语言发明的时候,如果没有编译器,那机器也就不会认识这所谓的汇编语言,毕竟计算机只认识二进制,所以我们就需要写一个编译器来将汇编语言转化为二进制,但是当时只有二进制编程,所以我们只能用二进制编程写出一个编译汇编语言的编译器!
当又有了C语言时,我们又需要写一个编译C语言的编译器,但是话说回来,编译器真的好写吗?并不好写,所以说我们踩在前人的肩膀上,用汇编语言写出一个编译C语言的编译器,要比直接用二进制写一个编译C语言的编译器要容易的多。
所以我们会存在编译和汇编两个步骤,不可缺少。
随着时代的进步,我们的编译器也在更新换代,二进制写出来的编译器,维护起来过于麻烦,既然编译器是软件,那我们也可以用二进制编译的汇编语言来写一个汇编语言编译的编译器!这里有点套娃的感觉,但是也挺好理解。从此之后,我们就可以用汇编语言写的编译器来编译汇编语言,也可以用汇编语言来升级编辑器。
那C语言呢?也是同理,我们可以用C语言写一个编译C语言的编译器,再经过汇编编译C的语言的编译器编译。就可以形成一个纯C的编译器。
自己编译自己,就是编译器自举的过程。