Makefile & CMake
人始终还是惰性动物,虽然寒假里已经强迫自己学了一些基本的
Makefile,但是当真的面对一个project进行应用的时候,还是举步维艰。因此就借着OOP Lab1的push, 重新整理一下对于Makefile的理解。至于
CMake,等将最近专业课的太多作业处理一下再整理。
Compile & Link (C++)
首先整理一下代码源文件到可执行文件的过程:
-
Preprocessing:预处理,将
#include的头文件所指引的源文件插入到对应文件中,进行宏定义的替换,并删除注释,生成临时文件*.i。 -
Compilation:编译,将预处理后的文件转换为汇编代码
*.s。g++ -S *.cpp
-
Assembly:汇编,将汇编代码转换为目标文件
*.o。-
g++ -c *.s -
g++ -c *.cpp
-
-
Linking:链接,将目标文件链接为可执行文件。
g++ -o *(.exe) *.o
-
AR: 生成链接库文件
ar rcs libxxx.a *.o
-
PS: -o 选项只是指定输出文件名,并没有其他特殊的意义,因此前面几条命令也可以使用-o选项指定输出文件名。
Multiple Files (C++)
-
*.h
-
只是作为该功能模块的接口,一般不包含具体的实现,通过
#include指引到对应的源文件中。 -
很多时候感觉由于翻译问题,”定义“、”声明“、”实现“之类的感觉完全分不清,但是一般可以简单理解为在头文件中不能开辟内存空间即可。
-
主要内容:
-
头文件保护
#ifndef#define#endif -
结构体、类的声明(重载运算符似乎需要直接实现,其他内部函数只需要声明)
-
函数的声明
-
宏定义
-
全局量的声明
extern(不可赋值,否则就需要占用内存空间了) -
其他需要的头文件,这样可以由该模块直接生成对应的OBJ目标文件,方便整个工程后续的链接。
-
-
-
*.cpp(功能模块源文件)
-
对头文件声明的部分进行具体实现
-
主要内容:
-
头文件引用
- 上述同名头文件
-
结构体、类的实现,不能重复出现声明部分,需要用
name::指定 -
函数的实现
-
全局量的定义(赋值)
-
-
-
Main.cpp
-
主函数,调用其他模块的函数,实现整个程序的功能。
-
正常情况下,只需要引用对应的头文件,然后调用对应的函数即可。
-
Makefile
Make loves c compilation. And every time it expresses its love, things get confusing.
-
Tutorial (通过大量举例,非常通俗易懂且形象
-
Docs (官方文档,详细全面,但是有点枯燥)
-
Makefile 我的理解,本质上就是一个自动化编译的脚本,通过指定一系列的规则和命令,来实现对于源文件的编译、链接等操作。下面我就简单整理了一下自己认为比较重要比较常用的一些点。
Varible
-
常用全部大写,用
(:)=赋值:=会立即展开,而=则是在使用时才展开 (因此可以用于递归定义)
-
+=追加赋值 -
用用
$()引用 -
关于值的传入(从WK老师的补充资料里学到的)
-
make命令后面可以直接跟上变量的赋值NAME=VALUE,这样可以在Makefile中使用这个变量$(NAME) -
make命令后面直接跟上变量的赋值NAME=VALUE,还可以当作C语言代码的宏定义,这样编译结果会不同C 语言可以通过
-D,用宏定义选项传入对应的变量值例如下面代码,可以使用
g++ -D ARGS=\"Hello World\" main.c -o main来编译得到main可执行文件(反斜杠是为了转义双引号一起传入避免当成宏定义的赋值),输出Hello World
-
Sign
-
特殊变量
-
$@目标文件 -
$^所有的依赖文件 -
$<第一个依赖文件 -
$?所有比目标文件新的依赖文件 -
$*无后缀的目标文件名
-
-
通配符
-
*匹配任意长度的字符串 -
?匹配单个字符 -
%匹配任意有长度的字符串,并且往往应用在Pattern Rule
-
Rule
-
Basic Rule
-
Target:
-
目标, 本质上是一个标号,用于指定一个规则的名字
-
特殊的,'all'为默认目标,'clean'为清理目标
-
-
Prerequisites:依赖文件,依赖文件变化才会执行相应命令
-
Command:命令
-
-
Pattern Rule
-
%.o:通配符,表示所有的.o文件
-
%.c:通配符,表示所有的.c文件
-
这里相当于将目录中所有的.c文件编译为对应的.o文件,而不需要一个个指定
-
虽然看起来相当高效,但是由于这样没有规则标号,难以控制,因此在实际应用中并不常用
-
-
Static Pattern Rule
-
targets: 这里往往是一个列表,表示一系列的目标文件
-
target-pattern: 一般使用
%通配符来获取文件信息 -
prereq-patterns: 同上,一般使用
%通配符来获取文件信息 -
command: 感觉非常像Verilog中的generate语句,通过一系列的规则来生成相似的功能模块(详见下方例子)
-
-
Pattern Filter
-
$(filter pattern, text): 从text中筛选出符合pattern的部分 -
可以配合上方的Pattern Rule使用,替换targets来实现更加灵活的功能
Makefile# Ex 1: .o files depend on .c files. Though we don't actually make the .o file. $(filter %.o,$(obj_files)): %.o: %.c echo "target: $@ prereq: $<" # Ex 2: .result files depend on .raw files. Though we don't actually make the .result file. $(filter %.result,$(obj_files)): %.result: %.raw echo "target: $@ prereq: $<" -
Function
-
$(wildcard pattern)- 通配符,获取当前目录下所有符合pattern的文件名
-
$(patsubst pattern, replacement, text)- 替换,将text中符合pattern的部分替换为replacement
-
$(addprefix prefix, names)- 添加前缀,将names中的每个元素都添加上prefix
-
$(addsuffix suffix, names)- 添加后缀,将names中的每个元素都添加上suffix
-
$(shell command)- 执行命令,将命令的输出作为函数的返回值
其他高阶用法
-
~~还没学~~
-
关于.h文件依赖的问题
-
修改了
.h文件,但是不修改.cpp文件,这时候makefile并不会重新编译对应的.o文件,因为它并不知道.h文件的修改。 -
换句话说,Makefile 只知道
.o依赖于对应的.cpp文件,但并不知道该.cpp文件具体依赖于哪些.h文件,因此需要手动指定依赖关系。 -
解决方案
-
~~手动补全关系~~
-
使用 gcc/g++ 的
-MMD选项,可以自动生成依赖关系文件*.d, 然后在Makefile中使用-include引用这些文件,就可以自动更新依赖关系了。
-
-
Example(以OOP Lab1为例)
-
项目简介:
-
实现一个简单的文本管理系统,要求生成
pdadd、pdlist、pdremove、pdshow4个可执行文件 -
编程时创建了一个file类,用于与文本文件进行交互,因此四个可执行文件都需要引用该类的头文件
-
-
【最简单粗暴版】
CC = g++
CFLAG = -std=c++11
SRC = .
OD = ..
FILE_ASSIST = $(SRC)/file.cpp
# 直接两两文件一绑,然后直接编译
$(OD)/pdadd: $(SRC)/pdadd.cpp $(FILE_ASSIST)
$(CC) $(CFLAG) $^ -o $(OD)/pdadd
$(OD)/pdlist: $(SRC)/pdlist.cpp $(FILE_ASSIST)
$(CC) $(CFLAG) $^ -o $(OD)/pdlist
$(OD)/pdshow: $(SRC)/pdshow.cpp $(FILE_ASSIST)
$(CC) $(CFLAG) $^ -o $(OD)/pdshow
$(OD)/pdremove: $(SRC)/pdremove.cpp $(FILE_ASSIST)
$(CC) $(CFLAG) $^ -o $(OD)/pdremove
clean:
rm -f $(OD)/pdadd $(OD)/pdlist $(OD)/pdremove $(OD)/pdshow
rm -f $(SRC)/*.o
- 【正常编译程序版】
CC = g++
CFLAG = -std=c++11
SRC = .
OD = ..
OBJ = $(SRC)/file.o # object files of file assist
ADDOBJ = $(SRC)/pdadd.o # object file for pdadd
LISTOBJ = $(SRC)/pdlist.o # object file for pdlist
REMOVEOBJ = $(SRC)/pdremove.o # object file for pdremove
SHOWOBJ = $(SRC)/pdshow.o # object file for pdshow
all: $(OD)/pdadd $(OD)/pdlist $(OD)/pdremove $(OD)/pdshow
# 通过.o文件链接得到可执行文件,避免了重复编译
$(OD)/pdadd: $(ADDOBJ) $(OBJ)
$(CC) $(CFLAG) -o $(OD)/pdadd $(ADDOBJ) $(OBJ)
$(OD)/pdlist: $(LISTOBJ) $(OBJ)
$(CC) $(CFLAG) -o $(OD)/pdlist $(LISTOBJ) $(OBJ)
$(OD)/pdremove: $(REMOVEOBJ) $(OBJ)
$(CC) $(CFLAG) -o $(OD)/pdremove $(REMOVEOBJ) $(OBJ)
$(OD)/pdshow: $(SHOWOBJ) $(OBJ)
$(CC) $(CFLAG) -o $(OD)/pdshow $(SHOWOBJ) $(OBJ)
# 通过从.cpp文件编译得到.o文件
.cpp.o:
$(CC) $(CFLAG) -c $< -o $@
clean:
rm -f $(OBJ) $(ADDOBJ) $(LISTOBJ) $(REMOVEOBJ) $(SHOWOBJ) $(OD)/pdadd $(OD)/pdlist $(OD)/pdremove $(OD)/pdshow
- 【高效版】
CC = g++
CFLAG = -std=c++11
SRC = .
OD = ..
OBJ = $(SRC)/file.o # object files of file assist
TARGETS = pdadd pdlist pdremove pdshow
OBJS = $(addsuffix .o, $(TARGETS)) file.o
all: $(OBJS) $(TARGETS)
# 通过static pattern rule,大大简化了生成*.o 和 *(.exe)的过程
$(TARGETS): %: $(SRC)/%.o $(OBJ)
$(CC) $(CFLAG) -o $(OD)/$@ $^
$(OBJS): %.o: $(SRC)/%.cpp
$(CC) $(CFLAG) -c $< -o $(SRC)/$@
clean:
cd $(SRC) && rm -f $(OBJS)
cd $(OD) && rm -f $(TARGETS)
CMake
目前的感觉就像是一个帮助你生成Makefile的工具,通过CMakeLists.txt文件来指定项目的编译规则,然后通过cmake PROJECT_DIR来生成对应的Makefile等工程文件到当前目录,然后通过make --build .来完成编译链接生成。
- Tutorial (官方教程,比较全面,但是不是特别好理解感觉,目前进度 STEP3)
Basic
最基本的 CMakeLists.txt 文件架构如下:
-
cmake_minimum_required(VERSION 3.10)- 指定CMake的最低版本
-
project(PROJECT_NAME VERSION 1.0)-
指定项目的名称以及版本
-
生成程序对版本的嵌入,可以通过
configure_file命令来实现-
configure_file(config.h.in config.h) -
这样在编译时便会在当前build目录下
config.h中生成对应的版本信息的如上宏定义 -
我们可以通过
target_include_directories(PROJECT PUBLIC "${PROJECT_BINARY_DIR}")引入该文件,并在源文件中通过#include "config.h"来引用这些宏定义,完成版本的嵌入
-
-
-
add_executable(EXECUTABLE_NAME SOURCE_FILES)- 指定生成可执行文件的名称以及对应的源文件
编译选项
-
set(CMAKE_CXX_STANDARD 11)-
指定C++的标准版本
-
set(CMAKE_CXX_STANDARD_REQUIRED ON)- 指定是否强制使用该标准版本
-
-
set(CMAKE_CXX_FLAGS "-Wall -Wextra")- 指定编译选项
-
add_library(tutorial_compiler_flags INTERFACE)target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)来指定编译特性
多文件组织
-
添加一个library(子目录)
-
add_library(LIBRARY_NAME STATIC/SHARED SOURCE_FILES)- 指定生成静态/动态库的名称以及对应的源文件 (这一步在子目录的CMakeLists.txt中完成)
-
target_link_libraries(EXECUTABLE_NAME/LIBRARY_NAME [PUBLIC] LIBRARY_NAME)- 指定可执行文件链接的库文件
-
target_include_subdirectory(EXECUTABLE_NAME DIRS)- 将对应目录加入可执行文件的头文件目录
-
-
使用INTERFACE控制LIBRARY
-
add_library(LIBRARY_NAME INTERFACE)- 指定生成一个INTERFACE库,不包含源文件,只包含头文件和编译选项
-
target_include_directories(LIBRARY_NAME INTERFACE DIRS)- 指定库文件的头文件目录
-
target_link_libraries(EXECUTABLE_NAME LIBRARY_NAME)- 这样直接指定可执行文件链接的库文件即可,不用手动处理头文件目录
-
-
利用变量进行灵活控制
-
option(VARIABLE ["COMMENT"] ON/OFF)可以产生对应的缓存变量,可以结合if() endif()控制编译过程 -
有个小问题就是更新不及时,有时要删除原来的 build 文件才能起作用
-
-
其他编译选项
-
Target_compile_features(TARGET PUBLIC cxx_std_11)- 指定编译特性
-
target_compile_options(TARGET PUBLIC -Wall -Wextra)- 指定编译选项
-
target_compile_definitions(TARGET PUBLIC “MACRO_NAME”)- 指定宏定义
-