GCC是GNU项目的编译器组件之一,也是GNU最具有代表性的作品。在GCC设计之初仅仅作为一个C语言的编译器,可是经过十多年的发展,GCC已经不仅仅能支持C语言;它现在还支持Ada语言、C++语言、Java语言、Objective C语言,Pascal语言、COBOL语言,以及支持函数式编程和逻辑编程的Mercury语言,等等。而GCC也不再单是GNU C Compiler的意思,而是GNU Compiler Collection也即是GNU编译器家族的意思了,目前已经成为Linux下最重要的编译工具之一。
GCC是一个交叉平台的编译器,目前支持几乎所有主流CPU处理器平台,它可以完成从C、C++、Objective C等源文件向运行在特定cpu硬件上的目标代码的转换,GCC不仅功能非常强大,结构也异常灵活,便携性(protable)与跨平台支持(cross- plantform. support)特性是GCC的显着优点,目前编译器所能支持的源程序的格式如下表所示。
GCC是一组编译工具的总称,其软件包里包含众多的工具,按其类型,主要有以下的分类。
用GCC编译程序生成可执行文件有时候看起来似乎仅通过编译一步就完成了,但事实上,使用GCC编译工具由C语言源程序生成可执行文件的过程并不单单是一个编译的过程,而要经过下面的几个过程。
在实际编译的时候,GCC首先调用cpp命令进行预处理,主要实现对源代码编译前的预处理,比如将源代码中指定的头文件包含进来。接着调用cc1命令进行编译,作为整个编译过程的一个中间步骤,该过程会将源代码翻译生成汇编代码。汇编过程是针对汇编语言的步骤,调用as命令进行工作,生成扩展名为.o的目标文件,当所有的目标文件都生成之后,GCC就调用连接器ld来完成最后的关键性工作——链接。
下面来详细了解下gcc编译的过程:
1.GCC编译器首先做的工作是预处理:调用-E参数可以让GCC在预处理结束后停止编译过程。
在该阶段,编译器将上述代码中的stdio.h编译进来,并且用户可以使用gcc的选项”-E”进行查看,该选项的作用是让gcc在预处理结束后停止编译过程。
一般来说,预处理器(cpp)根据以字符#开头的命令(directives),修改原始的C程序。如 GccDemo01.c中#include
编译器在这一步调用cpp工具来对源程序进行预处理,此时会生成*.i文件,上面也部分列出了GccDemo01.i文件中的内容。实际上使用cpp *.c -o *.i和使用gcc -E的效果一样。
2.接下来进行的是编译阶段
在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。
用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。汇编语言是非常有用的,它为不同高级语言不同编译器提供了通用的语言。如:C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
实际上使用ccl *.i -o *.s和使用gcc -S的效果一样。
3.紧接着就是汇编阶段
把编译阶段生成的”.s”文件转成目标文件,在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。
同理,也可以使用as命令来替代。
4.在成功编译之后,就进入了链接阶段。
在这里涉及到一个重要的概念:函数库。
gcc *.o -o filename(可执行文件名)
在这个源程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?
最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf” 了,而这也就是链接的作用。
函数库一般分为静态库和动态库两种。
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。Gcc在编译时默认使用动态库。
动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。(Linux下动态库文件的扩展名为".so"(Shared Object)。按照约定,所有动态库文件名的形式是libname.so(可能在名字中加入版本号)。这样,线程函数库被称作 libthread.so。
静态库的文件名形式是libname.a。共享archive的文件名形式是libname.sa。共享archive只是一种过渡形式,帮助人们从静态库转变到动态库。)
附加知识点:
1.使用ldd可以查看可执行文件(包含动态库和ELF格式),所包含的库:
(linux中默认的可执行文件是ELF格式)
2.使用readelf -h查看可执行文件elf的头;
3.nm查看可执行文件的函数表。
示例如下面的:
GCC是Linux下基于命令行的C语言编译器,其基本的使用语法如下。
gcc [option |filename]…
对于编译C++的源程序,其基本语法如下:
g++ [option |filename]…
其中option为GCC使用时的选项,而filename为需要GCC做编译的处理的的文件名。就GCC来说,其本身是一个十分复杂的的命令,合理的使用其命令选项可以有效地提高程序的编译效率、优化代码。
GCC拥有众多的命令选项,有超过 100个的编译选项可用,按其应有如下的分类。
(1)总体选项
Gcc的总结选项如表所示。
对于“-c”、“-E”、“-o”、“-S”选项在前一小节中已经讲解了其使用方法,在此主要讲解另外两个非常常用的库依赖选项“-I dir”和“-L dir”。
常用编译选项
-c选项:这是GCC命令的常用选项。-c选项告诉GCC仅把源程序编译为目标代码而不做链接工作,所以采用该选项的编译指令不会生成最终的可执行程序,而是生成一个与源程序文件名相同的以.o为后缀的目标文件。例如一个Test.c的源程序经过# gcc –c Test.h编译之后会生成一个Test.o文件。
-S选项:使用该选项会生成一个后缀名为.s的汇编语言文件,但是同样不会生成可执行程序。
-e选项:-e选项只对文件进行预处理,预处理的输出结果被送到标准输出(比如显示器)。
-v选项:在Shell的提示符号下键入gcc –v,屏幕上就会显示出目前正在使用的gcc版本的信息
-x language:强制编译器指定的语言编译器来编译某个源程序。
例如指令:# gcc -x c++ p1.c该指令表示强制采用C++编译器来编译C程序P1.c。
-I
在Linux下开发程序的时候,统常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能。一般情况下,Linux下的大多数函数都将头文件放到系统/usr/include目录下,而库文件则放到/usr/lib目录下。但在有些情况下并不是这样的,在这些情况下,使用GCC编译时必须指定所需要的头文件和库文件所在的路径。-I选项可以向GCC的头文件搜索路径中添加新的目录
# gcc –I /home/include -o test test.c
-I <dir>
正如上表中所述,“-I dir”选项可以在头文件的搜索路径列表中添加dir 目录。由于Linux中头文件都默认放到了“/usr/include/”目录下,因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,Gcc就会到相应的位置查找对应的目录。
比如在“/root/workplace/Gcc”下有两个文件:
/*hello1.c*/ #include int main() { printf("Hello!!\n"); return 0; }
/*my.h*/ #include |
这样,就可在Gcc命令行中加入“-I”选项:
[root@localhost Gcc] Gcc hello1.c –I /root/workplace/Gcc/ -o hello1 |
这样,Gcc就能够执行出正确结果。
在include语句中,“<>”表示在标准路径中搜索头文件,““””表示在本目录中搜索。
故在上例中,可把hello1.c的“#include
-L
类似于上面的情况,用来特别指定所依赖库所在的路径
如果使用不在标准位置的库,那么可以通过-L选项向GCC的库文件搜索路径中添加新的目录。例如,一个程序要用到的库libapp.so在/home/JACK/目录下,为了能让GCC能够顺利地链接该库,可以使用下面的指令:
# gcc -Test.c -L /home/JACK/ -lapp –o Test
这里的-L选项表示GCC去链接库文件libapp.so。在Linux下的库文件在命名时遵循了一个约定,那就是应该以lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在使用-L选项指定链接的库文件名时可以省去lib三个字母,也就是说GCC在对-lapp进行处理的时候,会自动去链接名为libapp.so的文件。
需要注意的是,“-I dir”和“-L dir”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。
-static选项:GCC在默认情况下链接的是动态库,有时为了把一些函数静态编译到程序中,而无需链接动态库就采用-static选项,它会强制程序连接静态库。
-o选项:在默认的状态下,如果GCC指令没有指定编译选项的情况下会在当前目录下生成一个名为 a.out的可执行程序,例如:执行# gcc Test.c命令后会生成一个名为a.out的可执行程序。因此,为了指定生成的可执行程序的文件名,就可以采用-o选项,比如下名的指令:
# gcc –o Test Test.c执行该指令会在当前目录下生成一个名为Test的可执行文件。
出错检查和警告提示选项
GCC编译器包含完整的出错检查和警告提示功能,比如GCC提供了30多条警示信息和3个警告级别,使用这些选项有助于增强程序的稳定性和更加完善程序代码的设计。
Gcc的告警和出错选项如表所示。
下面结合实例对这几个告警和出错选项进行简单的讲解。
如有以下程序段:
#include void main(){ long long tmp = 1; printf("This is a bad code!\n"); return 0; } |
这是一个很糟糕的程序。
-pedantic以ANSI/ISO C标准列出的所有警告
当GCC在编译不符合ANSI/ISO C语言标准的源代码时,如果在编译指令中加上了-pedantic选项,那么源程序中使用了扩展语法的地方将产生相应的警告信息。
允许发出ANSI C标准所列的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C 标准的。其运行结果如下所示:
[root@localhost Gcc]# Gcc –pedantic warning.c –o warning warning.c: 在函数“main”中: warning.c:5 警告:ISO C90不支持“long long” warning.c:7 警告:在无返回值的函数中,“return”带返回值 warning.c:4 警告:“main”的返回类型不是“int” |
可以看出,使用该选项查看出了“long long”这个无效数据类型的错误。
-w禁止输出警告信息
-Werror将所有警告转换为错误
Werror选项要求GCC将所有的警告当成错误进行处理,这在使用自动编译工具(如Make 等)时非常有用。如果编译时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,只有程序员对源代码进行修改并起相应的警告信息消除时,才能够继续完成后续的编译工作。
-Wall显示所有的警告信息
Wall选项可以打开所有类型的语法警告,以便于确定程序源代码是否是正确的,并且尽可能实现可移植性。
允许发出Gcc能够提供的所有有用的报警信息。该选项的运行结果如下所示:
[root@localhost Gcc]# Gcc –Wall warning.c –o warning warning.c:4 警告:“main”的返回类型不是“int” warning.c: 在函数“main”中: warning.c:7 警告:在无返回值的函数中,“return”带返回值 warning.c:5 警告:未使用的变量“tmp” |
使用“-Wall”选项找出了未使用的变量tmp,但它并没有找出无效数据类型的错误。
对Linux开发人员来讲,GCC给出的警告信息是很有价值的,它们不仅可以帮助程序员写出更加健壮的程序,而且还是跟踪和调试程序的有力工具。建议在用GCC编译源代码时始终带上-Wall选项,养成良好的习惯。
代码优化选项
代码优化是指编译器通过分析源代码找出其中尚未达到最优的部分,然后对其重新进行组合,进而改善代码的执行性能。GCC通过提供编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的Gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。
对于大型程序来说,使用代码优化选项可以大幅度提高代码的运行速度。
-O选项:编译时使用选项-O可以告诉GCC同时减小代码的长度和执行时间,其效果等价于-O1。
-O2选项:选项-O2告诉GCC除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度。
不同的优化级别对应不同的优化处理工作。如使用优化选项“-O”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“-O2”除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项“-O3”则还包括循环展开和其他一些与处理器特性相关的优化工作。
虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。
调试分析选项
-g/-g1/g2/g3选项:生成调试信息,GNU调试器可以利用该信息。GCC编译器使用该选项进行编译时,将调试信息加入到目标文件中,这样gdb调试器就可以根据这些调试信息来跟中程序的执行状态。
-pg选项:编译完成后,额外产生一个性能分析所需信息。
注意:使用调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此调试选项统常推荐仅仅在程序开发和调试阶段中使用。
下面举一个简单的例子来说明GCC的编译过程。首先用vi编辑器来编辑一个简单的c程序test.c,程序清单如下。
//Day02/2.1/GccDemo01.c #include #include #define N 100 int main(){ int i,r; i=20; r=i+N; printf("This is the gccdemo!\n"); printf("r=%d\n",r); } |
根据上面的内容,使用gcc命令来编译该程序。
可以从上面的编译过程看到,编译一个这样的程序非常简单,一条指令即可完成,事实上,这条指令掩盖了很多细节。我们可以从编译器的角度来看上述编译过程,这对于更好理解GCC编译工作原理有很好的帮助。
在C语法中引入很多预处理指令,这些指令影响到gcc的预编译处理结果。
下面详细介绍下各个预处理命令含义:
示例分析如下:
1、#define 、#undef
#include #define YQ #define LOUIS "ENGLISH NAME" int main(){ printf(“%s\n,YQ”); /*编译出错,使用gcc –E 查看预处理结果*/ printf("%s\n",LOUIS); /* #undef LOUIS */ printf("%s\n",LOUIS); /*编译出错LOUIS未声明*/ return 0; } |
2、#error、#warning
#include int main(){ int a=20; if(a==2){ //#error "错误很多" #warning "警告一下!" } return 0; } |
3、#if、#elif、#else、#endif
#include #define VERSION 3 #if (VERSION < 2) #error "版本低" #else #warning "版本高" #endif
int main(){ printf("Hello gcc使用!\n"); return 0; } |
4、#ifdef、#else、#endif(函数,可以两次声明,但不可以两次定义)
#include #define DEBUG #ifdef DEBUG #warning "调试" #else #warning "非调试" #endif //这个指示符号的使用还是比较广泛的 int main(){ printf("Hello gcc使用!\n"); return 0; } |
使用#ifdef、#define可以防止头文件二次引入。
5、#include、#include_next
说明:
1.系统头文件使用#include <…>
2.用户头文件使用#include “…”
规则:
1.系统头文件会在I参数指定得目录中优先查找。
2.用户头文件会在当前目录查找。
3.Unix标准系统目录
/usr/local/include
/usr/lib/gcc-lib/…/版本/include
/usr/…/include
/usr/include
4.编译C++优先查找/usr/include/g++v3
5.#include
6.#include的文件名不不含扩展,*、?无意义。除非文件名中包含*。
7.#line
#include int main(){ int re=0; printf("Hello gcc使用!\n"); for(int i=0;i<200){ re+=i; }
printf("out:%d\n",re); //代码行数被修改 #line 200 //另外得用法 //#line 200 "ch01_c.c"
printf("out:%d\n",re,a);//人为错误 printf("out:%d\n",re); return 0; } |
8、#pragma
所有GCC的pragma都定义两个词GCC +其他
1.#pragma GCC dependency 文件名 提示内容
测试文件的时间戳,当指定文件比当前文件新的时候产生警告。
#include #pragma GCC dependency "ch02.c" int main(){ int re=0; printf("Hello gcc使用!\n"); int i; for(i=0;i<200;i++){ re+=i; } printf("out:%d\n",re); return 0; } |
2.#pragma GCC poison
每次使用指定名字就会产生错误。
#include #pragma GCC dependency "ch02.c" #pragma GCC poison printf add int main() { int re=0; printf("Hello gcc使用!\n"); int i; for(i=0;i<200;i++) { re+=i; } printf("out:%d\n",re); return 0; } |
每次使用指定名字就会产生错误。
3.#pragma GCC system_header ----一般很少使用,还没有发现有什么功能
1.该指示符后的代码都做为系统头文件的一部分。
2.系统头文件往往不能完全遵循c标准,所以头文件中的警告信息 大多数都不显示(除非用#warning显示声明)
3.该指令 定义在c文件中无效,只能定义在 头文件中
提示:#pragma有一个等价的宏_Pragam
#include #pragma GCC dependency "ch02.c" //#pragma GCC poison printf add int main(){ int re=0; printf("Hello gcc使用!\n"); int i; _Pragma("GCC poison printf add") for(i=0;i<200;i++){ re+=i; } printf("out:%d\n",re); return 0; } |
8.1、# 运算符用于创建字符串 格式:#形参(中间可以有空格或者Tab),因为#后面必须跟形参,所以#运算符仅限于 函数式宏定义
#include #define STR(a) #a int main(){ int re=0; printf("Hello gcc使用:%s!\n",STR(99)); return 0; }//输出结果为:99 |
8.2、## 运算符用于连接前后的数据, 格式: data1##data2(可以有空格或者Tab)
注意:
1.结果:连起来是什么就是什么类型的结果如:99#20 结果是:9920 int类型
“ab##cd”连起来是”abcd” string类型;
2.Data1与data2既可以是形参,也可以不是形参,所以 ##既适用于 函数式宏定义,也适用于变量式宏定义。
#include #pragma GCC dependency "ch02.c" //#pragma GCC poison printf add #define HELLO(a) a##200 int main(){ int re=0; printf("Hello gcc使用:%s!\n",HELLO(99)); return 0; } |
预定义的宏很多,下面列出常用的。
示例:
#include int main(){ printf("Hello gcc使用:%d!\n",__LINE__); printf("Hello gcc使用:%s!\n",__DATE__); printf("Hello gcc使用:%s!\n",__BASE_FILE__); return 0; } |
上一篇:随机生成某几个汉字
下一篇:HashMap统计字符串出现的个数