目录
前言:
继上文介绍了vim 和 yum,相当于介绍了 文本编译器,我们可以利用vim写代码,那么写代码的我们了解了,现在应该了解编译器了吧?这不,gcc 和 g++就出来了,我们就简单介绍一下gcc 和 g++的一些区别,咱们的侧重点是在程序的翻译上面。
1 gcc和g++
我们没有了解到gcc和g++之前,在Linux上甚至没有办法编辑代码,那么现在我们可以了,gcc是一个只能编辑C语言代码的编辑器,我们使用的时候大概率会出现版本问题,g++同理,是一个编辑C++语言的编辑器,同时,也可以编辑C语言的代码。
刚才提及到的版本问题,这里演示:
当我们在.c文件里面写了for循环之后,gcc就编辑不了,说这是C99的,而因为我们编译器的版本没有升级,所以我们需要加上:
这就可以完成版本为C99的编译了,-o是指定编译成什么名。
在C++中,vs的后缀一般是.cpp,在Linux中,C++文件名的后缀可以是.cc .cpp甚至可以是.cxx,我们来编译试试:
这样就可以了,那么我们试试gcc编译C++的代码呢:
就会报这么一大堆的错误,说白了,就是gcc不认识C++中流罢了。
当然了,有的时候我们写代码写了新特性,就会导致编译失败,我们多注意,或搜索升级一下g++ gcc就可以了。
2 翻译过程
我们写好了代码,编译器成功编译了,我们不免会好奇,代码是如何经过一层一层的编译,逐渐变成了一个一个我们不认识的字符的?
那么就不免提及翻译过程,其实在C语言阶段我们就或多或少接触了些底层知识,比如汇编,比如代码编译的时候分为预处理,编译,汇编,链接部分。本文就着重于这四个部分,进行编译器背后的故事的介绍了。
2.1 预处理
结合C语言阶段的编译链接知识,我们知道预处理阶段是进行宏替换,头文件展开,去掉注释等工作的,还牵扯到了条件编译部分,那么我们想要看预处理阶段的代码怎么看呢?
只需要gcc -E test.c -o test.i即可:
这是我们的测试代码:
编译成功之后,我们使用vim打开文件看看:
800多行,可以判定为头文件展开了,M的位置变成了100,代表宏替换也完成了。中间的一大堆注释也不见了,可以判定为去掉注释成功了。
这里简单带一下条件编译的内容,主要用到指令 -D,测试代码为:
编译也是没有问题的,我们先来默认编译一下:
默认打印的是0,那么我们使用-D快捷定义一下:
D和V1之间可以空格可以不空,这就是条件编译。
此时可以发现里面的除了定义V2留下的,其他都没有了就,这是预处理阶段。
2.2 编译
可以发现,经过预处理部分,我们的代码还是C语言的代码,代码的本质是没有发生改变的,那么在编译阶段,代码开始变为汇编语言了,使用的指令就是gcc -S test.i ,我们可以从.c文件开始编译,也可以从.i文件开始编译,从.c文件开始无非就是走一次预处理阶段。
当我们打开了这个test.s文件,可以发现里面的东西已经变成了我们不认识的,说白了,就是变成了汇编代码,往后看:
就跟我们Vs调试的底层没有区别,都是些助记符什么的。
2.3 汇编
到汇编阶段的时候,我们就更看不懂了,因为这是从汇编阶段转向了二进制目标文件,其实我们平常也是经常接触的,比如:
.obj就是目标文件,在Linux中对应的目标文件我们需要使用gcc -c test.s -o test.o来完成:
里面就是诸如此类的乱码,这是在汇编阶段要做的事。
那么,我们是否思考过为什么代码编译的时候是从C语言->汇编->二进制目标文件的吗?
这就不得不牵扯到历史故事了,这是一个编译器自举的过程。
最开始是只有二进制的,使用的是二进制的打点方式来表达的代码,那么我们想要编译这个代码,就需要一个二进制的编译器,此时,对应了汇编部分的.o文件。前人使用二进制编写了一个二进制的编译器,这是前提。语言是跟随着时代发展的,所以后面许多语言就发明出来了,但是对应的编译器从哪里找呢?从头研发一个吗?好像有点麻烦,我们不是已经有了基础吗?就是二进制的编译器,前人使用二进制语言编写了两个X语言的编译器,A编译器编译B,获得了B.exe,那么B.exe的本质就是二进制语言,我们修改B.exe只需要不停的修改源代码,再使用A编译器就可以完成编译器的修改,此时基本过程已经创建,二进制编译器编译了一个X语言的编译器,那么编译X语言的时候就会走到二进制。而语言的发展是二进制到汇编的,所以,过程是C语言到汇编到二进制,二进制编译X语言的编译器,使得B.exe变成一个完美的X语言的编译器这个过程叫做编译器的自举。
对于历史故事我们应该了解一下。
2.4 链接
好了,预备工作已经做好了,现在只需要生成可执行文件就行了,此时就是链接要做的事情,那么为什么存在链接的这个过程呢?链接的过程是在做什么呢?怎么做的呢?这是我们在链接部分要考虑的事:为什么?是什么?怎么做?
第一个问题,什么是链接?
我们是否思考过为什么我在文本编译器里面写上了printf,引用一个头文件就可以实现打印了,难道是只需要一个头文件就可以使用函数吗?
并不完全是的,函数确实是在头文件里面没有错,但是呢,头文件来源于哪里呢?我们学习计算机的都应该知道,C语言有自己的标准库,C++有自己的标准库,那么标准库存放的是什么?标准库存放的是头文件没有问题,那么标准库在哪里呢?
所以,就牵扯到了链接了,我们写下了代码,引用了头文件,本地配置就会和标准库建立链接,所以我们才能够完成函数调用。
我们平常查看任务处理器的时候,不免的会发现很多.dll文件,或者是.lib文件,这实际上就是标准库,我们在安装编译器的时候,安装的不仅仅有头文件,还有该语言的标准库。
安装任何软件的时候,如果说涉及到了库,在Linux中,.so是动态库,.a是静态库,在windows中,.dll是动态库,.lib是静态库。
我们可以使用ldd命令来查看一个可执行程序,会打印出链接的什么库。我们可以看到/lib64/libc.so.6,库的名称是去掉前缀,再去掉后缀,前缀是lib 后面是.so.6,留下的是一个c,代表这就是C语言的标准库。因为是.so,所以这是一个动态库。
那么为什么有库的概念,这是因为可以提高效率,让我们粘在巨人的肩膀上。
那么为什么有静态库和动态库的概念?动态库可以理解你去网吧上网,使用了网吧的机器,静态库可以理解为你把网吧的机器搬回来了,那么二者的区别在于,动态库地方集中一个点,方便压缩空间,而静态库的区别是每个人都要拿一台电脑,会导致重叠的空间变大,导致效率不够高。
我们可以看看动态库和静态库的大小区别,当然了,因为静态库的大小确实很大,所以一般机器上是默认没有安装的,安装的指令如下:
sudo yum install glibc-static libstdc++-static
可以看到大小相差了一百倍之多,那么静态库的应用场景是什么呢?
比如你希望你的程序具有很强的跨平台性,过去了不用配置其他东西,那么你使用静态库,将所有东西都拷贝一遍,跨平台了自然就不需要配置了。
这就是链接部分的些许介绍。
感谢阅读!