GCC
全称GNU Compiler Collection
,GNU编译套件
GCC是由GNU开发的编程语⾔编译器,包括C、Cpp、Objective-C、Fortran、Java、Ada、Golang。
可以使用gcc -v查看自己的linux中是否安装了gcc
如果我们要编译的是c语言文件我们使用gcc,而如果我们要编译的是c++文件,我们要使用g++
gcc [option | filename]
使 用 gcc 进 行 的 编 译 过 程 是 一 个 相 对 复 杂 的 过 程 , 可 分 为以下四个阶段:
我们也可以直接编译,语法格式如下:
gcc [编译⽂件] -o [⽬标⽂件]
例如:gcc test.c -o test
我们甚至可以不指定目标文件名:
a.out是默认的生成文件
C语言的源文件 —> 生成*.i得中间文件
功能:处理文件中的#ifdef,#include和#define等预处理命令
语法格式:
gcc –E –o [目标文件] [编译文件]
gcc –E [编译文件] -o [目标文件]
例如:
编辑test.c文件,内容如下
#include
int main( )
{
int a;
scanf(“%d”,a);
printf(“a=%d”,a);
}
当我们执行编译命令gcc –E test.c –o test.i
之后,我们会得到错误提示:stdo.h:没有那个文件或目录
。但是scanf语句的错误不提示。
经过预编译之后
我们可以看见文件头部被引用的所有头文件都被展开了罗列在了最上面,而且所有的头文件都是从根目录开始描述的
文件尾部,我们的宏变量NUMBER可以发现已经被替换成了3,然后我们的注释也没了
中间文件*.i —> 汇编语言文件*.s
功能:此时检查语法错误。
语法格式:
gcc –S –o [目标文件] [编译文件]
gcc –S [编译文件] -o [目标文件]
例如:
沿用我们上面的代码,当我们执行gcc –S test.i –o test.s
命令的时候,这时候会提示format ‘%d’ expects type ‘int *’ but argument 2 has type ‘int’
汇编文件*.s —> 二进制机器代码*.o
语法格式:
gcc –c –o [目标文件] [编译文件]
gcc –c [编译文件] -o [目标文件]
二进制机器代码文件*.o —> 可执行的二进制代码文件
语法格式:
gcc –o [目标文件] [编译文件]
gcc [编译文件] -o [目标文件]
经过这一步之后我们就可以运行这个可执行文件了
注意执行的时候一定要是
./
,否则shell会把它当作变量处理
预处理:在这个阶段主要做了三件事: 展开头文件
、宏替换
、去掉注释行
编译:这个阶段需要 GCC 调用编译器对文件进行编译,最终得到一个汇编文件
汇编:这个阶段需要 GCC 调用汇编器对文件进行汇编,最终得到一个二进制文件
链接:这个阶段需要 GCC 调用链接器对程序需要调用的库进行链接,最终得到一个可执行的二进制文件
格式1:多文件同时编译
gcc 1.c 2.c 3.c –o test
$./test
格式2:每个文件分别进行编译,然后链接成可执行文件
gcc –c 1.c –o 1.o
gcc –c 2.c –o 2.o
gcc –c 3.c –o 3.o
gcc 1.o 2.o 3.o –o test
$./test //执行可执行程序
不管是 Linux 还是 Windows 中的库文件其本质和工作模式都是相同的,只不过在不同的平台上库对应的文件格式和文件后缀不同。程序中调用的库有两种 静态库
和动态库
,不管是哪种库文件本质是还是源文件,只不过是二进制格式只有计算机能够识别,作为一个普通人就无能为力了。
在项目中使用库一般有两个目的:
当我们拿到了库文件(动态库、静态库)之后要想使用还必须有这些库中提供的 API 函数的声明,也就是头文件,把这些都添加到项目中,就可以快乐的写代码了。
库:事先已经编译好的代码,经过编译后可以直接调⽤的⽂件,本质上来说是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。系统提供的库的路径
/usr/lib
/usr/lib64
Linux库⽂件名的组成:
前缀(lib)+库名+后缀(.a静态库;.so动态库)
libmm.a
:库名为mm的静态库;libnn.so
:库名为nn的动态库。静态库的代码在编译时就拷⻉到应⽤程序中,因此当有多个程序同时引⽤⼀个静态库函数时,内存中将会调⽤函数的多个副本。由于是完全拷⻉,因此⼀旦连接成功,静态库就不再需要了,代码体积⼤。
动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在 Linux 中动态链接库也可称之为共享库。动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。
动态库在程序内留下⼀个标记,指明当程序执⾏时,⾸先必须要载⼊这些库。在程序开始运⾏后调⽤库函数时才被载⼊,被调⽤函数在内存中只有⼀个副本,代码体积⼩。
动态库是有执行权限的,而静态库是没有执行权限的
在 Windows 中静态库一般以 lib 作为前缀,以 lib 作为后缀,中间是库的名字需要自己指定,即: libxxx.lib
在 Linux 中静态库以 lib 作为前缀,以.a 作为后缀,中间是库的名字自己指定即可,即: libxxx.a
生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar 工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。
静态库的制作分为以下几个步骤:
静态库制作实例:
例如我们现在有一个文件夹结构如下:
在这个目录中有几个关于算法的源文件:
这每一个源文件里面都对应着不同的算法函数,这些对应的算法函数声明是在include下的head.h中。main.c文件是用来对这些算法函数进行测试的。接下来我们就制作静态库
首先我们使用gcc的-c命令将几个源文件转化成为二进制文件,但是这时候我们发现报错了:
原因是因为在我们的源文件中包含了head.h的头文件,但是这个头文件没有被找到,所以我们要手动指定头文件目录,前面我们提到过使用-I
:
接下来我们再来看看文件目录:
可以发现我们用于生成静态库的二进制.o文件已经生成好了。然后我们使用ar进行打包:
可以看到libcalc.a就是我们的静态库了。我们的静态库生成之后还要给别人用,在发布的时候需要给两个文件:
我们把这两个文件都放到test中去,然后把我们的测试文件main.c文件也拷贝进来。然后我们来看看main.c文件和head.h文件:
此时我们编译main.c文件会报错;
他告诉我们没有找到函数的定义(也就是函数的实现),我们知道这些函数我们已经在libcalc.a文件中定义过了,我们只需要指定一下静态库的路径和名字:
在指定库的名字的时候我们要掐头去尾,也就是说把前缀,和文件后缀去掉
在 Linux 中动态库以 lib 作为前缀,以.so 作为后缀,中间是库的名字自己指定即可,即: libxxx.so
在 Windows 中动态库一般以 lib 作为前缀,以 dll 作为后缀,中间是库的名字需要自己指定,即: libxxx.dll
生成动态链接库是直接使用 gcc
命令并且需要添加 -fPIC(-fpic)
以及 -shared
参数。
-fPIC
或 -fpic
参数的作用是使得 gcc
生成的代码是与位置无关的,也就是使用相对位置。-shared
参数的作用是告诉编译器生成一个动态链接库。我们使用这个方法得到的.o文件和制作静态库时的.o文件是不一样的。我们在制作这个.o的时候除了加参数-c还需要加参数-fpic(可大写可小写,差别很微小)。如此生成的.o文件称之为与位置无关的代码。什么叫与位置无关?如果要解释这个问题就需要先了解一下进程。进程是磁盘上运行的一个应用程序,我们只要是得到一个进程,就会得到一个对应的虚拟地址空间。在这个虚拟地址空间需要加载一些代码,如果是静态库则直接会打包到可执行程序中,因此静态库对应的代码会直接放在代码区。如果使用的是动态库,库里面的代码是不会放在代码区的,而会放在动态库加载区,这个地方的代码是随着程序的运行并且调用到库里面的函数的时候才会把代码进行加载。因此在不同的进程中如果说要调用库文件对应的代码位置都是不一样的,加了fpic之后我们调用的库函数对应的代码在虚拟地址空间中用的是一个相对地址。
制作动态库的具体步骤:
动态库制作实例:
使用的文件夹还是制作静态库中的:
然后使用gcc对源文件进行汇编操作生成与位置无关的文件
接下来我们说说如何使用动态库:
和静态库一样,我们也要发布两个文件:
然后我们来编译我们的测试文件:
然后我们来执行app文件:
但是我们如果移动app这个可执行文件,会发现报错,动态库无法加载:
动态链接器是一个独立于应用程序的进程,属于操作系统,当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L 指定的路径。
那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
可执行文件内部的 DT_RPATH 段
系统的环境变量 LD_LIBRARY_PATH
系统动态库的缓存文件 /etc/ld.so.cache(不可修改,只能通过修改/etc/ld.so.conf文件再通过shell命令进行同步)
存储动态库 / 静态库的系统目录 /lib/, /usr/lib 等
按照以上四个顺序,依次搜索,找到之后结束遍历,最终还是没找到,动态连接器就会提示动态库找不到的错误信息。
我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库,这个命令叫做 ldd
语法:
ldd 可执行程序名
我们在执行这一个文件的时候需要四个动态库,而其中有一个库没有找到。
可执行程序生成之后,根据动态链接器的搜索路径,我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。
解决方案①:将库路径添加到环境变量 LD_LIBRARY_PATH 中
解决方案②:更新 /etc/ld.so.cache 文件
解决方案③:拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)
静态库:
优点:
缺点:
优点:
缺点:
Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。
Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的
版本控制软件。
Git 与常用的版本控制工具 SVN, Subversion 等不同,它采用了分布式版本库的方式,不必服务器端软件支持。
可以先使用git命令查看是否安装
如果没有安装,则执行以下命令:
sudo apt-get install git (ubuntu)
sudo yum install git (centos)
然后我们进行配置指定使用git的账号和用户名:
$ git config --global user.name "Your name"
$ git config --global user.email "Your email"
如果去掉 --global 参数只对当前仓库有效。
首先我们要创建版本库
版本库又称仓库(repository)
,仓库中存放被git管理的文件,每个文件的修改、删除,git都能够跟踪,可以方便追踪历史。
创建仓库方法:
[root@testpc ~]# mkdir repo_git
[root@testpc ~]# cd repo_git
[root@testpc repo_git]# git init
Initialized empty Git repository in /root/repo_git/.git/
创建成功,多了.git目录,用来跟踪管理版本库,不能删除
工作区和版本库:
工作区指工作目录,而工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库,该文件夹就是用于管理当前目录中所有文件的改动的。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
如下就是一个标准的.git
文件夹:
然后我们就可以向版本库中添加文件了
涉及命令:
git add filename
git add --all . 添加所有文件
工作区中的文件是可以被追踪的,但是只有告诉git哪些文件需要追踪,它才会显式的去追踪该文件,否则git永远会在你提交的时候告诉你工作区还有哪些文件处于Untracked状态。而add命令就是用来显式告诉git哪些文件从此时开始追踪。
然后我们就可以使用如下命令进行提交:
git commit
用add命令保存某个文件的修改,记录下该文件当前快照。然后用commit命令向分支上提交,位于分支上的每个点都是一次commit留下的。当然回滚的时候也是根据需要回滚到指定的点上。
例如:
前提:在刚创建的repo_git目录(或子目录)下建立文件,如 “README.txt”,添加内容
git add README.txt # 没有输出
git commit –m “A description for the…”
git status #查看状态
git commit -m [message]
[message] 可以是一些备注信息。
后续开发需要修改之前的文件,如README.txt, 想退回之前的版本
涉及的命令:
git status
:status命令是用来查看当前工作区状态的,也就是说它会把当前工作区的所有文件状态和本地分支上最近一次的提交进行比较,并列出所有做出的修改条目。
git diff
:diff命令也是用来查看当前状态的,只是它不同于status,它比较的是工作区和暂存区之间的区别。
git log
:查看历史提交信息
git reset
:reset命令能够实现回退历史版本
git reflog
: 记录每一次更改,可找到最新版本
reset命令详解
语法格式:
git reset [--soft | --mixed | --hard] [HEAD]
–mixed 为默认,可以不用带该参数,用于重置暂存区的文件与上一次的提交(commit)保持一致,工作区文件内容保持不变。
例如:
$ git reset HEAD^ # 回退所有内容到上一个版本
$ git reset HEAD^ hello.php # 回退 hello.php 文件的版本到上一个版本
$ git reset 052e # 回退到指定版本
–soft 参数用于回退到某个版本
例如:
$ git reset --soft HEAD~3 # 回退上上上一个版本
–hard 参数撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到上一次版本,并删除之前的所有信息提交
例如:
$ git reset --hard HEAD~3 # 回退上上上一个版本
$ git reset --hard bae128 # 回退到某个版本回退点之前的所有信息。
$ git reset --hard origin/master # 将本地的状态回退到和远程的一样
注意:谨慎使用 –-hard 参数,它会删除回退点之前的所有信息。
HEAD 说明:
- HEAD 表示当前版本
- HEAD^ 上一个版本
- HEAD^^ 上上一个版本
可以使用 ~数字表示:
- HEAD~0 表示当前版本
- HEAD^3 上上上一个版本
例如:
再次修改文件README.txt,并提交
git add README.txt #
git commit -m “add a word” # 提交修改版本
修改文件
退回到上一个版本
git reset --hard HEAD^
git checkout -- filename
,回到最近一次git commit或git add时的状态。
git reset HEAD filename
:撤销暂存区中的内容(git add)git rm filename
:删除提交到版本库中的文件 (错删,则git checkout – filename 从版本库恢复)使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。make 是一个命令工具,是一个解释 makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Visual C++ 的 nmake,QtCreator 的 qmake 等。
make 工具在构造项目的时候需要加载一个叫做 makefile 的文件,makefile 关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile 定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。
makefile 带来的好处就是 ——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。
makefile 文件有两种命名方式 makefile
和 Makefile
,构建项目的时候在哪个目录下执行构建命令 make 这个目录下的 makefile 文件就会别加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中。
每条规则的语法格式:
target1,target2...: depend1, depend2, ...
command
......
......
每条规则由三个部分组成分别是目标(target)
, 依赖(depend)
和命令(command)
:
目标(target)
: 规则中的目标,这个目标和规则中的命令是对应的
伪目标
依赖(depend)
: 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
命令(command)
: 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令
例如:
# 举例: 有源文件 a.c b.c c.c head.h, 需要生成可执行程序 app
################# 例1 #################
app:a.c b.c c.c
gcc a.c b.c c.c -o app
################# 例2 #################
# 有多个目标, 多个依赖, 多个命令
app,app1:a.c b.c c.c d.c
gcc a.c b.c -o app
gcc c.c d.c -o app1
################# 例3 #################
# 规则之间的嵌套
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# a.o 是第一条规则中的依赖
a.o:a.c
gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
gcc -c c.c
在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则,分析并执行相关的动作。但是需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,就会先将需要的依赖生成出来。
这样,makefile 中的某一条规则在需要的时候,就会被其他的规则调用,直到 makefile 中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。
例如:
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
gcc -c a.c
# 规则3
b.o:b.c
gcc -c b.c
# 规则4
c.o:c.c
gcc -c c.c
在这个例子中,如果执行 make 命令就会根据这个 makefile 中的 4 条规则编译这三个源文件。在解析第一条规则的时候发现里边的三个依赖都是不存在的,因此规则对应的命令也就不能被执行。
当依赖不存在的时候,make 就是查找其他的规则,看哪一条规则是用来生成需要的这个依赖的,找到之后就会执行这条规则中的命令。因此规则 2, 规则 3, 规则 4 里的命令会相继被执行,当规则 1 中依赖全部被生成之后对应的命令也就被执行了,因此规则 1 的目标被生成,make 工作结束。
拓:
如果想要执行 makefile 中非第一条规则对应的命令,那么就不能直接 make, 需要将那条规则的目标也写到 make 的后边,比如只需要执行规则 3 中的命令,就需要:make b.o
make 命令执行的时候会根据文件的时间戳判定是否执行 makefile 文件中相关规则中的命令。
例如;
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
gcc -c a.c
# 规则3
b.o:b.c
gcc -c b.c
# 规则4
c.o:c.c
gcc -c c.c
根据上文的描述,先执行 make 命令,基于这个 makefile 编译这几个源文件生成对应的目标文件。然后再修改例子中的 a.c, 再次通过 make 编译这几个源文件,那么这个时候先执行规则 2 更新目标文件 a.o, 然后再执行规则 1 更新目标文件 app,其余的规则是不会被执行的。
make 是一个功能强大的构建工具,虽然 make 需要根据 makefile 中指定的规则来完成源文件的编译。作为小白的我们编写 makefile 的时候难免写的不是那么严谨从而漏写一些构建规则,但是我们会发现程序还是会被编译成功。这是因为 make 有自动推导的能力,不会完全依赖 makefile。
比如:使用命令 make 编译扩展名为.c 的 C 语言文件的时候,源文件的编译规则不用明确给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对.c 文件的编译,生成对应的.o 文件。它使用命令 cc -c 来编译.c 源文件。在 Makefile 中只要给出需要构建的目标文件名(一个.o 文件),make 会自动为这个.o 文件寻找合适的依赖文件(对应的.c 文件),并且使用默认的命令来构建这个目标文件。
假设本地项目目录中有以下几个源文件:
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c
目录中 makefile 文件内容如下:
# 这是一个完整的 makefile 文件
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
通过 make 构建项目:
$ make
cc -c -o add.o add.c
cc -c -o div.o div.c
cc -c -o main.o main.c
cc -c -o mult.o mult.c
cc -c -o sub.o sub.c
gcc add.o div.o main.o mult.o sub.o -o calc
我们可以发现上边的 makefile 文件中只有一条规则,依赖中所有的 .o 文件在本地项目目录中是不存在的,并且也没有其他的规则用来生成这些依赖文件,这时候 make 会使用内部默认的构造规则先将这些依赖文件生成出来,然后在执行规则中的命令,最后生成目标文件 calc。
使用 Makefile 进行规则定义的时候,为了写起来更加灵活,我们可以在里边使用变量。makefile 中的变量分为三种:
用 Makefile 进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。makefile 中的变量是没有类型的,直接创建变量然后给其赋值就可以了。
创建变量之后一定要赋值
在给 makefile 中的变量赋值之后,如何在需要的时候将变量值取出来呢?
# 如何将变量的值取出?
$(变量的名字)
# 举例 add.o div.o main.o mult.o sub.o
# 定义变量并赋值
obj=add.o div.o main.o mult.o sub.o
# 取变量的值
$(obj)
自定义变量使用举例:
# 这是一个规则,普通写法
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)
在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:
# 这是一个规则,普通写法
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 # 代码优化 其中-03是最高级别
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)
Makefile 中的变量除了用户自定义变量和预定义变量外,还有一类自动变量。Makefile 中的规则语句中经常会出现目标文件和依赖文件,自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。
下面几个例子,演示一下自动变量如何使用。
# 这是一个规则,普通写法
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 $@ # 自动变量只能在规则的命令中使用
在介绍概念之前,先读一下下面的这个 makefile 文件:
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
# 语法格式重复的规则, 将 .c -> .o, 使用的命令都是一样的 gcc *.c -c
add.o:add.c
gcc add.c -c
div.o:div.c
gcc div.c -c
main.o:main.c
gcc main.c -c
sub.o:sub.c
gcc sub.c -c
mult.o:mult.c
gcc mult.c -c
在阅读过程中能够发现从第二个规则开始到第六个规则做的是相同的事情,但是由于文件名不同不得不在文件中写出多个规则,这就让 makefile 文件看起来非常的冗余,我们可以将这一系列的相同操作整理成一个模板,所有类似的操作都通过模板去匹配 makefile 会因此而精简不少,只是可读性会有所下降。
这个规则模板可以写成下边的样子,这种操作就称之为模式匹配
。
模式匹配 -> 通过一个公式, 代表若干个满足条件的规则
# 依赖有一个, 后缀为.c, 生成的目标是一个 .o 的文件, % 是一个通配符, 匹配的是文件名
%.o:%.c
gcc $< -c
这个函数的主要作用是获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。函数原型如下:
# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(wildcard PATTERN...)
参数: 指定某个目录, 搜索这个路径下指定类型的文件,比如: *.c
例如:
$(wildcard *.c ./sub/*.c)
得到的返回值格式:
a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c
使用举例;
# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c) # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c
这个函数的功能是按照指定的模式替换指定的文件名的后缀,函数原型如下:
# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)
函数使用举例:;
src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src))
# obj 的值为: a.o b.o c.o e.o