gcc概述
GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。
gcc是GCC中的C语言编译器,而g++是GCC中的C++编译器。本博客只讲解gcc,g++的语法和选项和gcc都是一致的。
gcc
编译C语言最基本的语法:
gcc test.c
你就完成了对test.c
文件的编译操作,该过程会默认生成一个名为a.out
的可执行文件,你可以直接运行a.out
。
如果你想要编译的同时,指定编译后生成文件的名称,就加上-o
选项,后紧跟着编译后文件的名称:
gcc test.c -o test.out
此时生成的可执行文件就叫做test.out
了。
gcc
默认不支持C99标准,如果你希望以C99标准编译该文件,加上选项-std=c99
:
gcc test.c -o test.out -std=c99
编译C语言要经过预处理
,编译
,汇编
,链接
的过程,可是为什么我们需要这些过程呢?这涉及到计算机语言的发展史了。
语言发展史
在最早的时候,对计算机编程是通过打孔纸带的,如果有孔就是1,没孔就是0。但是这种二进制编程的效率太低了,于是计算机的从业者就开始对二进制进行改进,诞生了汇编语言。汇编语言的诞生具有划时代的意义,于是就发展出了操作系统,编译器等东西。比如Unix的最初版,就是通过汇编语言写的。后来为了方便,又诞生了面向过程语言,其中最大名鼎鼎的毫无疑问是C语言。再后来人们觉得面向对象思想是符合人类的编程习惯的,就又有了C++,python,Java这样的高级语言。
现在问题就来了,不论是汇编语言,C语言,还是如今的高级语言。是先有语言,还是先有编译器?这就涉及到编译器自举过程。
编译器自举
在出现汇编语言之前,我们只有二进制语言来编程。但是人们发明汇编语言后,就要有对应的编译器来编译汇编语言,不然计算机就无法理解汇编语言。因为汇编语言无法被编译,所以我们就要先用二进制语言写一个汇编语言的编译器,然后我们的汇编语言就可以被计算机理解了。但是一旦我们的汇编语言可以被解释了,可以用于编程了,那么我就可以再用汇编语言写一个编译器了。从此以后,编译器就用汇编语言再来书写了。这个过程就叫做编译器自举。
同理,在C语言出来之前,我们无法用C语言写一个C语言的编译器,所以就用汇编语言写一个C语言的编译器。现在C语言就可以被计算机编译了,于是我们就可以再用C语言写一个C语言的编译器。
那么现在又有一个问题了,请问我们在解释C语言的时候,是直接把C语言解释为二进制指令,让计算机理解。还是先把C语言解释为汇编语言,然后再让汇编语言解释为二进制呢?不得不说,如果直接让C语言变成二进制语言,这个过程未免太麻烦了,二进制语言过于反人类了,相应的汇编语言就好很多。我们要在巨人的肩膀上看世界,汇编语言已经把转换为二进制这个工作做完了,那么我们只需要在汇编语言的基础上改进就好。因此C语言要先变成汇编语言,最后变成二进制。这就是为什么C语言要有预处理,编译,汇编的过程。
gcc的编译过程
现在我们再简单回顾一下以上三个阶段的功能,并且讲解gcc
的编译相关选项。
在test.c
中,有如下代码:
预处理
- 头文件展开
- 去注释
- 宏替换
- 条件编译
那么经过这个过程,还是C语言吗?答案是是的,该过程只是预先处理了一下C语言,把一些没必要的内容删除,减少后续工作的工作量。处理后依然是C语言代码。
使用gcc
时,带上-E
选项,就可以得到预处理后的文件:
gcc -E test.c -o test.i
此处我把编译后的文件命名为test.i
,一般而言经过预处理的文件都以.i
为后缀。
左侧是预处理后的文件test.i
,右侧是源文件test.c
:
- 可以看到,左侧代码经过了800多行才到main函数,这800行就是
<stdio.h>
头文件的展开 - 语句
#define M 100
没有了,并且main函数中的M
被替换为了100
,也就是完成了宏替换 - 被注释的四行
//printf("hello world!\n");
被删除了,也就是注释的删除
现在再看看条件编译,我把代码改为以下代码:
如果我们把VERSION2注释掉,那么就会输出"hello VERSION1.0";如果把VERSION1注释掉,那么就会输出"hello VERSION2.0";如果都注释掉,那么就输出"hello free"。我们确实可以在编写代码的时候,通过注释来进行条件编译,控制代码的版本。但是这个过程在编译的时候就可以直接控制。gcc可以在命令行中定义宏。
语法:
gcc test.c -o test1.exe -D VERSION1=1
以上命令中,-D VERSION=1
就相当于在代码中写了一句#define VERSION1 1
,-D
选项用于指定一个宏。
看到以下现象:
第一次-D VERSION=1
,最后一行输出的hello VERSION1.0
;第二次-D VERSION=2
,最后一行输出hello VERSION2.0
;第三次没有加任何宏定义,就输出了hello free
。
也就是说我们可以通过指令给编译器传递不同的宏,来动态裁剪代码。
比如说visual studio有社区版和专业版,社区版是免费的,专业版要收费,毫无疑问社区版是专业版经过一定阉割后产生的。那么在微软公司内部,这两个版本要分开维护吗?这样的话如果一个版本出现了问题,那还要去看看另外一个版本有没有相同的问题,然后修改两份代码,未免太麻烦了。因此完全可以采用条件编译,把需要被阉割掉的部分用条件编译包起来。最后在编译的时候,使用命令行传入不同的宏,实现一份代码两个版本。因此条件编译的最重要的应用场景就是一份代码发行多个版本的软件。
编译
该过程是把C语言的代码变成汇编语言的过程。
想要生成该阶段的文件,需要通过-S
选项:
gcc -S test.c -o test.s
该阶段的文件,一般以.s
结尾。
test.s
内部:
可以看到,内部确实已经变成汇编语言的文件了。
汇编
该过程是把汇编语言变成二进制的过程,这个过程生成的文件叫做目标文件
,全称为可重定位目标二进制文件
。这个文件虽然已经是二进制文件了,但是它还不是一个可执行文件。
想要获得该阶段的文件,需要使用选项-c
:
gcc -c test.c -o test.o
一般而言,该阶段的文件以.o
或者.obj
结尾。在Linux
中习惯用.o
,在Windows
中习惯用.obj
.
test.o
内部:
可以看到,其内部都是一些乱码,因为我是用的vim
是一款文本编辑器,其不能解析二进制文本,所以会出现乱码。
为什么该过程已经是一个二进制文件了,还是不能被计算机执行?计算机不是可以识别二进制吗?
这是因为缺少链接的过程,接下来我们看看链接都有哪些功能。
gcc的链接过程
我们的C语言内部,调用很多库函数,比如printf,scanf等等。它们并没有在编译的时候展开,不信你可以回去看看那个.i文件,绝对没有展开一个叫做printf的函数。那么C语言要如何拿到这个函数,并调用它呢?这就涉及到链接的过程。
如果你想要让你的.c,.i,.s,.o中的任意一个文件变成链接后的文件,不用带任何选项,直接执行gcc即可,因为直接执行就是生成可执行文件,这已经是链接后的文件了。
gcc -o test.exe test.c
这样最后生成的test.exe就是可执行文件了,在此.exe不是强制的,只是我前面已经写过太多test命名的文件了,使用后缀区分一下属性。在Windows中,可执行文件的后缀就是.exe,因为Windows通过后缀区分文件,而Linux不通过后缀区分文件,因此这个.exe可有可无,只用于帮助我们自己快速判断文件属性。
那么这个可执行文件test.exe
是如何链接到库的,又链接了那些库呢?
我们可以通过ldd
指令来查看一个可执行文件链接了那些库:
ldd test.exe
输出结果:
比如第二行的libc
就是C语言的标准库。另外的,它还指明了一些库在系统中的路径。也就是说我们的很多头文件,都已经早早地在Linux
中下载好了,因此我们可以在Linux
上运行C语言代码。
比如说/usr/include/
路径下的文件:
你可以看到很多非常非常熟悉的头文件,比如<stdio.h>
,<math.h>
,<stdlib.h>
等等。
在链接到库时,库分为两种:动态库
和静态库
。
动态库与静态库
- 静态库:编译链接时,把库文件的代码全部拷贝到可执行文件中,在运行时也就不再需要库文件了。
优点:只要形成了可执行文件,那么就脱离对库的依赖,可以自主运行,可移植性好
缺点:相同的资源拷贝多份,浪费资源,生成的文件比较大
- 动态库:在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,编译器会提供动态库的地址,当程序执行到指定的代码段,就会去动态库内部查找对应的内容。
优点:节省资源,整个操作系统所有使用动态库的程序,只需要一份库文件,内存中也只加载一份
缺点:一旦丢失,所有链接该库的程序都无法执行了
通过动态库实现的链接,叫做动态链接
,通过静态库实现的链接叫做静态链接
。这里有一个注意点,那就是:虽然动态库和静态库内部的函数是一样的,但是动态库和静态库是两份不同的文件,并不是说把动态库拷贝一份到代码中,就是静态链接了。这涉及到一些更深的知识,就不在gcc的章节中介绍了。
动静态库的后缀:
在
Linux
中,动态库以.so
为后缀,静态库以.a
为后缀在
Windows
中,动态库以.dll
为后缀,静态库以.lib
为后缀
我们直接以一般的形式编译一个文件:
gcc -o test1.exe test.c
就可以得到一个名为test1.exe
的文件。先通过ldd
查看相关的库:
可以看到,每个库中都包含了.so
的字段,说明gcc在编译时默认使用动态库。
我们也可以通过file
指令查看,执行file test1.exe
:
dynamically linked
字段就表示这是一个动态链接
的程序。
如果我们想要生成静态链接的文件,则额外加上选项-static
:
gcc -o test2.exe test.c -static
此时test2.exe
文件就是一个静态链接的文件了。
但是你大概率无法执行该命令,会出现以下报错:
这是因为Linux
默认是不带静态库的,要手动安装:
yum install -y glibc-static libstdc++-static
该指令需要root
权限,要么sudo
,要么以root
身份执行。其中glibc-static
是C语言静态库,libstdc++-static
是C++静态库。
先对比一下两个文件的大小:
可以看到,两个文件之间差不多相差了100
倍的大小,因此静态库非常浪费资源。
使用ldd
:
其很明确的告诉你,这不是一个动态链接的可执行文件not a dynamic executeable
。
再用file
:
statically linked
字段说明这是一个静态链接的可执行文件。