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”)- 指定宏定义
 
 
-