引言
gcc是GNU Compiler Collection(GNU编译器套装),是一套由GNU(Gnu's Not Unix) 开发的编程语言编译器。gcc是Linux操作系统下最常用的编译程序,它甚至是Linux平台编译器的事实标准。由gcc的manual可以看出, 其主要用于编译C及C++语言。本文以C语言为例,希望用最浅显易懂的方式谈谈GCC的通常用法。
这里摘抄了gcc manual的一小部分:有一定gcc使用经验的人可以作为参考;不太熟悉gcc的人可以绕过这里,直接跳到第1节——最简单的例子。
$ man gcc #comments: 这里表示在linux shell下敲入man gcc,以下类似。
NAME
gcc - GNU project C and C++ compiler
SYNOPSIS
gcc [-c│-S│-E] [-std=standard]
[-g] [-pg] [-Olevel]
[-Wwarn...] [-pedantic]
[-Idir...] [-Ldir...]
[-Dmacro[=defn]...] [-Umacro]
[-foption...] [-mmachine-option...]
[-o outfile] infile...
Only the most useful options are listed here; see below for the remain-
der. g++ accepts mostly the same options as gcc.
......
1. 最简单的例子(编译单个源文件)
首先,我们来看一个最简单的例子。通过编辑器(linux下常用的是emacs及vi)编写一个叫做example_1.c的源程序,内容如下:
#include <stdio.h>
int main()
{
printf("You are welcome to my blog!\n");
return 0;
}
我们可以用GCC来编译以上源文件,以生成可执行文件exec(我们可以把这种编译方式成为“一步到位”,这种命名方式来自博客http://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html)。
$ gcc example_1.c -o exec
这里的-o选项表示后面是输出文件名。(有些刚刚接触gcc的人(包括笔者)可能会以为-o表示后接目标文件*.o,这里需要稍微注意一下哦:)。)
这样,我们生成了可执行文件exec,运行它可以输出“You are welcome to my blog!”:
$ ./exec
You are welcome to my blog!
实际上,以上编译过程是分为四个阶段进行的,它们分别是:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)以及连接(Linking)。gcc的编译选项-E、-S、-c可以分别使编译过程在预处理、编译、汇编后停止。换句话说,我们可以将以上的“一步到位”拆分成以下四步:
$ gcc -E example_1.c -o example_1.i #预处理,生成中间文件example_1.i;完成#include(将.h插入.c文件)、#define、#ifdef等预处理命令
$ gcc -S example_1.i -o example_1.s #编译,生成汇编语言文件example_1.s
$ gcc -c example_1.s -o example_1.o #汇编,生成机器语言文件(目标文件)example_1.o
$ gcc example_1.o -o exec #连接,生成可执行二进制文件exec
一般情况下,-E及-S选项很少用到,我们常用的编译除了前面提到的“一步到位”方式外,还包括下面的“两步到位”方式:
$ gcc -c example_1.c -o example_1.o #编译(实际包括预处理、编译及汇编),生成机器语言文件(目标文件)example_1.o
$ gcc example_1.o -o exec #连接(将多个目标文件、库文件连接在一起),生成可执行二进制文件exec
有些读者可能会有疑问:既然我们有了“一步到位”的编译方式,“两步到位”的方式还有它的用武之地吗?当然是有的啦。读完第2节,就能找到答案啦。这里不是广告,确实是第2节中的例子才能更好地阐释这种“用武之地”。
2. 次简单的例子(编译多个源文件)
我们的程序变得稍微复杂了一点,包括两个源文件example_2_main.c、example_2_sub_func.c,以及一个头文件example_2_sub_func.h,它们一起完成了向标准输出(计算机屏幕)打印字符串"You are welcome to my blog!"。 目录树结构及源程序如下:
example_2_main.c:
#include "example_2_sub_func.h"
int main()
{
print_welcome();
return 0;
}
example_2_sub_func.c:
#include <stdio.h>
void print_welcome()
{
printf("You are welcome to my blog!\n");
}
example_2_sub_func.h:
void print_welcome();
我们可以使用“一步到位”的编译方式:
$gcc example_2_main.c example_2_sub_func.c -o exec
注:c语言里每一个源文件(.c文件)为一个编译单元;头文件并不是一个单独的编译单元,其内容会在预处理(preprocessing)过程中被插入到包含(#include)它的源文件中。
上面的编译过程同样可以拆分成“两步到位”的编译方式:
$ gcc -c example_2_main.c -o example_2_main.o #编译源文件example_2_main.c,生成目标文件example_2_main.o
$ gcc -c example_2_sub_func.c -o example_2_sub_func.o #编译源文件example_2_sub_func.c,生成目标文件example_2_sub_func.o
$ gcc example_2_main.o example_2_sub_func.o -o exec #连接目标文件example_2_main.o、example_2_sub_func.o,生成可执行文件exec
这样,读者可能能看出“两步到位”编译方式的好处啦:当程序包括多个编译单元的时候,如果其中一个编译单元发生改变,则只需要重新编译该单元,并连接生成可执行文件就行了。例如,这个例子包含两个编译单元:example_2_main.c及example_2_sub_func.c;如果example_2_main.c发生了改变,则我们仅需要重新编译该源文件并重新连接。这样可以避免当工程太大的时候编译太耗时。哈哈,这里回答第1节末尾的问题啦:)。
3. 稍复杂的例子(编译多个不在同一目录下的文件)
为了项目管理的需要,我们的源程序通常位于不同的目录下。例如我们把第2节例子中的example_2_main.c放在目录./main/下,而把example_2_sub_func.c及example_2_sub_func.h放在目录./sub_func/下。目录树结构如下:
在这种情况下,我们的“一步到位”编译命令为:
$ gcc -I./sub_func ./main/example_2_main.c ./sub_func/example_2_sub_func.c -o exec
其中-I(i的大写)为编译选项,表示头文件的搜索路径。这里头文件的搜索路径为./sub_func.
当然,以上的编译过程可以拆分为:
$ gcc -c ./sub_func/example_2_sub_func.c -o example_2_sub_func.o #编译源文件example_2_sub_func.c
$ gcc -c -I./sub_func ./main/example_2_main.c -o example_2_main.o #编译源文件example_2_main.c,其依赖位于目录./sub_func/下的头文件
$ gcc example_2_main.o example_2_sub_func.o -o exec #连接目标文件example_2_main.o、example_2_sub_func.o,生成exec
4. 较复杂的例子(连接库文件)
开发应用程序的时候,我们常常需要使用第三方函数库。函数库实际上是一些头文件(.h)和库文件(静态库.a或动态库.so)的集合。如果使用静态库,则静态库代码在编译(连接阶段)时就拷贝到了程序的代码段,程序的体积会膨胀;如果使用动态库,则程序中只保留库文件的名字和函数名,在运行时去查找库文件和函数体,程序的体积基本变化不大,但增加了程序的运行时间。
第5节会介绍生成库文件的方法,这一节我们学习如何使用库文件。
还是使用第2节中的程序作为例子。这里我们将example_2_main.c放在./main/目录下;头文件example_2_sub_func.h放在./sub_func/include/目录下;源文件example_2_sub_func.c不再存在,取而代之的是静态库文件libexample_2_sub_func.a(注意,静态库文件名以lib开始,以.a结束),其放在./sub_func/lib/目录下。目录树结构如下:
“一步到位”的编译方式为:
$ gcc -I./sub_func/include -L./sub_func/lib ./main/example_2_main.c -lexample_2_sub_func -o exec
其中-L为连接选项,后接库的搜索路径;-l(L的小写形式)也是连接选项,后接库的名字(去掉前缀lib及后缀.a)。注意:这里-l(L的小写形式)应该放在源文件./main/example_2_main.c的后面,否则无法连接成功。
类似地,“两步到位”的编译方式为:
$ gcc -c -I./sub_func/include ./main/example_2_main.c -o main.o
$ gcc -L./sub_func/lib main.o -lexample_2_sub_func -o exec
5. 如何生成库文件
生成静态库用ar命令:ar为归档命令,将多个.o文件归档在一起形成库文件,例如:
$ ar -rc libname.a a.o b.o #静态库文件名以lib为前缀,.a为后缀
生成动态库(共享库)用gcc,需加上-shared和-fpic选项:
$ gcc -fpic -shared -o libname.so name.c #动态库以lib为前缀,.so为后缀
6. 结尾
希望通过以上的方式能够让读者对linux下的gcc编译方式有一个初步的了解。
参考文献:
本文参考了gcc manual、维基百科、网络博客http://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html、http://developer.51cto.com/art/200609/32317.htm以及其他一些资料。