IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发工具 -> makefile的使用技巧 -> 正文阅读

[开发工具]makefile的使用技巧

从vc6.0到visual studio,编译代码的工作都是由IDE来完成,我们只需要点击编译按钮,即可将源文件编译成库文件或可执行文件,非常方便。到后来,由于跨平台开发和自动构建等需求,转而开始使用CMake作为代码构建的工具,CMake能灵活的组织代码和构建项目,而且跨平台。再后来,工作中发现有些场景下不得不使用makefile进行代码编译。比如,有些系统上未安装CMake工具,而且也不具备安装条件,此时,我们不得不编写makefile来构建项目。

makefile的编写逻辑其实很简单,目标:依赖。最终目标文件依赖.o文件,.o文件依赖.c文件。一个简单的makefile如下:

test:test.o
	gcc test.o -o test
test.o:test.c
	gcc -c test.c -o test.o
.PHONY:clean
clean:
	rm -f *.o test

以上makefile可以编译只有一个test.c的工程,最终生成test可执行文件。

以上的逻辑搞懂了,基本上就可以为任何工程编写makefile了。然而我们上面的例子太简单了,如果复杂一点的项目,源文件成百上千,这样编写要累死人。

下面版本将解决源文件多的问题:

SRC = $(wildcard /src/*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
test:$(OBJ)
	gcc $(OBJ) -o test
%.o:%.c
	gcc -c $< -o $@
.PHONY:clean
clean:
	rm -f *.o test

其中SRC是makefile中用户自定义的变量,wildcard是makefile中内置的函数,此函数后面的参数指定源文件的路径。第一条语句意思是将src/路径下所有.c文件名赋值给SRC变量,SRC类似于存放了所有源文件名的数组。第二条语句通过patsubst函数,将SRC中的.c改为.o赋值给OBJ,OBJ中保存了所有.c文件对应的.o文件名。后面生成目标文件时,可以使用前面定义好的变量,避免了写一大堆.o文件。通配符%表示任意一个,%.o:%.c表示任何一个.o文件依赖其对应的.c文件,比如a.o:a.c, b.o:b.c。$<代表依赖项,$@代表目标,在这里,$<代表某个.c文件,$@代表这个.c文件对应的.o文件。上面通过使用makefile的变量和通配符大大减少了makefile编写内容。

以上makefile其实是不完整的,或者说是有问题的。因为.o目标文件除了依赖.c文件外,还需要依赖.h文件。.o文件依赖哪些.h文件要看.c文件依赖哪些.h文件。上面第一个例子中,假设test.c文件中包含了test.h文件,那么test.o的依赖中还应包含test.h。

test.o:test.c test.h

第二个例子中忽略了.h依赖,而且对于一个大型项目,每一个.c文件依赖的.h可能都不一样。给每个.c文件找出它所依赖的.h文件,这个工作量也是有点大。有一种粗暴的方式就是把所有.h文件作为.c文件的依赖,这样,.c文件总能找到它的依赖。这样makefile的改动也很小,如下:

SRC = $(wildcard /src/*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
HEAD = $(wildcard /src/*.h)
HEAD += $(wildcard /include/*.h)
test:$(OBJ)
	gcc $(OBJ) -o test
%.o:%.c $(HEAD)
	gcc -c $< -o $@ 
.PHONY:clean
clean:
	rm -f *.o test

增加一个HEAD变量,HEAD变量里面包含了这个工程所依赖的所有头文件。通过+=可以对HEAD变量进行追加内容。最后将HEAD变量加到%.o:%.c语句后面。以上解决方案可以保证编译没问题。但是由于每个.c文件都依赖所有.h文件,那么当某一个头文件修改时,再次make进行编译,就会导致所有.o文件被重新编译,最终导致整个项目的编译。而我们期望修改一个文件,只编译依赖它的目标文件,而不是全部编译。如何找到每一个.c文件确切的头文件依赖?gcc提供的-MM参数可以得到一个.c文件所依赖的所有头文件,通过这个功能貌似可以解决上面的问题。

首先我们看看通过gcc -MM会得到什么。

$ gcc -MM main.c
$ main.o: main.c func.h

我们得到了main.o: main.c func.h这一句,这个正是makefile中.o目标文件的依赖规则语句。makefile具有隐式推导能力,比如在makefile中只有main.o: main.c func.h这一句,那么在makefile进行执行之前,会先推导出gcc -c main.c -o main.o,也就是说我们可以在makefile中只写一句 main.o: main.c func.h。因此,我们只需要将每个.c文件通过gcc -MM生成的结果放到makefile中就可以了。但是makefile又不可能动态更改,好在makefile有include功能。类似与C语言中的#include,可以包含文件内容进来,makefile的include也具有这个能力,所以,我们可以在makefile中通过命令生成.o文件的依赖规则内容,并写入文件,再通过include包含在makefile中。

SRC = $(wildcard /src/*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
INCLUDEFLAGS = -I./include
test:$(OBJ)
	gcc $(OBJ) -o test
%.o:%.c
	gcc -c $< -o $@ 
%.d:%.c
	@set -e; rm -f $@;	\
	gcc -MM $(INCLUDEFLAGS) $< > $@.$$$$;	\
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$
-include $(OBJ:.o=.d)
.PHONY:clean
clean:
	rm -f *.o test *.d

增加了$.d:%c规则,目的是执行下面的的shell命令。

@set -e;语句表示不输出shell命令执行过程;

rm -f $@;删除.d文件,$@代表目标文件,在%.d:%.c规则中,目标文件就是.d文件;

gcc -MM $< > $@.$$$$;这条语句就是生成.c的依赖规则,$<代表.c文件,将依赖规则写入$@.$$$$文件中,$@代表目标文件.d,后面的$$$$实际上是$$$符号转义需要一个$,即两个$$代表一个$,而$$最终在shell中会被替换为进程ID,最终$@.$$$$文件名可能是 test.d.6732,它只是一个临时文件,后面会删除。

sed 's,\($*\)\.0[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;这条语句分两部分看;

首先看sed命令 sed commond file,将生成的包含依赖规则的文件内容使用sed处理,处理命令s,\($*\)\.0[ :]*,\1.o $@ : ,g,这条命令是一个正则表达式替换命令,结构:s,要替换的内容,替换之后的内容,g

要替换的内容:\($*\)\.0[ :]*$*代表目标文件不包含后缀名,比如 test.d为目标,而 $*表示test,即:test.o;

替换之后的内容:\1.o $@ :,1代表查找到的第一个结果,即test。最终替换成test.o test.d;

最后将替换好的内容写入到$@,即test.d中;

rm -r $@.$$$$,删除临时文件;

上面的命令总结起来就是:通过gcc -MM命令生成.o文件的依赖规则,同时修改依赖规则,写入到.d文件中,最后在目标项中加入.d。

其结果就是在makefile中增加了如下语句:

main.o main.d: main.c func.h

最后-include,将.d文件包含到makefile中,$(OBJ:.o=.d)意思是所有.o对应的那个.d文件,即这条语句包含了所有.d文件。include 前面的"-"表示如果有错误忽略,因为,当第一次make时,不存在.d文件,include一定是找不到文件的。

至此,我的makefile基本形态已经确定了。

这里还有2个问题需要注意

  1. 刚才我们讲makefile会对main.o: main.c func.h进行隐式推导,会变成如下:

    main.o: main.c func.h
    	gcc -c main.c -o mian.o
    

    既然可以推导得到,那么我们makefile前面的

    %.o:%.c
    	gcc -c $< -o $@ 
    

    是不是多余的?

    这个不是多余的,可以理解为,一个规则声明,和隐式推导的规则不冲突,这个理论上是可以省略的,但是实际上一般不会省略,而且还有用处,比如,编译参数可以加在这里,因为隐式推导的规则无法添加编译参数。

  2. 为什么main.d也要作为目标项?

    因为main.d应该是可以根据依赖项变化的,比如让main.d像main.o一样依赖main.c和func.h。假如我们不让main.d作为目标项,那么就变成了main.o: main.c func.h,此时,我们修改func.h,在里面增加一个头文件包含如下:

    // func.h
    #include "in.h"	// 新增
    

    此时,我们make时,由于func.h发生了变化,所以main.o会被重新编译,这一次没有问题。接着,修改in.h文件,因为main.o只依赖main.c和func.h,再次make时,main.o没有重新编译!这当然不是我们所期望的。

    反过来想,当main.d作为目标项,即:main.o main.d: main.c func.h,这种情况下,当func.h进行修改时,main.d会像main.o一样重新生成,编译之后的main.d变成main.o main.d: main.c func.h in.h, 此时,我们再次修改in.h时,就能触发main.o被重新编译,这正达到了我们的要求。这也就是为什么会有sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;这句脚本去修改目标依赖项的原因。

上面对于.d文件的生成脚本是大家普遍使用的,而且网上都是这种写法,但是在我自己使用中发现这个脚本其实是有问题的。

.
├── func.c
├── func.h
├── include
│   ├── in2.h
│   ├── in.h
│   └── test.h
├── main.c
├── makefile
└── src
    └── test.c

上图所示的目录结构,运行gcc -MM命令:

$ gcc -MM -I./include src/test.c
test.o: src/test.c include/test.h include/in2.h

通过上面的脚本生成的.d文件内容为:

test.o: src/test.c include/test.h include/in2.h

test.d文件并没有被添加到目标项中。这是因为替换脚本并没有识别找到test.o,因为目录结构的存在,查找时,匹配字符串是src/test.o,带了路径。因此,没能正确匹配和替换。最终导致生成的.d文件中没有将.d添加为目标项,不将.d作为目标项导致的问题就是上面第2个问题。

正确的脚本:

%.d:%.c
     rm -f $@; \
     $(CC) -MM $(INCLUDEFLAGS) $< > $@.$$$$; \
     sed 's,\($(notdir $*)\)\.o[ :]*,\$*.o $@ : ,g' < $@.$$$$ > $@; \
     rm -f $@.$$$$ 

通过makefile内置函数notdir将查找项的路径去掉,在将带路径的.o和.d添加上去,最终由脚本生成的.d文件为:

src/test.o src/test.d : src/test.c include/test.h include/in2.h

完整版的makefile模板

INCLUDEFLAGS = -I. -I./include
CFLAGS = -g
LDFLAGS =
CC = gcc

SRC += $(wildcard ./*.c)
SRC += $(wildcard ./src/*.c)

OBJ = $(patsubst %.c, %.o, $(SRC))
DEP = $(patsubst %.c, %.d, $(SRC))
BIN = a.out

.PHONY:all
all:$(BIN)

$(BIN):$(OBJ)
	$(CC) $(OBJ) -o $@ $(LDFLAGS)

%.o:%.c
	$(CC) -c $< -o $@ $(CFLAGS) $(INCLUDEFLAGS)

%.d:%.c
	@set -e;rm -f $@; \
	$(CC) -MM $(INCLUDEFLAGS) $< > $@.$$$$; \
	sed 's,\($(notdir $*)\)\.o[ :]*,\$*.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$ 

-include $(OBJ:.o=.d)

.PHONY:clean
clean:
	rm -f $(OBJ) $(BIN) $(DEP)
  开发工具 最新文章
Postman接口测试之Mock快速入门
ASCII码空格替换查表_最全ASCII码对照表0-2
如何使用 ssh 建立 socks 代理
Typora配合PicGo阿里云图床配置
SoapUI、Jmeter、Postman三种接口测试工具的
github用相对路径显示图片_GitHub 中 readm
Windows编译g2o及其g2o viewer
解决jupyter notebook无法连接/ jupyter连接
Git恢复到之前版本
VScode常用快捷键
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:40:00  更:2022-04-09 18:41:29 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/14 15:14:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码