1. GCC 编译器

公欲善其事,必先利其器。首先,先弄懂GCC再说吧!呵呵

 

 

1.头文件
在使用 C 语言和其他语言进行程序设计的时候,我们需要头文件来提供对常数的定义和对系统及库函数调用的声明。对 C 语言来说,这些头文件几乎永远保存在/usr/include 及其下级子目录里。那些赖于所运行的  UNIX 或 Linux 操作系统特定版本的头文件一般可以
在/usr/include/sys或/usr/include/linux 子目录里找到。 其他的程序设计软件也可以有一些预先定义好的声明文件,它们的保存位置可以被相应的编译器自动查找到。比如,X 窗口系统的/usr/include/X1R6 子目录和GNU C++编译器的/usr/include/g++ -2子目录等。
在调用 C 语言编译器的时候,可以通过给出“  -I”编译命令标志来引用保存在下级子
目录或者非标准位置的头文件,类似命令如下:
[david@localhost linux]$ gcc -I /usr/openwin/include hello.c
该命令会使编译器在/usr/openwin/include子目录和标准安装目录两个位置查找 fred.c程
序里包含的头文件。

 

gcc 编 译 器

Linux的各发行版中包含了很多软件开发工具, 它们中的很多是用于C和 C++应用程序
开发的。本章将介绍如何使用Linux下的 C 编译器和其他 C编程工具。 gcc 简 介
在为Linux开发应用程序时, 绝大多数情况下使用的都是C语言, 因此几乎每一位Linux
程序员面临的首要问题都是如何灵活运用 C 编译器。目前 Linux 下最常用的 C 语言编译器
是gcc(GNU Compiler Collection),它是 GNU项目中符合 ANSI C标准的编译系统,能够编
译用C、C++和Object C等语言编写的程序。gcc不仅功能十分强大,结构也异常灵活。最
值得称道的一点就是它可以通过不同的前端模块来支持各种语言,如Java、Fortran、Pascal、
Modula-3 和Ada 等。gcc是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行
效率与一般的编译器相比,平均效率要高 20%~30%。

gcc所支持的语言
后缀名  所支持的语言
.c  C原始程序
.C  C++原始程序
.cc  C++原始程序
.cxx  C++原始程序
.m  Objective-C原始程序
.i  已经过预处理的C原始程序
.ii  已经过预处理的C++原始程序
.s  组合语言原始程序
.S  组合语言原始程序
.h  预处理文件(标头文件)
.o  目标文件
.a  存档文件
 
开放、自由和灵活是Linux的魅力所在,而这一点在 gcc上的体现就是程序员通过它能
够更好地控制整个编译过程。
在使用gcc编译程序时,编译过程可以细分为4个阶段: 
●  预处理(Pre-Processing) 
●  编译(Compiling)
●  汇编(Assembling)
●  链接(Linking)
Linux程序员可以根据自己的需要让 gcc在编译的任何阶段结束,检查或使用编译器在
该阶段的输出信息,或者对最后生成的二进制文件进行控制,以便通过加入不同数量和种类
的调试代码来为今后的调试做好准备。与其他常用的编译器一样,gcc也提供了灵活而强大
的代码优化功能,利用它可以生成执行效率更高的代码。
gcc 提供了 30 多条警告信息和 3 个警告级别,使用它们有助于增强程序的稳定性和可
移植性。此外,gcc还对标准的C和C++语言进行了大量的扩展,提高了程序的执行效率,
有助于编译器进行代码优化,能够减轻编程的工作量。 使 用  gcc
gcc的版本可以使用如下 gcc –v命令查看:
[david@DAVID david]$ gcc -v

 

实例3-1   hello.c
 
#include <stdio.h>
int main (int argc,char **argv) { 
printf("Hello Linux/n"); 
}
要编译这个程序,只要在命令行下执行如下命令:
[david@DAVID david]$ gcc hello.c -o hello
[david@DAVID david]$ ./hello
Hello Linux
这样,gcc 编译器会生成一个名为 hello 的可执行文件,然后执行./hello 就可以看到程
序的输出结果了。
命令行中  gcc 表示用 gcc 来编译源程序,-o  选项表示要求编译器输出的可执行文件名
为hello ,而hello.c是源程序文件。从程序员的角度看,只需简单地执行一条 gcc命令就可
以了;但从编译器的角度来看,却需要完成一系列非常繁杂的工作。首先,gcc需要调用预
处理程序 cpp,由它负责展开在源文件中定义的宏,并向其中插入#include 语句所包含的内
容;接着,gcc 会调用 ccl 和 as 将处理后的源代码编译成目标代码;最后,gcc 会调用链接
程序ld,把生成的目标代码链接成一个可执行程序。 
为了更好地理解gcc的工作过程,可以把上述编译过程分成几个步骤单独进行,并观察
每步的运行结果。
第一步要进行预编译,使用-E 参数可以让 gcc在预处理结束后停止编译过程: 
[david@DAVID david]$ gcc -E hello.c -o hello.i
此时若查看 hello.i 文件中的内容,会发现 stdio.h 的内容确实都插到文件里去了,而且
被预处理的宏定义也都作了相应的处理。
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "hello.c" # 1 "/usr/include/stdio.h" 1 3
# 28 "/usr/include/stdio.h" 3
# 1 "/usr/include/features.h" 1 3
# 291 "/usr/include/features.h" 3
# 1 "/usr/include/sys/cdefs.h" 1 3
# 292 "/usr/include/features.h" 2 3
# 314 "/usr/include/features.h" 3
# 1 "/usr/include/gnu/stubs.h" 1 3
# 315 "/usr/include/features.h" 2 3
# 29 "/usr/include/stdio.h" 2 3
# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3
# 213 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 3
typedef unsigned int size_t;
# 35 "/usr/include/stdio.h" 2 3
# 1 "/usr/include/bits/types.h" 1 3
# 28 "/usr/include/bits/types.h" 3
# 1 "/usr/include/bits/wordsize.h" 1 3
# 29 "/usr/include/bits/types.h" 2 3
# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3
# 32 "/usr/include/bits/types.h" 2 3
 
"hello.i" 838L, 16453C                         1,1           Top
下一步是将hello.i编译为目标代码,这可以通过使用-c参数来完成: 
[david@DAVID david]$ gcc -c hello.i -o hello.o
gcc默认将.i文件看成是预处理后的 C语言源代码,因此上述命令将自动跳过预处理步
骤而开始执行编译过程,也可以使用-x参数让 gcc从指定的步骤开始编译。最后一步是将生
成的目标文件链接成可执行文件: 
[david@DAVID david]$ gcc hello.o -o hello
在采用模块化的设计思想进行软件开发时,通常整个程序是由多个源文件组成的,相
应地就形成了多个编译单元, 使用gcc能够很好地管理这些编译单元。假设有一个由 david.c
和xueer.c两个源文件组成的程序, 为了对它们进行编译, 并最终生成可执行程序 davidxueer,
可以使用下面这条命令: 
[david@DAVID david]$ gcc david.c xueer.c -o davidxueer
如果同时处理的文件不止一个,gcc仍然会按照预处理、编译和链接的过程依次进行。
如果深究起来,上面这条命令大致相当于依次执行如下3 条命令: 
[david@DAVID david]$ gcc david.c -o david.o
[david@DAVID david]$ gcc  xueer.c -o xueer.o
[david@DAVID david]$ gcc david.o xueer.o -o davidxueer
在编译一个包含许多源文件的工程时, 若只用一条 gcc命令来完成编译是非常浪费时间
的。假设项目中有 100个源文件需要编译,并且每个源文件中都包含 10  000 行代码,如果
像上面那样仅用一条gcc命令来完成编译工作, 那么gcc需要将每个源文件都重新编译一遍,
然后再全部链接起来。很显然,这样浪费的时间相当多,尤其是当用户只是修改了其中某一
个文件的时候,完全没有必要将每个文件都重新编译一遍,因为很多已经生成的目标文件是
不会改变的。要解决这个问题,关键是要灵活运用 gcc,同时还要借助像 make这样的工具。
关于make,将在第5章作详细的介绍。  gcc警告提示功能 
gcc包含完整的出错检查和警告提示功能,它们可以帮助 Linux程序员尽快找到错误代
码,从而写出更加专业和优美的代码。先来读读例 3-2所示的程序,这段代码写得很糟糕,
仔细检查一下不难挑出如下毛病:
●  main函数的返回值被声明为 void,但实际上应该是 int; 
●  使用了GNU语法扩展,即使用 long long来声明 64 位整数,仍不符合 ANSI/ISO C
语言标准; 
●  main函数在终止前没有调用return语句。 
实例3-2   bad.c
 
#include <stdio.h>
void main(void)
{
  long long int var = 1;
  printf("It is not standard C code!/n");
}
下面看看 gcc 是如何帮助程序员来发现这些错误的。当 gcc 在编译不符合 ANSI/ISO C
语言标准的源代码时,如果加上了-pedantic选项,那么使用了扩展语法的地方将产生相应的
警告信息: 
[david@DAVID david]$ gcc -pedantic bad.c -o bad
bad.c: In function 'main':
bad.c:4: warning: ISO C89 does not support 'long long'
bad.c:3: warning: return type of 'main' is not 'int'
需要注意的是,-pedantic编译选项并不能保证被编译程序与 ANSI/ISO C标准的完全兼
容,它仅仅用来帮助Linux程序员离这个目标越来越近。换句话说,-pedantic选项能够帮助
程序员发现一些不符合 ANSI/ISO C标准的代码,但不是全部。事实上只有 ANSI/ISO C语
言标准中要求进行编译器诊断的那些问题才有可能被gcc发现并提出警告。
除了-pedantic之外,gcc还有一些其他编译选项也能够产生有用的警告信息。这些选项
大多以-W 开头, 其中最有价值的当数-Wall了, 使用它能够使 gcc产生尽可能多的警告信息。
例如: 
[david@DAVID david]$ gcc -Wall bad.c -o bad
bad.c:3: warning: return type of 'main' is not 'int'
bad.c: In function 'main':
bad.c:4: warning: unused variable 'var'
bad.c:6:2: warning: no newline at end of file
gcc给出的警告信息虽然从严格意义上说不能算作是错误,但很可能成为错误的栖身之
所。一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持简洁、优
美和健壮的特性。 
    在处理警告方面,另一个常用的编译选项是-Werror,它要求 gcc 将所有的警告当
成错误进行处理,这在使用自动编译工具(如 make 等)时非常有用。如果编译时带上-Werror
选项,那么gcc会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改。只有当相应的警告信息消除时,才可能将编译过程继续朝前推进。执行情况如下: 
[david@DAVID david]$ gcc -Werror bad.c -o bad
cc1: warnings being treated as errors
bad.c: In function 'main':
bad.c:3: warning: return type of 'main' is not 'int'
bad.c:6:2: no newline at end of file
对Linux 程序员来讲,gcc给出的警告信息是很有价值的,它们不仅可以帮助程序员写
出更加健壮的程序,而且还是跟踪和调试程序的有力工具。建议在用 gcc编译源代码时始终
带上-Wall选项,并把它逐渐培养成为一种习惯,这对找出常见的隐式编程错误很有帮助。 
库  依  赖
在 Linux 下使用 C 语言开发应用程序时,完全不使用第三方函数库的情况是比较少见
的,通常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能。从程序员的角度
看,函数库实际上就是一些头文件(.h)和库文件(.so 或者.a)的集合。虽然 Linux 下大多数函
数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下,但并不是所有
的情况都是这样。正因如此,gcc在编译时必须让编译器知道如何来查找所需要的头文件和
库文件。 
gcc 采用搜索目录的办法来查找所需要的文件,-I 选项可以向 gcc 的头文件搜索路径中
添加新的目录。例如,如果在/home/david/include/目录下有编译时所需要的头文件,为了让
gcc能够顺利地找到它们,就可以使用-I选项: 
[david@DAVID david]$ gcc david.c -I /home/david/include -o david
同样,如果使用了不在标准位置的库文件,那么可以通过-L 选项向 gcc 的库文件搜索
路径中添加新的目录。例如,如果在/home/david/lib/目录下有链接时所需要的库文件
libdavid.so,为了让gcc能够顺利地找到它,可以使用下面的命令: 
[david@DAVID david]$ gcc david.c -L /home/david/lib –ldavid -o david
值得详细解释一下的是-l 选项,它指示 gcc 去连接库文件 david.so。Linux 下的库文件
在命名时有一个约定,那就是应该以 lib三个字母开头。由于所有的库文件都遵循了同样的
规范,因此在用-l 选项指定链接的库文件名时可以省去 lib 三个字母。也就是说 gcc 在对-l
david进行处理时,会自动去链接名为 libdavid.so的文件。 
Linux 下的库文件分为两大类,分别是动态链接库(通常以.so 结尾)和静态链接库(通常
以.a 结尾),两者的差别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译
时静态加载的。默认情况下,gcc在链接时优先使用动态链接库,只有当动态链接库不存在
时才考虑使用静态链接库。如果需要的话可以在编译时加上-static选项,强制使用静态链接
库。例如,如果在/home/david/lib/目录下有链接时所需要的库文件 libfoo.so和 libfoo.a,为了
让gcc在链接时只用到静态链接库,可以使用下面的命令: 
[david@DAVID david]$ gcc foo.c -L /home/david/lib -static –ldavid -o   
david gcc代码优化
代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重
新进行组合,目的是改善程序的执行性能。gcc提供的代码优化功能非常强大,它通过编译
选项-On来控制优化代码的生成,其中 n 是一个代表优化级别的整数。对于不同版本的 gcc
来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从 0 变化到
2或3。
编译时使用选项-O 可以告诉 gcc同时减小代码的长度和执行时间,其效果等价于-O1。
在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转
(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。
选项-O2告诉gcc除了完成所有-O1 级别的优化之外,同时还要进行一些额外的调整工
作,如处理器指令调度等。
选项-O3则除了完成所有-O2级别的优化之外,还包括循环展开和其他一些与处理器特
性相关的优化工作。
通常来说, 数字越大优化的等级越高,同时也就意味着程序的运行速度越快。 许多Linux
程序员都喜欢使用-O2 选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较
理想的平衡点。 
下面通过具体实例来感受一下gcc的代码优化功能,所用程序如例 3-3所示。 
实例3-3   count.c
 
#include <stdio.h>
 int main(void)
{  double counter; 
   double result; 
   double temp; 
   for (counter = 0; counter < 4000.0 * 4000.0 * 4000.0  / 20.0 + 2030;    
counter += (5 - 3 +2 + 1 ) / 4)
     {  temp = counter / 1239; 
        result  = counter;    
       }  
       printf("Result is %lf/n", result);  
       return 0;
}
首先不加任何优化选项进行编译: 
[david@DAVID david]$ gcc -Wall count.c -o count
借助 Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间: 
[david@DAVID david]$ time ./count
Result is 3200002029.000000
real    1m59.357s
user    1m59.140s
sys     0m0.050s
接下来使用优化选项来对代码进行优化处理: 
[david@DAVID david]$ gcc -Wall count.c -o count2 在同样的条件下再次测试一下运行时间: 
[david@DAVID david]$ time ./count2
Result is 3200002029.000000
real    0m26.573s
user    0m26.540s
sys     0m0.010s
对比两次执行的输出结果不难看出,程序的性能的确得到了很大幅度的改善,由原来
的1 分59 秒缩短到了26 秒。这个例子是专门针对 gcc的优化功能而设计的,因此优化前
后程序的执行速度发生了很大的改变。尽管 gcc 的代码优化功能非常强大,但作为一名优
秀的 Linux 程序员, 首先还是要力求能够手工编写出高质量的代码。如果编写的代码简短,
并且逻辑性强,编译器就不会做更多的工作,甚至根本用不着优化。
优化虽然能够给程序带来更好的执行性能,但在如下一些场合中应该避免优化代码。 
●  程序开发的时候:优化等级越高,消耗在编译上的时间就越长,因此在开发的时候
最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的
代码进行优化。 
●  资源受限的时候:一些优化选项会增加可执行代码的体积,如果程序在运行时能够
申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,
因为由这带来的负面影响可能会产生非常严重的后果。 
●  跟踪调试的时候:在对代码进行优化的时候,某些代码可能会被删除或改写,或者
为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。 
加     速 
在将源代码变成可执行文件的过程中,需要经过许多中间步骤,包含预处理、编译、
汇编和连接。这些过程实际上是由不同的程序负责完成的。大多数情况下 gcc可以为 Linux
程序员完成所有的后台工作,自动调用相应程序进行处理。 
这样做有一个很明显的缺点,就是 gcc在处理每一个源文件时,最终都需要生成好几个
临时文件才能完成相应的工作,从而无形中导致处理速度变慢。例如,gcc在处理一个源文
件时,可能需要一个临时文件来保存预处理的输出,一个临时文件来保存编译器的输出,一
个临时文件来保存汇编器的输出,而读写这些临时文件显然需要耗费一定的时间。当软件项
目变得非常庞大的时候,花费在这上面的代价可能会变得很大。 
解决的办法是,使用 Linux 提供的一种更加高效的通信方式—— 管道。它可以用来同
时连接两个程序,其中一个程序的输出将直接作为另一个程序的输入,这样就可以避免使用
临时文件,但编译时却需要消耗更多的内存。 
注意:
在编译过程中使用管道是由 gcc 的-pipe 选项决定的。下面的这条命令就是借助 gcc 的
管道功能来提高编译速度的: 
[david@DAVID david]$ gcc -pipe david.c -o david
在编译小型工程时使用管道,编译时间上的差异可能还不是很明显,但在源代码非常
多的大型工程中,差异将变得非常明显。 gcc常用选项 
gcc作为Linux 下C/C++重要的编译环境,功能强大,编译选项繁多。为了方便大家日
后编译方便,在此将常用的选项及说明罗列出来,见表 3-2。
表3-2    gcc的常用选项
选  项  名  作     用
-c  通知gcc取消连接步骤,即编译源码并在最后生成目标文件
-Dmacro  定义指定的宏,使它能够通过源码中的#ifdef进行检验
-E  不经过编译预处理程序的输出而输送至标准输出
-g3  获得有关调试程序的详细信息,它不能与-o选项联合使用
-Idirectory  在包含文件搜索路径的起点处添加指定目录
-llibrary  提示连接程序在创建最终可执行文件时包含指定的库
-O、-O2、-O3  将优化状态打开,该选项不能与-g选项联合使用
-S  要求编译程序生成来自源代码的汇编程序输出
-v  启动所有警报
.h  预处理文件(标头文件)
-Wall  在发生警报时取消编译操作,即将警报看作是错误
-w  禁止所有的报警
gcc的错误类型及对策 
如果gcc编译器发现源程序中有错误, 就无法继续进行,也无法生成最终的可执行文件。
为了便于修改,gcc给出错误信息,必须对这些错误信息逐个进行分析、处理,并修改相应
的源代码,才能保证源代码的正确编译连接。.gcc给出的错误信息一般可以分为四大类,下
面我们分别讨论其产生的原因和对策。 
●  第一类:C语法错误
错误信息:文件source.c中第n行有语法错误(syntex errror)。这种类型的错误,一般都
是 C 语言的语法错误,应该仔细检查源代码文件中第 n 行及该行之前的程序,有时也需要
对该文件所包含的头文件进行检查。有些情况下,一个很简单的语法错误,gcc会给出一大
堆错误,我们最主要的是要保持清醒的头脑,不要被其吓倒,必要的时候再参考一下 C 语
言的基本教材。在这里推荐一本由Andrew Koenig写的《C 陷阱与缺陷》(此书已由人民邮
电出版社翻译出版),说得夸张一点就是此书可以帮助你减少 C 代码和初级 C++代码中的
90%的bug。
●  第二类:头文件错误 
错误信息:找不到头文件head.h(Can not find include file head.h)。这类错误是源代码文
件中包含的头文件有问题,可能的原因有头文件名错误、指定的头文件所在目录名错误等,
也可能是错误地使用了双引号和尖括号。 
●  第三类:档案库错误 
错误信息:连接程序找不到所需的函数库,例如: 
ld: -lm: No such file or directory  这类错误是与目标文件相连接的函数库有错误,可能的原因是函数库名错误、指定的
函数库所在目录名称错误等。检查的方法是使用 find 命令在可能的目录中寻找相应的函数
库名,确定档案库及目录的名称并修改程序中及编译选项中的名称。 
●  第四类:未定义符号 
错误信息:有未定义的符号(Undefined  symbol)。这类错误是在连接过程中出现的,可
能有两种原因: 一是用户自己定义的函数或者全局变量所在源代码文件, 没有被编译、 连接,
或者干脆还没有定义,这需要用户根据实际情况修改源程序,给出全局变量或者函数的定义
体;二是未定义的符号是一个标准的库函数,在源程序中使用了该库函数,而连接过程中还
没有给定相应的函数库的名称, 或者是该档案库的目录名称有问题,这时需要使用档案库维
护命令 ar 检查我们需要的库函数到底位于哪一个函数库中,确定之后,修改 gcc 连接选项
中的-l和-L项。 
排除编译、连接过程中的错误,应该说只是程序设计中最简单、最基本的一个步骤,
可以说只是开了个头。这个过程中的错误,只是我们在使用 C 语言描述一个算法中所产生
的错误,是比较容易排除的。我们写一个程序,到编译、连接通过为止,应该说刚刚开始,
程序在运行过程中所出现的问题,是算法设计有问题,说得严重点儿是对问题的认识和理解
不够,还需要更加深入地测试、调试和修改。一个程序,稍为复杂的程序,往往要经过多次
的编译、连接、测试和修改。  gcc是在Linux 下开发程序时必须掌握的工具之一。
以上对gcc作了一个简要的介绍,主要讲述了如何使用gcc编译程序、产生警告信息、
和加快 gcc 的编译速度。对所有希望早日跨入 Linux 开发者行列的人来说,gcc 就是成为一
名优秀的Linux程序员的起跑线。 关于调试  C 程序的更多信息请看第 4 章关于 gdb的内容。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值