C++编译之(2)-make及makefile编译过程

引言

前面我们介绍了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 dist

clean :
	@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: prerequisites
        recipe

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

变量的其他用法

$^ 表示所有的依赖文件

$@ 表示生成的目标文件

$< 代表第一个依赖文件

  • 一些常用的全局变量

这些变量与前面自定义的变量本质一样,都是可定义修改的,只不过这些变量全局的环境变量,可全局使用;

CFLAGSCXXFLAGSCPPFLAGS

选项 说明
-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.o
	g++ -o app main.o utils.o

main.o : main.c utils.h
	g++ -c main.c

utils.o : utils.c
	g++ -c utils.c

但是,实际上可以简写成如下所示

app : main.o utils.o
	cc -o app main.o utils.o

main.o : main.c utils.h

utils.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:clean

clean:
	-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:clean

clean:
	-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,我们在下一节中继续介绍

猜你喜欢

转载自blog.csdn.net/youlinhuanyan/article/details/128750195