CMake 进阶教程(二)

PointY Lv2

库的基础概念

什么是库

库(Library)是预先编译好的代码集合,可以被多个程序复用。它将常用功能封装起来,避免重复编写代码。

库的作用:

  • 代码复用:一次编写,多处使用
  • 模块化:将大型项目拆分为独立模块
  • 封装实现:隐藏实现细节,只暴露接口

静态库 vs 动态库

特性静态库(Static Library)动态库(Shared Library)
文件扩展名Linux/macOS: .a
Windows MSVC: .lib
Windows MinGW: .a
Linux: .so
macOS: .dylib
Windows MSVC: .dll
Windows MinGW 导入库: .dll.a
链接时机编译时链接到可执行文件运行时动态加载
文件大小可执行文件较大(包含库代码)可执行文件较小(不包含库代码)
内存占用每个程序独立占用内存多个程序共享同一份库代码
更新方式需要重新编译可执行文件只需替换库文件
依赖管理无运行时依赖需要确保库文件存在

文件命名规则:

平台/编译器静态库动态库(运行时)导入库(链接时)
Linux GCClibxxx.alibxxx.so无(直接链接 .so
macOS Clanglibxxx.alibxxx.dylib无(直接链接 .dylib
Windows MSVCxxx.libxxx.dllxxx.lib
Windows MinGWlibxxx.alibxxx.dlllibxxx.dll.a

💡 说明:Windows 下动态库需要两个文件:

  • 运行时文件.dll):程序运行时加载
  • 导入库(MSVC 的 .lib 或 MinGW 的 .dll.a):编译链接时使用

CMake 中的库类型

1
2
3
4
5
6
7
8
9
10
11
# 静态库
add_library(mylib STATIC src1.cpp src2.cpp)

# 动态库
add_library(mylib SHARED src1.cpp src2.cpp)

# 接口库(仅头文件库)
add_library(mylib INTERFACE)

# 对象库(编译但不归档)
add_library(mylib OBJECT src1.cpp src2.cpp)

静态库(Static Library)

什么是静态库

静态库在编译时被完整复制到可执行文件中。链接器会将库中被使用的函数代码提取出来,合并到最终的可执行文件。

优点:

  • 无运行时依赖,部署简单
  • 运行速度稍快(无动态加载开销)

缺点:

  • 可执行文件体积大
  • 更新库需要重新编译所有程序
  • 多个程序使用同一库时,内存占用高

创建静态库

项目结构:

1
2
3
4
5
6
7
8
mathlib/
├── CMakeLists.txt
├── include/
│ └── mathlib/
│ └── operations.h
└── src/
├── add.cpp
└── multiply.cpp

include/mathlib/operations.h:

1
2
3
4
5
6
#pragma once

namespace mathlib {
int add(int a, int b);
int multiply(int a, int b);
}

src/add.cpp:

1
2
3
4
5
6
7
#include "mathlib/operations.h"

namespace mathlib {
int add(int a, int b) {
return a + b;
}
}

src/multiply.cpp:

1
2
3
4
5
6
7
#include "mathlib/operations.h"

namespace mathlib {
int multiply(int a, int b) {
return a * b;
}
}

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.15)
project(MathLib VERSION 1.0.0)

# 创建静态库
add_library(mathlib STATIC
src/add.cpp
src/multiply.cpp
)

# 设置包含目录
target_include_directories(mathlib PUBLIC
${PROJECT_SOURCE_DIR}/include
)

# 设置 C++ 标准
target_compile_features(mathlib PUBLIC cxx_std_17)

使用静态库:

1
2
3
# 在另一个项目中
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mathlib)

静态库最佳实践

1. 头文件组织

1
2
3
4
5
6
7
8
include/
└── mylib/ # 库名作为命名空间
├── core.h # 公共接口
└── utils.h
src/
├── core.cpp # 实现文件
├── utils.cpp
└── internal.h # 私有头文件(不安装)

2. 接口设计原则

  • 使用命名空间避免符号冲突
  • 头文件使用 #pragma once 或 include guard
  • 最小化头文件依赖(前向声明)
  • 使用 PIMPL 模式隐藏实现细节

3. 常见问题

  • 符号重复定义:确保函数只在 .cpp 中实现,头文件只声明
  • 链接顺序:被依赖的库放在后面(现代 CMake 自动处理)
  • 循环依赖:重新设计模块边界,避免 A 依赖 B 且 B 依赖 A

4. 循环依赖的解决方案

循环依赖是指两个或多个库相互依赖,这会导致链接错误和设计问题。

错误示例:

1
2
3
4
5
add_library(libA ...)
target_link_libraries(libA PUBLIC libB)

add_library(libB ...)
target_link_libraries(libB PUBLIC libA) # 循环依赖!

解决方案:

方案 1:提取公共部分

1
2
3
4
5
6
7
8
# 将公共依赖提取到独立库
add_library(common ...)

add_library(libA ...)
target_link_libraries(libA PUBLIC common)

add_library(libB ...)
target_link_libraries(libB PUBLIC common)

方案 2:使用前向声明

1
2
3
4
5
6
// libA.h - 使用前向声明而不是包含 libB.h
class ClassB; // 前向声明

class ClassA {
ClassB* ptr; // 使用指针或引用
};

方案 3:重新设计模块边界

  • 分析依赖关系,确定哪个库应该是底层库
  • 将高层功能移到上层库,保持单向依赖

动态库(Shared Library)

什么是动态库

动态库在运行时被加载到内存,多个程序可以共享同一份库代码。

优点:

  • 可执行文件体积小
  • 更新库无需重新编译程序
  • 多个程序共享内存,节省资源
  • 支持插件式架构

缺点:

  • 需要管理运行时依赖
  • 版本兼容性问题(DLL Hell)
  • 启动时有动态加载开销

创建动态库

项目结构:

1
2
3
4
5
6
7
stringlib/
├── CMakeLists.txt
├── include/
│ └── stringlib/
│ └── string_util.h
└── src/
└── string_util.cpp

include/stringlib/string_util.h:

1
2
3
4
5
6
7
8
#pragma once
#include "stringlib/stringlib_export.h" // 自动生成的导出头文件
#include <string>

namespace stringlib {
STRINGLIB_EXPORT std::string to_upper(const std::string& str);
STRINGLIB_EXPORT std::string to_lower(const std::string& str);
}

src/string_util.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stringlib/string_util.h"
#include <algorithm>

namespace stringlib {
std::string to_upper(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
return result;
}

std::string to_lower(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
return result;
}
}

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
cmake_minimum_required(VERSION 3.15)
project(StringLib VERSION 1.0.0)

include(GenerateExportHeader)

# 创建动态库
add_library(stringlib SHARED
src/string_util.cpp
)

# 生成导出头文件
generate_export_header(stringlib
EXPORT_FILE_NAME include/stringlib/stringlib_export.h
)

# 设置包含目录
target_include_directories(stringlib PUBLIC
${PROJECT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/include # 包含生成的导出头文件
)

# 设置版本号
set_target_properties(stringlib PROPERTIES
VERSION ${PROJECT_VERSION} # 完整版本号 1.0.0
SOVERSION ${PROJECT_VERSION_MAJOR} # 主版本号 1
)

# 设置 C++ 标准
target_compile_features(stringlib PUBLIC cxx_std_17)

动态库特殊配置

1. 符号导出控制(GenerateExportHeader)

Windows 默认不导出符号,需要显式标记。推荐使用 CMake 的 GenerateExportHeader 模块,这是业界标准做法。

工作原理:

CMake 自动生成跨平台的导出宏头文件,处理 Windows 的 __declspec(dllexport/dllimport) 和 Linux 的 __attribute__((visibility))

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
include(GenerateExportHeader)

add_library(mylib SHARED src/mylib.cpp)

# 自动生成 mylib_export.h
generate_export_header(mylib
EXPORT_FILE_NAME include/mylib/mylib_export.h
)

target_include_directories(mylib PUBLIC
${PROJECT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/include # 包含生成的头文件
)

使用生成的导出宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// include/mylib/mylib.h
#pragma once
#include "mylib/mylib_export.h" // 包含生成的导出头文件

// 导出的类
class MYLIB_EXPORT MyClass {
public:
void public_method();
private:
void private_method(); // 私有方法不会被导出
};

// 导出的函数
MYLIB_EXPORT void my_function();

// 不导出的内部函数(不加宏)
void internal_helper();

生成的 mylib_export.h 内容(自动生成):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef MYLIB_EXPORT_H
#define MYLIB_EXPORT_H

#ifdef MYLIB_STATIC_DEFINE
# define MYLIB_EXPORT
#else
# ifndef MYLIB_EXPORT
# ifdef mylib_EXPORTS
/* 编译 DLL 时 */
# define MYLIB_EXPORT __declspec(dllexport)
# else
/* 使用 DLL 时 */
# define MYLIB_EXPORT __declspec(dllimport)
# endif
# endif
#endif

#endif

优点:

  • ✅ 自动处理跨平台差异(Windows/Linux/macOS)
  • ✅ 自动处理静态库/动态库切换
  • ✅ 只导出标记的符号,私有函数不导出
  • ✅ 业界标准(Qt、KDE、VTK 等大型项目都使用)
  • ✅ 减少手动维护的代码

高级选项:

1
2
3
4
5
6
generate_export_header(mylib
EXPORT_FILE_NAME include/mylib/mylib_export.h
EXPORT_MACRO_NAME MYLIB_API # 自定义宏名称
NO_EXPORT_MACRO_NAME MYLIB_PRIVATE # 显式标记不导出
DEPRECATED_MACRO_NAME MYLIB_DEPRECATED # 废弃符号宏
)

2. 版本号管理

1
2
3
4
set_target_properties(mylib PROPERTIES
VERSION 2.3.1 # 完整版本号(libmylib.so.2.3.1)
SOVERSION 2 # ABI 版本号(libmylib.so.2 -> libmylib.so.2.3.1)
)
  • VERSION:完整版本号,用于文件名
  • SOVERSION:ABI 兼容版本号,主版本号变化表示不兼容

3. RPATH 配置

Linux/macOS 下,可执行文件需要知道动态库的位置:

1
2
3
4
5
6
7
# 设置 RPATH(运行时库搜索路径)
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib" # 相对路径
)

# 或使用 CMake 自动处理
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

4. 安装配置

1
2
3
4
5
6
7
8
9
10
11
# 安装库文件
install(TARGETS stringlib
LIBRARY DESTINATION lib # .so 文件
ARCHIVE DESTINATION lib # .a 文件
RUNTIME DESTINATION bin # .dll 文件
)

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

库的链接与使用

基本语法:

1
2
3
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <lib1> [<lib2> ...]
)

示例:链接库到可执行文件

1
2
3
4
5
6
7
8
# 创建可执行文件
add_executable(myapp main.cpp)

# 链接静态库
target_link_libraries(myapp PRIVATE mathlib)

# 链接多个库
target_link_libraries(myapp PRIVATE mathlib stringlib)

示例:库之间的链接

1
2
3
4
5
6
7
8
9
10
11
# 创建底层库
add_library(core STATIC core.cpp)

# 创建上层库,依赖 core
add_library(utils STATIC utils.cpp)
target_link_libraries(utils PUBLIC core)

# 创建应用,只需链接 utils
add_executable(app main.cpp)
target_link_libraries(app PRIVATE utils)
# app 自动获得 core 的链接(因为 utils 使用了 PUBLIC)

链接可见性

可见性控制依赖的传递方式,这是现代 CMake 的核心概念。

三种可见性:

可见性含义使用场景
PRIVATE仅目标自己使用实现细节,不暴露给依赖者
PUBLIC目标和依赖者都能使用接口的一部分,需要传递给依赖者
INTERFACE仅依赖者使用,目标自己不使用仅头文件库,或仅传递编译选项

图解说明:

1
2
3
4
5
6
7
8
9
10
11
场景 1:PRIVATE
[App] --链接--> [LibA] --PRIVATE链接--> [LibB]
结果:App 只能使用 LibA,看不到 LibB

场景 2:PUBLIC
[App] --链接--> [LibA] --PUBLIC链接--> [LibB]
结果:App 可以使用 LibA 和 LibB(自动传递)

场景 3:INTERFACE
[App] --链接--> [HeaderOnlyLib] --INTERFACE链接--> [LibC]
结果:App 可以使用 LibC,但 HeaderOnlyLib 自己不编译任何代码

实际例子:

1
2
3
4
5
6
7
8
9
10
11
12
# 场景:网络库依赖 JSON 库
add_library(json STATIC json_parser.cpp)

add_library(network STATIC network.cpp)

# 情况 1:JSON 类型出现在 network.h 的公共接口中
# network.h: NetworkResponse parse_response(const json::Value& data);
target_link_libraries(network PUBLIC json) # 使用者需要知道 json

# 情况 2:JSON 仅在 network.cpp 内部使用
# network.h: NetworkResponse parse_response(const std::string& data);
target_link_libraries(network PRIVATE json) # 使用者不需要知道 json

选择原则:

  • 依赖出现在头文件的公共接口中 → PUBLIC
  • 依赖仅在实现文件中使用 → PRIVATE
  • 仅头文件库或仅传递编译选项 → INTERFACE

⚠️ 避免使用全局 link_libraries

错误做法:

1
2
3
link_libraries(Boost::filesystem)  # 全局影响所有目标
add_executable(app1 ...)
add_executable(app2 ...)

这会导致所有后续定义的目标都链接 Boost::filesystem,即使它们不需要。

正确做法:

1
2
3
4
5
add_executable(app1 ...)
target_link_libraries(app1 PRIVATE Boost::filesystem)

add_executable(app2 ...)
target_link_libraries(app2 PRIVATE Boost::system)

原因:

  • link_libraries 是全局命令,影响所有后续目标
  • target_link_libraries 是目标级别命令,只影响指定目标
  • 使用目标级别命令可以精确控制每个目标的依赖,避免不必要的链接

target_include_directories

控制头文件搜索路径,与 target_link_libraries 配合使用。

语法:

1
2
3
target_include_directories(<target>
<PRIVATE|PUBLIC|INTERFACE> <dir1> [<dir2> ...]
)

可见性说明:

可见性含义使用场景
PRIVATE仅目标自己使用内部实现的头文件
PUBLIC目标和依赖者都能使用库的公共接口头文件
INTERFACE仅依赖者使用仅头文件库(Header-only)

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建库
add_library(mylib src/mylib.cpp)

# PRIVATE:mylib 编译时使用,但链接 mylib 的目标不能使用
target_include_directories(mylib PRIVATE
${PROJECT_SOURCE_DIR}/src # 内部实现头文件
${PROJECT_SOURCE_DIR}/third_party # 第三方库(不暴露)
)

# PUBLIC:mylib 和链接 mylib 的目标都能使用
target_include_directories(mylib PUBLIC
${PROJECT_SOURCE_DIR}/include # 公共接口头文件
)

生成器表达式(Generator Expressions):

处理构建时和安装后的路径差异:

1
2
3
4
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # 构建时
$<INSTALL_INTERFACE:include> # 安装后
)

链接外部库

1. 链接系统库

1
2
3
4
5
6
7
8
9
# 数学库(Linux)
target_link_libraries(myapp PRIVATE m)

# 线程库(跨平台)
find_package(Threads REQUIRED)
target_link_libraries(myapp PRIVATE Threads::Threads)

# 动态链接库(Linux)
target_link_libraries(myapp PRIVATE dl)

2. 使用 find_package 查找第三方库

1
2
3
4
5
6
7
8
9
10
11
# 查找 OpenCV
find_package(OpenCV REQUIRED)
target_link_libraries(myapp PRIVATE OpenCV::OpenCV)

# 查找 Boost(指定组件)
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
target_link_libraries(myapp PRIVATE Boost::filesystem Boost::system)

# 查找 OpenMP
find_package(OpenMP REQUIRED)
target_link_libraries(myapp PRIVATE OpenMP::OpenMP_CXX)

3. 手动指定库路径(不推荐)

1
2
3
4
5
6
7
8
9
# 添加库搜索路径
link_directories(/usr/local/lib) # 不推荐:全局影响

# 直接指定库文件
target_link_libraries(myapp PRIVATE /usr/local/lib/libfoo.a) # 可以,但不便携

# 推荐:使用 find_library
find_library(FOO_LIB foo PATHS /usr/local/lib)
target_link_libraries(myapp PRIVATE ${FOO_LIB})

实战案例

多库项目

场景:创建一个包含多个库的项目,演示库之间的依赖关系。

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
multi_lib_project/
├── CMakeLists.txt
├── core/
│ ├── CMakeLists.txt
│ ├── include/core/
│ │ └── logger.h
│ └── src/
│ └── logger.cpp
├── utils/
│ ├── CMakeLists.txt
│ ├── include/utils/
│ │ └── string_helper.h
│ └── src/
│ └── string_helper.cpp
└── app/
├── CMakeLists.txt
└── main.cpp

core/include/core/logger.h:

1
2
3
4
5
6
#pragma once
#include <string>

namespace core {
void log(const std::string& message);
}

core/src/logger.cpp:

1
2
3
4
5
6
7
8
#include "core/logger.h"
#include <iostream>

namespace core {
void log(const std::string& message) {
std::cout << "[LOG] " << message << std::endl;
}
}

core/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
add_library(core STATIC
src/logger.cpp
)

target_include_directories(core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

target_compile_features(core PUBLIC cxx_std_17)

utils/include/utils/string_helper.h:

1
2
3
4
5
6
#pragma once
#include <string>

namespace utils {
std::string trim(const std::string& str);
}

utils/src/string_helper.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
#include "utils/string_helper.h"
#include "core/logger.h" // 依赖 core 库

namespace utils {
std::string trim(const std::string& str) {
core::log("Trimming string"); // 使用 core 库
// 简化实现
size_t start = str.find_first_not_of(" \t\n\r");
size_t end = str.find_last_not_of(" \t\n\r");
return (start == std::string::npos) ? "" : str.substr(start, end - start + 1);
}
}

utils/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
add_library(utils STATIC
src/string_helper.cpp
)

target_include_directories(utils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

# utils 的公共接口不暴露 core,但实现中使用了 core
target_link_libraries(utils PRIVATE core)

target_compile_features(utils PUBLIC cxx_std_17)

app/main.cpp:

1
2
3
4
5
6
7
8
9
10
11
#include "utils/string_helper.h"
#include "core/logger.h"
#include <iostream>

int main() {
std::string text = " Hello, CMake! ";
std::string result = utils::trim(text);

core::log("Result: " + result);
return 0;
}

app/CMakeLists.txt:

1
2
3
4
add_executable(app main.cpp)

# app 直接使用 utils 和 core
target_link_libraries(app PRIVATE utils core)

根 CMakeLists.txt:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.15)
project(MultiLibProject VERSION 1.0.0)

# 添加子目录
add_subdirectory(core)
add_subdirectory(utils)
add_subdirectory(app)

编译运行:

1
2
3
4
mkdir build && cd build
cmake ..
cmake --build .
./app/app

输出:

1
2
[LOG] Trimming string
[LOG] Result: Hello, CMake!

关键点:

  • core 是基础库,不依赖其他库
  • utils 依赖 core,使用 PRIVATE 因为 core 不出现在 utils 的公共接口中
  • app 同时链接 utilscore

跨平台动态库与 Header-Only 库

场景:创建一个跨平台的动态库和仅头文件库。

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cross_platform_lib/
├── CMakeLists.txt
├── shared_lib/
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── calc/
│ │ └── calculator.h
│ └── src/
│ └── calculator.cpp
├── header_only_lib/
│ ├── CMakeLists.txt
│ └── include/
│ └── templates/
│ └── array_utils.h
└── app/
├── CMakeLists.txt
└── main.cpp

shared_lib/include/calc/calculator.h:

1
2
3
4
5
6
7
8
9
10
#pragma once
#include "calc/calc_export.h" // 自动生成的导出头文件

namespace calc {
class CALC_EXPORT Calculator {
public:
double add(double a, double b);
double subtract(double a, double b);
};
}

shared_lib/src/calculator.cpp:

1
2
3
4
5
6
7
8
9
10
11
#include "calc/calculator.h"

namespace calc {
double Calculator::add(double a, double b) {
return a + b;
}

double Calculator::subtract(double a, double b) {
return a - b;
}
}

shared_lib/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
include(GenerateExportHeader)

add_library(calc SHARED
src/calculator.cpp
)

# 生成导出头文件
generate_export_header(calc
EXPORT_FILE_NAME include/calc/calc_export.h
)

target_include_directories(calc PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/include # 包含生成的导出头文件
)

# 设置版本号
set_target_properties(calc PROPERTIES
VERSION 1.0.0
SOVERSION 1
)

target_compile_features(calc PUBLIC cxx_std_17)

header_only_lib/include/templates/array_utils.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
#include <algorithm>
#include <vector>

namespace templates {
// 仅头文件库:所有实现都在头文件中
template<typename T>
T max_element(const std::vector<T>& vec) {
return *std::max_element(vec.begin(), vec.end());
}

template<typename T>
T min_element(const std::vector<T>& vec) {
return *std::min_element(vec.begin(), vec.end());
}
}

header_only_lib/CMakeLists.txt:

1
2
3
4
5
6
7
8
# 创建接口库(仅头文件库)
add_library(array_utils INTERFACE)

target_include_directories(array_utils INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)

target_compile_features(array_utils INTERFACE cxx_std_17)

app/main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "calc/calculator.h"
#include "templates/array_utils.h"
#include <iostream>
#include <vector>

int main() {
// 使用动态库
calc::Calculator calculator;
std::cout << "10 + 5 = " << calculator.add(10, 5) << std::endl;
std::cout << "10 - 5 = " << calculator.subtract(10, 5) << std::endl;

// 使用仅头文件库
std::vector<int> numbers = {3, 7, 2, 9, 1};
std::cout << "Max: " << templates::max_element(numbers) << std::endl;
std::cout << "Min: " << templates::min_element(numbers) << std::endl;

return 0;
}

app/CMakeLists.txt:

1
2
3
4
5
6
add_executable(app main.cpp)

target_link_libraries(app PRIVATE
calc # 动态库
array_utils # 仅头文件库
)

根 CMakeLists.txt:

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.15)
project(CrossPlatformLib VERSION 1.0.0)

add_subdirectory(shared_lib)
add_subdirectory(header_only_lib)
add_subdirectory(app)

关键点:

  • 动态库使用 GenerateExportHeader 自动生成跨平台导出宏
  • 仅头文件库使用 INTERFACE 类型,不编译任何源文件
  • INTERFACE 库的所有属性都使用 INTERFACE 关键字

集成第三方库

场景:创建一个使用 Boost 和多线程的网络应用。

项目结构:

1
2
3
4
5
6
7
8
9
10
network_app/
├── CMakeLists.txt
├── include/
│ └── network/
│ ├── tcp_client.h
│ └── tcp_server.h
└── src/
├── tcp_client.cpp
├── tcp_server.cpp
└── main.cpp

include/network/tcp_server.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
#include <boost/asio.hpp>
#include <memory>
#include <string>

namespace network {
class TcpServer {
public:
TcpServer(unsigned short port);
void start();
void stop();
private:
boost::asio::io_context io_context_;
unsigned short port_;
};
}

src/tcp_server.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "network/tcp_server.h"
#include <iostream>

namespace network {
TcpServer::TcpServer(unsigned short port) : port_(port) {}

void TcpServer::start() {
std::cout << "Server starting on port " << port_ << std::endl;
// 简化实现
}

void TcpServer::stop() {
std::cout << "Server stopped" << std::endl;
}
}

src/main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "network/tcp_server.h"
#include <iostream>

int main() {
try {
network::TcpServer server(8080);
server.start();
server.stop();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

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
cmake_minimum_required(VERSION 3.15)
project(NetworkApp VERSION 1.0.0)

# 查找第三方库
find_package(Threads REQUIRED)
find_package(Boost 1.70 REQUIRED COMPONENTS system thread)

# 创建网络库
add_library(network_lib
src/tcp_server.cpp
)

target_include_directories(network_lib PUBLIC
${PROJECT_SOURCE_DIR}/include
)

# 链接第三方库
target_link_libraries(network_lib
PUBLIC
Boost::system # Boost 类型出现在头文件中
Boost::thread
PRIVATE
Threads::Threads # 仅在实现中使用
)

target_compile_features(network_lib PUBLIC cxx_std_17)

# 创建应用程序
add_executable(network_app src/main.cpp)
target_link_libraries(network_app PRIVATE network_lib)

说明

  • 使用 find_package 查找 Threads 和 Boost 库
  • Boost 库使用 PUBLIC,因为 boost::asio 类型出现在头文件中
  • Threads 使用 PRIVATE,仅在实现中使用
  • 现代 CMake 使用 Boost::system 而不是 ${Boost_LIBRARIES}

代码复用技巧

自定义函数:封装常用操作

将重复的 CMake 代码封装成函数,可以提高代码复用性和可维护性。

场景:每次创建库都要设置相同的编译选项、警告级别、C++ 标准等,可以封装成函数。

在根 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
# 添加带标准配置的库
function(add_my_library lib_name)
# 解析参数
set(options SHARED STATIC)
set(multiValueArgs SOURCES LIBS)
cmake_parse_arguments(ARG "${options}" "" "${multiValueArgs}" ${ARGN})

# 确定库类型
if(ARG_SHARED)
set(lib_type SHARED)
else()
set(lib_type STATIC)
endif()

# 创建库
add_library(${lib_name} ${lib_type} ${ARG_SOURCES})

# 设置包含目录(假设标准项目结构)
target_include_directories(${lib_name}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)

# 设置 C++17 标准
target_compile_features(${lib_name} PUBLIC cxx_std_17)

# 添加警告选项
if(MSVC)
target_compile_options(${lib_name} PRIVATE /W4)
else()
target_compile_options(${lib_name} PRIVATE -Wall -Wextra -Wpedantic)
endif()

# 链接库
if(ARG_LIBS)
target_link_libraries(${lib_name} PUBLIC ${ARG_LIBS})
endif()
endfunction()

使用自定义函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建静态库
add_my_library(mathlib
SOURCES
src/add.cpp
src/subtract.cpp
LIBS
m # 数学库(Linux)
)

# 创建动态库
add_my_library(utils
SHARED
SOURCES
src/string_util.cpp
src/file_util.cpp
LIBS
Boost::filesystem
)

优点

  • 统一的编译选项和标准
  • 减少重复代码
  • 便于维护和修改

💡 提示:更复杂的模块化设计(如独立的 .cmake 模块文件、find_package 配置文件)在大型项目中才会用到,这里不展开讲解。


项目组织建议

小型项目(单个库)

1
2
3
4
5
6
7
8
project/
├── CMakeLists.txt
├── include/mylib/
│ └── mylib.h
├── src/
│ └── mylib.cpp
└── examples/
└── example.cpp

中型项目(多个库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
project/
├── CMakeLists.txt
├── cmake/ # CMake 模块
│ └── MyFunctions.cmake
├── libs/
│ ├── core/
│ │ ├── CMakeLists.txt
│ │ ├── include/core/
│ │ └── src/
│ └── utils/
│ ├── CMakeLists.txt
│ ├── include/utils/
│ └── src/
└── apps/
└── myapp/
├── CMakeLists.txt
└── main.cpp

大型项目(多模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
project/
├── CMakeLists.txt
├── cmake/
│ ├── Config.cmake.in
│ └── FindXXX.cmake
├── modules/
│ ├── module1/
│ │ ├── CMakeLists.txt
│ │ ├── include/
│ │ ├── src/
│ │ └── tests/
│ └── module2/
│ └── ...
├── external/ # 第三方库
└── docs/

参考资源

  • 标题: CMake 进阶教程(二)
  • 作者: PointY
  • 创建于 : 2026-05-18 21:12:17
  • 更新于 : 2026-07-14 14:55:25
  • 链接: https://siyuhong.github.io/2026/05/18/cmake-tutorial03/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论