跳转至

一、通用

【C++】g++指令使用指南-CSDN博客

二、NDK

NDK 使用入门 | Android NDK | Android Developers Android NDK API Reference | Android Developers

2.1 版本注意

只看API行为、产物类型,不绑定特定NDK版本;静态库本身不做链接,只做编译,无链接选项。

目标API 产物类型 编译&链接参数 平台关键行为变更节点 STL相关约束
≤15 可执行 不能用 -fPIE -pie-pie 链接参数 Android4.4‑及更早不支持PIE可执行,PIE程序直接崩溃 gnustl可用;libc++未正式落地
静态库 .a 编译:‑fPIC(推荐) 无链接参数,ar打包 静态库只是目标文件归档,本身不加载进进程;最终行为取决于最终链接成exe/so时的参数 静态库可内嵌STL对象;注意避免多重STL符号副本
动态库 .so 编译:‑fPIC 链接:‑shared;禁止‑pie so永远要求位置无关 gnustl
16‑20 可执行 可选 -fPIE -pie API16开始实验支持PIE;系统同时支持PIE/非PIE可执行,不强制 gnustl / libc++均可
静态库 .a 编译:‑fPIC(推荐) 无链接参数,ar打包 静态库本身无PIE/PIC属性;若该.a会被链接进.so必须用‑fPIC编译 同上,最终产物决定行为
动态库 .so 编译:‑fPIC 链接:‑shared;禁止‑pie so永远要求位置无关 gnustl / libc++
≥21 可执行 必须 -fPIE(编译) -pie(链接) API21(Android5.0) 强制PIE;非PIE可执行直接拒绝运行 gnustl / libc++;ABI不互通,不能跨STL边界传递容器对象
静态库 .a 编译:‑fPIC(推荐) 无链接参数,ar打包 1. .a被链接进PIE可执行.a可以不带‑fPIC,但建议统一‑fPIC 2. .a被链接进.so.a必须‑fPIC 静态库内嵌STL容易出现符号重复;尽量不在.a中实例化STL全局对象
动态库 .so 编译:‑fPIC 链接:‑shared;禁止‑pie so永远要求位置无关,PIE仅针对独立可执行 gnustl / libc++
参数 类型 作用对象 含义
-fPIC 编译选项(Compiler flag) .c/.cpp → .o 生成位置无关代码,用于动态库 .so
-fPIE 编译选项(Compiler flag) .c/.cpp → .o 生成位置无关代码,用于独立可执行程序
-pie 链接选项(Linker flag) .o → 最终exe 将目标文件链接输出为 PIE类型可执行文件

快速记忆:可执行用 fPIE + pie;动态库用 fPIC + shared;静态库只加 fPIC,不要pie不要shared。PIC英文为Position‑Independent Code, Position‑Independent Executable。

官方文档其实只有介绍性的说明,目前能了解到的最多的是--help的结果。

2.2 gnu

Android Bionic Libc 原理与实现综述_安卓libc-CSDN博客

深入Android系统(二)Bionic库咳咳,有木有发现这么多的了解字眼?因为从这几天本人大脑的表现来看,这种不常用的 - 掘金

深入Android系列 - apigfly的专栏 - 掘金

ATL深度解析(5)Bionic 兼容层 — 让两种 libc 在同一进程共存 | Wacao's Den

GCC4.9 的 exe不识别‑target 参数;目标架构已经固化在 exe 里面。 NDK r18 彻底删除 gcc 整套多目录工具链,全部迁移 clang+lld,不再存在aarch64‑linux‑android‑4.9目录。

项目中使用的是CMAKE,但太重了,经常有自测的需求,因此可以封装下shell函数,方便可执行文件自测,封装后和通用的gcc g++命令使用方式基本一致:

export NDK_R16B_ROOT="${NDK_R16B_ROOT:-/d/Android/android-ndk-r16b}"
export NDK_R16B_API="${NDK_R16B_API:-27}"
export NDK_R16B_STL="${NDK_R16B_STL:-static}"     # static | shared | system

android-gnu_gcc() {
    local B="${NDK_R16B_ROOT}/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin"
    "${B}/aarch64-linux-android-gcc.exe" \
        --sysroot="${NDK_R16B_ROOT}/platforms/android-${NDK_R16B_API}/arch-arm64" \
        -isystem "${NDK_R16B_ROOT}/sysroot/usr/include" \
        -isystem "${NDK_R16B_ROOT}/sysroot/usr/include/aarch64-linux-android" \
        -D__ANDROID_API__="${NDK_R16B_API}" \
        -fPIE -pie \
        "$@"
}

android-gnu_gpp() {
    local B="${NDK_R16B_ROOT}/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin"
    local G="${NDK_R16B_ROOT}/sources/cxx-stl/gnu-libstdc++/4.9"
    local -a STL_INC=() STL_LIB=()
    case "${NDK_R16B_STL}" in
        static|shared)
            STL_INC=(-isystem "${G}/include" -isystem "${G}/libs/arm64-v8a/include" -L "${G}/libs/arm64-v8a")
            STL_LIB=(-lgnustl_${NDK_R16B_STL}) ;;
        system)
            STL_INC=(-isystem "${NDK_R16B_ROOT}/sources/cxx-stl/system/include") ;;
        *) echo "android-gnu: NDK_R16B_STL 只能是 static / shared / system(当前=${NDK_R16B_STL})" >&2; return 1 ;;
    esac

    "${B}/aarch64-linux-android-g++.exe" \
        --sysroot="${NDK_R16B_ROOT}/platforms/android-${NDK_R16B_API}/arch-arm64" \
        -isystem "${NDK_R16B_ROOT}/sysroot/usr/include" \
        -isystem "${NDK_R16B_ROOT}/sysroot/usr/include/aarch64-linux-android" \
        -D__ANDROID_API__="${NDK_R16B_API}" \
        "${STL_INC[@]}" \
        -fPIE -pie \
        "$@" "${STL_LIB[@]}"
}

2.3 llvm

export NDK_R27D_ROOT="${NDK_R27D_ROOT:-/d/Android/android-ndk-r27d}"
export NDK_R27D_API="${NDK_R27D_API:-24}"
export NDK_R27D_STL="${NDK_R27D_STL:-static}"     # static | shared

android-llvm_gcc() {
    "${NDK_R27D_ROOT}/toolchains/llvm/prebuilt/windows-x86_64/bin/clang.exe" \
        --target="aarch64-linux-android${NDK_R27D_API}" \
        -fPIE -pie \
        "$@"
}

android-llvm_gpp() {
    local STL=
    case "${NDK_R27D_STL}" in
        static) STL="-static-libstdc++" ;;
        shared) STL= ;;
        *) echo "android-llvm: NDK_R27D_STL 只能是 static 或 shared(当前=${NDK_R27D_STL})" >&2; return 1 ;;
    esac

    "${NDK_R27D_ROOT}/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++.exe" \
        --target="aarch64-linux-android${NDK_R27D_API}" \
        -fPIE -pie \
        ${STL} \
        "$@"
}

2.3.1 编程工具

LLVM 是一套编译器基础设施(编译器后端框架);Clang 是基于 LLVM 的 C/C++/Objective‑C 编译器前端。

LLVM/Clang 本身是“可重定向”的通用工具链:一份代码能生成支持任意目标的编译器。官方对每个宿主平台(Windows / Linux / macOS)都会构建全套工具,而不是只构建该宿主做 Android 开发需要的那几个。

真正给 Android 用的只有一小部分

  • 编译:clang.exe / clang++.exe(及 .cmd wrapper)、ld.lld.exe
  • 直接手写 ld.lld,就等于你要把 clang driver 那套平台推导逻辑自己手工复现一遍(‑L、crt、dynamic‑linker),参数又多又容易错。
  • 远优先调用 clang++.exe,把平台细节交给 driver;只有调试研究的时候才去看 / 抄 ld.lld 的命令。
  • 静态库:llvm-ar.exe / llvm-ranlib.exe
  • 排查崩溃栈/看二进制:llvm-addr2line.exe、llvm-nm.exe、llvm-readelf.exe、llvm-objdump.exe、llvm-symbolizer.exe、llvm- cxxfilt.exe
  • 提体积:llvm-strip.exe
工具 核心定位 典型产出 重点说明
clang‑format.exe 代码格式化(空格、缩进、括号) 格式化后的源码 只改排版;不做语法语义、不查bug
clang‑tidy.exe Lint静态检查 + 部分自动修复 告警、可原地修复代码 做语义检查:bugprone、modernize、performance、readability;可调用clang静态分析器;支持.clang‑tidy配置文件
clang‑check.exe AST调试、快速语法语义检查 AST转储、编译告警 底层LibTooling;多用于开发clang插件;日常业务代码优先用clang‑tidy
llvm‑cov.exe + llvm‑profdata.exe 源码级覆盖率(PGO/‑fprofile‑instr‑generate) 行/分支覆盖率报告 需要编译插桩;运行生成.profraw;合并为.profdata;输出源码覆盖率报告
sancov.exe + sanstats.exeSanitizer‑Coverage(ASan/UBSan配套覆盖率,fuzz专用) 二进制PC地址、已覆盖/未覆盖函数 和ASan等sanitizer一起用;输出.sancov;不生成源码行级报告,只看基本块/函数是否命中,多用于libFuzzer模糊测试

2.4 动态库加载

linux/Android dlopen动态库搜索路径:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/your/

2.5 java加载动态库

java加到动态库并不会检查动态库的依赖,若运行时依赖动态库缺失时,才会报错。类似于C++ 运行时通过dlopen加载动态库。

package com.company.sdkname;

public class libtestmodule {
    static {
        System.loadLibrary("testsoname");
    }
    public static native <type> <JnifunctionName>( ...);
}

navtive 对接jni的cpp加入库的编译中,就能正常编译出可以被java使用的库。

2.6 程序运行

对于没有root权限的车机或Android手机,可以到/data/local/tmp路径下,更新LD_LABRARY_PATH,然后运行NDK直接编译出来的可执行文件,文件需要添加可执行权限。

三、windows local

3.1 MSVC

微软的C++编译工具链(MSVC,Microsoft Visual C++)是由微软公司开发的用于编译C++代码的工具套件。它包含编译器(cl.exe)、链接器(link.exe)和其他一些工具(如资源编译器(rc.exe)和宏处理器(c1.dll)等)。

MSVC支持多种编程范式,包括面向过程的编程、面向对象的编程和泛型编程等。它还支持多种平台,包括Windows、Linux和Mac OS等。

工具链版本:MSVC\14.29.30133 查看默认支持的C++版本:cl /?,vs2019默认支持C++14

通过命令行使用MSVC toolset

3.1.1 path

The MSVC command-line tools use the PATH, TMP, INCLUDE, LIB, and LIBPATH environment variables, and also use other environment variables specific to your installed tools, platforms, and SDKs.

3.1.2 start up

The simplest way to specify a particular build architecture in an existing command window is to use the Common7\Tools\vcvarsall.bat file. Use vcvarsall.bat to set environment variables to configure the command line for native 32-bit or 64-bit compilation. Arguments let you specify cross-compilation to x86, x64, ARM, or ARM64 processors.

环境设置后,就可以通过devenv来编译项目了。 devenv [解决方案文件 | 项目文件 | 任意文件.扩展名] [开关] 命令行开关一般会先后运行:/Clean, /Build 官网说明详见[-]

3.1.3 cmake use msvc

cmake是有平台限制的,下载exe安装包安装cmake

"C:\Program Files\CMake\bin\cmake.EXE" --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -SD:/tmp/test -Bd:/tmp/test/build -G "Visual Studio 16 2019" -T host=x86 -A win32

cmake --no-warn-unused-cli:在构建项目时输出警告信息。 -SD:/tmp/test 是设置源代码的路径,也就是你的项目文件所在的地方。

-Bd:/tmp/test/build是设置构建的路径,也就是生成的构建文件(如Makefile或者Visual Studio的.sln文件)存放在哪里。

CMake -G还有其他选项,例如:

  • Unix Makefiles:使用Unix make作为构建工具。
  • Ninja:使用Ninja作为构建工具。
  • Visual Studio 16 2019:生成适用于Visual Studio 2019的解决方案。
  • Visual Studio 16 2019 Win64:生成适用于64位Windows的Visual Studio解决方案。
  • Visual Studio 16 2019 ARM:生成适用于ARM版的Visual Studio解决方案。

3.1.4 MSBuild use MSVC

Use MSBuild (msbuild.exe) and a project file (.vcxproj) to configure a build and invoke the toolset without loading the Visual Studio IDE. It's equivalent to running the Build project or Build Solution command in the Visual Studio IDE.

MSBuild 预留属性和已知属性

在IDE(Visutal Studio)中配置项:配置管理器中设置,并在属性页中选择。在是一个环境变量,它代表了当前项目的配置名称。在Visual Studio中,每个项目通常都有多个配置,例如DebugRelease。这些配置用于控制编译器的编译选项、链接器的链接选项等。

3.1.5 属性对照表

属性 Win32 gcc 说明
开启调试信息 /Zi -g
编译优化 /Od -O0
禁用警告 /W0 -w
静态编译 /MT -static /MT静态链接的 C 运行时库

3.1.6 动态库加载顺序

启用"安全DLL查找模式"时,查找顺序如下:[-] a. 应用程序所在目录; b. 系统目录。GetSystemDirectory返回的目录,通常是系统盘\Windows\System32; c. 16位系统目录。该项只是为了向前兼容的处理,可以不考虑; d. Windows目录。GetWindowsDirectory返回的目录,通常是系统盘\Windows; e. 当前目录。GetCurrentDirectory返回的目录; f. 环境变量PATH中所有目录。

如果"安全DLL查找模式"被禁用,查找顺序如下: a. 应用程序所在目录; b. 当前目录。GetCurrentDirectory返回的目录; c. 系统目录。GetSystemDirectory返回的目录,通常是系统盘\Windows\System32; d. 16位系统目录。该项只是为了向前兼容的处理,可以不考虑; e. Windows目录。GetWindowsDirectory返回的目录,通常是系统盘\Windows; f. 环境变量PATH中所有目录。

运行exe依赖动态库的话,可以通过: 1、使用全路径加载: 将动态库的路径添加到$PATH

3.2 MINGW64

使用msys2的MINGW64终端:pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-gcc make,cmake默认是ninja构建。要使用makefile需指定-G "Unix Makefiles"

若项目依赖大量 mingw‑gcc 库,只想用 clang 编译器,仍用 libstdc++。则pacman -S mingw-w64-x86_64-clang mingw-w64-x86_64-lld

# 1.1 CMakeList.txt形式
# ⚠️必须放在 project() 之前;修改后删除build目录重生成
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_C_COMPILER "gcc")

project(MyProject LANGUAGES C CXX)

# 2. 命令行形式
cmake -G "Unix Makefiles" -B build -S . -DCMAKE_CXX_COMPILER="/c/Program Files/Git/mingw64/bin/g++.exe" -DCMAKE_C_COMPILER="/c/Program Files/Git/mingw64/bin/gcc.exe"

注:使用msys的相对路径可能不识别。我把git bash和msys2合并了,如果不合并,需要修改路径。

3.3 CLANG64

使用msys2的CLANG64终端打开:

pacman -S mingw-w64-clang-x86_64-clang mingw-w64-clang-x86_64-lld

四、嵌入式

4.1 none-eabi

重要背景:ARMCC (AC5 armcc) 已经停止新增功能,仅维护 bug 修复;新项目优先学习 Arm Compiler 6(armclang,基于 LLVM)。 工具链组成(脱离 Keil MDK 图形界面,命令行使用)

  • AC5:armcc(C 编译器)、armcpp(C++)、armasm(汇编器)、armlink(链接器)、fromelf(elf 转 hex/bin)
  • AC6:armclang(C/C++ 合一)、armasm-clangarmlinkfromelf
  • GCC-ARM:arm-none-eabi-gcc + .ld 链接脚本;Arm-CC 用 .sct 分散加载脚本(Scatter-file),不使用 ld 脚本。

Arm Compiler for Embedded | Arm Learning Paths

Compiler Reference Guide: Arm Compiler for Embedded Reference Guide

五、cmake

cmake网上已有的说明太多了,这里不再赘述,能上衔接的直接上链接。 zhihu-全小鱼-CMake 如何入门? 如何组织你的项目 · Modern CMake CMake Reference Documentation — CMake 4.4.2 Documentation 命令快查:https://cmake.org/cmake/help/latest/command/<命令名称>.html 注:和大多数工具一样,会基础命令和操作后,绝大多数需求都能解决了。剩下的就是优不优雅的问题了。

CMake 采用树形工程结构,工程由顶层CMakeLists.txt作为根,通过add_subdirectory递归引入子工程,构建完整的构建树。系统划分两类使用者,职责与工作模式相互隔离:

  1. 普通用户(应用开发者):聚焦业务构建流程 普通用户主要关注业务代码的构建逻辑,标准工作链路:创建子工程、定位文件路径、配置头文件搜索路径、添加源文件、指定目标产物类型、配置目标链接依赖。 用户可以通过CMAKE_C_FLAGS/CMAKE_CXX_FLAGS这类编译标志变量,将配置项直接透传给底层编译工具;同时也可以通过命令行‑DXXX=VALUE定义 CMake 变量,在脚本中依靠if条件分支,实现应用层的构建流程控制。
  2. 编译链开发者(工具链编写者):提供交叉编译环境 工具链开发者编写.cmake工具链文件,普通用户通过‑DCMAKE_TOOLCHAIN_FILE参数加载该文件,完成交叉编译环境初始化。 工具链脚本内部会读取命令行传入的‑D全局变量(例如平台选型、API Level 等配置),以此完成编译环境的配置;工具链开发者需要对外暴露可配置项,并配套文档,指导上层普通用户如何传入参数使用。

CMAKE注意事项

  • add_library, 新手需要注意的是 add = add a target to the project:向 CMake 项目新增一个库类型的构建目标, 而非target_link_libraries。
  • 顺序敏感
  • 大小写敏感
  • 作用域多种

5.1 顺序敏感实验

抛开变量和流程控制的代码, 以add_xxx为基准,target_xxx命令放到后面,其他的一般是放到前面。

set(SRCS a.cpp b.cpp)
add_library(mylib SHARED ${SRCS})     # 先建 target
list(REMOVE_ITEM SRCS "b.cpp")        # 之后才移除
add_definitions(-DAFTER_ADD_LIBRARY)  # 之后才加宏
add_library(mylib_obj OBJECT ${SRCS})

结果:

mylib(SHARED) 实际编译: a.cpp.o b.cpp.o ← REMOVE_ITEM 无效 mylib_obj(OBJ) 实际编译: a.cpp.o ← 生效 mylib 的 CXX_DEFINES = -DAFTER_ADD_LIBRARY -Dmylib_EXPORTS ← 宏却倒灌进去了

5.2 多种作用域下的命令

目录级(下文生效)——向下:当前目录下文+后续子目录,不向上冒泡 (只在当前目录解析栈帧修改);✅ 强依赖 add_subdirectory 解析顺序:

  • include_directories
  • link_directories
  • add_compile_definitions
  • add_compile_options
  • link_libraries

项目级——仅当前任务解析栈帧内修改:

  • project()
  • cmake_minimum_required()
  • option()
  • set(CACHE)

目标级——依赖链传播(PUBLIC/INTERFACE);❌ 不依赖目录顺序,只看 target_link_libraries

  • target_*
  • set_target_properties
功能 目录级命令(Directory-scope) 目标级命令(Target-scope)
头文件搜索路径 include_directories() target_include_directories()
库文件搜索路径 link_directories() target_link_directories()
编译宏定义 add_compile_definitions() target_compile_definitions()
编译选项 add_compile_options() target_compile_options()
链接库依赖 link_libraries() target_link_libraries()
链接选项 add_link_options() target_link_options()

5.3 变量赋值命令

  • set(VAR VALUE):普通变量赋值,无CACHE、无PARENT_SCOPE
  • aux_source_directory(dir VAR):扫描目录,把源文件列表存入当前栈帧普通变量;仅赋值,无目录级副作用;相对路径基于CMAKE_CURRENT_LIST_DIR
  • list(APPEND VAR ...):列表追加,修改当前栈帧普通列表变量
  • list(PREPEND VAR ...):列表头部插入
  • list(REMOVE_ITEM VAR ...):列表移除元素
  • list(FILTER VAR ...):列表过滤
  • list(SORT VAR ...):列表排序
  • unset(VAR):清除当前栈帧普通变量

5.4 变量

5.4.1 环境变量

环境变量$ENV{VAR}

5.4.2 内置变量

第一组:目录解析栈帧(由 add_subdirectory() 驱动;include() 不会改变这一组

变量 含义
CMAKE_CURRENT_SOURCE_DIR 当前正在处理的CMakeLists.txt所在源码目录;add_subdirectory切换;include保持调用者的值
CMAKE_CURRENT_BINARY_DIR 当前CMakeLists.txt对应的构建输出目录;add_subdirectory切换;include保持调用者的值

⚠️坑:include(xxx.cmake) 进入脚本内部,这两个仍然是调用方的目录,不是xxx.cmake所在目录。

第二组:list‑file执行上下文(脚本文件上下文;include / add_subdirectory 都会切换)

变量 含义
CMAKE_CURRENT_LIST_DIR 当前正在执行脚本文件的目录(CMakeLists.txt / .cmake都生效);写cmake模块首选
CMAKE_CURRENT_LIST_FILE 当前正在执行脚本文件的完整绝对路径(带文件名)
CMAKE_CURRENT_LIST_LINE 当前执行到的脚本行号

关键:include() 会切换这一组;CMAKE_CURRENT_LIST_DIR 拿到的是被include的脚本本身目录,不受调用方影响。

第三组:function() 内部专用(CMake ≥3.17,仅在函数体内有效;宏macro无效)

变量 含义
CMAKE_CURRENT_FUNCTION 当前正在执行的函数名称,用于调试打印
CMAKE_CURRENT_FUNCTION_LIST_DIR 定义该函数的那个文件所在目录(不是调用处的目录);函数内部读取同目录资源用这个
CMAKE_CURRENT_FUNCTION_LIST_FILE 定义函数的脚本完整路径
CMAKE_CURRENT_FUNCTION_LIST_LINE 函数定义所在的行号

第4组:project内置变量(任务解析栈帧)

变量 说明
PROJECT_NAME 最近一次project()的项目名称
PROJECT_SOURCE_DIR 最近一次project()所在源码目录
PROJECT_BINARY_DIR 最近一次project()对应的构建目录
PROJECT_VERSION 完整版本号(VERSION参数)

注意:CMAKE_PROJECT_NAME不属于本组,归到第5组全局变量。

第5组:全局内置变量(不受任何栈帧影响,整个配置周期固定)

变量 说明
CMAKE_SOURCE_DIR 最顶层源码根目录,顶层CMakeLists.txt所在目录,永不改变
CMAKE_BINARY_DIR 最顶层构建根目录,执行cmake的build目录,永不改变
CMAKE_PROJECT_NAME 顶层项目名称;只有顶层project()可以修改;子目录project不会改动它
CMAKE_VERSION CMake版本号,如3.26.0
CMAKE_COMMAND cmake可执行文件完整路径
CMAKE_ROOT CMake安装根目录
CMAKE_GENERATOR 当前使用生成器(Unix Makefiles / Ninja / Visual Studio…)
CMAKE_MAKE_PROGRAM 实际构建工具路径(make/ninja/msbuild)
CMAKE_SIZEOF_VOID_P 指针字节大小(4=32位,8=64位)

补充:CACHE缓存变量(set(...CACHE) / option())也属于全局持久状态,不属于栈帧,存在CMakeCache.txt

5.4.3 普通变量和缓存变量

  • option(ENABLE_SUB "enable sub feature" ON):是 set 的语法糖
  • set(FOO ON CACHE BOOL "description"):作用域特殊
写法 存储位置 作用域说明 是否写入 CMakeCache.txt 能否被 -D 命令行覆盖
set(VAR val) 目录上下文栈(普通变量) 目录级普通变量;父→子拷贝副本,子目录可以读取;子目录修改仅修改自身副本,不会回写到父目录;子目录新增普通变量父目录不可见;拷贝发生在 add_subdirectory() 进入子目录时刻 ❌ 否 ❌ 不能
set(VAR val CACHE TYPE "desc") 全局缓存池 全项目可读;必须执行到该行才创建缓存条目;执行之前的代码无法访问;不修改目录上下文栈;-D 命令行可以修改缓存条目 ✅ 是 ✅ 可以
set(VAR val PARENT_SCOPE) 父目录的普通变量(内存) 仅修改上一层父作用域的普通变量;当前子目录内该变量不会被改动;仅对父目录 add_subdirectory() 返回之后的下文生效;对子目录内部、孙子目录无直接影响;不会递归向上冒泡多层 ❌ 否 ❌ 不能

5.5 add_library类型

add_library(foo [STATIC|SHARED|MODULE|OBJECT] a.cpp)

SHARED vs MODULE 三平台对比:

✅ Linux:二进制格式完全一样(都是 ELF .so),区别仅为 CMake 构建语义(即语法限制被target_link_libraries的调用) ⚠️ macOS:二进制文件类型不一样;SHARED 是MH_DYLIB,MODULE 是MH_BUNDLE(bundle 插件) ⚠️ Windows:二进制都是 PE‑DLL,但产物集合不一样;SHARED 输出 DLL + 导入库,MODULE 只输出 DLL,无导入库

OBJECT的使用:

# 典型应用场景:单元测试复用源码,进行多个测试执行文件的生成,单元测试开启覆盖率、开盒宏等。
add_library(foo OBJECT a.cpp b.cpp c.cpp)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE $<TARGET_OBJECTS:foo>)

5.6 链接形式

链接方式

  • 隐式动态链接
  • 编译链接阶段:链接器把依赖库写入 ELF 的 DT_NEEDED 段。
  • 程序启动时:内核加载可执行文件,动态链接器自动遍历全部 DT_NEEDED 列表,递归加载所有依赖的.so。
  • 显式动态链接
  • 编译链接阶段:不写 DT_NEEDED;链接时只链接 libdl.so(dlopen/dlsym/dlclose 符号),不会把目标库加入 DT_NEEDED。
  • 运行时,代码执行到 dlopen 调用才主动请求动态链接器加载库。当它加载一个.so文件时,该 so 内部的 DT_NEEDED 依然会被动态链接器递归处理
  • 静态链接
  • 静态库 .a 只是一堆 .o 的打包,没有 DT_NEEDED、没有内部依赖记录。 当链接器处理静态库时:只提取当前未解析符号所需要的.o;不会自动去寻找该静态库所依赖的其它库。 依赖关系必须由链接命令行顺序显式给出。同 CMake 工程内部 target_link_libraries(A PRIVATE B)target_link_libraries(B PRIVATE C) CMake 会把依赖链展开,下游链接命令自动带上 -lA -lB -lC。 这是 CMake 的上层特性,不是静态链接器的递归能力。
  • 特殊情况:构建静态库.a时输入依赖动态库.so。例:A (SHARED)→B (STATIC)→C (SHARED)。A 动态链接 B,B 引用C的符号, 最终 B会没入A(严谨说法:提取 B.a 中需要的.o 对象拷贝进 A.so),同时 A DT_NEEDED C。
  • 特殊情况下的特殊情况: 通过底层编译工具链 ld -r 可以 C 作为 B 的静态链接输入,知道就行,并理解「动态库不一定用于动态链接,也可能是静态链接输入」
  • 静态库不一定用于静态链接,也可能被继续ar归档(追加.a或.o)。

5.7 依赖链

所有权属性相关详见接口可见性.md

Q: cmake 中,target_* 命令我都以 private 属性来出来, B 依赖 A 及 A 的头文件, C 我显式依赖 A 和 B 及其 AB 头文件 也是可以的吧

A: 语法上可以跑通,但属于手动堆砌依赖,没有利用 CMake 传递依赖机制。依赖链越长,维护爆炸。

对于依赖链:Comsumer → Provider → Third, 对于静态链接和隐式静态链接两种形式的处理(这里先不讨论cmake): 1️⃣ 当 Provider = STATIC(.a)

无论Consumer是静态还是动态,编译阶段都必须暴露Provider内部的Third依赖

  • Provider.a 只是.o归档,不会把Third“吃进去”。
  • 不管Consumer是.a还是.so,链接Consumer的时候,Third必须参与链接。
  • 想对外隐藏Third,只能做合并归档(把Provider.a + Third.a打成一个新.a)。

2️⃣ 当 Provider = SHARED(.so)

构建Provider.so的时候就完成链接,Consumer构建阶段不需要Third

  • 如果Third是.a:代码被拷贝进Provider.so;运行时不需要单独Third.a。
  • 如果Third是.so:Provider.so记录DT_NEEDED;运行时必须存在Third.so,编译期不用。

3️⃣ 特殊坑:Consumer = STATIC(.a) 依赖 SHARED(.so)

构建Consumer.a不会报错,但是静态库只是存符号引用,没有完成链接。 当你最后把Consumer.a链接到可执行程序时,必须提供Provider.so,否则报未定义。 👉 静态库不能“消化”动态库;动态库的符号引用只是被保存在静态库的.o里面。