gcc编译器学习
gcc and g++分别是gnu的c&c++编译器
gnu编译器gcc/g++在执行编译工作的时候,总共需要4步
1.预处理,生成.i的文件[预处理器cpp]
2.将预处理后的文件不转换成汇编语言,生成文件.s[编译器egcs]
3.有汇编变为目标代码(机器代码)生成.o的文件[汇编器as]
4.连接目标代码,生成可执行程序[链接器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.c wc-l 3199 几乎有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 1exit status译成功了,因为我们的语法是正确的。但是连接步骤却失败了,因为我们没有告诉编译器'shuffle'函数在哪里。那么,到底什么是连接?我们怎样告诉编译器到哪里寻这个函数呢?连接
连接器ld,使用下面的命令,接受前面由as创建的目标文件并把它转换为可执行文件
gcc-o game game.o shuffle.o这将把两个目标文件组合起来并创建可执行文件game。
连接器从shuffle.o目标文件中到shuffle函数,并把它包括进可执行文件。目标文件的真正好处在于,如果我们想再次使用那个函数,我们所要做的就是包含"deck.h"文件并把shuffle.o目标文件连接到新的可执行文件中。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论