Linux中的gcc是由GNU推出的一款功能强大的、性能优越的多平台编译器。gcc编译器能将C、C++语言源程序和目标程序编译、连接成可执行文件。
那么gcc和g++的区别又是什么呢?只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别。
但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译。
我们知道一个C语言程序要想被执行,就需要经过两个环境。第一个是翻译环境,这个环境下主要将我们的源代码转换成可执行的机器指令。第二个是执行环境,这个环境是要将我们翻译环境所生成的可执行程序在运行环境下输出我们想要的结果。
这里我们先来大概看一下程序翻译环境下所执行的四个阶段 预处理,编译,汇编,链接 的过程都会发生什么:
接下来我们就通过在Linux环境下进行测试程序翻译环境下的四个过程:预处理,编译,汇编,链接
。
在预处理的过程中我们的程序会有以下变化: 头文件的展开、条件编译、宏的替换和注释的删除
等操作。
下面我们就在Linux环境下通过一个简单的C语言程序来演示这个过程:
#include
#define N 666
#define CJL
int main()
{
printf("hello world hello world hello world hello world\n");
/*printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");*/
#ifdef CJL
printf("hello CJL\n");
#else
printf("hello world\n");
#endif
return 0;
}
下面我们来演示预处理这个阶段程序所发生的变化
命令:gcc –E test.c –o test.i
- gcc:表示用gcc这款编译器来编译test.c这个C语言程序
- 选项 -E:该选项的作用是让 gcc 在预处理结束后停止编译过程。
- 选项 -o:是指目标文件,
.i
为后缀的文件为已经过预处理的C原始程序。
这里我们可以看到当前目录下已经生成了.i
为后缀的目标文件。下面我们用vim编辑器来对比一下.i
文件和.c
文件中的内容:
这里我们可以看到原来文件中的注释已经消失不见了。同时文件的内容也增加了很多很多,这是因为头文件中的内容已经被展开到test.i这个文件中了,同时,条件编译的内容也已经被替换成了要执行的语句了。
编译的过程其实是将我们的C语言程序翻译成汇编语言的过程。在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
在这里我们可以使用 ‘’-S‘’ 选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。我们可以直接使用在预处理后生成的.i文件来进行操作。
命令:gcc -S test.i -o test.s
我们需要注意的是:编译产生的文件一般以.s
为后缀,下面我们来对比一下生成的.s
文件和.i
文件中的内容。
这里我们可以看到原来的.i文件中的C语言程序已经被转换成了对应的汇编代码。
汇编这个过程所执行的内容主要是将我们的.s文件中的汇编代码转换成可重定位的目标二进制文件。
和上面的过程一样我们接着使用编译后的.s文件来执行汇编这个操作,这里我们使用的是 “-c” 选项来执行我们的汇编过程。
命令:gcc -c test.s -o test.o
下面我们来看一下.o
文件中的内容,发现完全看不懂。
这里我们利用 od
指令来看一下文件中的二进制指令
链接过程所做的事情就是将我们汇编过程中形成的可重定位的目标二进制文件和C语言中的库文件合并,最终形成可执行程序。
命令:gcc test.o -o Test
如果我们不使用-o选项来指定文件生成的名字时,生成的默认文件的名字就是a.out
,这里我们依然和上面保持一致,自己来命名生成的文件的名称。
这里就生成了一个名字为Test
的可执行程序。当我们直接执行它时就可以生成我们最终想要的结果:
在这里涉及到一个重要的概念:函数库
我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
其实这是因为系统把这些函数实现都被做到名为libc.so.6
的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib
”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。其实链接的本质就是如何将调用的库函数和标准的库关联起来。
动态库:
动态库在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“
so
”,如前面所述的libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,这点可以通过 file 命令验证。
动态链接
链接的时候,如果是动态链接,找到动态库,然后拷贝动态库中我们需要的代码的地址到我们自己的可执行程序中相关的位置。
ldd命令
—查看依赖的动态库列表
这里我们需要补充一点知识:
库的本质也是文件,静态库的格式一般是:libxxxxxx.a
,静态库的前缀是以lib
为前缀,.a
为后缀。中间的xxxxxx
才是库的名称;而动态库的一般格式则是:libxxxxxx.so
,动态库的前缀是以lib
为前缀,.so
为后缀。中间的xxxxxx
是库的名称。
这里我们可以看到gcc编译得到的可执行程序是通过动态链接来链接C动态库的。
静态库:
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为“
.a
”。
静态链接
链接的时候如果是静态链接,找到静态库,然后拷贝静态库中我们需要的代码的到我们自己的可执行程序中相关的位置。
与动态库不同的是:C和C++静态库需要我们自己去安装。
sudo yum install -y glibc-static
sudo yum install -y libstdc++-static
当我们安装完成后就可以对我们的程序进行静态链接了。
命令:gcc 源文件 -o 目标文件 -static
这里我们可以看到静态链接的方式最终链接形成的可执行程序所占用的内存非常大。这是因为静态链接直接拷贝了静态库中的代码到我们的可执行程序中了。
下面让我们对比一下静态库/静态链接和动态库/动态链接的优缺点:
- 静态链接成功,我们的程序不需要依赖任何库,自己就可以独立运行。
- 动态链接成功,我们的程序还是需要依赖动态库,一旦动态库缺失,我们的程序便无法运行。
- 静态库由于是自生拷贝的问题,所以比较浪费空间。
- 动态库因为可以做到被大家所共享方法,所以真正的实现永远都是在库中。程序内部只有地址,比较节省空间。
- 静态库VS动态库:Linux默认使用的是动态链接和动态库。
首先我们需要安装一下gcc和g++:
sudo yum install -y gcc
sudo yum install -y gcc-c++ libstdc++-devel
当然这里还有一些gcc和g++常用的选项:
- -E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
- -S 编译到汇编语言不进行汇编和链接
- -c 编译到目标代码
- -o 文件输出到 文件
- -static 此选项对生成的文件采用静态链接
- -g 生成调试信息。GNU 调试器可利用该信息。
- -shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
- -O0
- -O1
- -O2
- -O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
- -w 不生成任何警告信息。
- -Wall 生成所有警告信息