makefile概述
使用 GCC 的命令行进行程序编译在单个文件下是比较方便的;对于一个企业级项目,通常会有许多源文件,有时也会按功能、类型、模块分门别类的放在不同的目录中,有时候也会在一个目录里存放多个程序的源代码,再使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令。
make工具在构造项目的时候需要加载一个叫做makefile
的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。可以说,任何一个Linux源程序都带有一个Makefile文件。
makefile文件有两种命名方式 makefile
和 Makefile
,构建项目的时候在哪个目录下执行构建命令 make,
这个目录下的 makefile 文件就会被加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中。
Makefile的优点
- 管理代码的编译,决定该编译什么文件,编译顺序,以及是否需要重新编译;
- 节省编译时间。如果文件有更改,只需重新编译此文件即可,无需重新编译整个工程;
- 一劳永逸。Makefile通常只需编写一次,后期就不用过多更改。
程序的编译与链接
在windows下,一般可以通过文件的后缀名来识别文件的类型。在Linux下大致上也是可以的。但是要明确的一点是,在linux下,文件的后缀与文件的类型是没有必然的联系的。这只是约定俗称的习惯罢了。
在linux 下进行C/C++开发,一般都是使用的gcc
编译器。
.o
文件,即目标文件。一般通过.c
或者.cpp
文件编译而来,相当于VC
编译出来的obj
文件.so
文件,shared object 共享库(对象),相当于windows下的dll。.a
文件,archive 归档包,即静态库。其实质是多个.o
文件打包的结果,相当于VC
下的.lib
文件
一般来说,无论是C
、C++
、还是pas
,首先要把源文件编译成中间代码文件,在Windows
下也就是.obj
文件,UNIX
下是.o
文件,即 Object File
,这个动作叫做编译(compile)。然后再把大量的Object File
合成执行文件,这个动作叫作链接(link)。
C/C++程序编译的过程
C/C++编译的几个过程。
- 预处理,展开头文件,宏定义,条件编译处理等。通过
gcc -E source.c -o source.i
或者cpp source.c
生成。 - 编译。这里是一个狭义的编译意义,指的是将预处理后的文件翻译成汇编代码的过程。通过
gcc -S source.i
生成。默认生成source.s
文件。 - 汇编。汇编即将上一步生成的汇编代码翻译成对应的二进制机器码的过程。通过
gcc -c source.s
来生成source.o
文件。 - 链接。链接是将生成目标文件和其引用的各种符号等生成一个完整的可执行程序的过程。链接的时候会进行虚拟内存的重定向操作。
上面四个步骤就是C/C++程序编译的几个基本步骤。.o
文件就是C/C++源码编译的结果。即上面所说的C/C++编译过程中的前三步。一般开发中很少将这三步分开来做,通常的做法是一步生成;只有第四个步骤链接
是复杂一点的。很多时候我们编译比较大的项目,报错的往往是在链接的时候缺少某些库,或者某些符号找不到定义,重定义等。
一般来说,每个源文件都应该对应于一个中间目标文件(.o
文件或是.obj
文件)
编译选项
一、makefile的规则
会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。因为,makefile
关系到了整个工程的编译规则。
我们要写一个Makefile
来告诉make命令
如何编译和链接这几个文件。我们的规则是:
- 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
- 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
1.1、命名规则
一般来说将Makefile命名为Makefile或makefile都可以,但很多源文件的名字是小写的,所以更多程序员采用的是Makefile的名字,因为这样可以将Makefile居前显示。
如果将Makefile命为其它名字,比如Makefile_Rule,也是允许的,但使用的时候应该采用以下方式:
make -f Makefile_Rule
1.2、基本规则
Makefile的基本规则为:
目标:依赖
(tab)规则
目标 --> 需要生成的目标文件
依赖 --> 生成该目标所需的一些文件
规则 --> 由依赖文件生成目标文件的手段
tab --> 每条规则必须以tab开头(敲击一次tab键),使用空格不行
例如我们经常写的gcc test.c -o test,使用Makefile可以写成:
test: test.c
gcc test.c -o test
其中,第一行中的test就是要生成的目标,test.c就是依赖,第二行就是由test.c生成test的规则。
Makefile中有时会有多个目标,但Makefile会将第一个目标定为终极目标。
make
并不管命令是怎么工作的,他只管执行所定义的命令。make
会比较targets
文件和prerequisites
文件的修改日期,如果prerequisites
文件的日期要比targets
文件的日期要新,或者target
不存在的话,那么,make
就会执行后续定义的命令。
1.3、工作原理
目标的生成: a. 检查规则中的依赖文件是否存在; b. 若依赖文件不存在,则寻找是否有规则用来生成该依赖文件。
比如上图中,生成calculator的规则是gcc main.o add.o sub.o mul.o div.o -o calculator,Makefile会先检查main.o, add.o, sub.o, mul.o, div.o是否存在,如果不存在,就会再寻找是否有规则可以生成该依赖文件。
比如缺少了main.o这个依赖,Makefile就会在下面寻找是否有规则生成main.o。当它发现gcc main.c -o main.o这条规则可以生成main.o时,它就利用此规则生成main.o,然后再生成终极目标calculator。
整个过程是向下寻找依赖,再向上执行命令,生成终极目标。
目标的更新: a. 检查目标的所有依赖,任何一个依赖有更新时,就重新生成目标; b. 目标文件比依赖文件时间晚,则需要更新。
比如,修改了main.c,则main.o目标会被重新编译,当main.o更新时,终极目标calculator也会被重新编译。其它文件的更新也是类推。
1.4、静态库与动态库
-l(小写的L)参数就是用来指定程序要链接的库,紧接着的是库名;
-L参数后是要链接的库文件的路径;
-I(大小的I)参数是用来指定头文件目录;
那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了。
既然是库,-l和-L才是正派的做法,比如同一目录下有libxxx.a文件和libxxx.so文件,gcc默认会链接so,改变这一默认行为的方法就是:将"-lxxx"改为"-l:libxxx.a"
1.4.1、静态库
1.4.2、动态库
二、变量
2.1、普通变量
用 Makefile 进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。makefile 中的变量是没有类型的,直接创建变量然后给其赋值就可以了。
# 错误, 只创建了变量名, 没有赋值
变量名
# 正确, 创建一个变量名并且给其赋值
变量名=变量值
# 如何将变量的值取出?
$(变量名)
# 举例 add.o div.o main.o mult.o sub.o
# 定义变量并赋值
obj=add.o div.o main.o mult.o sub.o
# 取变量的值
$(obj)
eg:
# 这是一个规则,普通写法
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
# 这是一个规则,里边使用了自定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)
2.2、自动变量
Makefile 中的规则语句中经常会出现目标文件和依赖文件,自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。
Makefile提供了很多自动变量,但常用的为以下四个。这些自动变量只能在规则中的命令中使用,其它地方使用都不行。
$@ --> 规则中的目标
$< --> 规则中的第一个依赖条件
$^ --> 规则中的所有依赖条件
$? --> 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
eg:
# 这是一个规则,普通写法
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
calc:add.o div.o main.o mult.o sub.o
gcc $^ -o $@ # 自动变量只能在规则的命令中使用
2.3、预定义变量
在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:
变 量 名 | 含义 | 默认值 |
---|---|---|
AR | 生成静态库库文件的程序名称 | ar |
AS | 汇编编译器的名称 | as |
CC | C语言编译器的名称 | cc |
CPP | C语言预编译器的名称 | $(CC) -E |
CXX | C++语言编译器的名称 | g++ |
FC | FORTRAN编译器的名称 | f77 |
RM | 删除文件程序的名称 | rm -f |
ARFLAGS | 生成静态库库文件程序的选项 | 无默认值 |
ASFLAGS | 汇编语言编译器的编译选项 | 无默认值 |
CFLAGS | C 语言编译器的编译选项 | 无默认值 |
CPPFLAGS | C 语言预编译的编译选项 | 无默认值 |
CXXFLAGS | C++语言编译器的编译选项 | 无默认值 |
FFLAGS | FORTRAN 语言编译器的编译选项 | 无默认值 |
eg:
# 这是一个规则,普通写法
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
CFLAGS=-O3 # 代码优化
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)
三、makefile的运算符
3.1、换行符
反斜杠(\)
是换行符的意思。便于Makefile
的易读
eg:
bin/total.exe: src/main.o \
src/hello.o
gcc $^ -o $@
src/main.o: src/main.c
gcc -c $^ -o $@ -I./inc/
src/hello.o: src/hello.c
gcc -c $^ -o $@ -I./inc/
#伪目标 .PHONY
.PHONY:clean
clean:
del /q bin src\hello.o src\main.o
3.2、简单的赋值运算符
=
“=”是最普通的等号,在Makefile中也是最容易搞错的赋值等号,使用“=”进行赋值,变量的值是整个makefile中最后被指定的值。
如下图所示,变量param终端打印出来的是param1在整个makefile最后一次被赋值的值yes而不是yes1。
3.3、立即赋值运算符
:=
“:=”表示直接赋值,赋予当前位置的值,变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。
相比于"=",":="才是真正意义上的直接赋值。
如下图,变量param终端上打印出来的是param被赋值时,param1最后一次被赋值的值yes1而不是整个makefile最后一次被赋值的yes。
3.4、默认赋值运算符
?=
"?="表示如果该变量没有被赋值,则赋予等号后的值
3.5、累加
+=
“+=”表示将等号后面的值添加到前面的变量上,每次添加的值用空格分隔开。
四、make的运行
4.1、指定目标
通常一个makefile会有多个目标,如果不指定目标,makefile会将第一个目标作为最终目标
下图可以看到若make后不指定目标,则会执行第一个目标params,当我们make后面指定include1目标则不再执行第一个目标params,而执行指定的目标include1;make后面也可以指定多个目标,用空格分隔开。
4.2、伪目标
我们编译后会生成许多文件,包括生成的.o
目标文件和可执行文件等,我们也应该提供一个清除它们的目标
以备完整地重编译而用。 (一般情况下约定俗成的是在makefile文件最后加上clean这一伪目标,在命令行执行make clean
来使用该目标)
因为,我们并不生成clean
这个文件。伪目标
并不是一个文件,只是一个标签,由于伪目标
不是文件,所以make
无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个目标
才能让其生效。当然,伪目标
的取名不能和文件名重名,不然其就失去了伪目标
的意义了。当前目录下若存在clean这个文件,则会报错,不会运行清除文件的命令。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记.PHONY
来显示地指明一个目标是伪目标
,向make
说明,不管是否有这个文件,这个目标就是伪目标
。
.PHONY : clean
只要有这个声明,不管是否有clean
文件,要运行clean
这个伪目标,只要在命令行执行make clean
,于是整个过程可以这样写:
#伪目标 .PHONY
.PHONY:clean
clean:
del /q bin src\hello.o src\main.o
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为默认目标
,只要将其放在第一个。一个示例就是,如果你的Makefile
需要一口气生成若干个文件,但你只想简单地敲一个make
完事,并且,所有的目标文件都写在一个Makefile
中,那么你可以使用伪目标
这个特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
gcc -o prog3 prog3.o sort.o utils.o
我们知道,Makefile
中的第一个目标会被作为其默认目标。我们声明了一个all
的伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如all
这个目标新。所以,其它三个目标的规则总是会被执行。也就达到了我们一口气生成多个目标的目的。.PHONY : all
声明了all
这个目标为伪目标
。
从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
make clean
将清除所有要被清除的文件。cleanobj
和cleandiff
这两个伪目标有点像子程序
的意思。我们可以输入make cleanall
和make cleanobj
和make cleandiff
命令来达到清除不同种类文件 的目的
4.3、注释
makefile没有块注释,只有行注释
其注释是用"#"字符,如下图:
4.4、命令前缀与打印
[不用前缀 ]输出执行的命令以及命令执行的结果, 出错的话停止执行
[前缀 @]只输出命令执行的结果, 出错的话停止执行
[前缀 -]命令执行有错的话, 忽略错误, 继续执行
如下图所示可以看到在命令前加前缀@命令不会在终端上显示出来,不加前缀会将执行的命令显示出来
五、使用函数
注:这一章节均使用6.1自动推导所示的测试程序
5.1 wildcard
# wildcard :这个函数的主要作用是获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表
$(wildcard PATTERN...)
该函数的参数PATTERN只有一个, 但是这个参数可以分成若干个部分, 可以指定搜索多个目录,每个路径之间使用空格间隔
eg:
注:非常常用
5.2 patsubst
# patsubst : 这个函数的功能是按照指定的模式替换指定的字符串,一般是拿来替换文件名的后缀, 函数原型如下:
# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)
# 参数功能:
pattern: 这是一个模式字符串, 需要指定出要被替换的文件名中的后缀是什么
replacement: 这是一个模式字符串, 指定参数pattern中的后缀最终要被替换为什么
text: 该参数中存储这要被替换的原始数据
# 返回值:
函数返回被替换过后的字符串。
eg:
如下图所示,变量SRCS的字符串中所有的./src/目录下的.c文件字符串被替换为./objs/下的.o文件字符串
注:非常常用
5.3 filter
$(filter <pattern>,<text>)
# 名称:过滤函数——filter。
# 功能:以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。
# 返回:返回符合模式<pattern>的字串。
eg:
5.4 filter-out
$(filter-out <pattern>,<text>)
# 名称:反过滤函数——filter-out。
# 功能:以<pattern>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词。
# 返回:返回不符合模式<pattern>的字串。
eg:
5.5 dir
$(dir <names...>)
# 名称:取目录函数——dir。
# 功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(/)之前的部分。如果没有反斜杠,那么返回./。
# 返回:返回文件名序列<names>的目录部分。
# 示例: $(dir src/foo.c hacks)返回值是src/ ./。
eg:
5.6 notdir
$(notdir <names...>)
# 名称:取文件函数——notdir。
# 功能:从文件名序列<names>中取出非目录部分。非目录部分是指最后一个反斜杠(/)之后的部分。
# 返回:返回文件名序列<names>的非目录部分。
# 示例: $(notdir src/foo.c hacks)返回值是foo.c hacks。
eg:
5.7 foreach
# foreach这个函数是用来做循环用的
# 它的语法是:
$(foreach <var>,<list>,<text>)
# 这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。
# 每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,
# 最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
如下图所示,将变量include的字符串逐一取出给到变量n(变量名称自定义),并执行在前面加上-I的表达式,以空格分隔,输出结果给到变量Include。
对大型项目头文件会在多个文件夹下,可以使用foreach对这些头文件路径循环加上-I作为编译选项。
也可使用通配符%也可以达到相同的效果。
六、隐含规则
6.1、自动推导
make
很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件
后都写上类似的命令,因为,我们的make
会自动识别,并自己推导命令
只要make
看到一个.o文件
,它就会自动的把.c文件
加在依赖关系中,如果make
找到一个main.o
,那么main.c
,就会是main.o
的依赖文件。并且cc -c main.c
也会被推导出来,于是,我们的makefile
再也不用写得这么复杂。
可以如下做一个简单的测试程序:
项目上创建以下几个文件夹:
src:存放源文件;
inc:存放头文件;
libs:存放库文件
bin:存放生成的可执行程序;
makefile文件
inc目录下:
sys.h
#ifndef _SYS_H_
#define _SYS_H_
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define long 2560 //宏定义分辨率
#define wide 1440
void hello();
#endif
src目录下:
main.c
#include"sys.h"
int main(int argc, char *argv[])
{
hello();
printf("success\n");
printf("long = %d,wide = %d\n",long,wide);
system("pause");
return 0;
}
hello.c
#include"sys.h"
void hello()
{
printf("Hello World!\n");
}
不使用自动推导的makefile文件:
CC = gcc
CFLAGS = -I ./inc/
bin/total.exe: src/main.o \
src/hello.o
${CC} $^ -o $@
src/main.o: src/main.c
${CC} -c $^ -o $@ ${CFLAGS}
src/hello.o: src/hello.c
${CC} -c $^ -o $@ ${CFLAGS}
#伪目标 .PHONY
.PHONY:clean
clean:
del /q bin src\hello.o src\main.o
使用自动推导的makefile文件:
CC = gcc
CFLAGS = -I ./inc/
bin/total.exe: src/main.o src/hello.o
${CC} $^ -o $@
#伪目标 .PHONY
.PHONY:clean
clean:
del /q bin src\hello.o src\main.o
这种方法,也就是make
的隐晦规则。
6.2、模式匹配
模式规则中,至少在规则的目标定义中要包含%
,否则,就是一般的规则。目标中的%
定义表示对文件名的匹配,%
表示长度任意的非空字符串。例如:%.c
表示以.c
结尾的文件名(文件名的长度至少为3),而%.o
则表示以.o
结尾的文件名。
如果%
定义在目标中,那么,目标中的%
的值决定了依赖目标中的%
的值,也就是说,目标中的模式的%
决定了依赖目标中%
的样子。例如有一个模式规则如下:
%.o : %.c
<command ......>
其含义是,指出了怎么从所有的.c
文件生成相应的.o
文件的规则。如果要生成的目标是a.o b.o
,那么%c
就是a.c b.c
。
一旦依赖目标中的%
模式被确定,那么,make
会被要求去匹配当前目录下所有的文件名,一旦找到,make
就会规则下的命令,所以,在模式规则中,目标可能会是多个的,如果有模式匹配出多个目标,make
就会产生所有的模式目标,此时,make
关心的是依赖的文件名和生成目标的命令这两件事。
下面这个例子表示了,把所有的c
文件都编译成.o
文件.
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
其中,$@
表示所有的目标的挨个值,$<
表示了所有依赖目标的挨个值
如上面自动推导所示的测试程序,自动推导会将生成的目标文件和依赖的源文件生成在一个目录下;如果我想将这些生成的目标文件统一生成在一个目录下该怎么做呐?
比如我想把所有的目标文件生成在objs文件夹下,先在项目中创建objs文件下,makefile文件可以写成如下:
#模式匹配
CC = gcc
CFLAGS = -I ./inc/
exe = bin/total.exe
OBJS = objs/main.o objs/hello.o
${exe}: ${OBJS}
@${CC} $^ -o $@
objs/%.o: src/%.c
@${CC} -c $< -o $@ ${CFLAGS}
@echo "success"
#伪目标 .PHONY
.PHONY:clean
clean:
del /q bin objs src\hello.o src\main.o
运行结果如下,会把当前src目录下(递归的,若src下还有其他文件夹有.c文件也会被编译成.o文件)所有的.c
文件都编译成.o
文件,可以看到目标文件均已生成在objs文件夹下。
七、条件判断
条件判断:
ifeq (<arg1>, <arg2>)
<text-if-true>
else
<text-if-false>
endif
三个关键字:ifeq、else和endif。
ifeq的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。
else表示条件表达式为假的情况。
endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。
eg:
八、定义命令包
定义命令包:
define <command-name>
command
...
endef
如下图,可以看到能够用define将多个命令定义为一个命令包,使用时引用命令包名就可以。
九、include的使用
在 Makefile 中包含其他文件,使用的关键字是 "include"。make 读取到 "include" 关键字时,会暂停读取当前的 Makefile,转去读取 "include" 所包含的文件,读取结束后再继读取当前的 Makefile 文件。
一、include的使用场合
(1)在一个工程文件中,每一个模块都有一个独立的 Makefile 来描述它的重建规则。它们需要定义一组通用的变量定义或者是模式规则。通用的做法是将这些共同使用的变量或者模式规则定义在一个文件中,需要的时候用 "include" 包含这个文件。
(2)或者,当根据源文件自动产生依赖文件时,我们可以将自动产生的依赖关系保存在另一个文件中,然后在 Makefile 中包含该文件。
二、include的使用方法
"include" 使用的具体方式如下:
include <filenames>
filenames 是 shell 支持的文件名(可以使用通配符表示的文件)。
"include" 关键字所在的行首可以包含空格(读取的时候空格会被自动的忽略),但不能使用 Tab 开始,否则会把 "include" 当作式命令来处理。包含的多个文件之间要使用空格分隔开。
十、编写一个makefile
接下来编写一个略微完善的makefile
首先做一个简单的测试程序:
项目上创建以下几个文件夹:
src:存放源文件(子目录为下图add与sys文件夹);
inc:存放头文件(子目录为下图add与hello和main文件夹);
libs:存放库文件
bin:存放生成的可执行程序;
makefile文件
构建如下:
注:这只是为了理解多目录下如何生成动态库,并与目标文件链接生成可执行程序;实际并不需要这么麻烦,理解就好。
创建以下测试程序:
inc目录下的sys目录下:
sys.h
#ifndef _SYS_H_
#define _SYS_H_
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define long 2560 //宏定义分辨率
#define wide 1440
void hello();
#endif
inc目录下的add目录下:
add.h
#ifndef _ADD_H_
#define _ADD_H_
int add(int a,int b);
#endif
src目录下的main目录下:
main.c
#include"sys.h"
#include"add.h"
int main(int argc, char *argv[])
{
hello();
printf("success\n");
printf("long = %d,wide = %d\n",long,wide);
int a=5,b=10;
int c = add(a,b);
printf("a + b = %d\n",c);
system("pause");
return 0;
}
src目录下的hello目录下:
hello.c
#include"sys.h"
void hello()
{
printf("Hello World!\n");
}
src目录下的add目录下:
add.c
#include"add.h"
int add(int a,int b)
{
return a+b;
}
makefile文件:
#最终生成的可执行程序
CC = gcc
exe := ./bin/isp1.exe
#生成的windows下的动态链接库目标
Libs_share := ./bin/libisp.dll
#搜索项目各目录下的源文件
SRCS := ${wildcard ./src/add/*.c ./src/hello/*.c ./src/main/*.c}
#链接动态库生成最终可执行程序的目标文件
main := ./src/main/main.o
#获得模式匹配生成的目标文件的路径和文件名字符串
OBJS := ${patsubst %.c,%.o,${SRCS}}
#windows下使用del删除文件是\,不能使用/作为删除文件的路径,需要替换掉
OBJS_Windows := ${subst /,\,${OBJS}}
#过滤掉main.o目标文件,其他目标文件一起生成动态库
Libs_OBJS := ${filter-out ./src/main/main.o,${OBJS}}
#累加含有头文件的目录路径字符串给到变量include
include := ./inc/sys/
include += ./inc/add/
#将头文件路径使用通配符%逐个添加-I,使用空格间隔用于编译选项
I_options := ${include:%=-I%}
#生成动态库的库名与动态库路径
Libs_share_Name := isp
Libs_share_Path := ./bin
#链接动态库编译选项
l_options := ${Libs_share_Name:%=-l%}
L_options := ${Libs_share_Path:%=-L%}
r_options := ${Libs_share_Path:%=-Wl,-rpath=%}
linking_options := ${l_options} ${L_options} ${r_options}
#执行isp1.exe,并依赖于${exe}
work : ${exe}
${exe}
#生成可执行程序isp1.exe,依赖目标文件main.o和动态库libisp.dll
${exe}: ${main} ${Libs_share}
${CC} $< -o $@ ${linking_options}
#除main.o以外的目标文件生成动态库
${Libs_share}: ${Libs_OBJS}
${CC} -shared $^ -o $@
#模式匹配,将项目目录下的所有源文件均生成目标文件,生成命令中制定了所需头文件的路径
#因为要生成动态库,编译选项中也使用了-fpic
%.o: %.c
${CC} -c $< -o $@ ${I_options} -fpic
#伪目标 .PHONY
.PHONY:clean work
#删除生成的文件,以便重编译
clean:
@del /q bin libs ${OBJS_Windows}
运行结果如下:
该makfile是将
1)src目录下的所有目录的源文件模式匹配生成对应的目标文件
2)将hello.o和add.o目标文件一起生成bin目录下的libisp.dll动态库
3)main.o目标文件与libisp.dll动态库链接生成bin目录下的isp1.exe可执行程序
问题:
编写程序过程中也遇到了问题,在Windows下生成的动态链接库要和可执行程序在同一目录下才可以执行,不在同一目录下会报错找不到动态库,但是makefile文件中生成可执行程序规则的编译选项中已经指定了动态库的库名和库路径;有时又不想他们放在一个目录下,看着就很难受。
生成静态库程序会被打包到可执行程序中,即便不在同一目录下找不到静态库也可以执行;但是动态库是运行可执行程序时才被动态加载,找不到动态库位置就加载不了。
有了解的朋友,帮忙在评论区解答下,万分感谢。
参考文献:
一文入门Makefile - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/56489231
Makefile | 爱编程的大丙 (subingwen.cn)https://subingwen.cn/linux/makefile/Linux中的动态库和静态库(.a/.la/.so/.o) - findumars - 博客园 (cnblogs.com)https://www.cnblogs.com/findumars/p/5421910.html6.2.CC++ 编译选项_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1EM41177s1?p=17&vd_source=e2bef9dc437270fd760c78aec572bc4b