标签: it |
gcc and g++分别是gnu的c & c++编译器
1.预处理,生成.i的文件[预处理器cpp]
2.将预处理后的文件转换成汇编语言,生成文件.s[编译器egcs]
3.将汇编变为目标代码(机器代码)生成.o的文件[汇编器as]
4.连接目标代码,生成可执行程序[链接器ld]
开始...使用GCC编译程序时,编译过程可以被细分为四个阶段:
◆ 预处理(Pre-Processing)
◆ 编译(Compiling)
◆ 汇编(Assembling)
◆ 链接(Linking)
.c为后缀的文件,C语言源代码文件;
.a为后缀的文件,是由目标文件构成的档案库文件;
.C,.cc或.cxx 为后缀的文件,是C++源代码文件;
.h为后缀的文件,是程序所包含的头文件;
.i 为后缀的文件,是已经预处理过的C源代码文件;
.ii为后缀的文件,是已经预处理过的C++源代码文件;
.m为后缀的文件,是Objective-C源代码文件;
.o为后缀的文件,是编译后的目标文件;
.s为后缀的文件,是汇编语言源代码文件;
.S为后缀的文件,是经过预编译的汇编语言源代码文件。
命令gcc首先调用cpp进行预处理,在预处理过程中,对源代码文件中的文件包含(include)、预编译语句(如宏定义define等)进行分析。接着调用cc1进行编译,这个阶段根据输入文件生成以.o为后缀的目标文件。汇编过程是针对汇编语言的步骤,调用as进行工作,一般来讲,.S为后缀的汇编语言源代码文件和汇编、.s为后缀的汇编语言文件经过预编译和汇编之后都生成以.o为后缀的目标文件。当所有的目标文件都生成之后,gcc就调用 ld来完成最后的关键性工作,这个阶段就是连接。在连接阶段,所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中连到合适的地方。
首先,我们应该知道如何调用编译器。实际上,这很简单。我们将从那个著名的第一个C程序开始。
#include <stdio.h>
int main()
{
printf("Hello World!/n");
}
把这个文件保存为 game.c
。 你可以在命令行下编译它:
gcc game.c
在默认情况下,C编译器将生成一个名为 a.out
的可执行文件。你可以键入如下命令运行它:
a.out Hello World 每一次编译程序时,新的a.out
将覆盖原来的程序。你无法知道是哪个程序创建了a.out
。我们可以通过使用-o
编译选项,告诉 gcc我们想把可执行文件叫什么名字。我们将把这个程序叫做game
,我们可以使用任何名字,因为C没有Java那样的命名限制。
gcc -o game game.c
game Hello World 到现在为止,我们离一个有用的程序还差得很远。如果你觉得沮丧,你可以想一想我们已经编译并运行了一个程序。因为我们将一点一点为这个程序添加功能,所以我们必须保证让它能够运行。似乎每个刚开始学编程的程序员都想一下子编一个1000行的程序,然后一次修改所有的错误。没有人,我是说没有人,能做到这个。你应该先编一个可以运行的小程序,修改它,然后再次让它运行。这可以限制你一次修改的错误数量。另外,你知道刚才做了哪些修改使程序无法运行,因此你知道应该把注意力放在哪里。这可以防止这样的情况出现:你认为你编写的东西应该能够工作,它也能通过编译,但它就是不能运行。请切记,能够通过编译的程序并不意味着它是正确的。
下一步为我们的游戏编写一个头文件。头文件把数据类型和函数声明集中到了一处。这可以保证数据结构定义的一致性,以便程序的每一部分都能以同样的方式看待一切事情。
#ifndef DECK_H
#define DECK_H
#define DECKSIZE 52 typedef struct deck_t
{
int card[DECKSIZE];
int dealt;
}deck_t;
#endif
把这个文件保存为 deck.h
。只能编译 .c
文件,所以我们必须修改 game.c。在game.c的第2行,写上 #include "deck.h"
。在第5行写上 deck_t deck;
。为了保证我们没有搞错,把它重新编译一次。
gcc -o game game.c
如果没有错误,就没有问题。如果编译不能通过,那么就修改它直到能通过为止。
预编译
编译器是怎么知道 deck_t
类型是什么的呢?因为在预编译期间,它实际上把"deck.h"文件复制到了"game.c"文件中。源代码中的预编译指示以"#"为前缀。你可以通过在gcc后加上 -E
选项来调用预编译器。
gcc -E -o game_precompile.txt game.c
wc -l game_precompile.txt
3199 game_precompile.txt 几乎有3200行的输出!其中大多数来自stdio.h
包含文件,但是如果你查看这个文件的话,我们的声明也在那里。如果你不用-o
选项指定输出文件名的话,它就输出到控制台。预编译过程通过完成三个主要任务给了代码很大的灵活性。
- 把"include"的文件拷贝到要编译的源文件中。
- 用实际值替代"define"的文本。
- 在调用宏的地方进行宏替换。
这就使你能够在整个源文件中使用符号常量(即用DECKSIZE表示一付牌中的纸牌数量),而符号常量是在一个地方定义的,如果它的值发生了变化,所有使用符号常量的地方都能自动更新。在实践中,你几乎不需要单独使用 -E
选项,而是让它把输出传送给编译器。
编译
作为一个中间步骤,gcc把你的代码翻译成汇编语言。它一定要这样做,它必须通过分析你的代码搞清楚你究竟想要做什么。如果你犯了语法错误,它就会告诉你,这样编译就失败了。人们有时会把这一步误解为整个过程。但是,实际上还有许多工作要gcc去做呢。
汇编
as
把汇编语言代码转换为目标代码。事实上目标代码并不能在CPU上运行,但它离完成已经很近了。编译器选项 -c
把 .c 文件转换为以 .o 为扩展名的目标文件。 如果我们运行
gcc -c game.c
我们就自动创建了一个名为game.o的文件。这里我们碰到了一个重要的问题。我们可以用任意一个 .c 文件创建一个目标文件。正如我们在下面所看到的,在连接步骤中我们可以把这些目标文件组合成可执行文件。让我们继续介绍我们的例子。因为我们正在编写一个纸牌游戏,我们已经把一付牌定义为 deck_t
,我们将编写一个洗牌函数。这个函数接受一个指向deck类型的指针,并把一付随机的牌装入deck类型。它使用'drawn' 数组跟踪记录那些牌已经用过了。这个具有DECKSIZE个元素的数组可以防止我们重复使用一张牌。
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "deck.h"
static time_t seed = 0;
void shuffle(deck_t *pdeck)
{
int drawn[DECKSIZE] = {0};
int i;
if(0 == seed)
{
seed = time(NULL);
srand(seed);
}
for(i = 0; i < DECKSIZE; i++)
{
int value = -1;
do
{
value = rand() % DECKSIZE;
}
while(drawn[value] != 0);
drawn[value] = 1;
printf("%i/n", value);
pdeck->card[i] = value;
}
pdeck->dealt = 0;
return;
}
把这个文件保存为 shuffle.c
。我们在这个代码中加入了一条调试语句,以便运行时,能输出所产生的牌号。这并没有为我们的程序添加功能,但是现在到了关键时刻,我们看看究竟发生了什么。因为我们的游戏还在初级阶段,我们没有别的办法确定我们的函数是否实现了我们要求的功能。使用那条printf语句,我们就能准确地知道现在究竟发生了什么,以便在开始下一阶段之前我们知道牌已经洗好了。在我们对它的工作感到满意之后,我们可以把那一行语句从代码中删掉。这种调试程序的技术看起来很粗糙,但它使用最少的语句完成了调试任务。以后我们再介绍更复杂的调试器。
请注意两个问题。
- 我们用传址方式传递参数,你可以从'&'(取地址)操作符看出来。这把变量的机器地址传递给了函数,因此函数自己就能改变变量的值。也可以使用全局变量编写程序,但是应该尽量少使用全局变量。指针是C的一个重要组成部分,你应该充分地理解它。
- 我们在一个新的 .c 文件中使用函数调用。操作系统总是寻找名为'main'的函数,并从那里开始执行。
shuffle.c
中没有'main'函数,因此不能编译为独立的可执行文件。我们必须把它与另一个具有'main'函数并调用'shuffle'的程序组合起来。
运行命令
gcc -c shuffle.c
并确定它创建了一个名为 shuffle.o
的新文件。编辑game.c文件,在第7行,在 deck_t类型的变量 deck
声明之后,加上下面这一行:
shuffle(&deck);
现在,如果我们还象以前一样创建可执行文件,我们就会得到一个错误
gcc -o game game.c
/tmp/ccmiHnJX.o: In function `main':
/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle'
collect2: ld returned 1 exit status
译成功了,因为我们的语法是正确的。但是连接步骤却失败了,因为我们没有告诉编译器'shuffle'函数在哪里。那么,到底什么是连接?我们怎样告诉编译器到哪里寻找这个函数呢?
连接
连接器ld
,使用下面的命令,接受前面由 as
创建的目标文件并把它转换为可执行文件
gcc -o game game.o shuffle.o
这将把两个目标文件组合起来并创建可执行文件 game
。
连接器从shuffle.o目标文件中找到 shuffle
函数,并把它包括进可执行文件。目标文件的真正好处在于,如果我们想再次使用那个函数,我们所要做的就是包含"deck.h" 文件并把 shuffle.o
目标文件连接到新的可执行文件中。
象这样的代码重用是经常发生的。虽然我们并没有编写前面作为调试语句调用的 printf
函数,连接器却能从我们用 #include <stdlib.h>
语句包含的文件中找到它的声明,并把存储在C库(/lib/libc.so.6)中的目标代码连接进来。这种方式使我们可以使用已能正确工作的其他人的函数,只关心我们所要解决的问题。这就是为什么头文件中一般只含有数据和函数声明,而没有函数体。一般,你可以为连接器创建目标文件或函数库,以便连接进可执行文件。我们的代码可能产生问题,因为在头文件中我们没有放入任何函数声明。为了确保一切顺利,我们还能做什么呢?
另外两个重要选项
-Wall
选项可以打开所有类型的语法警告,以便帮助我们确定代码是正确的,并且尽可能实现可移植性。当我们使用这个选项编译我们的代码时,我们将看到下述警告:
game.c:9: warning: implicit declaration of function `shuffle'
这让我们知道还有一些工作要做。我们需要在头文件中加入一行代码,以便告诉编译器有关 shuffle
函数的一切,让它可以做必要的检查。听起来象是一种狡辩,但这样做可以把函数的定义与实现分离开来,使我们能在任何地方使用我们的函数,只要包含新的头文件并把它连接到我们的目标文件中就可以了。下面我们就把这一行加入deck.h中。
void shuffle(deck_t *pdeck); 这就可以消除那个警告信息了。
另一个常用编译器选项是优化选项 -O#
(即 -O2)。这是告诉编译器你需要什么级别的优化。编译器具有一整套技巧可以使你的代码运行得更快一点。对于象我们这种小程序,你可能注意不到差别,但对于大型程序来说,它可以大幅度提高运行速度。你会经常碰到它,所以你应该知道它的意思。
调试
我们都知道,代码通过了编译并不意味着它按我们得要求工作了。你可以使用下面的命令验证是否所有的号码都被使用了
game | sort - n | less
并且检查有没有遗漏。如果有问题我们该怎么办?我们如何才能深入底层查找错误呢?
你可以使用调试器检查你的代码。大多数发行版都提供著名的调试器:gdb。如果那些众多的命令行选项让你感到无所适从,那么你可以使用KDE提供的一个很好的前端工具 KDbg。还有一些其它的前端工具,它们都很相似。要开始调试,你可以选择 File->Executable 然后找到你的 game
程序。当你按下F5键或选择 Execution->从菜单运行时,你可以在另一个窗口中看到输出。怎么回事?在那个窗口中我们什么也看不到。不要担心,KDbg没有出问题。问题在于我们在可执行文件中没有加入任何调试信息,所以KDbg不能告诉我们内部发生了什么。编译器选项 -g
可以把必要的调试信息加入目标文件。你必须用这个选项编译目标文件(扩展名为.o),所以命令行成了:
gcc -g -c shuffle.c game.c
gcc -g -o game game.o shuffle.o
这就把钩子放入了可执行文件,使gdb和KDbg能指出运行情况。调试是一种很重要的技术,很值得你花时间学习如何使用。调试器帮助程序员的方法是它能在源代码中设置“断点”。现在你可以用右键单击调用 shuffle
函数的那行代码,试着设置断点。那一行边上会出现一个红色的小圆圈。现在当你按下F5键时,程序就会在那一行停止执行。按F8可以跳入shuffle函数。呵,我们现在可以看到 shuffle.c
中的代码了!我们可以控制程序一步一步地执行,并看到究竟发生了什么事。如果你把光标暂停在局部变量上,你将能看到变量的内容。太好了。这比那条 printf
语句好多了,是不是?
小结
本文大体介绍了编译和调试C程序的方法。我们讨论了编译器走过的步骤,以及为了让编译器做这些工作应该给gcc传递哪些选项。我们简述了有关连接共享函数库的问题,最后介绍了调试器。真正了解你所从事的工作还需要付出许多努力,但我希望本文能让你正确地起步。你可以在 gcc
、 as
和 ld
的 man
和 info
page中找到更多的信息。
自己编写代码可以让你学到更多的东西。作为练习你可以以本文的纸牌游戏为基础,编写一个21点游戏。那时你可以学学如何使用调试器。使用GUI的KDbg开始可以更容易一些。如果你每次只加入一点点功能,那么很快就能完成。切记,一定要保持程序一直能运行!
要想编写一个完整的游戏,你需要下面这些内容:
- 一个纸牌玩家的定义(即,你可以把deck_t定义为player_t)。
- 一个给指定玩家发一定数量牌的函数。记住在纸牌中要增加“已发牌”的数量,以便能知道还有那些牌可发。还要记住玩家手中还有多少牌。
- 一些与用户的交互,问问玩家是否还要另一张牌。
- 一个能打印玩家手中的牌的函数。 card 等于value % 13 (得数为0到12),suit 等于 value / 13 (得数为0到3)。
- 一个能确定玩家手中的value的函数。Ace的value为零并且可以等于1或11。King的value为12并且可以等于10。
GCC编译选项解析
GCC是Linux下基于命令行的C语言编译器,其基本的使用语法如下。
gcc [option |filename]…
对于编译C++的源程序,其基本语法如下:
g++ [option |filename]…
其中option为GCC使用时的选项,而filename为需要GCC做编译的处理的的文件名。就GCC来说,其本身是一个十分复杂的的命令,合理的使用其命令选项可以有效地提高程序的编译效率、优化代码,GCC拥有众多的命令选项,有超过100个的编译选项可用,按其应有如下的分类。
-
常用编译选项
-
-c选项:这是GCC命令的常用选项。-c选项告诉GCC仅把源程序编译为目标代码而不做链接工作,所以采用该选项的编译指令不会生成最终的可执行程序,而是生成一个与源程序文件名相同的以.o为后缀的目标文件。例如一个Test.c的源程序经过下面的编译之后会生成一个Test.o文件
-
# gcc –c Test.h
-
-S选项:使用该选项会生成一个后缀名为.s的汇编语言文件,但是同样不会生成可执行程序。
-
-e选项:-e选项只对文件进行预处理,预处理的输出结果被送到标准输出(比如显示器)。
-
-v选项:在Shell的提示符号下键入gcc –v,屏幕上就会显示出目前正在使用的gcc版本的信息.
-
-x language:强制编译器指定的语言编译器来编译某个源程序。
例如下面的指令:
# gcc -x c++ p1.c
该指令表示强制采用C++编译器来编译C程序P1.c。
-
-I<DIR>选项:库依赖选项,指定库及头文件路径。
在Linux下开发程序的时候,统常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能。一般情况下,Linux下的大多数函数都将头文件放到系统/usr/include目录下,而库文件则放到/usr/lib目录下。但在有些情况下并不是这样的,在这些情况下,使用GCC编译时必须指定所需要的头文件和库文件所在的路径。-I选项可以向GCC的头文件搜索路径中添加新的目录<DIR>。例如,一个源程序所依赖的头文件在用户/home/include/目录下,此时就应该使用-I选项来指定。
# gcc –I /home/include -o test test.c
-
-L<DIR>:类似于上面的情况,用来特别指定所依赖库所在的路径
如果使用不在标准位置的库,那么可以通过-L选项向GCC的库文件搜索路径中添加新的目录。例如,一个程序要用到的库libapp.so在/home/zxq/lib/目录下,为了能让GCC能够顺利地链接该库,可以使用下面的指令:
# gcc -Test.c -L /home/zxq/lib/ -lapp –o Test
这里的-L选项表示GCC去链接库文件libapp.so。在Linux下的库文件在命名时遵循了一个约定,那就是应该以lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在使用-L选项指定链接的库文件名时可以省去lib三个字母,也就是说GCC在对-lapp进行处理的时候,会自动去链接名为libapp.so的文件
-
-static选项:GCC在默认情况下链接的是动态库,有时为了把一些函数静态编译到程序中,而无需链接动态库就采用-static选项,它会强制程序连接静态库。
-
-o选项:在默认的状态下,如果GCC指令没有指定编译选项的情况下会在当前目录下生成一个名为a.out的可执行程序,例如:执行# gcc Test.c命令后会生成一个名为a.out的可执行程序。因此,为了指定生成的可执行程序的文件名,就可以采用-o选项,比如下名的指令:
# gcc –o Test Test.c
执行该指令会在当前目录下生成一个名为Test的可执行文件。
-
出错检查和警告提示选项
GCC编译器包含完整的出错检查和警告提示功能,比如GCC提供了30多条警示信息和3个警告级别,使用这些选项有助于增强程序的稳定性和更加完善程序代码的设计,此类选项常用的如下。
-
-pedantic以ANSI/ISO C标准列出的所有警告
当GCC在编译不符合ANSI/ISO C语言标准的源代码时,如果在编译指令中加上了-pedantic选项,那么源程序中使用了扩展语法的地方将产生相应的警告信息。
-
-w禁止输出警告信息
-
-Werror将所有警告转换为错误
Werror选项要求GCC将所有的警告当成错误进行处理,这在使用自动编译工具(如Make等)时非常有用。如果编译时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,只有程序员对源代码进行修改并起相应的警告信息消除时,才能够继续完成后续的编译工作。
-
-Wall显示所有的警告信息
-Wall选项可以打开所有类型的语法警告,以便于确定程序源代码是否是正确的,并且尽可能实现可移植性。
对Linux开发人员来讲,GCC给出的警告信息是很有价值的,它们不仅可以帮助程序员写出更加健壮的程序,而且还是跟踪和调试程序的有力工具。建议在用GCC编译源代码时始终带上-Wall选项,养成良好的习惯。
-
代码优化选项
代码优化是指编译器通过分析源代码找出其中尚未达到最优的部分,然后对其重新进行组合,进而改善代码的执行性能。GCC通过提供编译选项-On来控制优化代码的生成,对于大型程序来说,使用代码优化选项可以大幅度提高代码的运行速度。
-
-
-O选项:编译时使用选项-O可以告诉GCC同时减小代码的长度和执行时间,其效果等价于-O1。
-
-O2选项:选项-O2告诉GCC除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度
-
-
调试分析选项
-
-g选项:生成调试信息,GNU调试器可以利用该信息。GCC编译器使用该选项进行编译时,将调试信息加入到目标文件中,这样gdb调试器就可以根据这些调试信息来跟中程序的执行状态。
-
-pg选项:编译完成后,额外产生一个性能分析所需信息。
-
注意:使用调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时
的开销,因此调试选项统常推荐仅仅在程序开发和调试阶段中使用。
下面举一个简单的例子来说明GCC的编译过程。首先用vi编辑器来编辑一个简单的c程序test.c,程序清单如下。
#include <stdio.h>
int main()
{
printf("Hello,this is a test!/n");
return 0;
}
根据上面的内容,使用gcc命令来编译该程序。
[root@localhost]# gcc –o test test.c
[root@localhost]# ./test
Hello,this is a test!
可以从上面的编译过程看到,编译一个这样的程序非常简单,一条指令即可完成,事实上,这条指令掩盖了很多细节。我们可以从编译器的角度来看上述编译过程,这对于更好理解GCC编译工作原理有很好的帮助。
GCC编译器首先做的工作是预处理:调用-E参数可以让GCC在预处理结束后停止编译过程。
# gcc –E test.c -o test.i
编译器在这一步调用cpp工具来对源程序进行预处理,此时会生成test.i文件,下面部分列出了test.i文件中的内容。