快速编写一个可复用的Makefile


前言

本文将从以下几个方面展开阐述:第一,分析项目的依赖关系;第二,写项目的Makefile编译项目;第三,优化Makefile文件。读懂这篇文章前提是得了解C/C++编译过程:C/C++编译过程

一、项目分析

1. 目录结构和源码

  • source
    • main.cpp
    • Time.cpp
    • Time.h
    • Makefile

main.cpp 代码如下(示例):

// main.cpp
#include "Time.h"

int main(int argc, char *argv[])
{
    Time time;
    time.printTime();
    return 0;
}`

Time.h 代码如下(示例):


// Time.h
#ifndef TIME_H
#define TIME_H

class Time
{
private:
    int m_nHour;
    int m_nMinute;
    int m_nSecond;
public:
    Time();
    Time(int hour, int minute, int second);
    ~Time();

    void printTime();
};
#endif

Time.cpp 代码如下(示例):

// Time.h
#include <iostream>
#include "Time.h"

Time::Time()
{

}

Time::Time(int hour, int minute, int second) : m_nHour(hour), m_nMinute(minute), m_nSecond(second)
{

}

Time::~Time()
{

}

void Time::printTime()
{
    std::cout << m_nHour << ":" << m_nMinute << ":" << m_nSecond << std::endl;
}

由于源码过于简单,这里不再赘述。

2. 分析依赖关系

使用G++ -MM 可以查看依赖关系,所以目标main.o 依赖于 main.cpp 和 Time.h;Time.o依赖于Time.cpp Time.h。

psy@ubuntu:~/Desktop/test/source$ g++ -MM main.cpp 
main.o: main.cpp Time.h
psy@ubuntu:~/Desktop/test/source$ g++ -MM Time.cpp 
Time.o: Time.cpp Time.h

二、编译项目

1. 使用显示规则

分析完项目的依赖关系之后就可以写Makefile了,我们先用显示规则写一个最简单的Makefile。后边逐步优化:

代码如下(示例):

# 第一版Makefile
main.exe: main.o Time.o
	g++ -o main.exe main.o Time.o

main.o: main.cpp Time.h
	g++ -o main.o -c main.cpp

Time.o: Time.cpp Time.h
	g++ -o Time.o -c Time.cp

执行make main.exe 效果如下:

psy@ubuntu:~/Desktop/test/source$ make main.exe
g++ -o main.o -c main.cpp
g++ -o Time.o -c Time.cpp
g++ -o main.exe main.o Time.o

最终生成了可执行文件main.exe,此时完成了简单项目的编译。

2. 伪目标

编译完成后,当前目录下多了main.exe,main.o,Time.o三个文件:

psy@ubuntu:~/Desktop/test/source$ ls
main.cpp  main.exe  main.o  Makefile  Time.cpp  Time.h  Time.o

那么如何实现大型项目make clean的清理工作呢,接下来我们稍微改一下Makefile:

# 第一版Makefile
main.exe: main.o Time.o
	g++ -o main.exe main.o Time.o
main.o: main.cpp Time.h
	g++ -o main.o -c main.cpp
Time.o: Time.cpp Time.h
	g++ -o Time.o -c Time.cpp
clean:
	rm *.exe *.o:w

执行 make clean

psy@ubuntu:~/Desktop/test/source$ make clean
rm *.exe *.o
psy@ubuntu:~/Desktop/test/source$ ls
main.cpp  Makefile  Time.cpp  Time.h
psy@ubuntu:~/Desktop/test/source$ 

可以看到此时多出的.o文件和.exe已被删除,我们接下来在当前目录下新创建一个名字为clean的文件,再执行make clean:

psy@ubuntu:~/Desktop/test/source$ touch clean
psy@ubuntu:~/Desktop/test/source$ make clean
make: 'clean' is up to date.

显然,我们执行make clean的目的是做目标文件的清理工作,但是make却不这么认为。把clean当做了一个目标文件处理。那么我们如何指定让make做清理工作呢,指定clean为伪目标:

# 第一版Makefile
.PHONY:clean  #伪目标
main.exe: main.o Time.o
	g++ -o main.exe main.o Time.o
main.o: main.cpp Time.h
	g++ -o main.o -c main.cpp
Time.o: Time.cpp Time.h
	g++ -o Time.o -c Time.cpp
clean:
	rm *.exe *.o

此时再执行make clean,就和我们期望的结果一致

psy@ubuntu:~/Desktop/test/source$ make clean
rm *.exe *.o

2. 普通变量

假设现在这个项目的源文件是C语言的,那么Makefile是不是需要把g++改为gcc呢,答案是肯定的。Makefile引入变量可以更方便我们维护和管理项目,接下来在Makefile中使用多个变量:

# 第二版Makefile 引入变量
.PHONY:clean  #伪目标

CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf

main.exe: main.o Time.o
	$(CXX) -o main.exe main.o Time.o

main.o: main.cpp Time.h
	$(CXX) -o main.o -c main.cpp

Time.o: Time.cpp Time.h
	$(CXX) -o Time.o -c Time.cpp

clean:
	$(RM) $(RM_FLAGS) *.exe *.o

效果和前面的一致,只是更加方便维护。注意变量的赋值有三种方式:第一种 = 是递归扩展;第二种 := 覆盖赋值;第三种,?= 如果已经定义那么不再赋值。尤其要注意第一种:

foo = $(bar)
bar = $(ugh)
ugh = Huh
echo $(foo)

$(foo)变量的值是Huh,所以变量在赋值时一定要注意细节。

2. 自动变量

仔细观察Makefile的规则,有一个共同点。都是目标:依赖,新行TAB键跟语句:

Time.o: Time.cpp Time.h
	$(CXX) -o Time.o -c Time.cpp

那么语句: $(CXX) -o Time.o -c Time.cpp 的-o后面跟的目标文件和-c后面跟的依赖文件是不变的,所以可以使用自动变量来完成目标和依赖的处理。

# 第三版Makefile 引入自动变量
.PHONY:clean  #伪目标

CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf

main.exe: main.o Time.o
	$(CXX) -o $@ $^

main.o: main.cpp Time.h
	$(CXX) -o $@ -c $<

Time.o: Time.cpp Time.h
	$(CXX) -o $@ -c  $<

clean:
	$(RM) $(RM_FLAGS) *.exe *.o

这里只介绍三个常用的自动变量,需要用到的时候再去查资料。$@:目标,$^:所有依赖,$<第一个依赖文件。

2. 常用函数

假设一个项目有成百上千个源文件。如果目标和依赖都手动去写。那工作量将不堪设想,有没有一个函数能获取当前目录下的所有源文以及根据源文件得到目标文件?我们再把Makefile文件修改一下:

# 第三版Makefile 引入自动变量
.PHONY:clean  #伪目标

CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf

SOURCES = $(wildcard *.cpp)              # 获取当前目录下的所有.cpp文件名
OBJS = $(patsubst %.cpp,%.o, $(SOURCES)) # 根据.cpp文件得到对用的.o文件

main.exe: $(OBJS)
	$(CXX) -o $@ $^

%.o: %.cpp
	$(CXX) -o $@ -c $^

clean:
	$(RM) $(RM_FLAGS) *.exe *.o

引入 wildcard 函数和 patsubst 函数之后就不必人为的去写明目标和依赖,这里介绍几个常用的函数:

$(wildcard *.cpp)              #  获取当前目录下的所有.cpp文件名
$(patsubst %.cpp,%.o, $(SOURCES)) #  根据.cpp文件得到对用的.o文件
$(filter %.cpp, $^)           #  过滤$^得到以.cpp后缀的文件
$(filter-ou %.cpp, $^)  #  过滤$^中所有以.cpp后缀的文件
$(strip $^)                   # 去除$^中多余的空格

三、优化Makefile

1. 模块化编译

大型项目一般都是分模块编译,这样更方便维护项目。将每个模块编译为独立的一个库(静态库/动态库)。最后链接为可执行文件。我们把项目分为两个模块00_Main和01_Time,目录结构如下所示:

  • source

    • 00_Main
      • main.cpp
      • Makefile
    • 01_Time
      • Time.cpp
      • Time.h
      • Makefile

    源文件的内容和依赖关系没发生变化,只是放到了不同的路径下。那么接下来我们分析一下这个项目如何编译。首先把00_Main编译为lib00_Main.so,再把01_Time编译为lib01_Time.so。最后链接为可执行文件。接下来我们修改一下两个模块中的Makefile:

# 第四版Makefile 共享库
DEBUG = n
MKDIR = mkdir
MKDIR_FLAGS = -p
RM = rm
RM_FLAGS = -rf
CXX = g++
CXX_FLAGS_R = -fPIC -shared -O3 -DNDEBUG -fexceptions -fnon-call-exceptions
CXX_FLAGS_D = -fPIC -shared -O0 -g3 -fexceptions -fnon-call-exceptions

SO_NAME = 00_Main   # 动态库名称
ROOT_PATH = ../..
INTERMEDIATE_DIR_R = $(ROOT_PATH)/build/Release/$(SO_NAME)
INTERMEDIATE_DIR_D = $(ROOT_PATH)/build/Debug/$(SO_NAME)
INCLUDE_DIR = -I$(ROOT_PATH)/source/01_Time -I$(ROOT_PATH)/source/00_Main

SOURCES = $(wildcard *.cpp)              # 获取当前目录下的所有.cpp文件名
OBJS = $(patsubst %.cpp,%.o, $(SOURCES)) # 根据.cpp文件得到对用的.o文件

lib$(SO_NAME).so: $(OBJS)
ifeq (y, $(DEBUG))
	cd $(INTERMEDIATE_DIR_D) && $(CXX) $(CXX_FLAGS_D) -o $@ $^ -Wl,-E
else
	cd $(INTERMEDIATE_DIR_R) && $(CXX) $(CXX_FLAGS_R) -o $@ $^ -Wl,-E
endif

%.o: %.cpp
ifeq (y, $(DEBUG))
	$(MKDIR) $(MKDIR_FLAGS) $(INTERMEDIATE_DIR_D) && $(CXX) $(CXX_FLAGS_D) -c -fmessage-length=0 $(INCLUDE_DIR) \
	-MMD -MP -MF"$(patsubst %.o,$(INTERMEDIATE_DIR_D)/%.d, $@)" -MT"$(patsubst %.o,$(INTERMEDIATE_DIR_D)/%.d, $@)" \
	-o $(INTERMEDIATE_DIR_D)/$@ $<
else
	$(MKDIR) $(MKDIR_FLAGS) $(INTERMEDIATE_DIR_R) && $(CXX) $(CXX_FLAGS_R) -c -fmessage-length=0 $(INCLUDE_DIR) \
	-MMD -MP -MF"$(patsubst %.o,$(INTERMEDIATE_DIR_R)/%.d, $@)" -MT"$(patsubst %.o,$(INTERMEDIATE_DIR_R)/%.d, $@)" \
	-o $(INTERMEDIATE_DIR_R)/$@ $<
endif

上面展示的Makefile是00_Main下的,同理01_Time下的Makefile也替换成这个。但贡献库的名称改为 SO_NAME = 01_Time # 动态库名称。这个Makefile可以复用,项目增加模块只需要拷贝到相同的目录下,改一下SO_NAME即可。

psy@ubuntu:~/Desktop/test/build/Release/00_Main$ ls
lib00_Main.so  main.d  main.o
psy@ubuntu:~/Desktop/test/build/Release/01_Time$ ls
lib01_Time.so  Time.d  Time.o

进入00_Main目录和01_Time目录分别运行make之后,可以看到已经创建了lib00_Main.so以及lib01_Time.so。

2. Debug和Realse版本

为了节省时间,上面的Makefile也实现了这个功能,把DEBUG = n改为DEBUG = y可以生成调试版本的动态库。调试版本和发布版本,实际上就是编译参数不同。

CXX_FLAGS_R = -fPIC -shared -O3 -DNDEBUG
CXX_FLAGS_D = -fPIC -shared -O0 -g3

3. 链接生成可执行文件

四、总结

提示:这里对文章进行总结:
本文仅仅简单介绍了Makefile的一些简单使用方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fjxx_psy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值