@TOC
编译工具GCC
GCC
全称GNU Compiler Collection
,GNU编译套件
GCC是由GNU开发的编程语⾔编译器,包括C、Cpp、Objective-C、Fortran、Java、Ada、Golang。
可以使用gcc -v查看自己的linux中是否安装了gcc
如果我们要编译的是c语言文件我们使用gcc,而如果我们要编译的是c 文件,我们要使用g
GCC基本语法
代码语言:text复制gcc [option | filename]
GCC编译流程
使 用 gcc 进 行 的 编 译 过 程 是 一 个 相 对 复 杂 的 过 程 , 可 分 为以下四个阶段:
- 预 处 理 ( Pre-Processing)
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
我们以一个名为hello的c语言源文件为例:
我们也可以直接编译,语法格式如下:
gcc [编译⽂件] -o [⽬标⽂件]
例如:gcc test.c -o test
我们甚至可以不指定目标文件名:
a.out是默认的生成文件
预处理阶段
C语言的源文件 ---> 生成*.i得中间文件
功能:处理文件中的#ifdef,#include和#define等预处理命令
语法格式:
代码语言:text复制gcc –E –o [目标文件] [编译文件]
gcc –E [编译文件] -o [目标文件]
- 选项“-E”可以使编译器在预处理结束时就停止编译
- 选项“-o”是指定GCC输出的结果。
- 编译文件一般以.c为后缀名,目标文件以.i为后缀名
例如:
编辑test.c文件,内容如下
代码语言:text复制#include <stdo.h>
int main( )
{
int a;
scanf(“%d”,a);
printf(“a=%d”,a);
}
当我们执行编译命令gcc –E test.c –o test.i
之后,我们会得到错误提示:stdo.h:没有那个文件或目录
。但是scanf语句的错误不提示。
操作实例:
我们有一个test.c文件如下:
经过预编译之后
我们可以看见文件头部被引用的所有头文件都被展开了罗列在了最上面,而且所有的头文件都是从根目录开始描述的
文件尾部,我们的宏变量NUMBER可以发现已经被替换成了3,然后我们的注释也没了
编译阶段
中间文件.i ---> 汇编语言文件.s
功能:此时检查语法错误。
语法格式:
代码语言:text复制gcc –S –o [目标文件] [编译文件]
gcc –S [编译文件] -o [目标文件]
- 选项“-S”可以使编译器完成编译阶段就停止
- 选项“-o”是指定GCC输出的结果。
例如:
沿用我们上面的代码,当我们执行gcc –S test.i –o test.s
命令的时候,这时候会提示format ‘%d’ expects type ‘int *’ but argument 2 has type ‘int’
操作实例:
以下就是预处理文件处理过后的汇编文件
汇编阶段
汇编文件.s ---> 二进制机器代码.o
语法格式:
代码语言:text复制gcc –c –o [目标文件] [编译文件]
gcc –c [编译文件] -o [目标文件]
- 选项“-c”可以使编译器完成汇编阶段就停止
- 选项“-o”是指定GCC输出的结果
操作实例:
打开之后里面都是二进制字符:
链接阶段
二进制机器代码文件*.o ---> 可执行的二进制代码文件
语法格式:
代码语言:text复制gcc –o [目标文件] [编译文件]
gcc [编译文件] -o [目标文件]
经过这一步之后我们就可以运行这个可执行文件了
操作实例:
注意执行的时候一定要是
./
,否则shell会把它当作变量处理
GCC常用参数
总结
- 预处理:在这个阶段主要做了三件事:
展开头文件
、宏替换
、去掉注释行
- 这个阶段需要 GCC 调用预处理器来完成,最终得到的还是源文件,文本格式
- 宏替换:用对应的值替换掉代码中的所有宏变量
- 展开头文件:从根目录进行描述
- 编译:这个阶段需要 GCC 调用编译器对文件进行编译,最终得到一个汇编文件
- 汇编:这个阶段需要 GCC 调用汇编器对文件进行汇编,最终得到一个二进制文件
- 链接:这个阶段需要 GCC 调用链接器对程序需要调用的库进行链接,最终得到一个可执行的二进制文件
多文件编译
格式1:多文件同时编译
代码语言:text复制gcc 1.c 2.c 3.c –o test
$./test
格式2:每个文件分别进行编译,然后链接成可执行文件
代码语言:text复制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 函数的声明,也就是头文件,把这些都添加到项目中,就可以快乐的写代码了。
Linux库的创建与使用
库:事先已经编译好的代码,经过编译后可以直接调⽤的⽂件,本质上来说是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。系统提供的库的路径
/usr/lib
/usr/lib64
Linux库⽂件名的组成:
代码语言:txt复制前缀(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)。
- 参数c:创建一个库,不管库是否存在,都将创建。
- 参数s:创建目标文件索引,这在创建较大的库时能加快时间。
- 参数r:在库中插入模块 (替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
静态库的制作分为以下几个步骤:
- 需要将源文件进行汇编,得到 .o 文件,需要使用参数 -c 在这里插入图片描述
- 将得到的 .o 进行打包,得到静态库 在这里插入图片描述配符*.o 也可以将所有.o文件列出来
- 发布静态库 在这里插入图片描述
静态库制作实例:
例如我们现在有一个文件夹结构如下:
在这个目录中有几个关于算法的源文件:
- add.c:加法源文件
- div.c:除法源文件
- mult.c:乘法源文件
- sub.c:减法源文件
这每一个源文件里面都对应着不同的算法函数,这些对应的算法函数声明是在include下的head.h中。main.c文件是用来对这些算法函数进行测试的。接下来我们就制作静态库
首先我们使用gcc的-c命令将几个源文件转化成为二进制文件,但是这时候我们发现报错了:
原因是因为在我们的源文件中包含了head.h的头文件,但是这个头文件没有被找到,所以我们要手动指定头文件目录,前面我们提到过使用-I
:
接下来我们再来看看文件目录:
可以发现我们用于生成静态库的二进制.o文件已经生成好了。然后我们使用ar进行打包:
可以看到libcalc.a就是我们的静态库了。我们的静态库生成之后还要给别人用,在发布的时候需要给两个文件:
- 静态库(也就是上图的libcalc.a)
- 头文件(也就是上图的head.h)
我们把这两个文件都放到test中去,然后把我们的测试文件main.c文件也拷贝进来。然后我们来看看main.c文件和head.h文件:
此时我们编译main.c文件会报错;
他告诉我们没有找到函数的定义(也就是函数的实现),我们知道这些函数我们已经在libcalc.a文件中定义过了,我们只需要指定一下静态库的路径和名字:
- -L: 指定库所在的目录 (相对或者绝对路径)
- -l: 指定库的名字,需要掐头 (lib) 去尾 (.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之后我们调用的库函数对应的代码在虚拟地址空间中用的是一个相对地址。
制作动态库的具体步骤:
- 将源文件进行汇编操作,需要使用参数 -c, 还需要添加额外参数 -fpic /-fPIC 在这里插入图片描述
- 将得到的.o 文件打包成动态库,还是使用 gcc, 使用参数 -shared 指定生成动态库 (位置没有要求) 在这里插入图片描述
- 发布动态库和头文件 在这里插入图片描述
动态库制作实例:
使用的文件夹还是制作静态库中的:
然后使用gcc对源文件进行汇编操作生成与位置无关的文件
然后我们再通过gcc命令对文件进行打包:
在这里我们也可以看到动态库是有执行权限的:
在这里插入图片描述
这个*号就代表所有人对其都有执行权限
接下来我们说说如何使用动态库:
和静态库一样,我们也要发布两个文件:
- 动态库文件
- 头文件
然后我们粘贴main测试文件:
然后我们来编译我们的测试文件:
然后我们来执行app文件:
但是我们如果移动app这个可执行文件,会发现报错,动态库无法加载:
解决动态库无法加载问题
库的工作原理
- 静态库如何被加载
- 在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。==当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。==
- 动态库如何被加载
- 在程序编译的最后一个阶段也就是链接阶段:
- 在 gcc 命令中虽然指定了库路径 (使用参数 -L ), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在。
- ==同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。==
- 可执行程序被执行起来之后:
- 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
- 当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载
- 动态库的检测和内存加载操作都是由动态连接器来完成的
- 在程序编译的最后一个阶段也就是链接阶段:
动态链接器
动态链接器是一个独立于应用程序的进程,属于操作系统,当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L 指定的路径。
那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
- 可执行文件内部的 DT_RPATH 段
- 系统的环境变量 LD_LIBRARY_PATH
- 系统动态库的缓存文件 /etc/ld.so.cache(不可修改,只能通过修改/etc/ld.so.conf文件再通过shell命令进行同步)
- 存储动态库 / 静态库的系统目录 /lib/, /usr/lib 等
按照以上四个顺序,依次搜索,找到之后结束遍历,最终还是没找到,动态连接器就会提示动态库找不到的错误信息。
我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库,这个命令叫做 ldd
语法:
代码语言:txt复制ldd 可执行程序名
例如:
我们在执行这一个文件的时候需要四个动态库,而其中有一个库没有找到。
解决方案
可执行程序生成之后,根据动态链接器的搜索路径,我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。
解决方案①:将库路径添加到环境变量 LD_LIBRARY_PATH 中
- 找到相关的配置文件
- 用户级别: ~/.bashrc —> 设置对当前用户有效
- 系统级别: /etc/profile —> 设置对所有用户有效
- 使用 vim 打开配置文件,在文件最后添加这样一句话: 在这里插入图片描述
- 让修改的配置文件生效 - 修改了用户级别的配置文件,关闭当前终端,打开一个新的终端配置就生效了 - 修改了系统级别的配置文件,注销或关闭系统,再开机配置就生效了 - 不想执行上边的操作,可以执行一个命令让配置重新被加载 在这里插入图片描述
解决方案②:更新 /etc/ld.so.cache 文件
- 找到动态库所在的绝对路径(不包括库的名字)比如:/home/robin/Library/
- 使用 vim 修改 /etc/ld.so.conf 这个文件,将上边的路径添加到文件中 (独自占一行) 在这里插入图片描述
- 更新 /etc/ld.so.conf 中的数据到 /etc/ld.so.cache 中 在这里插入图片描述
解决方案③:拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)
静态库和动态库的优缺点
静态库:
优点:
- 静态库被打包到应用程序中加载速度快
- 发布程序无需提供静态库,移植方便
缺点:
- 相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存
- 库文件更新需要重新编译项目文件,生成新的可执行程序,浪费时间。
动态库;
优点:
- 可实现不同进程间的资源共享
- 动态库升级简单,只需要替换库文件,无需重新编译应用程序
- 程序猿可以控制何时加载动态库,不调用库函数动态库不会被加载
缺点:
- 加载速度比静态库慢,以现在计算机的性能可以忽略
- 发布程序需要提供依赖的动态库
分布式版本控制系统git
git简介
Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。
Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的
版本控制软件。
Git 与常用的版本控制工具 SVN, Subversion 等不同,它采用了分布式版本库的方式,不必服务器端软件支持。
git安装
可以先使用git命令查看是否安装
如果没有安装,则执行以下命令:
代码语言:text复制sudo apt-get install git (ubuntu)
sudo yum install git (centos)
然后我们进行配置指定使用git的账号和用户名:
代码语言:text复制$ git config --global user.name "Your name"
$ git config --global user.email "Your email"
如果去掉 --global 参数只对当前仓库有效。
git基本使用
首先我们要创建版本库
版本库又称仓库(repository)
,仓库中存放被git管理的文件,每个文件的修改、删除,git都能够跟踪,可以方便追踪历史。
创建仓库方法:
代码语言:text复制[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。
在这里插入图片描述add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
如下就是一个标准的
.git
文件夹:在这里插入图片描述
然后我们就可以向版本库中添加文件了
涉及命令:
代码语言:text复制git add filename
git add --all . 添加所有文件
工作区中的文件是可以被追踪的,但是只有告诉git哪些文件需要追踪,它才会显式的去追踪该文件,否则git永远会在你提交的时候告诉你工作区还有哪些文件处于Untracked状态。==而add命令就是用来显式告诉git哪些文件从此时开始追踪==。
然后我们就可以使用如下命令进行提交:
代码语言:text复制git commit
用add命令保存某个文件的修改,记录下该文件当前快照。然后用commit命令向分支上提交,位于分支上的每个点都是一次commit留下的。当然回滚的时候也是根据需要回滚到指定的点上。
例如:
代码语言:text复制前提:在刚创建的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命令详解==
语法格式:
代码语言:text复制git reset [--soft | --mixed | --hard] [HEAD]
--mixed 为默认,可以不用带该参数,用于重置暂存区的文件与上一次的提交(commit)保持一致,工作区文件内容保持不变。
例如:
代码语言:text复制$ git reset HEAD^ # 回退所有内容到上一个版本
$ git reset HEAD^ hello.php # 回退 hello.php 文件的版本到上一个版本
$ git reset 052e # 回退到指定版本
--soft 参数用于回退到某个版本
例如:
代码语言:text复制$ git reset --soft HEAD~3 # 回退上上上一个版本
--hard 参数撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到上一次版本,并删除之前的所有信息提交
例如:
代码语言:text复制$ git reset --hard HEAD~3 # 回退上上上一个版本
$ git reset --hard bae128 # 回退到某个版本回退点之前的所有信息。
$ git reset --hard origin/master # 将本地的状态回退到和远程的一样
注意:谨慎使用 –-hard 参数,它会删除回退点之前的所有信息。
HEAD 说明:
HEAD 表示当前版本
HEAD^ 上一个版本
HEAD^^ 上上一个版本
可以使用 ~数字表示:
HEAD~0 表示当前版本
HEAD^3 上上上一个版本
例如:
代码语言:text复制再次修改文件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 从版本库恢复)
Make/Makefile
使用 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 文件,分别位于不同的项目目录中==。
Makefile编写规则
每条规则的语法格式:
代码语言:shell复制target1,target2...: depend1, depend2, ...
command
......
......
每条规则由三个部分组成分别是目标(target)
, 依赖(depend)
和命令(command)
:
目标(target)
: 规则中的目标,这个目标和规则中的命令是对应的- 通过执行规则中的命令,可以生成一个和目标同名的文件
- 规则中可以有多个命令,因此可以通过这多条命令来生成多个目标,所有目标也可以有很多个
- 通过执行规则中的命令,可以只执行一个动作,不生成任何文件,这样的目标被称为
伪目标
依赖(depend)
: 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。- 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用
- 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空
- 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套
- 依赖可以根据要执行的命令的实际需求,指定很多个
命令(command)
: 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令- 例如:通过某个命令编译文件、生成库文件、进入目录等。
- 动作可以是多个,每个命令前必须有一个Tab缩进并且独占占一行。
例如:
代码语言: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
Makefile工作原理
规则的执行
在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则,分析并执行相关的动作。但是需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,就会先将需要的依赖生成出来。
这样,makefile 中的某一条规则在需要的时候,就会被其他的规则调用,直到 makefile 中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。
例如:
代码语言:shell复制# 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 文件中相关规则中的命令。
- 目标是通过依赖生成的,因此正常情况下:目标时间戳 > 所有依赖的时间戳 , 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件,那么规则中的命令就不会被执行。
- 当依赖文件被更新了,文件时间戳也会随之被更新,这时候 目标时间戳 < 某些依赖的时间戳 , 在这种情况下目标文件会通过规则中的命令被重新生成。
- 如果规则中的目标对应的文件根本就不存在, 那么规则中的命令肯定会被执行。
例如;
代码语言:shell复制# 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 文件),并且使用默认的命令来构建这个目标文件。
假设本地项目目录中有以下几个源文件:
代码语言:shell复制$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c
目录中 makefile 文件内容如下:
代码语言:shell复制# 这是一个完整的 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 构建项目:
代码语言:shell复制$ 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 中的变量赋值之后,如何在需要的时候将变量值取出来呢?
代码语言:shell复制# 如何将变量的值取出?
$(变量的名字)
# 举例 add.o div.o main.o mult.o sub.o
# 定义变量并赋值
obj=add.o div.o main.o mult.o sub.o
# 取变量的值
$(obj)
自定义变量使用举例:
代码语言:shell复制# 这是一个规则,普通写法
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 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:
代码语言:shell复制# 这是一个规则,普通写法
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 中的规则语句中经常会出现目标文件和依赖文件,==自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用==。
下表中是一些常见的自动变量:
下面几个例子,演示一下自动变量如何使用。
代码语言:shell复制# 这是一个规则,普通写法
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 文件:
代码语言:shell复制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
这个函数的主要作用是获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。函数原型如下:
代码语言:shell复制# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(wildcard PATTERN...)
参数: 指定某个目录, 搜索这个路径下指定类型的文件,比如: *.c
- 参数功能:
- PATTERN 指的是某个或多个目录下的对应的某种类型的文件,比如当前目录下的.c 文件可以写成 *.c
- 可以指定多个目录,每个路径之间使用空格间隔
- 返回值:
- 得到的若干个文件的文件列表, 文件名之间使用空格间隔
例如:
代码语言:shell复制$(wildcard *.c ./sub/*.c)
得到的返回值格式:
代码语言:shell复制a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c
使用举例;
代码语言:shell复制# 使用举例: 分别搜索三个不同目录下的 .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
这个函数的功能是按照指定的模式替换指定的文件名的后缀,函数原型如下:
代码语言:java复制# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)
- 参数功能:
- pattern: 这是一个模式字符串,需要指定出要被替换的文件名中的后缀是什么
- 文件名和路径不需要关心,因此使用 % 表示即可 通配符是 %
- 在通配符后边指定出要被替换的后缀,比如: %.c, 意味着 .c 的后缀要被替换掉
- replacement: 这是一个模式字符串,指定参数 pattern 中的后缀最终要被替换为什么
- 还是使用 % 来表示参数 pattern 中文件的路径和名字
- 在通配符 % 后边指定出新的后缀名,比如: %.o 这表示原来的后缀被替换为 .o
- text: 该参数中存储这要被替换的原始数据
- 返回值:
- 函数返回被替换过后的字符串。
- pattern: 这是一个模式字符串,需要指定出要被替换的文件名中的后缀是什么
函数使用举例:;
代码语言:shell复制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