Make 与 CMake:从手动编译到自动构建

视频学习资源

构建

在软件开发中,构建是指将源码文件和资源文件转换成可以运行的应用程序的过程

image-20260531204647660

构建通常会包括编译、链接、打包和部署等步骤。当只有一个源文件的时候我们可以直接使用一些编译器的指令,来手动完成构建的过程

比如可以使用gcc来编译一个C语言的源文件

$ gcc hello.c -o hello

或者使用javac来编译一个Java的源文件

$ javac hello.java

但项目庞大且复杂时,源文件、头文件和库文件有成百上千个时,手动编译就不现实了。这个时候就可以借助一些自动化的构建工具,来完成整个构建过程。

image-20260531204524918

不同平台会使用不同的构建工具,比如Make、CMake和Maven等等

下面介绍C语言项目中最常用的构建工具Make和CMake的基本用法

我们从一个最简单的场景说起:当你只有两个源文件 main.chello.c 时,一条 gcc main.c hello.c -o hello 就足够了。但当源文件变成几十上百个,并且分散在不同目录,还要链接不同的库,直接用 gcc 命令就会变得极其痛苦。

于是,自动化构建工具出现了。

1. Make 是什么?

Make 是一个经典的构建工具,它通过读取一个叫做 Makefile 的文件来确定:

  • 哪些文件需要先编译
  • 哪些文件需要重新编译
  • 如何执行编译链接命令

核心思想就是依赖管理增量构建:只有当源文件比目标文件“更新”时,才重新构建,避免每次都全量编译。

1.1 Makefile 的基本语法

Makefile 由一系列 规则(rule) 组成:

目标(target): 依赖(prerequisites)
	命令(recipe)

总的就是说两件事:

  • 第一行:文件的依赖是什么
  • 第二行:如何生成这个文件

注意:命令前必须是 Tab 键,不能用空格替代。

例如,一个极简的 Makefile:

hello: main.o hello.o
	gcc main.o hello.o -o hello

main.o: main.c
	gcc -c main.c

hello.o: hello.c
	gcc -c hello.c

clean:
	rm -f hello *.o
  • hello 是最终要生成的可执行文件名字,它依赖于 main.ohello.o

  • main.o 依赖于 main.c,命令 gcc -c main.c 负责生成它

  • clean 是一个伪目标,用于清理生成的文件

    关于伪目标的理解:

    有的时候我们可能会需要执行一些并不生成文件的操作,比如清理临时文件,生成文档,打包等等,这个时候就是使用伪目标来定义这些规则。伪目标就是一个不生成文件的目标。他只是个标签,用来执行这些操作

    那在后续执行的时候在make后面带赛clean就会执行。这样就会删除所有.o文件

    make clean
    

    需要注意的地方:如果我们本地目录下有文件的文件名是clean,那么make clean的命令就会失效,因为这是make把clean当成文件名处理了。这种情况下我们在第一行显示的告诉Make这个clean是个伪目标

    .PHONY: clean
    
    hello: main.o hello.o
    	gcc main.o hello.o -o hello
    

    其他伪目标

    • all:但我们执行make时,Make会默认执行第一个规则,但是如果我们想要生成的目标文件不止一个的话,就可以使用all这个伪命令。示例如下,然后执行命令make all【当all是第一条规则时,如本例,可以直接make,不用带all】即可自己也会显示我们的提示信息“all done”

      .PHONY clean all
      
      all: hello world
      	echo "all done"
      
      hello: main.o hello.o
      	gcc main.o hello.o -o hello
      	
      world: main.o hello.o
      	gcc main.o hello.o -o hello
      
      main.o: main.c
      	gcc -c main.c
      
      hello.o: hello.c
      	gcc -c hello.c
      
      clean:
      	rm -f hello *.o
      

执行 make 时,默认会找到第一个目标 hello,并根据依赖关系逐层构建。如果我们只改了 main.cmake 只会重新编译 main.o 并重新链接 hello

理解

大家应该知道这样写也是可以的,但是这看似省事,但并不在工程中引用

hello: main.c hello.c
	gcc main.c hello.c -o hello

因为如果这样写的话无论哪个文件有更新,都要重新编译这两个文件,这是因为我们省区了生成中间文件【.o文件】的步骤。实际中,更加规范和标准的做法是将编译和链接分开来写。也就是上面示例的写法。

1.2 使用变量和自动推导

重复写文件名和命令很容易出错,我们可以用变量自动变量简化:

CC = gcc
CFLAGS = -Wall -g
TARGET = hello
OBJS = main.o hello.o

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

main.o: main.c
	$(CC) $(CFLAGS) -c $<

hello.o: hello.c
	$(CC) $(CFLAGS) -c $<

clean:
	rm -f $(TARGET) *.o
  • $@ 代表目标文件(hello
  • $^ 代表所有依赖文件(main.o hello.o
  • $< 代表第一个依赖文件(main.chello.c

更激进地,Make 还能用模式规则(利用通配符)自动推导 .c.o 的编译命令,最终简化为:

CC = gcc
CFLAGS = -Wall -g
TARGET = hello
OBJS = main.o hello.o

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $<

clean:
	rm -f $(TARGET) *.o

到这里,你已经能看懂绝大多数手写的 Makefile 了。


2. 为什么还需要 CMake?

手写 Makefile 有两个痛点:

  1. 跨平台困难。Make 的语法在 Windows 下需要 NMake 或 MinGW,命令和路径处理各不相同。
  2. 项目规模一大,依赖关系、子目录、库查找、安装等会让 Makefile 变得极其臃肿。

CMake 应运而生:它不直接构建项目,而是生成构建文件(例如 Unix 下的 Makefile,Windows 下的 Visual Studio 工程,或者 Ninja 文件)。也就是说,CMake 是一层更高阶的抽象,我们写 CMakeLists.txt,CMake 帮我们生成平台相关的构建配置。

3. CMake 的核心概念

3.1 一个最小的 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(Hello)

add_executable(hello main.c hello.c)

三行代码就能构建一个可执行文件,比手写 Makefile 简单得多。这里做的事情:

  • 声明 CMake 最低版本要求
  • 定义项目名称
  • 添加一个名为 hello 的可执行文件,由 main.chello.c 编译生成

3.2 构建流程

通常使用 外部构建(out-of-source build),让生成的临时文件与源码分离:

mkdir build
cd build
cmake ..
make

cmake .. 会读取上层目录的 CMakeLists.txt,并在 build 目录下生成 Makefile,之后 make 完成编译。如果需要用其他生成器,可以指定:
cmake -G "Visual Studio 17 2022" ..

3.3 常用的 CMake 命令与变量

以下示例涵盖了实际项目中常见的操作:包含目录、添加库、链接库等。

cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

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

# 添加子目录(通常用于管理多个模块)
add_subdirectory(src)

# 收集源文件(更灵活的方式)
file(GLOB_RECURSE SOURCES src/*.cpp src/*.h)

# 生成可执行文件
add_executable(myapp ${SOURCES})

# 将头文件目录提供给编译器
target_include_directories(myapp PRIVATE include)

# 添加一个库(STATIC/SHARED)
add_library(mylib STATIC lib/mylib.cpp)

# 将库链接到可执行文件
target_link_libraries(myapp PRIVATE mylib)

常用变量:

变量 含义
CMAKE_SOURCE_DIR 顶层 CMakeLists.txt 所在目录
CMAKE_BINARY_DIR 执行 cmake 的 build 目录
PROJECT_NAME 当前项目名称
CMAKE_CXX_STANDARD 使用的 C++ 标准

作用域关键字 PUBLICPRIVATEINTERFACE 用于精确控制头文件/库的传播范围,是与现代 CMake 紧密相关的重点,后面进阶学习时会经常遇到。

3.4 查找外部库

用 CMake 查找系统中的库非常方便:

find_package(OpenCV REQUIRED)
target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})
target_include_directories(myapp PRIVATE ${OpenCV_INCLUDE_DIRS})

通过 find_package,CMake 会自动根据模块或配置文件寻找库的位置并设置好对应的变量。

4. 对比总结

特性 Make CMake
语言 Makefile(自己的语法) CMakeLists.txt(高层 DSL)
跨平台 依赖 Make 实现 (GNU/nmake) 生成不同平台的构建文件
增量构建 支持 继承底层构建工具(如 Make)
依赖管理 手动指定 内置查找模块、包管理(FetchContent等)
可读性 小项目清晰,大项目繁杂 结构清晰,模块化程度高

一句话理解:Make 直接告诉编译器该怎么做,CMake 先帮你生成“该怎么做”的说明书,然后再交给 Make 或其他工具去执行。

5. 实战小抄:从读懂到会用

看懂 Makefile 的检查点

  • 寻找第一个目标(默认构建目标)
  • 关注变量定义和 $(...) 引用
  • 理解 $@, $^, $< 的含义
  • 看到 %.o: %.c 就是模式规则,自动处理同类型文件

看懂 CMakeLists.txt 的检查点

  • add_executableadd_library 是构建核心
  • target_link_librariestarget_include_directories 决定编译和链接的依赖关系
  • find_package 负责查找外部依赖
  • set(...) 用于设置变量和选项
  • 注意 if(), else(), endif() 的逻辑分支以及循环
posted @ 2026-06-01 11:08  韩熙隐ario  阅读(23)  评论(0)    收藏  举报