目录
本文用简单的实践方式,介绍下Makefile多文件编程,以及Makefile制作Linux动态库、静态库的方法。
一、多文件编程
为了代码工程的具有较好的可维护性、可读性以及低耦合的要求。一般情况下,工程都会以多文件方式存在,将不同功能的方法函数写在不同的源文件中,供其它文件调用。
写一个简单的例子:
比如现在,我们要计算两个数的加法,和两个数的乘法。
那么,“加法”的功能函数实现单独写在add.c源文件中、“乘法”的功能函数实现单独写在mul.c源文件中。另外,源文件中所有的函数声明,应写在源文件的同名头文件中,在这里,add.c中有一个函数add(),其函数声明写在add.h中;mul.c中有一个函数mul(),其函数声明写在mul.h中,供其它源文件调用。
然后,写一个最简单的main函数,加入两者的头文件,实现对两个功能函数的调用。
基于此,我们现在得到五个文件:add.h、add.c、mul.h、mul.c、main.c。大概,就是这么个关系吧:
下面,使用mkdir命令新建一个目录,然后编写一下这五个文件,五个文件的代码,很简单:
//add.h文件
int add(const int a, const int b);
//add.c文件
#include<stdio.h>
#include"add.h"
int add(const int a, const int b)
{
return a + b;
}
//mul.h文件
int mul(const int a, const int b);
//mul.c文件
#include<stdio.h>
#include"mul.h"
int mul(const int a, const int b)
{
return a * b;
}
//main.c文件
#include<stdio.h>
#include"mul.h"
#include"add.h"
int main(int argc, char* argv[])
{
int a = 10;
int b = 20;
printf("add:%d\tmul:%d\n", add(a,b), mul(a,b));
}
完成后,目录下,应该是这样:
我们通过以下指令,先编译生成各自的目标文件(add.o, mul.o, main.o)。然后进行链接,生成可执行文件main,并执行。
运行正确。(这里gcc编译的知识,如果不会要补充下呦,gcc -E -s -c -o参数分别代表什么意思~)
二、Makefile 简单实例
2.1 简介
Makefile主要用来在一个工程中,管理多文件的编译。
如第一张最后的编译过程,我们写了三个gcc -c命令,分别编译生成了三个.o目标文件,然后又通过gcc,将他们链接,最后生成了可执行文件。如果你改了add.c的函数实现,那么,以上编译、链接步骤就要重新手动执行一次。如果是大型的工程,改其中一个源文件,可不就要根据编译逻辑,敲良久~
Makefile就是你写一个编译规则,这个编译规则用来规定工程中所有源文件的编译顺序,那么以后你改动任何一个源文件,规则是不变的,只需要执行make一下,他就会按照预定的规则将文件重新编译。即便是添加删除文件,只需要在对应的规则上稍作修改即可,一劳永逸,节省时间。
https://www.jianshu.com/p/29eb94f029c7这里介绍了有关Makefile的基本原理,可以看一下。
2.2 格式
Makefile的格式很简单,也很好理解:
******
目标: 依赖
(tab)规则
******
“目标” 是需要生成的目标文件;“依赖”是生成该目标所需的一些文件;“规则”是由依赖文件生成目标文件的手段。用一句话概括,其实就是:把“依赖”根据“规则”编译成“目标”。
要注意:每条“规则”必须以 tab 开头,不能使用空格等。
2.3 实践
在第一章的代码目录下,编写一个Makefile,管理工程:
main: main.o add.o mul.o
gcc main.o add.o mul.o -o main
main.o: main.c
gcc main.c -c
add.o: add.c
gcc add.c -c
mul.o: mul.c
gcc mul.c -c
#.PHONY:clear表示clear是一个伪目标.
#如果不加.PHONY,当前目录下由clear的文件就会执行不了,且提示该文件是最新的.
#rm前边多减号表示:不管出现什么问题,都要做后边多事情
.PHONY:clear
clear:
rm *.o
保存,当前目录含有六个文件:
在当前目录下输入make,执行Makefile编译规则,可见,生成了main执行文件,并且执行后,结果正确:
另外,在我们的Makefile中,有一个clear目标,并且声明了它为.PHONY伪目标。且他没有依赖文件,只有规则,规则是删除所有.o文件。如果我们想这样做的话,直接在当前目录下,输入make+目标名即可,即“make clear”:
三、使用Makefile制作动态库、静态库
3.1 Linux下的库(动态库、静态库)简介
本小节对Linux下的库做了基本的概要总结,摘录博文地址:【C语言】Linux下动态库和静态库详解
3.1.1 什么是库
在windows平台和linux平台下都大量存在着库。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
由于windows和linux的平台不同(主要是编译器、汇编器和连接器的不同),因此二者库的二进制是不兼容的。
3.1.2 库的种类
Linux下的库有两种:静态库和动态库(共享库)。
静态库的后缀是.a;
动态库的后缀是.so
静态库的名字一般为libxxxx.a,其中xxxx是该lib的名称
动态库的名字一般为libxxxx.so.major.minor,xxxx是该lib的名称,major是主版本号, minor是副版本号。当然,主、副版本号也可以不写。
二者的不同点在于:代码被载入的时刻不同。
静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。
动态库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。
3.1.3 库存在的意义
库是别人写好的现有的,成熟的,可以复用的代码,你可以使用但要记得遵守许可协议。
现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
共享库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
3.1.4 在linux下如何制作库文件
静态库制作分两步:
Step 1.由源文件编译生成一堆.o,每个.o里都包含这个编译单元的符号表
Step 2.ar命令将很多.o转换成.a,成为静态库
动态库的制作由gcc加特定参数编译产生。
3.1.5 查看可执行程序依赖的库(ldd命令)
例如# ldd /bin/lnlibc.so.6
=> /lib/libc.so.6 (0×40021000)/lib/ld-linux.so.2
=> /lib/ld- linux.so.2 (0×40000000)
可以看到ln命令依赖于libc库和ld-linux库
3.1.6 如何让系找到库
如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:
1.编辑/etc/ld.so.conf文件,加入库文件所在目录的路径
2.运行ldconfig,该命令会重建/etc/ld.so.cache文件
3.2 Makefile制作静态库
我们在第二章中,用两个源文件add.c、mul.c分别实现了两个数的相加和相乘操作,他们有一个特点,都是数学运算,我们可以把这两个源文件,添加到一个库中,成为一个math库,这个库,就可以用在任何需要计算两数相加、两数相乘的地方。
本节先介绍math静态库(libmath.a)的制作和使用方法。
重写上一章的Makefile:
a.out: main.c libmath.a
gcc main.c -L . -l math -o a.out
#上边这一行: -L参数:指定目录; -l参数:要链接的库,"-l math"可以连写成"-lmath";
#这里注意,math库不要写"labmath.a"
libmath.a: add.o mul.o
ar -r libmath.a add.o mul.o
#上边这一行:将两个.o文件加入math库中
add.o: add.c
gcc add.c -c
mul.o: mul.c
gcc mul.c -c
#.PHONY:clear表示clear是一个伪目标.
#如果不加.PHONY,当前目录下由clear的文件就会执行不了,且提示该文件是最新的.
#rm前边多减号表示:不管出现什么问题,都要做后边多事情
.PHONY:clear
clear:
rm *.o a.out libmath.a
注释应该写的很清楚了,主要过程大概梳理为:编译add.c和mul.c生成目标文件add.o和mul.o,将add.o和mul.o添加到libmath.a库中。最后,让main.c在编译过程中,链接libmath.a静态库,生成最后可执行文件a.out。
执行下make
3.3 Makefile 制作动态库
本节介绍math动态库(libmath.so)的制作和使用方法。
重写Makefile:
a.out: main.c libmath.so
gcc main.c -L . -l math -o a.out
#上边这一行: -L参数:指定目录; -l参数:要链接的库,"-l math"可以连写成"-lmath";
#这里注意,math库不要写"labmath.so"
libmath.so: add.o mul.o
gcc -shared add.o mul.o -o libmath.so
# ar -r libmath.a add.o mul.o 对比一下静态库的方法.
# -shared:表示输出结果是共享库类型
add.o: add.c
gcc -c -fpic add.c
mul.o: mul.c
gcc -c -fpic mul.c
#上边这几行,添加了参数"-fpic",表示将源文件编译成带有PIC标志的目标文件.
.PHONY:clear
clear:
rm *.o a.out libmath.so
#注意,动态库执行时,要把生成多动态库文件移动到/lib目录下!
解释都在上述的Makefile文件中了,不再累述了~
make一下后不能马上执行main可执行文件,因为这时候还没有把动态库放到系统能找到的地方。
我们需要把make生成的动态库libmath.so拷贝到/lib目录,在执行a.out的时候,系统会去/lib中查找相应的动态库,进行链接。其它方法,请参考本文3.2.6小结。
操作和执行结果:
3.4 Makefile的改进(以动态库为例)
前面我们介绍了用Makefile编译多文件、制作动态库和静态库的方法。
其实,这些Makefile的使用是很初级化的,为了让Makefile更好用,学问还是很深的,这里给出一些基本的概念和改进思想。这样看完,应该算是直到Makefile是个什么东西了,以后做项目需要的时候,可以再逐步深入即可。
3.4.1 基本替换和赋值
(1) 变量:
OJBC:一般用来代表.o文件
CC:代表编译
(2) 自动变量:
$@:表示目标文件
$%:如果目标是函数库文件,那么$%表示库文件的成员. 比如静态库f.a,$%表示他的成员m.o sort.o.
$+:所有的依赖文件,不去重复
$^:所有的依赖文件,去重复
$<:第一个依赖文件
$?:所有的依赖文件,空格隔开,这些依赖文件比目标还要新(修改时间比目标晚)
(3) 赋值:
:= 简单赋值,常规理解的赋值,只对当前语句的变量有效
(中间被重新赋值了,前边多不会改变,后边的会改变为新值)
= 递归赋值,赋值语句可能影响多个变量,所有目标变量相关的其它变量都受影响
(最后被赋值是什么,全文引用该变量的地方都变成什么)
?= 条件赋值,如果变量之前未定义,使用符号中的值定义变量,若已定义(之前被赋值过),该赋值语句无效
+= 追加复制,原变量用空格隔开的方式追加一个新值
3.4.2 对3.2中静态库的Makefile改进
CC := gcc
OJBC = add.o
OJBC += mul.o
TARGET := a.out
MAIN_C = main.c
#以下是动态库相关的变量 这里使用递归赋值,math头文件在任何一处改变,全文件变量随之改变.
#libmath.a
A_LIB_NAME = math
A_LIB = lib$(A_LIB_NAME).a
$(TARGET): $(MAIN_C) $(A_LIB)
$(CC) $< -L . -l $(A_LIB_NAME) -o $(TARGET)
$(A_LIB): $(OJBC)
ar -r $@ $^
%.o:%.c
$(CC) $< -c
.PHONY:clear
clear:
rm $(OJBC) $(TARGET) $(A_LIB)
#原来的写法:
#a.out: main.c libmath.a
# gcc main.c -L . -l math -o a.out
#
#libmath.a: add.o mul.o
# ar -r libmath.a add.o mul.o
#
#add.o: add.c
# gcc add.c -c
#mul.o: mul.c
# gcc mul.c -c
#
#.PHONY:clear
#clear:
# rm *.o a.out libmath.a
为什么要用定义CC变量($(CC))呢?为了交叉编译好用,比如,如果想在Ubuntu上交叉编译一个arm架构的应用程序,使之能在arm上运行,那么就需要将makefile中所有的gcc改成 arm-none-linux-gnueabi-gcc交叉编译工具链,为了到时候方便改动,可以在文件启示位置,将编译工具设为变量。其它很多变量也是这个问题,为了方便改名、替换。
为什么要使用%.o、%.c代替文件?很明显,很多相同的.c文件编译成.o文件,他们的规则都是一样的,比如你想在math静态库中增加“减法”方法,是不是makefile中,不用再增加gcc -c生成目标文件的操作了。
为什么使用${OJBC}使用递归赋值?当你想增加新的文件,比如math静态库中增加“减法”,在makefile中,只需要用+=追加赋值的方法,就可以让全文的${OJBC}都改变,方便增删调试。另外还可以用与Makefile的if..else..条件语句中,实现不同情况的变量差异化。
自动变量是很常见的,主要用在“规则”中,对“目标”和“依赖”进行一种符号化的引用(此说法可能不太正规哈,主要是表其意思)。也是一样的,为了方便,为了增减文件的时候,可以更少的更改Makefile,更好的一劳永逸
当然,Makefile还有很多东西可以补充,这里只带大家入门,应对自己写的简单的小工程差不多了,公司中,大的工程一般会有已经写好的Makefile,自己添加代码和文件基本也不会把Makefile结构都改了。总的来说,Makefile看懂,知道思想,用的时候知道查即可吧。
个人认为,没有统一的模板,只有统一的思想~。没有说非得用自动变量或怎么样就好、主要目的是适合自己的工程。