C/C++ 项目构建过程
- 预处理:预处理器将 C/C++源码(cpp, hpp, c, h, inl 等)中的头文件嵌入进去(查找包含文件),展开由 define 定义的宏,移除注释并解析条件编译指令,最终生成一个不包含预处理代码的 文件:。
C/C++ 项目的完整构建流程,涵盖预处理、编译、汇编、链接及加载步骤。详细对比了静态库与动态库的构建命令、优缺点及适用场景,并提供了基于 CMake 的跨平台项目构建实战指南。内容包括 Windows 下 Visual Studio 与 Linux 下 Makefile 的配置差异,涉及 CMakeLists.txt 的编写规范、库的输出目录设置、RPATH 配置以及多模块项目的组织方式。

.ig++ -E main.cpp -o main.ig++ -S main.i -o main.s或 g++ -S main.cpp -o main.s。-c 进行汇编得到 .o 文件,不是一个文本文件,并不能直接查看其内容。但是可以通过反汇编指令 objdump -d main.o 将机器码翻译回汇编代码进行查看。g++ main.o la lb,当然了,现代链接器可能对这一点进行了优化,也就是不需要强调链接的顺序也能正确链接。在 Linux 系统中,静态库通常以 .a 作为后缀,并以 lib 开头,例如 libxxx.a。如果有动态库的话,链接器会标记引用,记录下动态库的位置。.so、Windows 下的 .lib、MacOS 下的 .dylib,在编译时被编译为位置无关代码Position Independent Code)并将其加载到内存中的任意可用位置,实现多进程共享。以 Linux 系统为例
假设现在有三个文件 main.cpp, add.cpp, minus.cpp,我需要将后面两个编译成静态库,下面是命令行实现源文件的构建与加载运行过程:
g++ -c add.cpp minus.cpp main.cpp ar rcs libmath.a add.o minus.o g++ main.o libmath.a -o exec /exec
上面的命令过程是:先编译所有的 cpp 文件,每个 cpp 文件生成对应的目标文件 .o,然后使用 ar 命令将 add 和 minus 打包成数学静态库 libmath.a,接着将 main 和 libmath 链接为可执行文件 exec,最后直接运行可执行文件。
和上面一样的例子
g++ -c -fPIC add.cpp minus.cpp g++ -shared -o libmath.so add.o minus.o g++ -c main.cpp g++ main.o -L. -lmath -o exec
生成目标文件,PIC 告诉编译器:生成的代码不要依赖于被加载到内存中的绝对地址。接下来由目标文件创建动态库。第 4 行命令告诉链接器记录引用,-L. 告诉编译器在当前目录中搜索动态库,-lmath 是告诉编译器链接标准命名为 math 的动态库,标准命名 lib<标准>.so,math 前面的 l 表示跨平台的快捷方式。可以使用命令 ldd exec(list dynamic dependencies)来查看 exec 所需的动态库。
如果项目不是特别庞大的话,Windows 系统下还是推荐使用静态库,能避免很多麻烦。
手动使用命令行构建项目过于繁琐。CMake 是一个 Build Generator,只需要我们在每个目录下编写 CMakeLists.txt 即可。作为一个自动化构建工具,它会根据平台的不同生成对应的构建文件,比如 Linux 系统下的 Makefile。本节的重点在构建项目的流程,而不是 CMake 的语法。
现在我有一个较为复杂的项目,项目结构如下:
<...>/ ├── Demo/ │ ├── CMakeLists.txt (【1】顶层 CMakeLists) │ ├── main/ │ │ ├── main.cpp │ │ └── CMakeLists.txt (【2】main 的 CMakeLists) │ ├── math/ │ │ ├── add.cpp │ │ ├── add.h │ │ └── CMakeLists.txt (【3】math 的 CMakeLists) │ ├── logger/ │ │ ├── logger.cpp │ │ ├── logger.h │ │ └── CMakeLists.txt (【4】logger 的 CMakeLists) │ └── dependencies/ │ └── (比如:some_header.h) │ └── build/ (生成 VS 解决方案的地方)
math 编译为动态库,logger 编译为静态库,提供给主程序使用。Demo 文件夹下还有一个依赖文件夹 dependencies,不作为项目,仅作为主程序包含头文件。
首先确保电脑上安装了 CMake,并且配置了系统环境变量:使用 cmake --version 命令来检验。以 Visual Studio 2019 为例,使用 CMake 构建 vs 的解决方案。
接下来编写每层的 CMakeLists.txt:
# CMake 最低版本要求,VS2019 建议 3.15 或更高
cmake_minimum_required(VERSION 3.15)
# 顶层项目名称
project(Demo CXX)
# CXX 表示 C++ 项目
# 设置输出目录
# 这可以确保 .exe 和 .dll 文件在同一个目录下,方便运行
# ${CMAKE_BINARY_DIR} 指向 'build' 目录
# 1. 可执行文件 (.exe)
# 输出到:build/bin/Debug/main_app.exe
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 2. 动态库 (.dll) 和 动态库的导入库 (.lib)
# 输出到:build/bin/Debug/math.dll 和 math.lib
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 3. 静态库 (.lib)
# 输出到:build/lib/Debug/logger.lib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 包含子项目
# 最佳实践是先添加库,再添加依赖它们的可执行文件
add_subdirectory(logger)
add_subdirectory(math)
add_subdirectory(main)
# 1. 定义静态库 (STATIC) 目标 "logger"
add_library(logger STATIC logger.cpp logger.h )
# 2. "发布" 头文件目录
# 任何链接到 "logger" 的项目,都将自动获得这个 include 路径
target_include_directories(logger PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
# CMAKE_CURRENT_SOURCE_DIR 指向 Demo/logger
)
# 1. 定义动态库 (SHARED) 目标 "math"
add_library(math SHARED add.cpp add.h )
# 2. "发布" 头文件目录
# 任何链接到 "math" 的项目,都将自动获得这个 include 路径
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
# CMAKE_CURRENT_SOURCE_DIR 指向 Demo/math
)
# 3. (Windows 平台) 自动导出 DLL 符号
# 这是一个简便方法,可以避免在 .h/.cpp 中手动添加 __declspec(dllexport)
# CMake 会尝试导出所有全局符号
set_target_properties(math PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE )
# 1. 定义可执行文件目标
# 避免使用 "main" 与目录名混淆
add_executable(main_app main.cpp)
# 2. 添加 "dependencies" 文件夹到 include 路径
# ${CMAKE_SOURCE_DIR} 指向顶层 "Demo" 目录
target_include_directories(main_app PRIVATE ${CMAKE_SOURCE_DIR}/dependencies )
# 3. 链接 "logger" 和 "math" 库
# CMake 会自动处理:
# - 找到 logger.lib (静态库)
# - 找到 math.lib (动态库的导入库)
# - 将 Demo/logger 和 Demo/math 目录添加到 main_app 的 include 路径
# (因为它们在各自的 CMakeLists 中设置了 PUBLIC)
target_link_libraries(main_app PRIVATE logger math )
进入 build 目录,打开命令行执行命令
cmake -G "Visual Studio 16 2019"../Demo
其中,-G "Visual Studio 16 2019" 指定使用 VS2019 64 位(默认)生成器。../Demo 表示 build 上一级目录的 Demo 文件夹下的项目。如果顺利的话,可以看到命令行的构建结果。
若失败,可尝试打开命令行终端 "x64 Native Tools Command Prompt for VS 2019",或者配置系统环境变量,将 \\Microsoft_Visual_Studio\\2019\\Professional\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64 配置进去,以确保 CMake 能找到 VS。
构建完成后进入 build 文件夹,打开 Demo.sln 解决方案即可用 VS 来 Debug 项目。或者在 build 的命令行下使用命令 cmake --build . 默认构建 Debug 版本,如果需要 release 版本则在命令后面添加 --config Release:
这个和打开 VS2019 并右键项目选择'生成解决方案'是一样的效果。
打开各项目的属性设置,可以发现 CMake 帮我们自动配置好了,不必再手动添加。
根据在顶层 CMakeLists.txt 中设置的路径,可以得到最终生成的产物位置:
build/bin/Debug/main_app.exebuild/bin/Debug/math.dll (因为它们在同一个目录,main_app.exe 运行时可以立即找到 math.dll)build/lib/Debug/logger.lib (在链接 main_app.exe 时,链接器会自动从这里找到它)针对上述项目,在 Linux 平台下差不多。只需要修改动态库的 CMakeLists 设置 Demo/math/CMakeLists.txt:
add_library(math SHARED add.cpp add.h )
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} )
# (跨平台) 仅在 Windows 上设置导出属性
if(WIN32)
set_target_properties(math PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE )
endif()
如果是跨平台的项目,则从 10 行开始可以像上面那样写,如果仅仅在 Linux 上构建,则可以删除。同时,在 Linux 上,库文件(.so 动态库和 .a 静态库)的标准约定是都放在 lib 目录下,而不是像 Windows 那样把 .dll 和 .exe 放在一起。因此顶层的 CMakeLists.txt 可以修改如下(可修改可不修改):
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# Linux RPATH 设置
# RPATH (Runtime Path) 告诉可执行文件在哪里寻找 .so 动态库。
# $ORIGIN 是一个特殊变量,指向可执行文件所在的目录。
# # 这条设置会"烧录"一个相对路径到 main_app 中,告诉它:
# "去你 (main_app) 所在的目录的上一级(..),然后去 lib 目录找 .so"
# (即:build/bin/../lib -> build/lib)
# # 这样设置后,你可以直接在 build/bin 目录下运行 ./main_app,
# 而无需设置 LD_LIBRARY_PATH 环境变量。
if(UNIX AND NOT APPLE) # $ORIGIN 在链接时会被解析
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
endif()
在 Linux 上默认使用 Makefiles(或 Ninja)。在 build 目录下构建 Release 或 Debug(用于 GDB 调试)版本需要明确指定:
cmake -DCMAKE_BUILD_TYPE=Release ../Demo
cmake -DCMAKE_BUILD_TYPE=Debug ../Demo
当然不指明也行:
cmake ../Demo
CMake 会在 build 目录中生成一个 Makefile,接下来要生成可执行文件,可以通过以下两种操作:
make -j8
其中 -j8 表示 8 核心编译,加快编译速度,可要可不要。
cmake --build . -- -j8 # 末尾的 -- -j8 是把参数传给底层的 make,这一坨也是可选的
这样可以生成一个可执行文件,使用命令 ./bin/main_app 即可运行。如果 CMakeLists.txt 里面没有指明链接生成的文件,即 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin),那么可执行文件默认生成在 build 下。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online