引言
前面我们介绍了c++的编译工具,使用g++实现对单个文件,多个文件,静态库动态库的编译;我们继续以该项目为例讲解;
g++ 的编译使用入门教程-点这里查看
我们继续以前面的目录解构为例,这里给出上一节的目录如下:
- mutilFilesDemo- include // 头文件目录- HelloTools.h- Prints.h- libs // 库子项目目录- ToolLibs.h- ToolLibs.cpp- libToolLibs.a // 静态库- src // 源码目录- module // 源码模块- Prints.cpp // Prints类- HelloTools.cpp // HelloTools类- main.cpp // main类
我们简单再回顾直接使用g++的编译上面项目的流程;该项目包含一个自项目静态库;所以我们需要首先编译子项目mutilFilesDemo/libs
生成库文件libToolLibs.a
,然后再进行编译主项目
step.1 编译子项目
# 进入子项目
cd mutilFilesDemo/libs
g++ -c ToolLibs.cpp
ar crv libToolLibs.a ToolLibs.o
step.编译主项目
# 回到主项目
cd mutilFilesDemo
# 编译并链接到静态库
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -L ./libs -l ToolLibs -o main
这样我们就完成整个编译了;
倘若,我们开发一个大工程,有很多的模块,很多子项目、代码文件,同时依赖很多静态库、动态库的话 ,显然采用这种方式工作量是非常大的。
于是我们创造出了一个make的工具及Makefile文件,Makefile文件记录编译相关的策略及配置项,我们只需要一个make命令即可完成编译,是不是很优雅?
Makefile如何编写?
首先,Makefile的本质其实可以理解为一个构建shell配置脚本;说白了,就是将我们前面写的g++ xxx.cpp -o xxx
等一长串命令,预先写好,然后输入make
命令,就自动执行了
理论上来说,你写shell脚本也可以实现c++的编译,再退一万步,只要你愿意,你可以一条条g++
地输入
我们先看一个Makefile的最简单例子
Makefile
文件
help :@echo "help info"dist :@mkdir distclean :@rm -rf dist
我们执行一下make
,输入help info
字符串
$ make
help info
其实就是执行了@echo help info
这条命令(前面加了@,表示不显示命令字符串自本身)
这个
Makefile
没啥用,然而,确实就是一个合法的Makefile
文件,所以其实我们可以从侧面窥探出这个Makefile
其实并没有什么高深的技术,它本质就是执行一系列的脚本,而不管你执行的是啥东西(当然,make工具添加许多非常方便的脚本函数,都是在g++手动编译过程中遇到的麻烦事转摸索过来的经验工具函数);Makefile
更多的是一种自动化构建思想;
对于Makefile
的规则,始终默认执行文件中的第一个标签的脚本,这里即是help
;如果希望执行其他标签的脚本可在make命令的后面跟上相应的标签,如:make dist
实际上,Makefile官方介绍中,是这样介绍这个基本规则的,Makefile的编写基本规则如下
target: prerequisitesrecipe
target: 通常是程序生成(输出)的一个或多个文件名,例如:可执行文件或目标文件;它也可以是要执行任务的名称,例如用于清理生成文件的 clean 任务
prerequisites: 先决条件是用于生成 target 文件的输入文件或是完成 target 任务前需要先执行的任务 。一个 target 可以没有先决条件,也可以有一个或多个先决条件(比如,编译的依赖文件,这里还隐藏着的规则是,如果这些依赖发生了改变,则目标文件将会重新编译,着这个规则可以大大减少我们重新编译的效率)
recipe: 中文翻译为菜谱,它是 make 用于生成 target 文件或完成 target 任务而执行一系列 shell 命令。这些命令可以放在同一行里,也可以每个命令占一行。值得注意的是,recipe 默认以制表符开头,而不是空格
其实写到这里,你已经有办法可以写一个简单的符合Makefile的c++构建脚本了;
我们把Makefile
再改造成如下内容:
build :@echo "开始编译c++"g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -L ./libs -l ToolLibs -o main@echo "end"
执行一下make
命令
$ make
开始编译c++
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -L ./libs -l ToolLibs -o main
end
编译成功!
当然Makefile
的功能远不止这些,我们继续学习一下Makefile的基本语法规则
基本语法规则
1、变量
- 变量的定义
make的变量与shell的变量很像,但是make的变量可以出现许多特殊字符,包括空格(=
除外),如下所示
objs = main.o model.o view.o controller.o
注意变量的赋值有两种,一种是取直接赋值(
:=
),领外一种是最后计算赋值(=
);遗憾的我们熟悉的编程赋值语法是这一种最后计算赋值=
;这意味着如果赋值内容存在变量,而该变量在后面的脚本中,被改变了,那边这个赋值是采用最终值来计算赋值的;所以搞不清楚用法的话,最好变量赋值最好只做一次赋值
,避免踩坑
- 变量的使用
make中的变量采用$()
的方式使用,如下所示
build: $(objs)g++ $(objs) -o app
变量的其他用法
$^
表示所有的依赖文件
$@
表示生成的目标文件
$<
代表第一个依赖文件
- 一些常用的全局变量
这些变量与前面自定义的变量本质一样,都是可定义修改的,只不过这些变量全局的环境变量,可全局使用;
CFLAGS
、CXXFLAGS
、CPPFLAGS
选项 | 说明 |
---|---|
-c | 用于把源码文件编译成 .o 对象文件,不进行链接过程 |
-o | 用于连接生成可执行文件,在其后可以指定输出文件的名称 |
-g | 用于在生成的目标可执行文件中,添加调试信息,可以使用GDB进行调试 |
-Idir | 用于把新目录添加到include路径上,可以使用相对和绝对路径,“-I.”、“-I./include”、“-I/opt/include” |
-Wall | 生成常见的所有告警信息,且停止编译,具体是哪些告警信息,请参见GCC手册,一般用这个足矣! |
-w | 关闭所有告警信息 |
-O | 表示编译优化选项,其后可跟优化等级0\1\2\3,默认是0,不优化 |
-fPIC | 用于生成位置无关的代码 |
-v | (在标准错误)显示执行编译阶段的命令,同时显示编译器驱动程序,预处理器,编译器的版本号 |
LDFLAGS
选项 | 说明 |
---|---|
-llibrary | 链接时在标准搜索目录中寻找库文件,搜索名为liblibrary.a 或 liblibrary.so |
-Ldir | 用于把新目录添加到库搜索路径上,可以使用相对和绝对路径,“-L.”、“-L./include”、“-L/opt/include” |
-Wl,option | 把选项 option 传递给连接器,如果 option 中含有逗号,就在逗号处分割成多个选项 |
-static | 使用静态库链接生成目标文件,避免使用共享库,生成目标文件会比使用动态链接库大 |
LIBS
如: LIBS = -lpthread -lm -lpthread -liconv
2、函数
make 中的函数用于处理 Makefile 文件中的文本,例如:计算操作的文件列表,“菜谱”中使用的命令等。
- 函数的调用
函数的调用与变量的使用有点像,格式如下所示:
$(function arguments)
例如常用的函数实例
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))
build:@echo $(subst o,q,hello world)
我们一起来看看上面的函数代表的函数:
第一条指令,wildcard *.cpp
获取所有源文件,并最终赋值给SRC变量;
第二条指令,patsubst %.cpp, %.o, $(SRC)
,把SRC字符串中,所有.cpp
替换成.o
,结果赋值给OBJ变量
第三条指令,把hello world
字符串中的o
替换成q
make还有一些更简单的规则,如一些常用的编译命令,像下面这样写是完整的写法
app : main.o utils.og++ -o app main.o utils.omain.o : main.c utils.hg++ -c main.cutils.o : utils.cg++ -c utils.c
但是,实际上可以简写成如下所示
app : main.o utils.occ -o app main.o utils.omain.o : main.c utils.hutils.o : utils.c
3、嵌套执行Makefile
如果我们有子项目,那么我们可以采用嵌套Makefile来执行,如我们前面的libs
静态库子项目
subsystem:cd subdir && $(MAKE)
等价于
subsystem:$(MAKE) -C subdir
为啥用一个变量MAKE表示呢,也许我们的make
需要一些参数,所以定义成一个变量比较有利于维护;
这两种方式都会先进入subdir
目录后,再执行make
命令
make -C <dir>
表示在<dir>
目录下执行make命令;即使用<dir>
目录下的Makefile
文件执行;当然,你也可以使用-f path/to/you/Makefile
完全直接指定Makefile
路径
有时,我们希望/不希望传递一下变量到下一级的Makefile中,那么可以使用这样的声明
export/unexport
来声明变量,或者直接使用export
把所有变量往下传递
export var1 var2 var3
Makefile
实战练习
前面讲了这么多,那我们开始提到的项目,到底该如何写这个Makefile呢?请看下面的目录
- mutilFilesDemo- include // 头文件目录- HelloTools.h- Prints.h- libs // 库子项目目录- ToolLibs.h- ToolLibs.cpp- Makefile // 子项目Makefile- src // 源码目录- module // 源码模块- Prints.cpp // Prints类- HelloTools.cpp // HelloTools类- main.cpp // main类- Makefile // 主项目Makefile
我们分别来看这两个Makefile
怎么写,首先我们来看子项目的Makefile
# 子项目Makefile
CC=g++
# 获取当前工作路径
WORK_DIR:=$(CURDIR)
# 设置目标名
Target:=libToolLibs.a# 统一的文件目录命名
DEFAULT_BUILD_DIR?=build
DEFAULT_TMP_DIR?=tmp
DEFAULT_BIN_DIR?=bin# ===========
# 各种目录设置
# ===========
# 设置源文件目录,可设置多个
SRC_PATH:=$(WORK_DIR)
# 兼容:直接编译的编译目录|作为子项目时的编译目录
WORK_BUILD_DIR:=$(if $(TOP_DIR),$(TOP_DIR)/$(DEFAULT_BUILD_DIR)/sub_projects/$(notdir $(WORK_DIR)),$(WORK_DIR))
# 设置编译目录
BUILD_PATH:=$(if $(TOP_DIR),$(WORK_BUILD_DIR),$(WORK_BUILD_DIR)/$(DEFAULT_BUILD_DIR))
# 设置编译临时目录
OBJ_PATH:=$(BUILD_PATH)/$(DEFAULT_TMP_DIR)
# 设置编译最终文件目录
BIN_PATH:=$(BUILD_PATH)/$(DEFAULT_BIN_DIR)# ===================
# 各种文件名字符串的处理
# ===================
# 获取源文件目录下所有带路径的cpp文件列表
SRC:=$(foreach dir,$(SRC_PATH),$(wildcard $(dir)/*.cpp))
# 获取纯cpp文件名(不带路路径,去掉cpp文件目录)
SRC_FILES:=$(notdir $(SRC))
# 生成.o文件名列表(不带路径)
OBJ_FILES:=$(patsubst %.cpp,%.o,$(SRC_FILES))
# 为.o文件列表加上编译目录(完整文件路径)
OBJ_WITH_PATH_FILE:=$(addprefix $(OBJ_PATH)/,$(OBJ_FILES))# 显示信息
$(info ============ sub =============)
$(info "Target => $(Target)")
$(info "WORK_DIR => $(WORK_DIR)")
$(info "BUILD_PATH => $(BUILD_PATH)")
$(info "OBJ_WITH_PATH_FILE => $(OBJ_WITH_PATH_FILE)")
$(info "CPPFLAGS => $(CPPFLAGS)")
$(info "LDFLAGS => $(LDFLAGS)")
$(info "LIBS => :$(LIBS)")
$(info ------------------------------)# ================
# 各编译目标任务处理
# ================
# 编译目标
all:build_prepare $(Target)
# 连接目标
$(Target):$(OBJ_WITH_PATH_FILE) ar crv $(BIN_PATH)/$@ $^
# 主项目编译:所有的*.cpp编译生成相应的*.o文件
$(OBJ_PATH)/%.o:%.cpp$(CC) -c -o $@ $<
# 创建编译目录
build_prepare:@if [ ! -d $(BUILD_PATH) ]; then \mkdir -p $(OBJ_PATH); \mkdir -p $(BIN_PATH); \fi.PHONY:cleanclean:-rm -rf $(BIN_PATH)/$(Target) $(OBJ_WITH_PATH_FILE)
子这个Makefile
我把他设计成,即可以独立编译,又可与主项目集成编译
上面的代码,几乎每条指令都有注释,我们直接对子项目的Makefile
文件执行一下make
看看效果
$ cd mutilFilesDemo/libs
$ make
============ sub =============
"Target => libToolLibs.a"
"WORK_DIR => /home/compMutilFilesMakeDemo/libs"
"BUILD_PATH => /home/compMutilFilesMakeDemo/libs/build"
"OBJ_WITH_PATH_FILE => /home/compMutilFilesMakeDemo/libs/build/tmp/ToolLibs.o"
"CPPFLAGS => "
"LDFLAGS => "
"LIBS => :"
------------------------------
g++ -c -o /home/compMutilFilesMakeDemo/libs/build/tmp/ToolLibs.o ToolLibs.cpp
ar crv /home/compMutilFilesMakeDemo/libs/build/bin/libToolLibs.a /home/compMutilFilesMakeDemo/libs/build/tmp/ToolLibs.o
a - /home/compMutilFilesMakeDemo/libs/build/tmp/ToolLibs.o
执行成功后,多了一个build目录,所有构建的临时文件及最终的目标文件都在这个目录下
再上主项目的Makefile
文件内容如下:
CC=g++
# 获取当前工作路径
WORK_DIR:=$(CURDIR)
TOP_DIR:=$(CURDIR)
# 设置目标名
Target:=mainApp
# 设置子项目生成目标库名,可设置多个
SubPorjectLibName:=ToolLibs# 统一的文件目录命名
DEFAULT_BUILD_DIR:=build
DEFAULT_TMP_DIR:=tmp
DEFAULT_BIN_DIR:=bin
# 全局有效
export TOP_DIR DEFAULT_BUILD_DIR DEFAULT_TMP_DIR DEFAULT_BIN_DIR# ===========
# 各种目录设置
# ===========
# 设置子项目目录,可设置多个
SUB_PRO_PATH:= $(WORK_DIR)/libs
# 设置源文件目录,可设置多个
SRC_PATH:= $(WORK_DIR) \$(WORK_DIR)/src \$(WORK_DIR)/src/modules
# include头目录
INCLUDE_PATH=$(WORK_DIR)/include
# 设置编译目录
BUILD_PATH:=$(WORK_DIR)/$(DEFAULT_BUILD_DIR)
# 设置编译临时目录
OBJ_PATH:=$(BUILD_PATH)/$(DEFAULT_TMP_DIR)
# 设置编译最终文件目录
BIN_PATH:=$(BUILD_PATH)/$(DEFAULT_BIN_DIR)
# 子项目目录名称(去路径)
SUB_PRO_DIR_NAME:=$(notdir $(SUB_PRO_PATH))
# 子项目BIN目录,可用于主项目的链接库目录(加前后路径)
SUB_PRO_BIN_PATH:=$(addsuffix /$(DEFAULT_BIN_DIR),$(addprefix $(BUILD_PATH)/sub_projects/,$(SUB_PRO_DIR_NAME)))# ===================
# 各种文件名字符串的处理
# ===================
# 获取源文件目录下所有带路径的cpp文件列表
SRC:=$(foreach dir,$(SRC_PATH),$(wildcard $(dir)/*.cpp))
# 获取纯cpp文件名(不带路路径,去掉cpp文件目录)
SRC_FILES:=$(notdir $(SRC))
# 生成.o文件名列表(不带路径)
OBJ_FILES:=$(patsubst %.cpp,%.o,$(SRC_FILES))
# 为.o文件列表加上编译目录(完整文件路径)
OBJ_WITH_PATH_FILE:=$(addprefix $(OBJ_PATH)/,$(OBJ_FILES))# ====================
# 添加Makefile的全局变量
# ====================
# 添加头文件目录
CPPFLAGS+=$(addprefix -I,$(INCLUDE_PATH))
# 添加链接的库目录
LDFLAGS+=$(addprefix -L,$(SUB_PRO_BIN_PATH))
# 添加链接的库名
LIBS+=$(addprefix -l,$(SubPorjectLibName))
# 为g++添加源文件搜索目录
VPATH=$(SRC_PATH)# 显示信息
$(info =========== main ==============)
$(info "Target => $(Target)")
$(info "WORK_DIR => $(WORK_DIR)")
$(info "BUILD_PATH => $(BUILD_PATH)")
$(info "OBJ_WITH_PATH_FILE => $(OBJ_WITH_PATH_FILE)")
$(info "CPPFLAGS => $(CPPFLAGS)")
$(info "LDFLAGS => $(LDFLAGS)")
$(info "LIBS => $(LIBS)")
$(info ===============================)# ================
# 各编译目标任务处理
# ================
# 编译目标
all:build_prepare $(SUB_PRO_PATH) $(Target)
# 连接目标
$(Target):$(OBJ_WITH_PATH_FILE) $(CC) -o $(BIN_PATH)/$@ $^ $(LDFLAGS) $(LIBS)
# 主项目编译:所有的*.cpp编译生成相应的*.o文件
$(OBJ_PATH)/%.o:%.cpp$(CC) -c -o $@ $<
# 创建编译目录
build_prepare:@if [ ! -d $(BUILD_PATH) ]; then \mkdir -p $(OBJ_PATH); \mkdir -p $(BIN_PATH); \fi
#子项目执行
$(SUB_PRO_PATH):ECHO_MSG@echo start compile:$@@make -C $@
ECHO_MSG:@echo Begin Sub Projects Compile@echo All Subs:$(SUB_PRO_PATH).PHONY:cleanclean:-rm -rf $(BIN_PATH)/$(Target) $(OBJ_WITH_PATH_FILE)cleanAll:-rm -rf $(BUILD_PATH)
同样,我们进入主项目,执行一下make
$ cd mutilFilesDemo
$ make
=========== main ==============
"Target => mainApp"
"WORK_DIR => /home/compMutilFilesMakeDemo"
"BUILD_PATH => /home/compMutilFilesMakeDemo/build"
"OBJ_WITH_PATH_FILE => /home/compMutilFilesMakeDemo/build/tmp/main.o /home/compMutilFilesMakeDemo/build/tmp/HelloTools.o /home/compMutilFilesMakeDemo/build/tmp/Prints.o"
"CPPFLAGS => -I/home/compMutilFilesMakeDemo/include"
"LDFLAGS => -L/home/compMutilFilesMakeDemo/build/sub_projects/libs/bin"
"LIBS => -lToolLibs"
===============================
Begin Sub Projects Compile
All Subs:/home/compMutilFilesMakeDemo/libs
start compile:/home/compMutilFilesMakeDemo/libs
make[1]: Entering directory '/home/compMutilFilesMakeDemo/libs'
============ sub =============
"Target => libToolLibs.a"
"WORK_DIR => /home/compMutilFilesMakeDemo/libs"
"BUILD_PATH => /home/compMutilFilesMakeDemo/build/sub_projects/libs"
"OBJ_WITH_PATH_FILE => /home/compMutilFilesMakeDemo/build/sub_projects/libs/tmp/ToolLibs.o"
"CPPFLAGS => "
"LDFLAGS => "
"LIBS => :"
------------------------------
g++ -c -o /home/compMutilFilesMakeDemo/build/sub_projects/libs/tmp/ToolLibs.o ToolLibs.cpp
ar crv /home/compMutilFilesMakeDemo/build/sub_projects/libs/bin/libToolLibs.a /home/compMutilFilesMakeDemo/build/sub_projects/libs/tmp/ToolLibs.o
a - /home/compMutilFilesMakeDemo/build/sub_projects/libs/tmp/ToolLibs.o
make[1]: Leaving directory '/home/compMutilFilesMakeDemo/libs'
g++ -c -o /home/compMutilFilesMakeDemo/build/tmp/main.o main.cpp
g++ -c -o /home/compMutilFilesMakeDemo/build/tmp/HelloTools.o /home/compMutilFilesMakeDemo/src/HelloTools.cpp
g++ -c -o /home/compMutilFilesMakeDemo/build/tmp/Prints.o /home/compMutilFilesMakeDemo/src/modules/Prints.cpp
g++ -o /home/compMutilFilesMakeDemo/build/bin/mainApp /home/compMutilFilesMakeDemo/build/tmp/main.o /home/compMutilFilesMakeDemo/build/tmp/HelloTools.o /home/compMutilFilesMakeDemo/build/tmp/Prints.o -L/home/compMutilFilesMakeDemo/build/sub_projects/libs/bin -lToolLibs
最后,我们进入build/bin
看看我们主项目的编译结果,并执行一下
$ cd compMutilFilesMakeDemo/build/bin
$ ./mainApp
Hello world!
MAX_NUM+n:110
=================================
使用静态库-add(a,b)
结果为:a+b=500
结果与上一节的直接用g++
一样,而后续的可维护性大大的增加,这就是make/m=Makefile
的魅力所在;
尽管IDE很漂亮,但是掌握make/Makefile
才是你走向程序员的巅峰的必由之路,尤其是linux下的开发更是如此!
这是一个非常棒的例子,里面包含了许多自动化构建项目的一些思路,供大家参考使用!
如此呕心沥血之作,必然离不开各个广大码友的贡献,这里最后附属一些参考文献!
C++编译之(1)-g++单/多文件/库的编译
跟我一起写Makefile
官网文档
make/Makefile
很美好,燃鹅写Makefile
的工程量也不小,于是还有大神继续砥砺前行,发明了cmake/CMakeLists.txt
来自动制作Makefile,我们在下一节中继续介绍