C/C++编译总结 (Linux下g++、makefile、automake)Makefile笔记

指路上一篇: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工作流程
  1. 读入所有makefile
  2. 读入include的文件
  3. 如果有读入的目标的规则的话,先使用规则将目标更新,更新完再将内容包含进文件
  4. 初始化(展开变量、展开函数)
  5. 推导隐晦规则,分析所有规则
  6. 为所有目标创建依赖关系链
  7. 根据依赖关系,决定哪些目标需要重新生成(比较修改时间)
  8. 执行命令
 
 
n makefile的执行流程,完整版
 
4933453013ca4747b1870bafbd59c54b.png
 
 
 
 
 
 
 
 
 
 
 
二、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,因此头文件的改动对源文件的影响是比其他文件大的多,所以才加上。
 
参考资料:
  1. 跟我一起写makefile 陈皓
  2. GNU make(make的官方文档,以我浅薄的英语只能看个目录,需要自取)
 
 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值