指路上一篇:C/C++编译总结 (Linux下g++、makefile、automake)
C/C++编译总结 (Linux下g++、makefile、automake)
!! 阅读前提示:
1)本文是自己的学习过程和经验的总结,和大多数人一样,不懂就搜度娘、csdn、博客园等等,内容难免有不足之处和理解不到位的情况,请见谅。所有知识来源于网络,摘录内容会指明出处。
2)本文默认读者对基本名词已经清楚,如编译,c++,linux等。
3)本文更新于2023/02/03
4)有疑问可以私信交流
2023/02/03更新
下面正文未作改动,从接触makefile到写这篇文章再到现在有小半年了,工作中几个小工程是自己写的makefile进行编译,但是体验不咋样,写makefile太麻烦了,刚学完去写会有成就感(老子牛皮🥳),时间久了(什么乱七八糟的磨磨叽叽😩),最近在搞嵌入式项目,才知道Qt有个qmake(自动生成makefile的工具),能自动生成何必手写呢🤩太香了,不想学makefile的宝子们可以去试试qmake。
Makefile笔记
先来唠叨几句,在熟悉了上面的g++命令,已经可以编译代码了,对于编译自己写的练习或者几个源文件的小项目,还能手动敲敲命令,but,真正工作了,开发项目了,一个工程中的源文件、头文件、依赖库多的是,就拿我刚工作接手的一个项目吧,vs2019打开,一个解决方案下13个项目,好几个静态库,好几个动态库,好几个组件库,每个库17、8个头文件、源文件。在Windows的vs下,配配项目属性没有语法问题项目就可以编译了,但凡拿到Linux下去编,17*13=221,200多个源文件g++命令得敲到猴年马月啊,所以makefile是非常有必要去学会的一个工具。
Makefile我是跟着“跟我一起写makefile 陈皓”这本书学的,这本书不厚就75页,满满干货,有时间推荐学一学。
提示1:学习makefile前,最好掌握一些Linux的shell命令行知识。
提示2:练习写makefile时,如果觉得执行结果莫名其妙,那就是隐晦规则的锅。
使用makefile工具,应该是这些流程:
1)编写项目代码(源文件和头文件)
2)编写makefile文件(指明要如何去编译这个工程)
3)运行makefile(没有安装make的话还有安装)
以下要讲的就是2)和3)的部分:
-
Makefile的语法(主要内容)
-
Makefile的安装和使用(make命令)
-
最后会给出几个makefile常用模板
写几个makefile中几个典型,哪里不懂点哪里
| 看哪块 |
|
%.xx : %.xx
| 隐晦规则 |
|
.c.o:
CC xxxxxx
|
隐晦规则
|
这是老版本的语法
|
.PHONY
|
伪目标
|
|
.SUFFIXES
|
隐晦规则
|
|
$@ $^...
|
自动化变量
|
|
一、makefile的语法
1 makefile的两条核心规则
在makefile中有两条核心规则,这两条核心规则是一定要先搞懂的。(放心,这不难)
1)最基本的规则
目标:依赖
「TAB」命令
看见上面这条公式别慌,聪明的你,不感觉这很眼熟吗,嘻嘻,“目标:依赖”这是啥,这不就是,二里面讲的-M那个g++生成的依赖关系的那个东西嘛,所以我才在二里面着重说了-M那个选项很重要。(上一篇文章)
目标可以是:一个文件,一个文件集,一个便签,一类文件等等。
命令指的是shell命令,所以说写makefile要懂一点shell命令行的知识
下面举几个例子:
main.o : main.cpp
g++ -c main.cpp –o main.o
|
file1.o file2.o : def.cpp
echo “this is test”
|
clear:
rm –rf *.o
|
*.o : *.cpp
g++编译命令
|
上面四个就是常见的编译规则的形式
2)所有规则执行条件:
依赖新于目标(所谓的新,指的是文件的修改日期),或者目标不存在
好了两条核心规则说完了,不难吧,在说别的内容之前,先把makefile中的常识整理一下吧。
2 Makefile小常识
1)注释符:#
#强烈推荐在一行的起始位置写,非常不建议在依赖或者命令后面写注释,不熟练的时候可能会出现各种小问题,makefile中的注释和C++的注释很不一样。
2)makefile以制表符作为命令起始
上面的依赖关系的命令前,我写了tab,如果你在写makefile时,命令前不用tab,将会报错[*** missing separator. Stop.],意思是“缺失分割符,停止”,一定要打tab,敲空格都不行。
3)换行符 \(反斜杠)
4)
生成第一个依赖关系,是makefile的终极任务。
第一个就是你写在makefile文件最前面的那一个
5)有特殊含义的符号
|
含义
|
|
*
|
零个或多个字符
|
通配符
|
?
|
一个字符
|
通配符
|
[...]
|
|
通配符
|
~
|
代表当前用户的根目录
|
|
%
|
一个或任意个字符
|
模式
|
@
|
不显示命令本身
|
|
-
|
忽略命令的错误
|
|
3 makefile的组成部分:<变量定义>,<文件指示>,<显示规则>,<隐晦规则>,<注释>
3.1 makefile的变量(大多数同shell变量)
makefile同shell脚本中非常不一样的地方就是,变量定义(小声嘀咕,刚开始以为shell也能这样定义,后来发现这是makefile的专属技能)
(makefile)
|
|
=
|
赋值,多次赋值后,变量的值为最后一次赋值的值
|
:=
|
定义赋值,值始终保持定义时的值,后续不会改变
|
?=
|
如果没有被赋值过,则赋值
|
+=
|
追加赋值
|
|
|
3.2 显示规则的写法
目标:依赖
「TAB」命令
或
(静态模式)
目标:模式 :依赖
「TAB」命令
用途:在目标集中可能会存在多种目标,使用模式可以从中筛选出特定的目标进行操作
第一种用于
main.o : main.cpp这种很明确的规则中
第二种用于
OBJ=fun1.cpp fun2.cpp pfun1.py pfun2.py (随便写的几个例子)
¥{OBJ} : %.cpp : def.h
这种,中间的模式对前面的目标做了筛选
3.2.1 指定文件查找路径
在你的 目标:依赖 中可能会出现不在当前目录下的文件,因此你要指定,当makefile在当前目录找不到文件了,去哪里找文件
指定方法:
VPATH = 路径名:路径2:路径3:...
!!区分:
这个不同于编译命令里的-I选项,vpath告诉makefile去哪找文件,g++的-I选项告诉g++去哪找文件看这个例子,有一个目录
-
Fdir
-
links//一个目录,存在一些公共代码
-
def.h
-
-
project
-
include
-
A.h
-
-
source
-
main.cpp
-
makefile
-
-
-
在main.o : main.cpp 这个依赖关系中,
不需要指定VPATH,所有文件都在当前目录下
而在main.o : main.cpp A.h def.h 这个依赖关系下
不指定VPATH,则会提示找不到文件
推荐使用第二种依赖关系
为什么要用第二种依赖关系?
makefile 的规则是依赖比目标新,才会去执行命令,如果你的头文件,或者说你cpp里用到的,你#include到源文件的但你没写在依赖中,一旦你改了头文件,makefile是不会知道的,哪怕你把头文件删了,makefile也只会提示你
'XXX' is up to date"
3.2.2 伪目标
.PHONY
3.3 隐晦规则(makefile的自动推导)
隐晦规则是一种惯例,例如.cpp文件编译成.o文件,makefile提前定义好了一些规则
3.3.1 已定义好的隐晦规则
|
规则
|
命令
|
|
c
|
x.o -> x.c
|
$(CC) -c $(CPPFLAGS) $(CFLAGS)
|
|
c++
|
x.o -> x.c
|
$(CXX) -c $(CPPFLAGS) $(CFLAGS)
|
|
汇编与预处理
|
x.o -> x.s
|
$(AS) $(ASFLAGS)
|
|
pascal、fortran等
|
略
|
|
提示:编译-C选项是汇编哦(憋问我为什么要写这句,问就是我自己也忘了)
3.3.2 隐晦规则用到的变量与默认值
|
默认值
| 说明 |
AR
|
ar
|
|
AS
|
as
|
|
CC
|
cc
|
|
CXX
|
g++
|
|
CPP
|
$(CC) -E
|
|
RM
|
rm -f
|
|
//参数变量
|
//以下参数默认均为空
|
|
CFLAGS
|
|
c编译参数
|
CXXFLAGS
|
|
c++编译参数
|
LDFLAGS
|
|
链接器参数
|
3.3.3 定义隐晦规则的模式规则
%.后缀名1:%.后缀名2; 命令集合
3.3.4 老式的后缀规则
3.3.5 自动化变量
4 makefile中的函数
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
n-1 包含其他makefile
-
方法:[-]include <文件名> //减号是可选项
-
执行过程:
-
查找文件
-
文件存在则将文件包含到当前行,找不到文件则提示警告
-
makefile读取完成,再包含一遍,首次包含失败的文件(一般是文件不存在)
-
仍有文件包含失败
-
没有减号,提示错误
-
有减号,忽略错误继续执行
-
-
-
查找顺序:
-
当前目录
-
make 执行时 -I 或 --include-dir 参数指定的位置,如果有这个参数的话
-
/usr/local/bin或/usr/include
-
-
使用场景:
-
自动生成依赖性
-
[欢迎补充]
-
n-2 makefile工作流程
-
读入所有makefile
-
读入include的文件
-
如果有读入的目标的规则的话,先使用规则将目标更新,更新完再将内容包含进文件
-
初始化(展开变量、展开函数)
-
推导隐晦规则,分析所有规则
-
为所有目标创建依赖关系链
-
根据依赖关系,决定哪些目标需要重新生成(比较修改时间)
-
执行命令
n makefile的执行流程,完整版

二、makefile的使用
在shell命令行中输入make命令,自动寻找本目录下makefile文件执行
$make
结果:寻找本目录下的名为GUNmakefile、makefile或Makefile的文件
$make –f 文件名
结果:指定文件运行
|
|
|
-C <dir>
|
--directory=<dir>
|
指定makefile的目录
|
-f
|
--file
|
指定文件
|
-n
|
--just-print
--dry-run
--recon
|
仅显示要执行的命令序列,但不执行
|
-s
|
--slient
|
禁止显示命令
|
-I <dir>
|
--include-dir=<dir>
|
被包含的include的文件的搜索路径
|
-i
|
--ignore-errors
|
忽略错误
|
-k
|
--keep-going
|
忽略错误继续执行
|
-S
|
--no-keep-going
|
取消-k选项
|
-w
|
--print-directory
|
|
-t
|
--touch
|
更新目标文件时间,假装更新目标
|
-q
|
--question
|
找目标
|
-B
|
--always-make
|
完全重新编译
|
-b
|
|
忽略和其他版本的兼容性
|
-m
|
|
忽略和其他版本的兼容性
|
-h
|
--help
|
显示帮助信息
|
-p
|
--print-data-base
|
显示makefile的所有数据
|
-r
|
|
禁止使用任何隐晦规则
|
-v
|
|
显示makefile的版本信息
|
可以参考的伪目标
clean
|
install
|
print
|
tar
|
tags
|
|
模板
总控
说明:一个项目下,可能有多个要编译的小项目,例如:
-
ParentDir
-
“总控makefile”
-
Project1
-
makefile
-
-
Project2
-
源文件
-
makefile
-
-
-
每个小项目都有每个项目的makefile
此时,可以编写一个“总控makefile”一键编译,当然也可以切换到每个小项目下手动编译
每
#写入makefile的路径,例如./Project1/ ./Project2/源文件/
SUBDIR=子路径1 子路径2 …
#make命令和编译参数
MAKE=make -C
COMPILER:
@set -e; for i in ${SURDIR}; do ${MAKE} $$i; done
.PHONY:clean
clean:
@for i in ${SUBDIR}; do ${MAKE} $$i clean; done
#如果makefile里都定义了某一个标签,你可以继续加入
标签:
@for i in ${SUBDIR}; do ${MAKE} $$i 标签; done
自动依赖性
自动依赖性根据不同的目录结构写了两个模板,不过大同小异
模版1:适用于源文件都在一起的情况
原始目录结构
-
Project
-
header
-
若干个头文件xxx.h
-
-
source
-
若干个源文件xxx.cpp
-
Makefile
-
-
编译后的目录结构
-
Project
-
header
-
若干个头文件xxx.h
-
-
source
-
deps
-
若干个对应xxx.cpp的依赖关系文件xxx.dep
-
-
objs
-
若干个对应xx.cpp的目标文件xxx.o
-
-
若干个源文件xxx.cpp
-
Makefile
-
-
#需要修改和填写的位置都使用尖括号<>标明
#最终要得到的程序,可以是可执行文件,静态库或者动态库
EXE=<文件名称>
#源文件
SRCS:=$(wildcard *.cpp)
#目标文件,通过替换函数生成
OBJS=$(SRCS:.cpp=.o)
#依赖关系文件
DEPS=$(SRCS:.cpp=.dep)
DIR_DEPS=deps
DIR_OBJS=objs
OBJS:=$(addprefix ${DIR_OBJS}/,${OBJS})
DEPS:=$(addprefix ${DIR_DEPS}/,${DEPS})
DIR=${DIR_DEPS} ${DIR_OBJS}
#以下是编译参数
CXX=<编译器>
INCLUDE=<头文件路径>
LDFLAGS=<依赖库路径和库名称>
CXXFLAGS=<编译参数> ${INCLUDE}
#指定make去哪里找文件
vpath %.h 路径1
vpath %.h 路径2
. . .
#规则
${EXE}:${DIR_OBJS} ${OBJS}
<编译命令>
-include ${DEPS}
${DIR}:
-@mkdir $@
#解释一下,这里把编译语句也写入dep文见里了,可以避免
${DIR_DEPS}/%.dep : ${DIR_DEPS} %.cpp %.h
@echo -n “${DIR_OBJ}/” > $@ \
<可以在这写提示语句> \
${CXX} -MM -E $(filter %.cpp,$^) ${INCLUDE} >> $@;
@echo ‘ $${CXX} -o $$@ -c $$(filter %.cpp,$$^) $${CXXFLAGS}’ >> $@
.PHONY:clean
clean:
rm -rf ${DIR} ${EXE}
模版2:适用于源文件被分成若干个文件夹的情况
原始目录结构
-
ProjectProject
-
header
-
folder1
-
若干头文件
-
-
folder2
-
若干头文件
-
-
-
source
-
deps
-
若干个对应xxx.cpp的依赖关系文件xxx.dep
-
-
objs
-
若干个对应xx.cpp的目标文件xxx.odeps
-
若干个对应xxx.cpp的依赖关系文件xxx.dep
-
-
objs
-
若干个对应xx.cpp的目标文件xxx.o
-
-
folder1
-
若干个源文件xxx.cpp
-
-
folder2
-
若干源文件
-
-
main.cpp
-
Makefile
-
-
header
-
folder1
-
若干头文件
-
-
folder2
-
若干头文件
-
-
-
source
-
folder1
-
若干个源文件xxx.cpp
-
folder2
-
若干源文件
-
-
main.cpp
-
Makefile
-
-
编译后的目录结构
-
Project
-
header
-
folder1
-
若干头文件
-
-
folder2
-
若干头文件
-
-
-
source
-
deps
-
若干个对应xxx.cpp的依赖关系文件xxx.dep
-
-
objs
-
若干个对应xx.cpp的目标文件xxx.o
-
-
folder1
-
若干个源文件xxx.cpp
-
-
folder2
-
若干源文件
-
-
main.cpp
-
Makefile
-
-
#需要修改和填写的位置都使用尖括号<>标明
#最终要得到的程序,可以是可执行文件,静态库或者动态库
EXE=<文件名称>
#源文件
SOURCE:=$(wildcard *.cpp 路径/*.cpp 路径2/*.cpp)
#去掉前缀,拿到文件名
SRCS=$(notdir ${SOURCE})
#目标文件,通过替换函数生成
OBJS=$(SRCS:.cpp=.o)
#依赖关系文件
DEPS=$(SRCS:.cpp=.dep)
DIR_DEPS=deps
DIR_OBJS=objs
OBJS:=$(addprefix ${DIR_OBJS}/,${OBJS})
DEPS:=$(addprefix ${DIR_DEPS}/,${DEPS})
DIR=${DIR_DEPS} ${DIR_OBJS}
#以下是编译参数
CXX=<编译器>
INCLUDE=<头文件路径>
LDFLAGS=<依赖库路径和库名称>
CXXFLAGS=<编译参数> ${INCLUDE}
#指定make去哪里找文件
vpath %.h 路径1
vpath %.h 路径2
. . .
#这里和模板1有区别,因为.cpp文件在不同的文件夹,这里要把所有文件夹列一下才行
vpath %.cpp 路径1
vpath %.cpp 路径2
. . .
vpath %.cpp 路径n
#规则
${EXE}:${DIR_OBJS} ${OBJS}
<编译命令>
-include ${DEPS}
${DIR}:
-@mkdir $@
#解释一下,这里把编译语句也写入dep文见里了,可以避免
${DIR_DEPS}/%.dep : ${DIR_DEPS} %.cpp %.h
@echo -n “${DIR_OBJ}/” > $@ \
<可以在这写提示语句> \
${CXX} -MM -E $(filter %.cpp,$^) ${INCLUDE} >> $@;
@echo ‘ $${CXX} -o $$@ -c $$(filter %.cpp,$$^) $${CXXFLAGS}’ >> $@
#这个模式是给没有头文件的源文件使用的,例如main.cpp
${DIR_DEPS}/%.dep : ${DIR_DEPS} %.cpp
@echo -n “${DIR_OBJ}/” > $@ \
<可以在这写提示语句> \
${CXX} -MM -E $(filter %.cpp,$^) ${INCLUDE} >> $@;
@echo ‘ $${CXX} -o $$@ -c $$(filter %.cpp,$$^) $${CXXFLAGS}’ >> $@
.PHONY:clean
clean:
rm -rf ${DIR} ${EXE}
这里解释一下,为什么${DIR_DEPS}/%.dep : ${DIR_DEPS} %.cpp %.h,这里我要加上头文件。
这里不加%.h完全可以使用,我加头文件的原因,是因为一个源文件依赖的文件,绝大部分都在cpp文件最上面include或者在对应的头文件里include,因此头文件的改动对源文件的影响是比其他文件大的多,所以才加上。
参考资料:
-
跟我一起写makefile 陈皓
-
GNU make(make的官方文档,以我浅薄的英语只能看个目录,需要自取)