Make 和 CMake 详细使用教程

Make 和 CMake 详细使用教程

1. 概述

Make 和 CMake 是 C/C++ 开发中最常用的两种构建工具:

  • Make:通过 Makefile 定义构建规则,直接执行编译链接操作
  • CMake:跨平台构建系统生成器,通过 CMakeLists.txt 生成 Makefile 或其他构建文件

本文将详细介绍这两种工具的使用方法,包含丰富的示例和详细注释。

2. Make 工具详细教程

2.1 Makefile 基本结构

Makefile 由以下几部分组成:

  • 变量定义
  • 规则(目标、依赖、命令)
  • 伪目标

简单 Makefile 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 定义变量:可执行文件名称
TARGET = hello_world

# 定义变量:源文件列表
SRCS = main.cpp utils.cpp

# 定义变量:目标文件列表(将 .cpp 替换为 .o)
OBJS = $(SRCS:.cpp=.o)

# 定义变量:编译器
CC = g++

# 定义变量:编译选项
CFLAGS = -Wall -g -O0

# 默认目标(第一个目标)
all: $(TARGET)

# 链接规则:生成可执行文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^

# 编译规则:将 .cpp 文件编译为 .o 文件
%.o: %.cpp
$(CC) $(CFLAGS) -c $< -o $@

# 伪目标:清理生成的文件
clean:
rm -f $(OBJS) $(TARGET)

2.2 Make 变量

Make 变量分为两种:普通变量和递归变量

1
2
3
4
5
6
7
8
9
10
11
# 普通变量:立即展开
VAR := value

# 递归变量:延迟展开
VAR = value

# 条件赋值:仅当变量未定义时赋值
VAR ?= default_value

# 追加赋值:在现有变量后追加
VAR += additional_value

2.3 自动变量

自动变量用于简化规则定义:

1
2
3
4
5
6
$@  # 目标文件的名称
$^ # 所有依赖文件的列表(去重)
$< # 第一个依赖文件的名称
$? # 所有比目标新的依赖文件的列表
$* # 匹配的模式部分
$+ # 所有依赖文件的列表(保留重复)

2.4 Make 函数

Make 提供了丰富的函数用于处理字符串和文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 字符串处理函数
$(subst from,to,text) # 替换字符串
$(patsubst pattern,replacement,text) # 模式替换
$(strip string) # 去除空格
$(findstring find,text) # 查找字符串
$(filter pattern...,text) # 过滤匹配的字符串
$(filter-out pattern...,text) # 过滤不匹配的字符串

# 文件处理函数
$(wildcard pattern) # 匹配文件
$(dir names...) # 提取目录部分
$(notdir names...) # 提取文件名部分
$(suffix names...) # 提取后缀名
$(basename names...) # 提取基名

# 示例:获取所有 .cpp 文件
SRCS = $(wildcard src/*.cpp)

# 示例:将 .cpp 文件转换为 .o 文件
OBJS = $(patsubst src/%.cpp,obj/%.o,$(SRCS))

2.5 条件语句

Make 支持条件判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义调试选项
DEBUG ?= 1

# 条件编译
ifeq ($(DEBUG), 1)
CFLAGS += -g -DDEBUG
TARGET := $(TARGET)_debug
else
CFLAGS += -O2
endif

# 条件判断示例
ifneq ($(OS), Windows_NT)
LDFLAGS += -lpthread
endif

2.6 高级 Makefile 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 定义项目名称
PROJECT_NAME = myapp

# 定义源文件目录
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
BIN_DIR = bin

# 获取所有 .cpp 文件
SRCS = $(wildcard $(SRC_DIR)/*.cpp)

# 将源文件转换为目标文件
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))

# 定义编译器和编译选项
CC = g++
CFLAGS = -Wall -I$(INC_DIR) -std=c++17

# 定义链接器和链接选项
LD = g++
LDFLAGS = -lm

# 定义目标文件
TARGET = $(BIN_DIR)/$(PROJECT_NAME)

# 默认目标
all: $(TARGET)

# 创建输出目录
$(OBJ_DIR) $(BIN_DIR):
mkdir -p $@

# 编译规则:依赖目标目录
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@

# 链接规则:依赖目标目录
$(TARGET): $(OBJS) | $(BIN_DIR)
$(LD) $(LDFLAGS) $^ -o $@

# 清理目标
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)

# 运行目标
run: $(TARGET)
./$(TARGET)

# 调试目标
debug: $(TARGET)
gdb ./$(TARGET)

# 声明伪目标
.PHONY: all clean run debug

3. CMake 工具详细教程

3.1 CMakeLists.txt 基本结构

CMakeLists.txt 由以下几部分组成:

  • 项目定义
  • 变量设置
  • 目标定义
  • 依赖管理
  • 构建配置

简单 CMakeLists.txt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 指定 CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 定义项目名称和使用的语言
project(HelloWorld CXX)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加可执行目标
add_executable(hello_world main.cpp utils.cpp)

# 添加包含目录
target_include_directories(hello_world PRIVATE include)

# 添加链接库
target_link_libraries(hello_world PRIVATE m)

3.2 CMake 变量

CMake 变量分为几种类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 设置普通变量
set(VAR value)

# 设置缓存变量(可在命令行中修改)
set(VAR value CACHE STRING "Description")

# 设置环境变量
set(ENV{VAR} value)

# 获取环境变量
set(VAR $ENV{VAR})

# 取消变量定义
unset(VAR)

# 条件赋值
if(NOT DEFINED VAR)
set(VAR default_value)
endif()

3.3 目标定义

CMake 支持多种目标类型:

1
2
3
4
5
6
7
8
9
10
11
# 添加可执行目标
add_executable(target_name source1.cpp source2.cpp)

# 添加静态库
add_library(lib_name STATIC source1.cpp source2.cpp)

# 添加动态库
add_library(lib_name SHARED source1.cpp source2.cpp)

# 添加对象库(不链接为库,仅编译对象文件)
add_library(lib_name OBJECT source1.cpp source2.cpp)

3.4 目标属性

CMake 允许为目标设置各种属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 设置编译选项
target_compile_options(target_name PRIVATE -Wall -Wextra)

# 设置包含目录
target_include_directories(target_name
PUBLIC include/public # 对目标和依赖它的目标可见
PRIVATE include/private # 仅对目标可见
)

# 设置链接库
target_link_libraries(target_name
PUBLIC lib1 # 链接到目标并传递给依赖它的目标
PRIVATE lib2 # 仅链接到目标
)

# 设置预处理器定义
target_compile_definitions(target_name PRIVATE DEBUG=1)

3.5 查找外部依赖

CMake 提供了 find_package 命令查找外部依赖:

1
2
3
4
5
6
7
8
9
10
11
# 查找 Boost 库
find_package(Boost REQUIRED COMPONENTS filesystem system)

# 链接 Boost 库
target_link_libraries(target_name PRIVATE Boost::filesystem Boost::system)

# 查找 OpenCV 库
find_package(OpenCV REQUIRED)

# 链接 OpenCV 库
target_link_libraries(target_name PRIVATE ${OpenCV_LIBS})

3.6 构建类型

CMake 支持多种构建类型:

1
2
3
4
5
6
7
8
9
10
11
# 设置默认构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# 可用的构建类型:Debug, Release, RelWithDebInfo, MinSizeRel

# 根据构建类型设置不同的编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(target_name PRIVATE DEBUG=1)
endif()

3.7 安装规则

CMake 支持定义安装规则:

1
2
3
4
5
6
7
8
9
10
11
12
# 安装可执行文件
install(TARGETS target_name
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

# 安装头文件
install(DIRECTORY include/ DESTINATION include)

# 安装配置文件
install(FILES config.ini DESTINATION etc)

3.8 高级 CMakeLists.txt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 最低 CMake 版本要求
cmake_minimum_required(VERSION 3.14)

# 项目定义
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 设置构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# 定义源文件目录
set(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
set(INC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include)
set(TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/tests)

# 查找源文件
file(GLOB_RECURSE SOURCES ${SRC_DIR}/*.cpp)
file(GLOB_RECURSE HEADERS ${INC_DIR}/*.hpp)

# 添加可执行目标
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})

# 设置包含目录
target_include_directories(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${INC_DIR}>
$<INSTALL_INTERFACE:include>
PRIVATE
${SRC_DIR}
)

# 设置编译选项
target_compile_options(${PROJECT_NAME}
PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -pedantic>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra -pedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)

# 链接库
target_link_libraries(${PROJECT_NAME}
PRIVATE
# 示例:链接系统库
m
)

# 启用测试
include(CTest)
if(BUILD_TESTING)
# 添加测试子目录
add_subdirectory(tests)
endif()

# 安装规则
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}-targets
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

# 安装头文件
install(DIRECTORY ${INC_DIR}/ DESTINATION include)

# 导出目标
install(EXPORT ${PROJECT_NAME}-targets
FILE ${PROJECT_NAME}-targets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION lib/cmake/${PROJECT_NAME}
)

# 创建配置文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)

# 安装配置文件
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}-config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
@ONLY
)

install(FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake
DESTINATION lib/cmake/${PROJECT_NAME}
)

4. 实际项目示例

4.1 Make 项目示例

假设我们有以下项目结构:

1
2
3
4
5
6
7
my_project/
├── include/
│ └── utils.h
├── src/
│ ├── main.cpp
│ └── utils.cpp
└── Makefile

Makefile 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 项目名称
PROJECT = my_app

# 目录结构
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
BIN_DIR = bin

# 编译器和编译选项
CC = g++
CFLAGS = -Wall -I$(INC_DIR) -std=c++17

# 链接器和链接选项
LD = g++
LDFLAGS = -lm

# 源文件和目标文件
SRCS = $(wildcard $(SRC_DIR)/*.cpp) # 查找所有 .cpp 文件
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS)) # 转换为 .o 文件
TARGET = $(BIN_DIR)/$(PROJECT) # 最终可执行文件

# 默认目标
all: $(TARGET)

# 创建输出目录
$(OBJ_DIR) $(BIN_DIR):
mkdir -p $@ # 创建目录,-p 选项确保父目录存在

# 编译规则:将 .cpp 文件编译为 .o 文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@ # $< 是第一个依赖文件,$@ 是目标文件

# 链接规则:将 .o 文件链接为可执行文件
$(TARGET): $(OBJS) | $(BIN_DIR)
$(LD) $(LDFLAGS) $^ -o $@ # $^ 是所有依赖文件

# 清理目标
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR) # 删除生成的文件和目录

# 运行目标
run: $(TARGET)
./$(TARGET) # 运行程序

# 调试目标
debug: $(TARGET)
gdb ./$(TARGET) # 使用 gdb 调试

# 声明伪目标(这些目标不是实际的文件)
.PHONY: all clean run debug

4.2 CMake 项目示例

假设我们有以下项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
my_project/
├── include/
│ └── mylib/
│ └── utils.hpp
├── src/
│ ├── main.cpp
│ └── mylib/
│ └── utils.cpp
├── tests/
│ ├── CMakeLists.txt
│ └── test_utils.cpp
└── CMakeLists.txt

根目录 CMakeLists.txt 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 指定 CMake 最低版本要求
cmake_minimum_required(VERSION 3.14)

# 定义项目名称、版本和语言
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 设置默认构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# 添加库子目录
add_subdirectory(src/mylib)

# 添加可执行文件
add_executable(myapp src/main.cpp)

# 链接库
target_link_libraries(myapp PRIVATE MyLib)

# 设置包含目录
target_include_directories(myapp PRIVATE src)

# 启用测试
include(CTest)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()

src/mylib/CMakeLists.txt 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定义库的源文件
file(GLOB_RECURSE LIB_SOURCES "*.cpp")
file(GLOB_RECURSE LIB_HEADERS "../../include/mylib/*.hpp")

# 创建库
add_library(MyLib ${LIB_SOURCES} ${LIB_HEADERS})

# 设置包含目录
target_include_directories(MyLib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)

# 设置编译选项
target_compile_options(MyLib
PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -pedantic>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra -pedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)

tests/CMakeLists.txt 内容

1
2
3
4
5
6
7
8
9
10
11
# 查找 Google Test
find_package(GTest REQUIRED)

# 添加测试可执行文件
add_executable(test_utils test_utils.cpp)

# 链接库
target_link_libraries(test_utils PRIVATE MyLib GTest::GTest GTest::Main)

# 添加测试
gtest_discover_tests(test_utils)

5. 总结

Make 和 CMake 是 C/C++ 开发中最常用的构建工具:

  • Make:适合简单项目,通过 Makefile 直接定义构建规则
  • CMake:适合复杂项目和跨平台开发,通过 CMakeLists.txt 生成 Makefile 或其他构建文件

选择建议:

  • 小型项目或需要精确控制构建过程:使用 Make
  • 大型项目、跨平台项目或需要管理复杂依赖:使用 CMake
  • 实际开发中,通常将两者结合使用:CMake 生成 Makefile,然后使用 Make 执行构建

通过本文的教程和示例,您应该能够熟练使用 Make 和 CMake 构建各种规模的 C/C++ 项目。