GCC学习笔记
本文出自https://shuwoom.com博客,欢迎访问!
(1)简单介绍
首先用vi编辑器创建一个c程序文件(以.c结尾)
如:
vi hello.c
#include <stdio.h>
int main()
{
printf(“Hello World!\n”);
return 0;
}
创建好hello.c文件后,保存退出,接下来就是进行编译程序。
gcc hello.c
此时,用ls -al命令查看当前目录下的文件,可以发现,多出了一个a.out文件,注意该文件权限的最后一列是x,即表示该文件是可执行文件,让我们执行文件看看会有什么结果!
./a.out
命令窗口出现了“Hello World”。但现在有个问题,如果我们不想生成的可执行使用默认名称,那要怎么办呢?这里可以使用-o这一选项决定生成文件的名称,我们就给执行文件命名为run吧。
gcc hello.c -o run
这次,在用ls -al查看当前目录,发现出现了run可执行文件,再次运行该文件:
./run
结果和./a.out是一样的。
(2)wall选项
有了上面的基础后,我们继续接下来的教程。这次我们修改上面的.c文件:
vi hello.c
使之内容如下,实现简单的相加功能:
#include <stdio.h>
int main()
{
double x = 1.3, y = 2.4;
printf(“sum:%d”, x + y);
return 0;
}
仔细的观察,会发现,输出格式上问题,我们要输出的是一个double型的数据,但输出格式是按照整形输出,虽说这不影响程序的运行,但却可能是一个隐藏的错误。让我们按照上一节的方法编译文件:
gcc hello.c -o run
结果,编译器没有提示任何的警告。这对于一些对代码规范要求比较严格的人来说,是一个严重的漏洞。那么,要怎样才可以看到提示警告呢。我们可以使用-Wall选项来显示警告。
gcc -Wall hello.c -o run
这次编译器就显示警告消息了。接下来,我们的大部分例子都是使用-Wall这一选项,我会在之后的内容中对-Wall的使用进一步详解。
(3)多文件编译
在编程中,我们经常会调用其他文件的函数,因为不可能把所有的函数都在一个文件上实现,这样会让代码很难管理。那么如果在linux上,存在多文件关系又要怎样编译呢?下面我举一个简单例子:
首先是,add.c文件
vi add.c
int add(int x, int y){
return x + y;
}
接着就是包含该函数声明的头文件math.h
vi math.h
int add(int x, int y);
然后是调用add这一函数的main.c文件:
vi main.c
#include <stdio.h>
#include”math.h”
int main()
{
int sum = 0;
sum = add(4, 3);
printf(“sum:%d\n”, sum);
return 0;
}
这里,我简单地讲讲gcc编译器的工作流程,首先是对源程序进行预处理,然后是生成目标文件,最后是将目标文件链接在一起形成可执行文件。大概的流程是这样,想要知道更具体的细节,大家可以自己到网上去找相关的资料,这里我就不展开讲了。接下来就是生成目标文件:
gcc -Wall -c add.c
gcc -Wall -c main.c
使用ls -al查看当前目录,可以见到新增了两个目标文件,分别是add.o 和main.o。这里,我们使用-c表示只生成目标文件,不生成可执行文件。所以编译其并没有报错。接下来就是将两个目标文件链接在一起形成最终的可执行文件。
gcc -Wall main.o add.o -o result
查看当前目录,这是就出现了result这一可执行文件,让我们运行看看是否程序是否正确。
./result
没错,输出的结果就是7。这是大家就会疑问了,难道就没有简单一点的方法吗,其实上面的步骤可以用一句命令来代替:
gcc -Wall main.c add.c -o result
我之所以将上面一种比较麻烦的方法是想让大家了解一下gcc的编译流程,因为到了后面我们还会继续用到这种方法。
这时,一些细心的朋友可能会发现math.h文件没有在命令行列,那是因为#include”math.h”中编译器已经将math.h中的内容包含到main.c中了。
(4)多文件编译2
上面的程序很简单,这次我们创建一个更复杂点的程序,让我们把math.h头文件下的函数补充完善
, 分别追加minus.c divide.c以及multiply.c其实就是把我们常用的加减乘除功能全部实现。
vi minus.c
int minus(int x, int y){
return x – y;
}
vi divide.c
int divide(int x, int y){
return x / y;
}
vi multiply.c
int multiply(int x, int y){
return x * y;
}
然后是将各个函数的声明添加之math.h文件中,
vi math.h
int add(int x, int y);
int minus(int x, int y);
int divide(int x, int y);
int multiply(int x, int y);
同时,我们修改一下main.c,让其调用divide函数:
vi main.c
#include <stdio.h>
#include”math.h”
int main()
{
int sum = 0;
sum = add(4, 3);
printf(“sum:%d\n”, sum);
int result = 0;
result = divide(4,2);
printf(“result:%d”, result);
return 0;
}
这次由于main.c中,我们只调用了add和divide函数,则编译命令如下:
gcc -Wall main.c add.c divide.c -o result
查看当前目录,result已经生成,运行一下程序:
./result
结果正常。我们继续把剩下的两个minus.c和multiply.c生成目标文件,供以后使用。这时用ls -al查看当前目录,我们就有add.o、minus.o、divide.o、multiply.o和main.o四个目标文件。
让我们回过头来看看前面的代码,细心的朋友会发现,我们的divide函数有个漏洞,如果分母y为0,那么程序就会出错,我们再次编辑divide.c文件:
vi divide.c
添加一个判断语句即可。
#include <stdio.h> //由于我们调用了printf系统自带的函数
int divide(int x, int y){
if(y == 0){
printf(“y can't be zero\n”);
return 0;
}
return x / y;
}
然后,我们修改main.c,测试一下分母为0时是否可以正常运行。
vi main.c
#include <stdio.h>
#include”math.h”
int main()
{
int sum = 0;
sum = add(4, 3);
printf(“sum:%d\n”, sum);
int result = 0;
result = divide(4,0);
printf(“result:%d”, result);
return 0;
}
这时我们就有疑问了,当前目录下的result可执行程序是修改之前的编译生成的可执行文件,那么修改之后就得重新编译一次。这里,由于我们使用的是多文件的方法,那么只需重新编译那些改动过的文件
,这里我们需要重新编译divide.c和main.c文件,前面一个大家自然理解,至于后面一个呢,那是因为main.c函数调用了divide函数,那要想使用更新后的函数,自然就的重新编译编译一边才可以享受更新服务了,这其实就是linux上常说的文件依赖。一个文件A依赖文件B,当文件B发生改动时,不仅文件B要重新编译,文件A也要重新编译一遍。这里由于其他文件没有发生改动,就无需重新编译一遍,
这样就大大的提高了编译的效率,这也是我们使用多文件管理的原因之一。
好了,说了那么多,让我们直接运行下代码看看结果先吧!
gcc -Wall main.o add.o divide.o -o result
结果显示说,分母y不能为0,看来程序正常运行了。
这次教程就到此为止。
(5)动态链接和静态链接
这一次教程,我们来说说关于linux下的库。库说白了其实就是一个个目标文件的集合。linux上一共有两种库文件类型,一种是以.a格式的静态链接库,另一种是以.so格式的动态链接库。下面我们来讲讲静态链接库。上一次的教程,我们生成了四个目标文件,分别add.o、minus.o、divide.o、multiply.o。这次,我们使用ar程序把这四个目标文件打包成一个静态链接库,以后要调用多个函数时,就不必一个一个的链接对应的目标文件。废话不多说,我们直接用代码来解释:
ar的使用格式:
ar cr libName.a file1.o file2.o file3.o …...
注释:libName.a中前缀lib和后缀.a固定,Name是静态链接库的名称。
ar cr libtest.a add.o、minus.o、divide.o、multiply.o
执行命令后,我们查看当前目录下的文件,就多出了一个libtest.a文件。接着,我们就直接使用libtest.a来重新编译源程序:
gcc -Wall main.c libtest.a -o result2
程序结果和之前的一样。我们也可以使用下面这种方法:
gcc -Wall main.c -L -ltest. -o result2
注意,上面的”-L.”不能缺少,因为,使用”-l“,编译器查找的是系统默认的库文件地址,而不是当前目录,故需要使用-L来说明库文件地址,由于我们的libtest.a在当前目录下,故直接使用“.”表示当前目录。以后,我们向别人提供第三方函数库时,如果不想让别人看到源代码,那么就可以只提供.a静态链接库和包含所有函数声明的头文件即可。
a.静态链接
在之前所有的教程里,我们都是把生成的目标文件、静态链接库以及头文件放在同一个文件夹下,这样不仅显得很杂乱,也不便管理源文件,一旦程序的文件数目庞大后,问题就愈加突出。下面我们就对程序文件整理一下,一般来说,头文件放在include 文件夹下,静态链接库lib文件夹下。下面先创建两个文件夹,并把相应的文件移动到对应文件夹下。
mkdir include
mkdir lib
mv libtest.a lib
mv math.h include
由于我们已经将四个目标文件(add.o、minus.o、divide.o、multiply.o)打包成静态链接哭libtest.a,那么就可以删除这四个文件:
rm add.o minus.o divide.o multiply.o
这是当前目录就只剩下main.c、add.c、minus.c、divide.c、multiply.c这五个源文件。
首先,我们用比较麻烦的方法一步步地来编译源文件,首先是生成目标文件,由于add.c、minus.c、divide.c、multiply.c这四个源代码的目标文件的已经生成并被打包到静态链接库中,故我们只需生成main.c的目标文件即可:
gcc -Wall -Iinclude -c main.c
这里-Iinclude中-I后面接的是main.c中所引用头文件(math.h)的地址,这里使用的是相对地址,其实也可以使用绝对地址,当由于不同的linux系统,他们的文件位置可能有写差异,这就造成我们程序的移植性很差,所以还是建议大家使用相对地址。
接下就链接目标文件即可。
gcc -Wall main.o -Llib -ltest -o result3
这里-Llib是-L后面接的是静态链接库的地址,由于我们的libtest.a在lib中,故接lib。
b.动态链接
动态链接的一个好处是,同一个库文件资源,可以供多个程序共享使用,可以省下许多内存空间。但是,在移植到另一个操作系统环境时,如果该系统没有相应的动态链接库或设置相应的路径,那么程序就无法运行,这在移植性方面,相对静态链接方式就差一些。下面开始进入主题:
动态链接库的后缀是.so(在linux上,windows上是.dll)。其命名方式为:libxxx.so,
创建方法:
gcc -c -fpic A.c B.c .............
gcc -shared A.o B.o .... -o libxxx.so
这里-fpic:由于共动态链接库不同,他们在运行时加载,因而地址是很随机的,加上-fpic选项,可以保证编译生成的东西是地址无关的。
我们这次创建一个libtest.so动态链接库。
gcc -c -fpic add.c minus.c divide.c multiply.c
gcc -shared add.o minus.o divide.o multiply.o -o libtest.so
现在,我们已经生成了一个libtest.so动态链接库。
编译链接:
gcc -Wall main.c -L. -ltest -o run
-L.表示库文件在当前目录下,-ltest就是libtest.so文件,注意,这里不是运行时,而是编译链接过程。。。
现在我们运行run执行文件,这时,会报错,说找不到libtest.so文件。
这是因为,程序运行时,查找动态链接库文件是到默认的系统目录下找的。解决办法有两个:
一、将libtest.so复制到/usr/lib目录下
二、修改路径:export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH,在路径下添加当前路径。
再次运行执行文件,这次就可以正常运行了。