cpp 实践
开发社区
搜集的 C++ 问答社区
相关资源
C++ 比较庞杂,开源社区资源,整理如下:
资源 | 组织 | 类型 |
---|---|---|
ISO CPP | isocpp | 规范 |
C++ Core Guidelines | isocpp | 规范 |
C++ Note | TOMO-CAT | 基本语法 |
C++ Tips | Abseil 及 Google | 规范 |
Google C++ Style Guide | 规范 | |
C++ Stories | Bartłomiej Filipek | 博客 |
AwesomePerfCpp | fenbf | 博客 |
awesome cpp cn | jobbole | 框架 |
awesome cpp | fffaraz | 框架 |
C++ Insights | cppinsights | 工具 |
C++ Compiler Explorer | godbolt | 工具 |
Computer simulator | cpulator | 工具 |
sandbox | melpon | 工具 |
Reshaper 激活码 | Reshaper | 插件 / 工具 |
Visual Assist | Visual Assist | 插件 / 工具 |
Performance Effect | johnnysswlab | blog |
ISO/IEC C++ | ISO/IEC | 标准规范 |
ISO/IEC C | ISO/IEC | 标准规范 |
ISO/IEC POSIX | ISO/IEC | 标准规范 |
cpp-best-practices | cpp-best-practices | Jason Turner |
cppnext | Alex Dathskovsky blog | 博客 |
moderncpp | Alan De Freitas | 博客 |
cpp-resources | sandordargo | github 仓库 |
c-cpp-notes | caiorss | 博客 |
vscode
Devcontainer
推荐使用 devcontainer 配置 cpp 开发环境,只需要在 devcontainer.json 中添加
1 | "name": "C++", |
其中, Dockerfile.dev 中基础镜像选择 mcr.microsoft.com/devcontainers/cpp 进行扩展:
1 | ARG VARIANT=ubuntu-22.04 |
Extsension
用于 Cpp 开发的常用 VSCode 插件:
-
C/C++:官方提供的 C++ 插件,提供了代码补全、语法高亮、调试等功能。
-
C++ Intellisense:提供了更强大的代码补全功能,支持头文件、宏定义等。
-
C++ TestMate:提供了一个测试资源管理器,可以方便地运行和调试 C++ 测试。
-
C++ Insights:提供了一个侧边栏,可以方便地查看 C++ 代码的编译器输出。
-
CMake Tools:提供了一个集成的 CMake 工具,可以方便地生成和构建 C++ 项目。
1 | "extensions": [ |
Debug
links:
CI
GitHub Action
Resource Template
cpp 项目模板
cmake 模块模板
CMake
CMake 是 C++ 非常成熟的开发工具链。
links:
CMake Beginner
-
官方教程: 包括版本、库、目标、安装、测试、打包
CMake Variables
CMAKE_MODULE_PATH
CMAKE_MODULE_PATH
是以分号分隔的列表,供 include()
或 find_package()
使用。初始为空,由用户设定。
links:
CMAKE_PREFIX_PATH
CMAKE_PREFIX_PATH
是以分号分隔的列表,供 find_package()
, find_program()
, find_library()
, find_file()
和 find_path()
使用,初始为空,由用户设定.
links:
Platform Specific
-
osx: MACOS, APPLE
-
windows: WIN32
-
linux: LINUX, UNIX AND NOT APPLE
links:
CMake Configuration
CMake 配置区分可分为单配置类型生成器和多配置类型生成器
-
单配置类型:
Makefile Generators
和Ninja
-
多配置类型:
Visual Studio
,Xcode
, orNinja Multi-Config
配置内置变量说明
-
CMAKE_BUILD_TYPE,指定配置生成器的构建类型。
这静态指定将在此构建树中构建的构建类型(配置)。可能的值为空、 Debug
、 Release
、 RelWithDebInfo
、 MinSizeRel
,...
这个变量只对单配置生成器(例如 Makefile Generators
和 Ninja
)有意义,即那些在 CMake 运行以生成构建树时选择单个配置的生成器与在生成的构建环境中提供构建配置选择的多配置生成器相反。有许多 per-config
属性和变量(通常遵循干净的 SOME_VAR_<CONFIG>
命令约定),例如 CMAKE_C_FLAGS_<CONFIG>
,指定为大写: CMAKE_C_FLAGS_[DEBUG|Release|RELWITHDEBINFO|MINSIZEREL|...]
。例如,在配置为构建类型 Debug
的构建树中,CMake 将看到将 CMAKE_C_FLAGS_DEBUG
设置添加到 CMAKE_C_FLAGS
设置中。另见 CMAKE_CONFIGURATION_TYPES
。
请注意,配置名称不区分大小写。此变量的值将与调用 CMake 时指定的值相同。例如,如果 -DCMAKE_BUILD_TYPE=Release
被指定,则该值 CMAKE_BUILD_TYPE
将 Release
。该变量配置类型总是存在。
-
CMAKE_CONFIGURATION_TYPES,指定多配置生成的构建类型。
该类型在单配置类型是总是为空。否则,按照生成器默认的类型生成 Debug
、 Release
、 RelWithDebInfo
、 MinSizeRel
等类型。
-
GENERATOR_IS_MULTI_CONFIG,获取当前是否是多配置类型。使用示例:
1 | # multi config setting |
CMakePresets
用于 CMake 通用的 configure、build、test 等配置步骤。
CMakePresets 模板
确保环境
-
cmake >=3.20
-
Visual Studio 环境
C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\
C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64
C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\bin
-
Linux 环境
- g++
- clang++
模板一
1 | { |
模板二
1 | { |
模板三
1 | { |
CMake Regex
规则约束
在 CMake 中,可以在几个上下文中使用正则表达式,例如 string(REGEX MATCH)
命令和 ctest
命令的 -R
选项。与 Ruby 或 Perl 等相比,该正则表达式具有低功能,例如,字母数字和下划线 _
中所述修改相应参数的值。此外,也不能使用表示重复的 {n}
等。可以使用的元字符如下:
-
开头
^
-
末尾
$
-
任意字符
.
-
匹配中列出的字符
[]
及其范围-
-
非匹配列出的字符
[^]
-
重复 0 次以上
*
-
一次或多次重复
+
-
0 次,或者 1 次
?
-
指定的模式之一
|
-
分组
()
CMAKE_MATCH_<n>
为组 0 到 9 捕获由最后一个正则表达式匹配的组 <n>
。组 0 是整个匹配项。组 1 到 9 是 ()
语法捕获的子表达式。
当使用正则表达式匹配时,CMake 将匹配内容填充到 CMAKE_MATCH_<n>
变量中。匹配时, CMAKE_MATCH_COUNT
变量保存匹配表达式的数量。
正则常用场景
可以在以下位置使用正则表达式
links:
命令
显示读取 pyproject.toml 版本号
1 | # unless specified version, read the file pyproject.toml version information |
变量
模块
命令行
-
ctest -L/-R/-E/-LE
CMake Project
cmake-init
cmake-init 专用于初始化基于 CMake 构建的项目。只需要简单的运行命令 cmake-init <project-name>
, 即可根据提示初始化不同的构建方案.
1 | pip install cmake-init |
CMake Vscode Integration
Vscode CMake Tools
links:
CMake Tips
这里总结了一下 CMake 实际项目中应用的技巧。
集成 C/C++ threads
CMAKE 中提供了单独的 threads 模块集成方式,这里是示例 threads 利用了 C API pthread 实现 RAII 锁。
[!CAUTION]
using std::thread in a library loaded with dlopen leads to a sigsev
构建结果输出到统一路径
配置将在构建生成的 Runtime,Libraries,archives 都生成到对应的 binary 目录,而不是对应源码位置的子目录下。
[!TIP]
将构建生产构件都输出当对应的目录,有利于测试如 Gtest,否则对应得测试会生成到相应得同名目录下,造成.exe 或 elf 找不到依赖库等 crash 。
1 | # GNU install standard dirs |
配置 _DEBUG 宏
通常 Windows 下调试时会配置 _DEBUG 宏,但在 Linux 下需要手动增加配置。
1 | # Debug configuration |
追加 CMake 脚本路径
通常会将工程的 cmake 脚本放到一个 cmake 的目录,然后统一在 CMAKE_MODULE_PATH 中导入,方便后续调用相应模块。
1 | # CMake module tools import custom cmake modules |
CMake 中注释
#[[
注释可以跨越多行,并且不会被编译器解释为代码。
[!CAUTION]
多行注释不会被 cmake-format 格式化。
1 | #[[ This is |
1 | #[=======================================================================[.rst: |
将选项通过目标链接
其它目标依赖通过将编译或链接选项生成一个 INTERFACE 目标,进而传递该目标包含的各种配置选项。
1 | # Add a common compile setting interface |
搜索文件
方式一: file(GLOB_RECURSE ...)
搜索文件的方式可以方便地获取指定目录下的所有文件,包括子目录中的文件。然而,这种方式并不推荐在大型项目中使用,因为它会导致以下问题:
-
性能问题:由于搜索整个目录树,因此会导致性能问题,特别是在大型项目中。
-
不稳定性:由于该命令依赖于文件系统,因此可能会受到文件系统的限制,例如文件名长度限制。
-
可读性问题:使用该命令可能会使 CMakeLists.txt 文件变得难以阅读和维护。
因此,对于大型项目,建议手动列出所有源文件,以便更好地控制构建过程。如果仍然想使用该命令,请确保的项目规模较小,并且已经测试了该命令的性能和稳定性。
方式二:aux_source_directory(<dir> <variable>)
是一个旧的 CMake 命令,用于将指定目录中的所有源文件添加到一个变量中。然而,这个命令已经被弃用了,因为它不能正确地处理源文件的依赖关系。相反,应该使用 file(GLOB_RECURSE ...)
命令来获取源文件列表,然后将它们添加到目标中。
导出符号
控制 cpp 符号导出在动态或插件链接库是非常必要的步骤,在 windows 中符号默认都不导出,linux 中默认都导出。
通常为了跨平台实现编译链接,需要使用针对性处理。
1 | # only export all symbols on windows but no static symbols |
links:
禁用 CMakeLists.txt 路径构建
对构建目录存在 CMakeLists.txt 脚本目录的情况报错,其目的是强制将构建缓存和构建脚本分离。
1 | # Error if building out of a build directory |
符号可见性
CMake 中 Visibility Control 标准的库符号导出,需要严格控制。windows 下默认不导出,linux 默认全导出。这里通过 CMake 控制符号导出能减小这部分工作量。
通常尽可能预设将所有符号隐藏,然后使用 generate_export_header 生成符号导出修饰前缀。
1 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) |
库版本控制
对于动态库,需要做 Library Version 时,CMake 也提供了工具。
1 | add_library(Example ...) |
[!WARNING]
如果缺失 SOVERSION,如果 Example 只做了一个简单的内部修改,然后增加了 patch 为 2.4.8,对于依赖于 Example 的 2.4.7 版本的项目来说,简单更新 Example 库就不再合适,而是需要重新编译链接指向 2.4.8,但这实际上是不必要的。所以都建议加上 SOVERSION。
包版本控制,需要导出版本 cmake 脚本。
1 | # require AConfig.cmake AConfigVersion.cmake |
使用工具自动生成
1 | include(CMakePackageConfigHelpers) |
links:
打包库
配置安装规则 (Installing Rules)。安装规则包括安装运行库、头文件、文档等等,以下是不断对安装规则不断改进的过程。
links:
1 | # Version 1: 只是安装对应工件 |
构建库及依赖的 RPATH,通常需要小心设置。
-
Build and Test, CMake 会将 RPATH 给到库及相关执行程序。
-
Package and Install, CMake 将 RPATH 替换为空。
-
Run and Failed, 缺少 RPATH,通常由 DT_RPATH 及 DT_RUNPATH 确定,如果两者都存在,则前者被丢弃。LD_LIBRARY_PATH 在 DT_RPATH 之后评估,DT_RUNPATH 在 LD_LIBRARY_PATH 之后评估。优先级顺序为 DT_RPATH -> LD_LIBRARY_PATH -> DT_RUNPATH。
[!CAUTION]
配置 SO 动态库搜索规则,当使用-Wl,-rpath,path/to/so/location,--disable-new-dtags
时为 DT_RPATH 配置,当使用-Wl,-rpath,path/to/so/location,--enable-new-dtags
时为 DT_RUNPATH 配置(gcc-7 已设置默认打开配置)。DT_RPATH 搜索到后,就不会再考虑 LD_LIBRARY_PATH,所以也推荐配置 DT_RUNPATH。
综上,动态库的搜索路径优先级是:
-
DT_RPATH
-
LD_LIBRARY_PATH
-
DT_RUNPATH
从历史的角度来说,一开始是只有 DT_RPATH 的,问题是 DT_RPATH 在编译时一旦设了就不能靠 LD_LIBRARY_PATH 来自定义加载的路径了,每次要测不同的库的时候(放的位置可能不同)就得重新 build 可执行文件,这样很烦。因此才引入了 DT_RUNPATH ,编译后在运行时还可以用 LD_LIBRARY_PATH 来覆盖掉,这样就不用每次重新编译了,只需要 NEEDED 里的 Value 一致即可。
1 | # Simple rpath configuration |
[!NOTE]
当使用动态链接器(ld.so)加载共享库时,$ORIGIN 可以用于指定相对于可执行文件的路径的搜索目录。这使得共享库可以相对于可执行文件的位置进行定位,而不是使用绝对路径或者在系统范围内搜索共享库。@loaderpath(macOS) 和 $ORIGIN(Unix) 都是用于在可执行文件和共享库中指定动态链接器搜索依赖项的路径的特殊字符串,指向包含当前可执行文件或共享库的目录。它们的主要区别在于它们的作用范围和解析方式。
@loaderpath 不能在环境变量中使用。
$ORIGIN 的解析是在运行时进行的,而不是在链接时进行的。这意味着 $ORIGIN 可以用于在不同的环境中运行相同的可执行文件或共享库,而不需要修改它们的路径。例如,如果可执行文件或共享库位于 /path/to/myapp/bin/myapp 中,则 $ORIGIN 将被解析为 /path/to/myapp/bin/,即可执行文件所在的相对路径,无论在哪个目录中运行它。
另见 stackoverflow 及 ld.
以下是一个示例,其中使用了 $ORIGIN 变量来加载共享库:
假设有一个可执行文件 /home/user/myapp,它需要加载一个共享库 libfoo.so,该共享库位于可执行文件所在目录的 lib 子目录中。可以使用以下命令来加载共享库:
1 | cd /home/user |
在这个例子中,将共享库复制到可执行文件所在目录的 lib 子目录中,并使用 $ORIGIN/lib 来指定共享库的搜索目录。这使得动态链接器可以相对于可执行文件的位置找到共享库。
根据之前的讨论,为 bin executable 配置 rpath 为当前路径及 lib 所在路径如下:
1 | #[=======================================================================[.rst: |
完整的安装规则配置 demo,参考仓库如下:
FetchContent
FetchContent 参考应用示例:
配置库 libConfig.cmake
为了为静态库 lib 编写 CMake 配置文件,需要创建一个名为 libConfig.cmake 的文件。在该文件中,可以设置库的导入路径、链接库和其他相关设置。
以下是一个示例 libConfig.cmake 文件的模板,集成使用 include 该模块即可
1 | # 设置lib的导入路径 |
需要根据您的库的实际情况修改上述模板中的路径和其他相关设置。确保将 <path_to_lib_include_directory>
替换为 lib 的包含目录的路径,将 <path_to_lib_library_file>
替换为 lib 的库文件的路径,并根据需要添加或修改其他设置。
CMake Debugger
从 3.27 开始,CMake 支持调试器,可通过 vscode 调试 CMake 脚本。
links:
include_gurad
提供一个 include_guard 避免重复引入
1 | include_gurad(GLOBAL) |
集成常用的静态检测工具
在 cmake 中集成静态检查工具.
-
iwyu: CMAKE_CXX_INCLUDE_WHAT_YOU_USE
-
lwyu: CMAKE_LINK_WHAT_YOU_USE
-
cpplint: CMAKE_CXX_CPPCHECK
-
cppcheck: CMAKE_CXX_CPPCHECK
-
clang tidy: CMAKE_CXX_CLANG_TIDY
links:
避免 ;
展开干扰
通常 CMake 中 list 以字符串以 ;
分隔的方式展开。这样会导致 CMake 引用变量时候出现问题,需要做特殊处理。
1 | # 获取以.specific_suffix为后缀的文件 |
CMake 中处理 rpath
links:
CMake install imported lib
links:
Platform check
cmake 提供对平台功能的模块。
1 | include(CheckIncludeFile) |
links:
CMake ctest
ctest 可执行文件是 CMake 测试驱动程序。CMake 生成的构建树是为使用 enable_testing () 和 add_test () 命令的项目创建的,它具有测试支持。该程序将运行测试并报告结果。
links:
cdash
cdash 用于发布 CMake 运行 ctest 的测试结果。
links:
vcpkg
vcpkg 是 C++ 比较成熟的包管理器,覆盖大部分平台。
links:
Integration
vcpkg 集成模式包含 Classic mode
和 Manifest mode
。
links:
dependencies
overrides version
使用 overrides 固化版本。
links:
triplet
custom triplet
links:
port
创建 port 提交示例
links:
patch port
当需要改动源码来通过编译时,可以使用 git diff 生成 patch 文件和 port 一起提交到 registry
links:
empty policy
空包策略是 VCPKG 中的一种特性,它允许用户安装一个空的库包,以便在项目中占位,但实际上不包含任何库文件 (包含函数、类、变量等代码的二进制文件)。这在某些情况下可能很有用,例如当用户需要占位符来满足其他依赖关系时。
代码步骤:
-
设置一个名为 VCPKG_POLICY_EMPTY_PACKAGE 的变量。
-
将该变量的值设置为 enabled,表示启用空包策略。
vcpkg-cmake-wrapper.cmake
自定义在 vcpkg 的 find_package 执行 include 的模块.
baseline
生成或更新 baseline
1 | vcpkg x-update-baseline --add-initial-baseline |
update port
在私有 vcpkg 存储库上,可以使用 vcpkg 可执行文件生成所有 rev 解析哈希并提交 ports.
1 | vcpkg format-manifest ports/mypackage/vcpkg.json |
vcpkg 加速
使用代理加速
-
clash 或其它代理方式加速访问 github
-
国内私域加速(企业加速等)
搭建私域缓存
参考 vcpkg 构建缓存。
国内公开缓存
常用命令
export
将包导出作为缓存开发环境,但是不推荐
1 | # 使用原始格式导出,可以搭建 vcpkg 离线构建环境,只需要在 CMakeLists 中设置 CMAKE_TOOLCHAIN_FILE 为导出的离线 vcpkg.cmake 即可。 |
links:
install
1 | # libredwg 包名,tools 功能feature,x64 架构,windows 平台 |
Github Actions
links:
vcpkg-registry
create registry
links:
-
https://learn.microsoft.com/zh-cn/vcpkg/maintainers/registries
-
https://learn.microsoft.com/en-us/vcpkg/maintainers/registries
-
https://devblogs.microsoft.com/cppblog/registries-bring-your-own-libraries-to-vcpkg/
using registry
links:
vcpkg configuration
使用 vcpkg 引导开发环境,通过配置 vcpkg-configuration.json
即可配置项目需要的各种工具.
links:
canon
links:
结合 cmake
conan 提供在 cmake 中运行 conan install 的 cmake 模块,该模块已集成在 cmake-registry 中,可通过 vcpkg 配置.
links:
xmake
xmake 由国人开发的比较全面的构建系统。中文版,是一个基于 Lua 的轻量级现代化 c/c++ 的项目构建工具,主要特点是:语法简单易上手,提供更加可读的项目维护,实现跨平台行为一致的构建体验。
ref 参考
clang-format
安装 pip/choco/mamba/apt/yum install clang-format
Format Style Options 格式配置说明
配置样例一
1 | # .clang-format |
WSL
WSL 是 Windows 上的 Linux 子系统。
安装
-
在 Store 中安装 Windows subsystem for Linux。
-
配置非 root 用户登录密码提示。
1 | echo $USER ALL=\(root\) NOPASSWD:ALL | tee /etc/sudoers.d/$USER \ |
-
打开 systemd
1 | echo \[boot\]\nsystemd=true >> /etc/wsl.conf |
重启 wsl,wsl --shutdown <your linux distro>
。
1 | # 验证 systemd |
文件共享
-
通过路径
/mnt/c/path/to/windows
-
通过命令打开浏览器
explorer .
Windows
MSVC
Visual Studio
编译
-
/PROFILE: 打开 VS 测试覆盖率
插件
链接
在 windows 上完整的动态链接参考官方文档,DLL 搜索路径参考 动态库链接顺序。
动态链接
links:
windows 下动态链接信息保存在 .idata 节中 , .idata 是指导入数据表(Import Data Table)。idata 是 PE 文件格式中的一个部分,用于描述程序在运行时所需的外部函数和符号。 其中的 Directory Table 包含了每个 dll 的链接信息,包含有以下信息:
-
ImportLookupTable(导入查找表) offset:
- Ordinal/Name flags
- Ordinal (序号)
- Hint/Name offset: 函数名称相对于 Hint/Name Table 的偏移量
- Hint idx to export table
- Function name
-
DLL timestamp(filled at bind time)
-
Forwarder Chain
-
DLL Name(offset in image)
-
ImportAddressTable offset
- Imported function address
在当前程序引用库时,对应入函数调用处使用 jmp 指令时,会通过 .idata 查询链接进当前程序被编译进的引用库 dll1.lib 的对应函数跳转地址,并转到库 dll1.dll 中的 .edata 找到最终的调用实现。如下图:
符号可见性
默认符号不到处,除非使用以下声明
-
__declspec (export): 添加符号到 .edata
-
__declspec (import): 基本不做太多工作,主要用于优化加速符号调用跳转
链接库搜索路径
Windows 会按照以下顺序搜索目录:
-
程序所在的目录
-
当前目录
-
Windows 系统目录(例如,C:\Windows\System32)
-
Windows 目录(例如,C:\Windows)
-
PATH 环境变量中列出的目录
[!TIP]
可以使用setx path "%path%;C:\my\directory"
,将其它的链接路径设置到环境变量中。请注意,%path%
是一个特殊的变量,它包含了当前path
环境变量的值,环境变量是永久的。
查看依赖 dll
1 | & "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC/14.34.31933/bin/Hostx64/x64/dumpbin.exe" /dependents xxx.dll |
MSVC C++ 代码分析
Visual Studio 提供了多种用于分析和提升 C++ 代码质量的工具。例如 Visual Studio (MSVC-170) C++ 代码分析
MFC
简单的 MFC 示例
示例使用 n 次拟合曲线,并实现功能点选 3 个点,自动画出拟合曲线。实现使用 CMake + vcpkg 工具链,对于 MFC 编译,需要增加配置编译选项,否则,会报错:
-
AFXDLL: 配置 AFXDLL 宏
-
CMAKE_MFC_FLAG: 默认改为动态连接 mfc 库
-
WIN32: 设置正确的 /SUBSYSTEM 表示一个窗口程序,而不是一个终端程序。
1 | cmake_minimum_required(VERSION 3.18 FATAL_ERROR) |
在 vcpkg 中配置使用依赖 Eigen3
1 | { |
1 |
|
LLVM on Windows
links:
Unix
编译和链接
links:
通常比较成熟的 IDE 会将这个过程封装,对用户无感。
-
编译
简单来说,将用户代码,编译成机器可执行的指令,一般指对单个文件进行编译。
1 | # 获取 ellf 的头部信息 |
如果代码中有调用其它的库或对象,在代码编译成单个对象文件时,调用符号时会将对应位置的反汇编先设置为 0,然后在链接这一步组装(生成调用地址)。
在代码块中,可以找到需要为调用函数生成地址的重定位表(.reloc)。
1 | # 获取重定位表,在 .text 可以找到需要生成调用地址的函数 |
-
链接
将编译之后的所有目标文件,库(动态库、静态库)组合拼装成一个独立的可执行文件。
这里,链接器,会根据目标文件、静态库中的重定位表找到需要修正地址的函数及全局变量。
如果,在链接时没有提供需要的库,链接时会警告 undefined reference
。
以上编译及链接过程通常使用 IDE 或脚本自动完成。比如 makefile 管理依赖的脚本:
1 | # 定义一个目标 main 依赖树 |
可执行文件或对象文件的节表和内存映射
对象文件或可执行文件的节表和内存布局是两个不同的概念,它们描述了程序在不同层次上的组织和存储方式。
节表(Section Table)是对象文件或可执行文件中的一种数据结构,用于记录文件中各个节的信息。它包含了每个节的名称、大小、偏移量等属性。节表是静态的,它在编译或链接过程中生成,并嵌入到最终的二进制文件中。节表描述了程序在文件中的组织形式,但并不直接映射到内存中的布局。
内存布局(Memory Layout)是程序在运行时分配和使用内存的方式。当程序被加载到内存中运行时,操作系统会为程序分配一块内存空间,并将程序的各个部分(例如代码、数据、堆栈)放置在不同的内存区域中。内存布局是动态的,它由操作系统和运行时环境决定,并且可以因为程序的运行状态而发生变化。
节表描述了程序在文件中的组织形式,而内存布局描述了程序在运行时的内存分配和布局情况。节表用于链接器和调试器在加载文件时定位和解析各个节的数据,而内存布局则决定了程序在运行时如何访问和使用内存中的数据和代码。
节表
对象文件或可执行文件的节表是一个数据结构,用于记录文件中各个节的信息。具体的节表结构和命名约定可能因不同的对象文件格式而有所不同,但以下是一些常见的节表条目及其相对位置的示例:
-
代码节(Text Section):存储程序的可执行代码。它通常位于节表的开始位置。
-
数据节(Data Section):存储程序的已初始化的全局变量和静态变量。它通常紧随代码节之后。
-
BSS 节(BSS Section):存储程序的未初始化的全局变量和静态变量。它通常位于数据节之后。
-
符号表节(Symbol Table Section):存储符号表的相关信息,包括函数、变量、常量等的名称、类型和地址等。它通常位于 BSS 节之后。
-
字符串表节(String Table Section):存储字符串常量的表,用于存储符号表中的名称和其他字符串。它通常紧随符号表节之后。
-
重定位表节(Relocation Table Section):存储需要进行重定位的代码和数据的相关信息。它通常位于字符串表节之后。
-
动态链接节(Dynamic Linking Section):存储动态链接相关的信息,如动态链接库的名称和依赖关系等。它通常位于重定位表节之后。
-
调试信息节(Debug Information Section):存储调试器使用的调试信息,如源代码映射、变量名称和行号等。它通常位于动态链接节之后。
-
其他自定义节:根据具体的对象文件格式和编译器的实现,还可能存在其他自定义的节,用于存储特定的数据或代码。
内存映射
内存映射 (Memory Map) 通常用于描述编译及链接将程序的实体放到不同的段地址上:
-
代码 (text or code), 用于存放代码
-
字面量 (literal), 用于存放已初始化只读数据
-
数据 (data), 用于存放已初始化读写数据
-
符号 (bss,Block Started by Symbol), 用于存放未初始化读写数据
-
栈 (stack), 用于存储函数的
局部变量、函数参数、函数调用的上下文信息和返回地址
等。栈以后进先出(LIFO)
的方式分配和释放内存。每当调用函数时,栈都会分配一块新的内存空间,当函数返回时,栈会释放该内存空间。栈通常位于高地址的部分,靠近内存的顶部。栈的地址增长方向可以是向下(从高地址向低地址增长)或向上(从低地址向高地址增长),具体取决于操作系统和编译器的实现。大多数常见的操作系统(如 Windows、Linux 等)使用向下增长的栈,也就是栈底位于高地址,栈顶位于低地址。 -
堆 (heap), 用于存储程序
运行时动态创建的对象、变量或数据结构
。堆的大小在程序运行期间可以动态增长或缩小,需要手动管理内存的分配和释放。地址生长方向和栈相反,并靠近内存的底部. -
命令行参数及环境变量,用于存储程序运行时传递给程序的命令行参数和环境变量的值的一部分。通常以字符串数组的形式存储在内存中,数组中的每个元素对应一个命令行参数。这些参数在内存中是连续存储的,通常在堆栈之后的位置。命令行参数和环境变量的具体存储方式和访问方式取决于操作系统和编程语言的实现。在 C 语言中,可以使用 argc 和 argv 参数来获取命令行参数,使用 environ 变量来获取环境变量。
-
共享库 (shared library), 用于存储程序所需的外部共享库的代码和数据。动态链接库通常在程序加载时被动态加载到内存中,以供程序在运行时使用。
静态链接
将程序所需库在编译时根据重定位表链接到 elf 中。
这样不需要依赖库,提高运行性能,减少运行时库加载时间。
-static
选项告诉编译器使用静态链接,-o main
指定输出文件名为 main
,main.c 是源代码文件名,-lm
选项告诉编译器链接 math
库。
1 | gcc -static -o main main.c -lm |
Position Independent Code
非 PIC, 调用符号跳转地址保存的是相对代码位置的绝对地址.
在 PIC 中,** 隐藏符号(Hidden Symbol)** 跳转是通过相对偏移量来实现的,通过将代码段的基址与相对偏移量相加来计算函数或变量的实际地址。这使得代码可以在内存中的不同位置加载,而不需要重新定位符号。
可见符号,使用过 got 进行跳转的: got 表存储了全局符号的实际地址。全局符号是在程序或共享库中定义的全局变量和函数。由于动态链接的特性,这些全局符号的地址在程序加载时是未知的。
当程序或共享库被加载到内存中时,动态链接器(ld.so)会负责填充.got 表中的地址。它将解析和定位全局符号的实际地址,并将这些地址写入.got 表中。
在程序执行期间,当需要访问全局符号时,程序会通过.got 表来获取实际地址。它会使用.got 表中的偏移量来计算全局符号的地址,然后进行访问。
通常建议所有动态库都使用 PIC, 但 - fPIC 并没有作为隐式传递和 - shared 一起传递使用.
Resolution Time
默认链接时,链接使用:
-
--allow-shlib-undefined
: 允许未定义的动态符号,通常可以使用参数强制检查,如--no-allow-shlib-undefined
(ld 上时递归生效,在 gold/ldd 中不递归) 或者-z defs
/--no-undefined
Lazy Bind
在 Linux 中,动态链接的 Lazy bind(延迟绑定)是一种优化技术,用于在程序运行时推迟对外部函数的解析和绑定。
当程序需要调用一个外部函数时,Lazy bind 的工作如下:
-
首先,程序会通过 PLT(Procedure Linkage Table,过程链接表)来查找外部函数的地址。
-
如果外部函数的地址在 PLT 的 GOT(Global Offset Table,全局偏移表)中已经被填充,那么程序会直接跳转到该地址执行函数。
-
如果外部函数的地址在 GOT 中还未被填充,那么程序会跳转到 PLT 中的一个特殊条目,该条目负责进行解析和绑定。
-
解析和绑定的过程由动态链接器(ld.so)完成。它会在首次调用时,根据外部函数的名称在共享库中进行符号查找,并将实际地址填充到 GOT 中。
-
一旦地址被填充到 GOT 中,程序会跳转到该地址执行函数。此后,对该外部函数的调用将直接使用已绑定的地址,而不需要再执行解析和绑定过程。
Lazy bind 的优势在于,它推迟了对外部函数的解析和绑定,只在真正需要调用时才进行。这可以减少启动时间和内存开销,特别是对于大型程序和具有大量外部函数调用的程序来说。然而,这也可能导致在首次调用时的一定延迟,因为需要进行解析和绑定操作。
Linux: 默认打开,通过环境变量 LD_BIND_NOW 控制
Windows: 默认关闭,通过链接器 / DELAYLOAD:<xxx.dll>
[!CAUTION]
Windows 下延迟绑定下动态库函数地址不相等,可能导致程序崩溃。
Procedure Linkage Table(PLT)
在 Linux 中,PLT(Procedure Linkage Table,过程链接表)是用于实现动态链接的重要数据结构之一。
PLT 是一个函数调用的中间层,用于处理动态链接的函数调用。当程序中的代码需要调用一个外部函数时,它会通过 PLT 来实现。
PLT 中的每个条目都包含了两个部分:GOT(Global Offset Table,全局偏移表)和跳转指令。GOT 用于存储外部函数的实际地址,而跳转指令用于跳转到正确的地址。
当程序第一次调用一个外部函数时,PLT 会执行以下步骤:
-
在 GOT 中查找外部函数的实际地址。由于初始时 GOT 中的地址是未知的,所以会跳转到 PLT 的下一个条目。
-
PLT 中的跳转指令将控制权转移到动态链接器(ld.so)中的相应解析函数。解析函数负责定位并填充 GOT 中的地址。
-
解析函数会将外部函数的实际地址写入 GOT 中,并跳转回 PLT 中的下一个条目。
-
现在,GOT 中已经存储了外部函数的实际地址。PLT 中的跳转指令将控制权转移到该地址,实现对外部函数的调用。
在后续的函数调用中,PLT 会直接使用 GOT 中存储的实际地址,而不需要再执行解析函数。这样可以提高函数调用的效率。
符号可见性
默认符号都可见,需要手动隐藏不需要的符号,这可以一定程度优化链接和加载动态库可维护性。
隐藏符号:
-
-fvisibility=hidden
-
-fvisibility-inlines-hidden
-
-fvisibility-ms-compat
-
__attribute__((visibility("hidden")))
静态符号(static symbols): 指在编译时由编译器创建的符号,它们只在当前编译单元(如一个源文件或一个库)中可见。静态符号在编译时进行识别和分配内存,并在链接时被解析和链接到最终的可执行文件中。
动态符号(dynamic symbols): 指在编译时不可见的符号,它们在运行时由动态链接器(dynamic linker)解析和绑定到正确的地址。动态符号可以在多个编译单元之间共享,并且可以在运行时被其他程序使用。
[!TIP]
nm -D, --dynamic (Display dynamic symbols instead of normal symbols): 仅显示动态符号。
简而言之,静态符号是在编译时创建的,只在当前编译单元中可见,而动态符号是在运行时由动态链接器解析和绑定的,可以在多个编译单元之间共享。
动态链接
动态链接是在程序运行时将所需的库加载到内存中的过程,将符号查询、地址重定位推迟到程序加载或符号调用的时候。
这样可以减小可执行文件的大小,并且可以使多个程序共享同一个库,从而节省内存,实现二进制代码级别复用。
.dynamic/.dynsym 节保存动态链接信息. 如下
不同于 windows, linux 下动态链接信息是分开保存的,链接的 lib 保存在 .dynamic 节中,链接的符号保存在 .dynsym 节中.
动态链接在数据段 .data 预留一个叫全局偏移表 got(Global Offset Table)区域保存专用于保存全局变量和函数跳转地址。在 .code 中调用动态库时搜先查找 GOT,GOT 中的地址会在动态库加载时替换为真正的动态库中的地址。这里利用了 GOT 和代码段相对位置是固定的,可以利用 CPU 相对寻址实现。这样 GOT 在每个进程中都保留一部分很小的副本,但可以实现代码被所有进程共享,这种方式称为 PIC (地址无关代码)。一般为减小函数符号重定位开销,操作系统会用程序链接表 PLT (Procedure Linkage Table) 进行延迟到函数第一次调用的时候,因为绝大多数函数不会被调用。一般这个过程会在代码段用一段桩代码,在第一次调用时去查询真正的跳转地址并更新 GOT,具体会更新复杂。
在 Linux 下,动态库是通过内存映射的方式加载到进程的地址空间中的。当多个进程加载同一个动态库时,它们会共享同一个物理内存页,这样可以节省内存空间并提高系统性能。具体来说,当一个进程加载一个动态库时,动态链接器会将动态库的代码段、数据段等内容映射到进程的地址空间中。如果另一个进程也需要加载同一个动态库,动态链接器会检查该动态库是否已经在其他进程中加载过。如果已经加载过,动态链接器会将该动态库的内存映射到新进程的地址空间中,并且新进程和原进程会共享同一个物理内存页。这样,多个进程就可以共享同一个动态库的代码和数据,从而节省内存空间并提高系统性能。需要注意的是,如果多个进程需要修改同一个动态库的数据,就需要使用线程同步机制来保证数据的一致性。否则,不同进程之间对同一个动态库的数据修改可能会相互影响,导致程序出现错误。
1 | # (-shared)指令生成位置无关(-fPIC)的动态库, |
查看依赖的动态库
1 | # 查看 main 依赖的动态库 |
查看库提供的实现符号
1 | # 查看 libc 的提供的动态符号 |
查看系统能搜索到的库
1 | ldconfig -p | grep libc |
生成动态库常用指令
-
-soname 选项用于在动态库文件中指定一个名为 soname 的标记,该标记指定了动态库的名称,通常用于版本区分。
1 | # 编译器将生成名为libexample.so.1的动态库文件,并在其中指定一个名为soname的标记,该标记的值为libexample.so。此外,编译器还将在当前目录中创建一个名为libexample.so的符号链接,该符号链接指向libexample.so.1。这样,当其他程序链接到该动态库时,它们将使用libexample.so作为动态库的名称。 |
-
-rpath 选项用于指定动态库文件的搜索路径。
-
-Wl 将逗号分隔的选项列表传递给链接器。
1 | # 编译器将使用-rpath=/path/to/rpath选项将rpath加入链接选项, |
查看及控制动态库加载
-
LD_DEBUG 查看程序在运行时搜索动态库的过程,例如
LD_DEBUG=all ./program
-
LD_LIBRARY_PATH 指定动态库的搜索路径,指定先搜索动态库路径,例如
LD_LIBRARY_PATH=$(pwd) ./program
-
LD_PRELOAD 指定要预加载的动态库。当程序在运行时需要加载动态库时,动态链接器会首先加载 LD_PRELOAD 环境变量指定的动态库,然后再搜索其他动态库。例如
LD_PRELOAD=/path/to/libmylib.so
[!TIP]
动态库链接及运行行为,可以直接使用工具 patchelf 强行更改。patchelf 一个用于动态链接器和可执行文件的工具,可用于在没有重新编译的情况下更改程序的运行时属性。使用 patchelf,可以修改二进制文件的动态链接器路径,设置程序的默认库路径,设置二进制文件的运行权限,甚至可以通过修改二进制文件的段来添加或删除符号。
以下是使用 patchelf 的示例命令:
1 | # 将二进制文件的动态链接库路径设置为当前目录 |
[!CAUTION]
注意,使用 patchelf 需要小心谨慎,因为不当的更改可能会导致程序无法正常工作。在对二进制文件进行更改之前,请务必确保知道自己在做什么,并备份原始文件以防发生问题。
显式动态链接
显示动态链接通常用于实现基于库级别的插件热更新.
Unix 软件编译安装
本节描述了,针对编译安装软件的 --enable-shared and run path
参数注意要点。
在 linux 上安装非标准的编译安装,附带 --enable-shared 时,需要注意运行库是否和标准位置的运行库是否冲突,是否涉及覆盖等问题。
[!CAUTION]
The problem is, that on most Unix systems (with the notable exception of Mac OS X), the path to shared libraries is not an absolute path. So, if you install Python in a non-standard location, which is the right thing to do so as not to interfere with a system Python of the same version, you will need to configure in the path to the shared library or supply it via an environment variable at run time, like LD_LIBRARY_PATH. You may be better off avoiding --enable-shared; it’s easy to run into problems like this with it.From Python Issue27685, Issue 27685: altinstall with --enable-shared showing incorrect behaviour
Python 编译安装示例
python 编译成动态库安装能增加更多功能,但当附带 --enable-shared 时,需要附带安装位置的运行库的 run path 保证运行库位置能被正确找到。
绝对共享库路径
如下,指定了 python 的安装路径,并指定了对应的运行库的绝对路径所在 run path。
1 | ./configure --enable-shared --prefix=/opt/python LDFLAGS=-Wl,-rpath=/opt/python/lib |
这里 /opt/python/lib 没有在系统共享库搜索路径中,需要手动将动态库的安装目录添加到 ld 的搜索路径。
找到运行库路径,链接器默认的动态库搜索范围包括 /lib 、/usr/lib 以及 /etc/ld.so.conf 配置文件中包含的目录。
这里参考 python 安装非标准位置问题
1 | # 使用链接器查看可搜索范围 |
配置非标准位置动态库加载路径
1 | # 找到运行库所在路径 |
相对共享库路径
如下,指定了 python 的运行库安装路径,并指定了对应的运行库的相对路径所在 run path。
1 | ./configure --enable-shared \ |
GDB
主要描述了针对 GDB 工具的相关使用及工具。
一般来说,GDB 主要帮助完成下面五个方面的功能:
1、启动程序,可以按照自定义的要求随心所欲的运行程序。
2、可让被调试的程序在所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时程序中所发生的事 即挂载调试。
4、可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG。
5、当程序产生核心段错误时,可以进行 CORE 文件调试,查找错误。
gdb dashboard
GDB dashboard 是一个独立的.gdbinit 文件,使用 Python API 编写,支持模块化界面,显示有关正在调试的程序的相关信息。其主要目标是减少检查当前程序状态所需的 GDB 命令数量,从而允许开发人员主要关注控制流。
links:
gdb 快速打印 vector 等容器
GDB 默认是不会打印 C++ 标准库的常用容器,需要通过访问容器属性(非规范命名),才能看到容器中所包含的值。这里介绍了一种 gdb 自动加载脚本,辅助打印容器内容。
下载文件,将之保存为~/.gdbinit 就可以使用打印命令了。
打印 list 用 plist 命令,打印 vector 用 pvector,依此类推。
相关命令如下:
1 | # |
1 | (gdb) pvector cur |
gdb 配合 vscode 调试 / 附加
linux 下 gdb 针对 core 复现及错误现场跟踪是一个非常强大的工具。配合 vscode 能够可视化的调试错误问题。
前置条件:
-
C/C++ MS plugin
-
GDB
在 launch.json 文件中配置如下:
1 | { |
1 | # 模板中 core 文件调试方式,即相当于: |
links:
gdb 搜索源码位置
由于源码可能会变动,所以可能需要手动指定源码位置,方便调试时显示源码。
相关命令:
1 | # 查看编译源码x信息位置 |
links:
GCC/G++
本节主要描述有关 gcc 各个方面应用及问题。
常用编译参数
-
-std=c++20
: 用于指定应该使用哪个 C++ 标准来编译程序 -
-Wpedantic
: 告诉在遇到不符合 ISO C++ 标准的代码时发出警告。这包括一些编译器特定的扩展和一些可能导致移植性问题的代码。使用可以帮助编写更符合标准、更具有移植性的代码。 -
-Wall
: 告诉启用所有的警告信息,这可以帮助发现代码中的潜在问题。然而,-Wall 并不真的启用所有的警告,只是启用了一部分被认为最有用的警告。 -
-Wextra
: 告诉启用一些额外的警告信息,这些警告信息在 -Wall 中没有被启用。这些额外的警告可以帮助发现代码中的一些不常见的问题。
优化选项 Optimize-Options
GCC 优化选项
在开启编译优化的开关时,GCC 编译器的目的是:优化程序的性能和减少代码的大小,尽管会以牺牲编译时间和程序的可调试能力为代价。对于不同的优化级别开启的对应优化开关可以通过 gcc -Q -O1 --help=optimizers
来查看对应的开启优化列表。
常用的优化选项
-
-O0
默认的优化选项,减少编译时间和生成完整的调试信息。
-
-O/-O1
这两个都是开启 level 1 的编译优化。开启编译优化会导致更长的编译时间,对于大函数还会消耗更多的内存空间。level1 的编译优化下,编译器会尝试减少代码段大小和优化程序的执行时间,但不执行需要消耗大量编译时间的优化。
-
-O2
相比于 - O1,-O2 打开了更多的编译优化开关
-
-O3
在 - O2 的基础上,level 3 的级别优化
-
-Os
优化生成的目标文件的大小
-
-Ofast
为了提高程序的执行速度,GCC 可以无视严格的语言标准。-Ofast 会开启所有 - O3 的编译开关,且会对不符合标准的程序进行优化。
-
-Og
优化调试信息。相对于 - O0 生成的调试信息,-Og 是为了能够生成更好的调试信息。和 - O0 一样,-Og 选项关闭了很多优化开关。
[!NOTE]
如果同时使用多个不同 level -O 优化选项来进行编译,编译器会根据最后一个 - O 的 level 来决定采用那种优化级别。
-
-Q -v
输出编译器 - O 打开的优化编译选项
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// test.cpp
void CustomSigHandler(int signum) {
std::ostringstream os;
os << "Abnormal signal is triggered:" << signum << '\n';
std::cout << os.str();
}
class Point {
public:
int getX() { return x; }
int x;
};
struct Line {
Point s;
};
int main(int argc, char* argv[]) {
signal(SIGSEGV, &CustomSigHandler); // handle SIGSEGV
signal(SIGABRT, &CustomSigHandler); // handle SIGABRT
// access read illegally
std::shared_ptr<Line> l;
l->s.getX(); // without signal emitted when set -fipa-pure-const
// access read illegally
// int *p=NULL;
// *p=0;
return 0;
}1
g++ -O -Q -v test.cpp -o test &> ops.out
-
-fno-flag
关闭编译指定 flag 的编译选项。
1
g++ -O -fno-ipa-pure-const test.cpp -o test
-
-fomit-frame-pointer
对于不需要帧指针的函数,不要将帧指针保存在寄存器中。这避免了保存、设置和恢复帧指针的指令;它还为许多函数提供了一个额外的寄存器。这也使得在某些机器上无法进行调试。参考
参考
GCC 调试选项
GCC 允许将 - g 与 - O 配合使用。GCC 开启优化编译选项的结果有时可能会令人惊讶:
-
声明的某些变量可能被删除;
-
控制流走到意想不到的位置;
-
有些语句可能不会执行,因为它们计算的是常量结果或它们的值已经在手边;
-
有些语句可能会在不同的位置执行,因为它们已经移出了循环。
-
GCC 允许编译时添加额外的调试信息,以便程序进行调试,大部分情况下,需要编译选项 - g 就可以满足调试需求。
如果没有使用其他优化选项,请考虑将 - Og 与 - g 一起使用。在完全没有 - O 选项的情况下,一些编译器收集对调试有用的信息根本不会运行,因此 - Og 可能会带来更好的调试体验。
-
g0:不生成调试信息,相当于没有使用 - g;
-
g1:生成最小的调试信息,足够在不打算调试的程序中进行堆栈查看。最小调试信息包括函数描述,外部变量,行数表,但不包括局部变量信息。
-
g2:默认 - g 的调试级别;
-
g3:相对 - g,生成额外的信息,例如所有的宏定义;
[!NOTE]
和 - O 一样,如果多个级别的 - g 选项同时存在,最后的选项会被生效
[!TIP]
综上,很多项目的线上版本都是使用”-O2 -g” 的编译选项进行编译,以便发生问题的时候容易定位。但这有一个很大的弊端就是目标文件会比不开启调试信息的情况下大很多,所以一般对外发布的软件都是不含有调试信息的 Release 版本,同时也会发布含有调试信息的 debug 版本,两者的性能是一样的只是 debug 多了调试信息而已。
Clang/Clang++
常用编译参数
-
-std=c++20
: 同 gcc -
-Wpedantic
: 同 gcc -
-Wall
: 同 gcc -
-Wextra
: 同 gcc
Jupyter
使用 Jupyter 编写丰富的可执行的富文档。
-
Local setup
1 | mamba create -n cling |
Docs
sphinx
links:
Doxygen
links:
注释特殊标记
1 | // 标记代码 |
Coverage
对 C/C++ 项目进行代码覆盖率的度量
links:
-
gcov
- https://zhuanlan.zhihu.com/p/402463278
- https://wiki.documentfoundation.org/Development/Lcov
- https://medium.com/@naveen.maltesh/generating-code-coverage-report-using-gnu-gcov-lcov-ee54a4de3f11
- https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html
- https://blog.csdn.net/yanxiangyfg/article/details/80989680
- https://manpages.ubuntu.com/manpages/bionic/man1/gcov-8.1.html
- https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html#Instrumentation-Options
- C++ 语言的单元测试与代码覆盖率 - 阿里云开发者社区 (aliyun.com)
-
llvm
- https://llvm.org/docs/CommandGuide/llvm-cov.html#llvm-cov-gcov
- https://www.jetbrains.com/help/clion/code-coverage-clion.html#compiler-flags
- https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#id1
- https://stackoverflow.com/questions/58400297/trying-to-view-code-coverage-with-llvm-cov
- https://marco-c.github.io/2018/01/09/code-coverage-with-clang-on-windows.html
- http://www.stablecoder.ca/2018/01/15/code-coverage.html
- http://logan.tw/posts/2015/04/28/check-code-coverage-with-clang-and-lcov/
QA
配置原理
编译参数
-
GNU:
- compile options: -fprofile-arcs -ftest-coverage
- link options: -fprofile-arcs
- link lib: gcov
-
Clang:
- compile options: -fprofile-instr-generate -fcoverage-mapping --coverage (或 - fprofile-arcs -ftest-coverage)
- link options: -fprofile-instr-generate -fcoverage-mapping
-
Windows:
- opencppcoverage/vstest.console.exe
[!TIP]
-fprofile-arcs -ftest-coverage: gcov required
[!TIP]
-fprofile-instr-generate -fcoverage-mapping: llvm-cov
生成
-
GNU 方式一,使用 gcov 作为媒介
1 | # Install lcov(genhtml in inclusion) |
-
GNU 方式二,单独依赖 lcov(间接使用 gcov )
1 | # Install lcov(genhtml in inclusion) |
-
Clang
1 | # Run test and generate *.profraw |
-
Windows
1 | vstest.console.exe *.dll /EnableCodeCoverage /Collect:"Code Coverage;Format=Xml" /ResultsDirectory:"<coverage result dir>" |
集成三方
测试集成三方
-
GitLab
CMake 示例
links:
Program
这张图片展示了 ELF 加载到内存的一个数据结构关系。
Linux Program 内存模型
Linux 的 ELF 内存模型。ELF(Executable and Linkable Format)是一种可执行文件格式,常用于 Linux 系统中的可执行文件、共享库等。ELF 文件在内存中的布局由 ELF 头、程序头表、节区头表和节区数据组成。其中,程序头表描述了 ELF 文件在内存中的布局,包括代码段、数据段、BSS 段等。节区头表描述了 ELF 文件中各个节区的信息,如名称、大小、偏移量等。节区数据则包含了 ELF 文件中各个节区的实际数据。
在 Linux 中,每个进程都有自己的虚拟地址空间,其中包括代码段、数据段、堆、栈等。当一个 ELF 文件被加载到内存中时,它的各个节区数据会被映射到进程的虚拟地址空间中相应的位置。例如,代码段会被映射到进程的代码段区域,数据段会被映射到进程的数据段区域。由于每个进程都有自己的虚拟地址空间,因此不同进程中同一 ELF 文件的虚拟地址空间布局可能会不同。
Assembly
Assembly language is a low-level programming language that is specific to a particular computer architecture. It is often used for system-level programming, such as operating systems, device drivers, and embedded systems.
常用工具
学习 Assembly 的 C/C++ 超级工具 Godbolt
[!TIP]
Godbolt is a web-based tool that allows you to see the assembly code generated by different compilers for a given piece of code. It can be useful for understanding how your code is translated into machine code and for optimizing your code for a specific architecture.
寄存器
RSP: The rsp register in x86-64 assembly language is the stack pointer register and is used to keep track of the top of the stack. The stack is a region of memory that is used to store temporary data, such as function parameters and local variables.
[!TIP1] When a function is called, the
rsp
register is decremented to allocate space on the stack for the function’s parameters and local variables. When the function returns, the rsp register is incremented to deallocate the stack space that was used by the function.
[!TIP2] To allocate 4 bytes in assembly, you can use the
sub
instruction to decrement the stack pointer register (rsp
) by 4 bytes. This will allocate 4 bytes of space on the stack for temporary data, such as function parameters or local variables.
Here is an example of how to allocate 4 bytes in x86-64 assembly language:
1 | sub rsp, 4 ; allocate 4 bytes on the stack |
This instruction decrements the value of rsp by 4, effectively allocating 4 bytes of space on the stack. To deallocate the space when it is no longer needed, you can use the add instruction to increment the value of rsp by the same amount:
1 | add rsp, 4 ; deallocate 4 bytes from the stack |
RBP: The rbp register in x86-64 assembly language is the base pointer register and is used to access function parameters and local variables relative to the current stack frame.
[!TIP]
At the beginning of a function, the instructionspush rbp
andmov rbp, rsp
are commonly used to set up the function’s stack frame.push rbp
pushes the value of the previous stack frame’s base pointer onto the stack, andmov rbp, rsp
moves the value of the stack pointer register into the base pointer register, effectively creating a new stack frame for the current function.
[!TIP2]
By using the rbp register to access function parameters and local variables, the function can allocate space on the stack for its own variables and use the base pointer register to access them.
RDI: The rdi register is one of the general-purpose registers in the x86-64 architecture, and it is typically used to pass the first argument to a function.
RSI: The rsi register is another general-purpose register in the x86-64 architecture, and it is typically used to pass the second argument to a function.
[!TIP]
The instructionmov QWORD PTR [rbp-8], rdi
is a move instruction in assembly language. It moves the value in the rdi register, which is the first argument to the function, into the memory location at [rbp-8], which is a local variable on the stack.
RAX: The rax register is one of the general-purpose registers in the x86-64 architecture, and it is typically used to hold the return value of a function.
[!TIP]
The instructionmov rax, QWORD PTR [rbp-8]
is a move instruction in assembly language. It moves the value in the memory location at [rbp-8], which is a local variable on the stack, into the rax register for later use in the function.
EDX/EAX: The edx/eax register is one of the general-purpose registers in the x86-64 architecture, and it is typically used to hold a 32-bit value.
RBX: The rbx register in x86-64 assembly language is one of the general-purpose registers that can be used to hold data or memory addresses. It is a 64-bit register, so it can hold values between 0 and 2^64-1.
指令
As for common assembly instructions, there are many different instructions that can be used depending on the architecture and the specific task at hand. However, some common instructions include:
MOV: This instruction moves data from one location to another. For example, MOV EAX, 5
moves the value 5 into the EAX register.
ADD/SUB: These instructions perform addition and subtraction operations. For example, ADD EAX, EBX
adds the value in the EBX register to the value in the EAX register.
CMP: This instruction compares two values and sets flags based on the result. For example, CMP EAX, EBX
compares the values in the EAX and EBX registers and sets flags based on whether EAX is greater than, less than, or equal to EBX.
JMP: This instruction jumps to a different location in the code. For example, JMP label
jumps to the location labeled “label”.
CALL/RET: These instructions are used for function calls and returns. CALL function calls the function at the specified location, and RET returns from a function.
DD in the .data section of an assembly program stands for “define doubleword”. A doubleword is a 32-bit value, which is equivalent to 4 bytes. The dd directive is used to reserve space in memory for a variable and initialize it to a specific value.
下面是一些完整示例:
1 | ; Here is an example program that uses the MOV instruction to move the value 5 into the EAX register, adds the value in the EBX register to the value in the EAX register using the ADD instruction, and then returns the result: |
1 | ; Here is another example program that uses the CMP instruction to compare the values in the EAX and EBX registers and then jumps to a different location in the code based on the result: |
1 | ; To write an assembly program that adds two numbers, you can use the ADD instruction. |
arm assembly
links:
C
C 内存管理
API
C 语言主要提供 malloc
、realloc
、calloc
、alloca
与 aligned_alloc
等内存分配函数来实现对内存的分配功能。
malloc
1 | void * malloc (size_t size); |
该函数用于从堆中分配内存空间,内存分配大小为 size。如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL。
calloc
1 | void * calloc (size_t num, size_t size ); |
该函数用于从堆中分配 num
个相邻的内存单元,每个内存单元的大小为 size
。如果内存分配成功则返回第一个内存单元的首地址;否则内存分配失败,则返回 NULL
。从功能上看,calloc
函数与语句 malloc(num*size)
的效果极其相似。但不同的是,在使用 calloc
函数分配内存时,会将内存内容初始化为 0。
realloc
1 | void * realloc (void * ptr, size_t size ); |
该函数用于更改已经配置的内存空间,它同样是从堆中分配内存的。当程序需要扩大一块内存空间时,realloc
函数试图直接从堆上当前内存段后面的字节中获得更多的内存空
间,即它将首先判断当前的指针是否有足够的连续存储空间,如果有,则扩大 ptr
指向的地址,并且将 ptr
返回(返回原指针);如果当前内存段后面的空闲字节不够,那么将先按照 size
指定的大小分配空间(使用堆上第一个能够满足这一要求的内存块),并将原有数据从头到尾拷贝到新分配的内存区域,然后释放原来 ptr
所指内存区域,同时返回新分配的内存区域的首地址,即重新分配存储器块的地址。
需要注意的是,参数 ptr
为指向先前由 malloc
、calloc
与 realloc
函数所返回的内存指针,而参数 size
为新分配的内存大小,其值可比原内存大或小。其中:
-
如果
size
值比原分配的内存空间小,内存内容不会改变(即新内存保持原内存的内容),且返回的指针为原来内存的首地址(即ptr
)。 -
如果
size
值比原分配的内存空间大,则realloc
不一定会返回原来的指针,原内存的内容保持不变,但新多出的内存则设为初始值。 -
最后,如果内存分配成功,则返回首地址;如果内存分配失败,则返回
NULL
。
alloca
1 | void * alloca (size_t size); |
相对与 malloc
、calloc
与 realloc
函数,函数 alloca
是从栈中分配内存空间,内存分配大小为 size
。如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL
。也正因为函数 alloca
是从栈中分配内存空间,因此它会自动释放内存空间,而无需手动释放。
aligned_alloc
1 | void * aligned_alloc (size_t alignment,size_t size); |
该函数属于 C11 标准提供的新函数,用于边界对齐的动态内存分配。该函数按照参数 alignment
规定的对齐方式为对象进行动态存储分配 size
个 size_t
类型的存储单元。如果内存分配成功,则返回首地址;否则内存分配失败,则返回 NULL
。
相对于 malloc
函数,aligned_alloc
函数保证了返回的地址是能对齐的,同时也要求 size 参数是 alignment
参数的整数倍。从表面上看,函数 calloc
相对 malloc
更接近 aligned_alloc
,但 calloc
函数比 aligned_alloc
函数多了一个动作,那就是会将内存内容初始化为 0
。
内存分配函数的返回值
函数名 | 成功返回 | 失败返回 | errno |
---|---|---|---|
malloc | 指向被分配内存的指针 | NULL | ENOMEM |
aligned_alloc | 指向被分配内存的指针 | NULL | ENOMEM |
calloc | 指向被分配内存的指针 | NULL | ENOMEM |
realloc | 指向重新分配内存的指针 | NULL | ENOMEM |
在调用表 1 中的这些内存分配函数时,必须进行返回值检查,以便能够及时得到内存分配是否成功与失败(如果分配失败则返回 NULL
指针),这样也可以避免因为内存分配错误而导致的不可预知和意外程序行为发生,如下面的示例代码所示:
1 | char *p = (char *)malloc(100); |
除通过使用 “if(p==NULL)”
或者 “if(p!=NULL)”
语句进行简单防错处理之外,如果指针 p
是函数的参数,那么还可以在函数的入口处用 assert(p !=NULL)
进行检查,从而避免发生内存分配未成功却使用了它的情况。
内存资源的分配与释放
内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行,在 C 语言中,如果内存的分配和释放在不同的模块或抽象层内,不仅会加大程序员追踪内存块生命周期的负担,而且可能会导致内存泄漏、内存双重释放(double-free
)、非法访问已经释放的内存、写入已释放或未分配的内存区域等问题。看下面一段示例代码:
1 |
|
在上面的示例代码中,p 的内存是在 AllocMemory
函数中进行分配的,然后再将它通过语句 “CompareMemorySize(p,size)
” 传给 CompareMemorySize
函数。在 CompareMemorySize
函数中,首先通过语句 “if(size<MIN_MEM_SIZE)
” 检查 p
所分配的内存长度,如果内存长度小于最小值 (MIN_MEM_SIZE)
,则释放 p
。
然后,再将 CompareMemorySize
函数的返回值 “-1
” 返回给调用者 AllocMemory
函数。在 AllocMemory
函数中执行语句 “if(CompareMemorySize(p,size)==-1)
” 条件成立,再次释放 p。
很显然,这样不仅违背了 “内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行” 的原则,同时导致了内存的双重释放。因此,需要对代码做如下修改:
1 |
|
现在,函数 CompareMemorySize
与 AllocMemory
的职责很清楚了。其中,CompareMemorySize
函数只负责检查内存分配的长度,而内存的分配与释放都放在 AllocMemory
函数内进行。这样不仅不会导致内存的双重释放,而且完全遵从 “内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行” 原则。
返回指针进行强制类型转换
在 C 语言中,“void
” 被称为 “无类型”,而 “void*
” 则被称为 “无类型指针”。之所以称 “void*
” 为 “无类型指针”,是因为它可以指向任何数据类型
。因此,对于任何类型 “T
” 都可以转换为 “void
”,而 “`void 也可以转换为任何类型 “T*”。
也正是因为 “void
” 的这个特征,它常被用在如下两个方面:
-
对函数返回的限定,即如果函数没有返回值,那么应将其声明为
void
类型。 -
对函数参数的限定,即如果函数无参数,那么声明函数参数为
void
。
当然,内存管理函数也不例外,如 malloc
、realloc
、calloc
、alloca
与 aligned_alloc
函数的返回都是 void*
类型。但需要特别注意的是,在使用这些内存管理函数进行内存分配时,必须将返回类型 void*
强制转换为指向被分配类型的指针。如下面的代码所示:
1 | char *p = (char *)malloc(10 * sizeof(char)); |
当然,为了能够简单调用,也可以将 malloc 函数使用 define 定义成如下形式:
1 |
|
现在,调用就简单多了,如下面的代码所示:
1 | char *p = MALLOC(char); |
下面的宏为大家提供了更多方便:
1 | /*malloc*/ |
指向一块合法的内存
在 C 语言中,只要是指针变量,那么在使用它之前必须确保该指针变量的值是一个有效的值,它能够指向一块合法的内存,并从根本上避免未分配内存或者内存分配不足的情况发生。
看下面一段示例代码:
1 | struct phonelist |
对于上面的代码片段,在定义结构体变量 list
时,并未给结构体 phonelist 内部的指针变量成员 “char*name
” 与 “char*tel
” 分配内存。这时候的指针变量成员 “char*name
” 与 “char*tel
” 并没有指向一个合法的地址,从而导致其内部存储的将是一些未知的乱码。
因此,在调用 strcpy
函数时,如 “strcpy(list.name,"Abby")
” 语句会将字符串 "Abby
" 向未知的乱码所指的内存上拷贝,而这块内存 name
指针根本就无权访问,从而导致程序出错。
既然没有给指针变量成员 “char*name
” 与 “char*tel
” 分配内存,那么解决的办法就是为指针变量成员分配内存,使其指向一个合法的地址,如下面的示例代码所示:
1 | list.name = (char*)malloc(20*sizeof(char)); |
除此之外,下面的错误也是大家经常容易忽视的:
1 | struct phonelist |
不难发现,上面的代码片段虽然为结构体指针变量 plist
分配了内存,但是仍旧没有给结构体指针变量成员 “char*name
” 与 “char*tel
” 分配内存,从而导致结构体指针变量成员 “char*name
” 与 “char*tel
” 并没有指向一个合法的地址。因此,应该做如下修改:
1 | plist->name = (char*)malloc(20*sizeof(char)); |
由此可见,对结构体来说,仅仅是为结构体指针变量分配内存还是不够的,还必须为结构体成员中的所有指针变量分配足够的内存。
分配足够的内存空间
对于上面的结构体指针变量 plist 的内存分配语句:
1 | plist = (struct phonelist*)malloc(sizeof(struct phonelist)); |
如果不小心误写成如下形式会怎么样呢?
1 | plist = (struct phonelist*)malloc(sizeof(struct phonelist*)); |
虽然这里只是简单地将 “sizeof(struct phonelist)
” 误写成了 “sizeof(struct phonelist*)
”,但将会因为结构体指针变量 plist
内存分配不足而导致程序的内存错误发生。类似的示例还有许多,如下面的代码所示:
1 | void f(size_t len) |
在上面的示例代码中,内存分配语句 “p=(long*)malloc(len*sizeof(int))
” 使用了 “sizeof(int)
” 来计算内存的大小,而不是 sizeof(long)
,这显然是不对的,应该修改成 sizeof(long)
,当然,也可以用 “sizeof(*p)
”。
除此之外,对于数组对象尤其要注意内存分配的问题,如下面的代码所示:
1 |
|
对于上面的示例,当一个结构体中包含数组成员时,其数组成员的大小必须添加到结构体的大小中。因此,上面示例的正确内存分配方法应该按照如下方式进行:
1 |
|
由上面的几个示例代码片段可见,对于 malloc
、calloc
、realloc
与 aligned_alloc
内存分配函数中长度参数的大小,必须保证有足够的范围来表示对象要存储的大小。如果长度参数不正确或者可能被攻击者所操纵,将可能会出现缓冲区溢出。与此同时,不正确的长度参数、不充分的范围检查、整数溢出或截断都会导致分配长度不足的缓冲区。因此,一定要确保内存分配函数的长度参数能够合法地分配足够数量的内存。
禁止执行零长度的内存分配
根据 C99 规定,如果在程序中试图调用 malloc
、calloc
与 realloc 等系列内存分配函数分配长度为 0 的内存,那么其行为将是由具体编译器所定义的(如可能返回一个 null
指针,又或者是长度为非零的值等),从而导致产生不可预料的结果。
因此,为了保证不会将 0 作为长度参数值传给 malloc、calloc 与 realloc 等系列内存分配函数,应该对这些内存分配函数的长度参数进行合法性检查,以保证它的合法取值范围。
如下面的代码所示:
1 | size_t len; |
避免大型的堆栈分配
C99 标准引入了对变长数组的支持,如果变长数组的长度传入未进行任何检查和处理,那么将很容易被攻击者用来实施攻击,如常见的 DOS 攻击。看下面的示例代码:
1 | int CopyFile(FILE *src, FILE *dst, size_t bufsize) |
在上面的示例代码中,数组 “char buf[bufsize]
” 的长度将根据 CopyFile
函数的 bufsize
参数来决定,这显然不符合要求的。对于这种情况,可以通过一个 malloc
调用来替换掉这个变长数组。与此同时,如果 malloc
函数内存分配失败,还可以对返回值进行检查,从而防止程序异常终止等情况发生。如下面的示例代码所示:
1 | int CopyFile(FILE *src, FILE *dst, size_t bufsize) |
避免内存分配成功,但并未初始化
在通常情况下,导致这种错误的主要原因有两个:
-
没有初始化的观念。
-
误以为内存的默认初值全部为零,从而导致引用初值错误(如数组)。
其实,内存的默认初值究竟是什么并没有统一的标准。如 malloc
函数分配得到的内存空间就是未初始化的,而它所分配的内存空间里可能包含出乎意料的值。因此,一般在使用该内存空间时,就需要调用函数 memset
来将其初始化为全 0
。如下面的示例代码所示:
1 | int * p = NULL; |
对于 realloc
函数,同样需要使用 memset
函数对其内存进行初始化。而对于数组,也别忘了赋初值,即便是赋零值也不可省略,千万不要嫌麻烦。
C 宏(macro)
特殊宏
1 | __DATE__ : 在原文件中插入当前的编写日期 |
__VA_ARGS__
用于实现变参函数,将函数宏的形参列表最后的参数用省略号(…)表示即实现了变参函数。__VA_ARGS__
用于在宏替换部分中,表明省略号代表什么。
例如:
1 |
|
##__VA_ARGS__
宏前面加上##
的作用在于,当可变参数的个数为 0 时,这里的##
起到把前面多余的 ",
" 去掉的作用,否则会编译出错。
一般这个用在调试信息上多一点.
#
表示将参数转换为字符串输出:
1 |
|
运行结果为:
1 | a:1 |
##
用于类函数宏的替换部分,也可以用于对象宏的替换部分。主要用于将两个语言符号组成单个语言符号,为宏扩展提供一种连接实际变元的手段;
1 |
|
定义一个宏,求两个数中的最大数
-
合格
1
-
中等
1
-
良好
1
2
3
4
5
6
7
8
9
10
11
12
int main(void)
{
int i = 2;
int j = 6;
printf("max=%d",MAX(i++,j++));
return 0;
} -
优秀
1
2
3
4
5
6
7
8
9
10
11
12
13
int main(void)
{
int i = 2;
int j = 6;
printf("max=%d\n",MAX(int,i++,j++));
printf("max=%f\n",MAX(float,3.14,3.15));
return 0;
} -
流弊
1
2
3
4
5在这个宏定义中,使用了
typeof
(GCC 的扩展) 关键字用来获取宏的两个参数类型
。干货在(void) (&x == &y);
这句话,简直是天才般的设计!-
用来给用户提示一个警告,对于不同类型的指针比较,编译器会给一个警告,提示两种数据类型不同;
-
当两个值比较,比较的结果没有用到,有些编译器可能会给出一个 warning,加个 (void) 后,就可以消除这个警告!
-
-
完美
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23max(x, _x)
在新版的宏中,内部的临时变量不再由程序员自己定义,而是让编译器生成一个独一无二的变量,这样就避免了同名冲突的风险。宏 **__UNIQUE_ID** 的作用就是生成了一个独一无二的变量,确保了临时变量的唯一性。关于它的使用,可以参考文章,写的很好。
-
参考
函数指针
指向普通函数的指针
1 | int int_add(int a, int b) |
上例中,int_operator
会被编译器解释成类型 int(*)(int, int)
的一个指针。
调用方式还可以写作:(*int_operator)(4, 5)
,这样的好处是让人一眼就能看到 int_operator
是一个函数指针。注意:函数指针和指向函数的返回值的类型和参数都必须严格一致;
函数指针数组
1 | int (*pFuncArray[10])(); |
[]
的优先级高于 *
,该语句将 pFuncArray
声明为拥有 10 个元素的数组,每一个元素都是指向一个函数的函数指针,该函数没有参数,返回值类型为 int
;
注意不能写作:int ((*pFuncArray)[10])()
,这样会产生编译错误;(*pFuncArray)[10]
表明了 pFuncArray
是一个指针,该指针指向一个含有 10个元素的数组
;其类型为 int()()
,显然,编译不能通过。
将上面的声明转换为 typedef
格式,会使程序可读性增加:
1 | typedef int(*pFunc)(); |
如果需要调用其中的第三个函数,那么调用方式为:
1 | pFuncArray[2](); |
指向‘函数指针数组’的指针
1 | int cmp_len(const char *str1, const char *str2) |
声明分解说明如下:
-
(*ppCmps)
:表明ppCmps
是一个指针; -
(*ppCmps)[2]
:后面紧跟[2]
,表明ppCmps
是一个指向‘两个元素数组’的指针 -
PCMP_FUNC
表明了该数组元素的类型,它是指向函数的指针,返回值为int
,有两个const char*
类型的参数; -
实际上语句
PCMP_FUNC (*ppCmps)[2] = &pCmpFuncs;
将会被编译器解释为:1
int (*(*ppCmps)[2])(const char*, const char*) = &pCmpFuncs;
int (*)(const char*, const char *)
:表明了该数组元素的类型,它是指向函数的指针,返回值为int
,有两个const char*
类型的参数;
函数指针与类
在 C++
语言中,使用函数指针可以指向类的一个成员函数或变量,虽然这种用法很少能用到,不过了解一下还是有点必要的。
-
为了支持这类指针,
C++
有三个特殊的运算法符:::*,.->,.*
-
指向成员函数的指针必须与向其赋值的函数类型匹配,这包括:
- 参数的类型和个数;
- 返回值类型;
- 它所属的类型;
-
指向成员变量的指针必须与向其赋值的变量类型匹配,这包括
- 变量类型;
- 它所属的类型;
-
成员函数和变量必须被绑定到一个对象或者指针上,然后才能得到调用对象的
this
指针,然后才能指向成员变量或函数; -
类
AClass
的成员变量int m_iValue
,其完整类型是:int AClass::m_iValue
; -
类
AClass
的成员函数int Add(int)
,其完整类型是:int AClass::Add(int)
;
注意:指向类的静态变量或静态成员函数的指针和普通成员不同;
指向类成员变量的指针
1 | class AClass |
指向类成员函数的指针
1 | class AClass |
指向类静态成员的指针
类的静态成员属于该类的全局对象和函数,并不需要 this 指针;因此指向类静态成员的指针声明方式和普通指针相同。
类指针和普通指针的声明和调用方式完全相同;唯一的不同就是设置指向的对象时,仍然需要类信息,这一点和指向普通成员的指针相同。
1 | class AClass |
OOP In C
多线程
todo
pthreads
Unix/Linux thread standard 基于 wiki 示例。
CPP
links:
CPP Project
项目布局遵循规范:
现代 C++ 工程布局通常使用成熟的工具生成,比如 cmake-init 及示例。
1 | pip3 install cmake-init |
Files in cpp
cpp 作为编译型静态语言,考虑了各方面原因,通常不同的文件后缀,习惯于放置不同的代码段以作为区分,不完全包含后缀有 .h/.cpp/.def/.inc/.imp/impl/.tpp
。
.hpp and .cpp
通常普通的类及函数都声明于 .h
和 .cpp
中。
.inc/.imp/.impl/.tpp
.inc/.inl/.def
-
宏定义或预编译
-
模板和
constexpr
操作 -
也可作为类似
.hpp
作用或作为.hpp
的实现部分,如 X Macro
.imp/.impl/.tpp
-
模板定义
links:
Learning Basics
assert
assert 可以结合说明,给出运行时更多信息。
Namespace
命名空间除了避免命名冲突外,还有以下特殊使用方法 1
-
unnamed namespace:用于分类只属于当前给定文件的实现。
-
inline namespacce:用于设定上一级命名空间的默认版本。
Template
static assert
-
static assert for equality type
1 | // 使用 std::is_same, std::is_same_v, decltype, declval, std::decy, std::decy_t |
Fold expressions
Fold expressions 折叠表达式在 c++17 引入,用于避免需要为模板写多个版本以使用可变模板参数及编译效率。
1 | template<> |
使用折叠表达式,书写更简便,且有利于编译器模板实例化:
1 | template<typename ...Args> |
使用参考示例
折叠表达式展开到参数包如下:
表达式 | 展开 |
---|---|
(... op pack) |
((pack1 op pack2) op ...) op packN |
(init op ... op pack) |
(((init op pack1) op pack2) op ...) op packN |
(pack op ...) |
pack1 op (... op (packN-1 op packN)) |
(pack op ... op init) |
pack1 op (... op (packN-1 op (packN op init))) |
当 op 为如下操作时,空的参数包值自动推导为默认值:
Op | Default Value |
---|---|
&& |
true |
|| |
false |
, |
void() |
这里是一个实际代码示例:
1 | template <typename... Args> |
相关参考
require(C++20)
C++20 新增 require,简化了模板复杂性,以更符合语义方式实现约束。
可调对象
对可调用对象进行约束,需要分别对参数及返回值进行设置。
std::variant and std::any
std::variant 类型安全的 union。
std::any 类型安全的实现 copy 语义的单值容器。
[!TIP]
使用这两个容器可以实现简单动态属性。
Iterator and Tools
-
std::next
-
std::partition
-
std::distance
在 STL 中使用迭代器操作数据结构,进而实现算法是非常普遍的做法。
1 | // quick sort |
容器 Container
links:
并发 thread
links:
sharing data
the Risk of mutexes: https://www.modernescpp.com/index.php/the-risk-of-mutexes/
Prefer to mutex: https://www.modernescpp.com/index.php/prefer-locks-to-mutexes/
API Design
“Make interfaces easy to use correctly and hard to use incorrectly.—Scott Meyers”。这很容易达成一致,但很难遵循。这里提供一些技巧来如何改进的 API,避免接口增长带来的陷阱。
Better naming
使用更表意的命名方式,命名非常困难,但能让事情变简单。
1 | // bad |
Use strong types
使用健壮的类型,尽量保证在编译或运行给出更多信息,健壮的类型不完全检测包括:
1 |
|
Avoid easily swappable params
避免两个类型相等的参数很容易错用,注释不如实际类型使用安全。
1 | // good 每个参数都能清楚表达应有的含义 |
避免相同类型设置为函数签名的参数,除了惯用表达外。更健全的类型能在让编译器更易报错
1 | // Enum class to the rescue |
1 | using FilePtr = std:unique_ptr<FILE,decltype([](FILE *f){fclose(f);})> |
Carefully think about intent
接口设计要紧贴真实用户意图,关注调用 api 方的可能的行为。
[[nodiscard]]
Instructs the compiler to generate a warning if a return value is dropped. Can be applied to types or function declarations.
C++23 fixes a minor loophole in the standard and now allows [[nodiscard]]
with lambdas.
Summary
-
Can have a message to explain the error
[[nodiscard("Lock objects should never be discarded")]]
-
Should be used extensively.Any non-mutating (getter/accessor/const)
-
function should be
[[nodiscard]]
-
Can be checked enforced with static analysis
noexcept
noexcept
notifies the user (and compiler)that a function may not throw an exception.If an exception is thrown from that function, terminate
MUST be called.
Never Return a Raw Pointer
-
It simply raises too many questions.Who owns it?Who deletes it?Is it a singleton global?
-
Consider owning_ptr, non_owning_ptror some kind of wrapper to document intent,if you must.
Consistent Error Handling
-
Use one consistent method of reporting errors in your library
-
Strongly avoid out-of-band error reporting
(get_last_error() or errno )
-
Make errors impossible to ignore(no returning an error code!)
-
Never use std:optional<>to indicate an error condition.(it does not convey a reason, and the reason becomes out of bound).
-
Consider std::expected<>(C++23) or similar.
Avoid Implicit Conversions
-
std::filesystem::path and std::string-view appear to be strongly typed but are not
-
Implicit conversions between const char*,string,string_view,and path break type safety
-
Conversion operators and single parameter constructors (including variadic and ones with default parameters)should beexplicit
=delete Problematic Overloads
-
Any function can be =deleted
-
If you =delete a template, it will become the match for any non-exact parameter, and prevent implicit conversions.
Guideline for Design Classes: easy change and extensions
Design class fundamental principles
-
Single-Responsibility Principle(SRP) - simplify changes
-
Open-Closed Principle(OCP) - simplify the extensions
-
Don’t Repeat Yourself(DRY) - simplify change
A strategy-based solution(Separate the structure from the single behavior, and then broker the single behavior with a policy)
Resist the urge to put everything into one class. Separate concerns!
If you use OO programming, use it properly.
The Single behavior can be implemented with:
-
a class family with inheritance.(90s+ or 00s+)
-
a template class.(c++11+)
1 | class Circle; |
-
a callback function(eg. std::function)(c++11+)
1 | class Circle; |
The template method-based solution(Non-Virtual Interface Idoim(NVI))
-
separated concerns and simplified changed(SRP).
-
enabled internal changes with no impact on callers.
1 | class PersistenceInterface { |
Design for testability. How to test a private member?
-
#define private public. No
-
Make the test a friend. Maybe
-
Make the member public. No
-
Derive the test class from the tested class. Maybe
-
Separate concerns. Recommended
- Move the member into a private namespace …
- … or into another class (as a separate service).
-
Using gmock to define some
MOCK_METHOD
to access to private members.
1 | template< |
Resource Management
-
If you can avoid defining default operations, do The Rule of 0.
-
Core Guideline C.32: If a class has a raw pointer (T*) or reference (T&), consider whether it might be owning.
-
Core Guideline C.33: If a class has an owning pointer member, define a destructor.
-
Core Guideline R.3: A raw pointer (a T*) is non-owning
-
RAII is the most important idioms.
-
Core Guideline C.21: If you define or =delete any default operation,
define or =delete them all. Manager resources can be possessed by samrt pointers.- std::unique_ptr(The Rule of 5)
- std::shared_ptr(The Rule of 5/0)
Data Member Initialization
Obeying to Cpp Core Guideline
Prefer Free Function
设计功能接口在大多数时候应该使用非成员函数(free function)
接口设计基本规则遵循:
1 | if(f needs to be virtual) |
links:
Encapsulate in namespace
使用 Namespace 封装隔离,避免命名冲突
Use std::string_view
使用 std::string_view 提供了一种更安全、更灵活的方式来处理字符串文字,它允许编译时评估,避免潜在的缓冲区溢出,并提供方便的接口来处理字符串
1 | constexpr std::string_view cnt = "abc"; // instead of inline const std::string or inline const char* |
Make constants in configuration
将常量配置配置为读取配置解析获取,避免重复编译.
Comment the purpose and usage
对代码补充目的性的注释和用法示例,增加可读性.
Use byte array or data structure
使用字节数组或数据结构表达十六进制流.
1 | std::array<uint8_t,4> bytes{0x1,0x2,0x3,0x4}; |
Seal coupled parameters
但传递的参数时描述一个东西时,考虑将其封装为一个结构。常见的用法是将 buffer 和长度作为参数,此时可考虑将其封装为一个结构或使用 std::span.
Seal too many defaults
将过多的参数封装为结构体,增加可读性.
links:
1 | class server{ |
Must be initialized
C++20 提供了结构体命名委托初始化器,若没有传参数,gcc -Wextra 给出警告,clang/msvc 不做检查,所以有必要对类型设置必须初始化约束.
links:
1 | template<typename T> |
Create Unique type with enum for Integer types
1 | enum class server_id: int {}; |
Error Handling Strategies
Design Pattern
links:
Idioms C++
links:
X Macro
X macro 是一种可靠地维护并行代码和 / 或数据列表的技术,其对应的项必须以相同的顺序声明或执行,例如,列表元素的定义在 .inl
文件中 示例。
RAII-Resource Acquisition Is Initialization
资源获取即初始化 (简称 RAII )是 C++ 防止内存泄露一个很好解决方案,它结合构造函数和析构函数,把资源生命周期和对象生命周期绑定起来,在构造函数中获取资源(这些错误会引发异常),然后将其释放到析构函数中(永不抛出),并且不需要显式清理,从而防止忘记释放资源;参考示例。
Thread-safe singleton
links:
Policy-based class Design
links:
基于策略设计又名 policy-based class design 是一种基于 C 计算机程序设计模式,以策略(Policy)为基础,并结合 C 的模板元编程。就是将原本复杂的系统,拆解成多个独立运作的 “策略类别”,每一组 policy class 都只负责单纯如行为或结构的某一方面。多重继承由于继承自多组 Base Class,故缺乏型别消息,而 Templetes 基于型别,拥有丰富的型别消息。多重继承容易扩张,而 Templetes 的特化不容易扩张。Policy-Based Class Design 同时使用了 Template 以及 Multiple Inheritance 两项技术,结合两者的优点,看下面例子:
1 | template <class Policy1, class Policy2, class Policy3> |
PolicyBasedClass 则称为宿主类别(host class),只需要切换不同 Policy Class,就可以得到不同的需求。Policy 不一定要被宿主(host)继承,只需要用委托(delegation)完成这一工作。但 policies 必须遵守一个隐含的 constraint,接口(interface)必须一样,故参数不能有巨大改变。
Policy class 有点类似回调函数(callbacks),但不同的是,callback 只是一个函数,至于 policy class 则尽可能包含许多的 functions (methods), 有时还会结合状态变量(state variables)与其他各式各样的类型,如嵌套类型(nested types)。
policy 的一个重要的特征是,宿主类别(host class)经常(并不一定要)使用多重继承的机制去使用多个 policy classes. 因此在进行 policy 拆解时,必须要尽可能达成正交分解(Orthogonal Decomposition),policy 彼此独立运作,不相互影响。
Template Template Parameter
在 C++ 的 Policy-Based Design 中,用来建构 Template 的类别参数(也就是 policy class),本身亦可以是一个 Template 化的类别,形成所谓的 Template Template Parameter。
如果 Read ()、Write () 有各种不同类型的参数时,可以利用 template 的不完全具现化 (Incomplement Instantiation) 特征检实现各个参数不同的成员函数(member function)。在 host class 中,可以撰写不同参数版本的 Read (…) 函数,这有赖于 c++ compiler 的协助。
1 | template |
上述的 class T 即是一个 Template Template Parameter,这使得 Policy Class 更具扩展性与弹性,能够处理各种类型的实体(instance)。
-
Hello World 简单示例
1 | template< |
后续可以更容易的撰写其他的 Output policy, 单靠创造更新的 Policy class 并实现 print 于其中。
Pimpl - Pointer to implementation
Pimpl 是一种广泛使用的削减编译依赖项的技术,具体讨论参考,看下面例子可能就明白了:
1 | // cppreference 官方示例 |
此技巧用于构造拥有稳定 ABI 的 C++ 库接口,及减少编译时依赖。
links:
CRTP -The curiously recurring template pattern
CRTP (奇异递归模板模式)是一种在编译期实现多态方法,是对运行时多态一种优化,多态是个很好的特性,但是动态绑定比较慢,因为要查虚函数表。而使用 CRTP,完全消除了动态绑定,降低了继承带来的虚函数表查询开销。
-
从模板类继承,
-
使用派生类本身作为基类的模板参数。
1 |
|
这样做的目的是在基类中使用派生类。从基础对象的角度来看,派生对象本身就是对象,但是是向下转换的对象。因此,基类可以通过将 static_cast 自身放入派生类来访问派生类.
实现简单策略调用示例
1 | /** |
HAP - Handler on Aggregation with Polymorphism
HAP 提供了一种方法如何在使用多态和虚方法时保持通用性和良好的性能,以及如何使用聚合。
-
虚方法对集合整体分发一次。
-
使用基于类策略模式简化策略 Handler 的调用。
-
处理器 Handler 仅在运行时可知。
NVI - Non Virtual Interface
仅暴露非虚方法接口,避免调用歧义.
1 | struct HandlerBase { |
MutextProtected
这里提供了一种 C++ 方式的并行访问数据加锁模式,通常通过 RAII 实现。
另见,Andreas Kling - MutextProtected、Synchronized - folly、MutextProtected - reddit。
TEBI - Type erease better than inheritance
在解耦,类型擦除更优于继承,避免了多重继承,导致依赖过多。
参考示例:
links:
Dependency Injection
-
抽离实际系统的简单的系统或部分功能。
-
不适用于小项目
-
暂时不关注,用于之后追加
-
用于测试
links:
基本流程
1 | Data in(Usage) -> |Interface -> Capture(Implementation) -> Synthesize(Implementation) -> |Interface -> Results out(Usage) |
基本方法
-
使用方法参数注入 Link-time(不建议使用)
-
使用虚方法和继承
-
使用模板
-
使用类型擦除
方法参数
需要实现两次,一侧用于实际生产的代码,另一侧实现用于测试
pros:
-
对实际代码不改变
-
允许一定范围的测试
cons: -
难以管理,需要对测试的组件部分都写一遍
-
违反 ODR/UB
-
更像集成测试
使用虚方法和继承
创建一个接口基类,或从存在的类继承
-
能处理很多接口
-
易于理解的重写机制
-
易于增加到原有的代码
google mock 示例:
使用模板
gmock 示例:
c++20 concept:
使用类型擦除
Dependency Injection Basic
Setter DI
Method DI
Constructor DI
Reflection
lock-free
-
原子操作 (atomic operation): atomic
-
lock-free data structure
-
内存顺序 (memory ordering): memory_order
-
原子标志 (atomic flag): atomic_flag
ABI C++
links:
unwind exception 堆栈异常展开
这里介绍 C++ exception handling,stack unwinding 的一个应用。Exception handling 有多种 ABI (interoperability of C++ implementations),其中应用最广泛的是 Itanium C++ ABI: Exception Handling
stack unwind 堆栈展开
Stack unwinding 主要有以下作用:
-
获取 stack trace,用于 debugger、crash reporter、profiler、garbage collector 等
-
加上 personality routine 和 language specific data area 后实现 C++ exceptions (Itanium C++ ABI)。参见 C++ exception handling ABI
Stack unwinding 可以分成两类:
synchronous: 程序自身触发的,C++ throw、获取自身 stack trace 等。这类 stack unwinding 只发生在函数调用处 (在 function body 内,不会出现在 prologue/epilogue)
asynchronous: 由 signal 或外部程序触发,这类 stack unwinding 可以发生在函数 prologue/epilogue
MinGW ABI
How to Link MSVC DLLs with MinGW GCC in Windows
links:
-
https://stackoverflow.com/questions/34177792/link-a-msvc-compiled-dll-in-a-mingw-built-project
-
https://stackoverflow.com/questions/7241047/linking-lib-files-with-mingw
-
interoperability of libraries created by different compilers brands
Native API
jni
java 提供 invocation api 供三方调用 jvm native api
QA:
-
创建 JVM 虚拟机及句柄:
links:
Weekly Practice
Void Pointer Arithmetic
避免指针运算有利于提高安全及代码通用性
links:
std::expected/std::optional
c++23 提出的有力的返回值 / 错误处理工具。向后兼容实现 tl-expected
core guideline
Development Utilities
这里搜集了一些用于增强 cpp 开发效率的工具。
links:
Optimization
perf
links:
-
https://stackoverflow.com/questions/25129751/how-can-i-use-perf-to-profile-my-code
-
https://dev.to/etcwilde/perf---perfect-profiling-of-cc-on-linux-of
Guidelines
links:
-
https://www.geeksforgeeks.org/competitive-programming-a-complete-guide
-
the art of writing efficient programs
测试
依赖非代码文件
当测试代码依赖于非代码文件时,需要慎重考虑,是否能拆成更小的测试用例,或者使用 mock 对象。
如果无法避免,可以使用合理的资源管理机制。
links:
GTest Main
当 Gtest 开启在 cmake 中配置时,可以忽略在测试文件中 main 定义
1 | include(GoogleTest) |
通常省略以下定义
1 | int main(int argc, char const* argv[]) { |
语义优化(语言相关)
减少 virtual
cin.tie 和 ios::sync_with_stdio
links:
算法
数据结构(容器)
hash
sort
排序算法是一种将一组数据按照特定顺序进行排列的算法。常见的排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序和堆排序等。这些算法的实现方式各不相同,但它们的目的都是将数据按照一定的规则进行排序。
常见的排序算法
-
冒泡排序:冒泡排序是一种简单的排序算法,它的基本思想是通过不断交换相邻的元素来将较大的元素逐渐 “冒泡” 到数组的末尾。冒泡排序的时间复杂度为 O (n^2),空间复杂度为 O (1)。
-
选择排序:选择排序是一种简单的排序算法,它的基本思想是每次从未排序的元素中选择最小的元素,然后将其放到已排序的元素末尾。选择排序的时间复杂度为 O (n^2),空间复杂度为 O (1)。
-
插入排序:插入排序是一种简单的排序算法,它的基本思想是将未排序的元素插入到已排序的元素中的正确位置。插入排序的时间复杂度为 O (n^2),空间复杂度为 O (1)。
-
快速排序:快速排序是一种高效的排序算法,它的基本思想是通过不断地分治和递归来将数组分成两个子数组,然后对子数组进行排序。快速排序的时间复杂度为 O (nlogn),空间复杂度为 O (logn)。
-
归并排序:归并排序是一种高效的排序算法,它的基本思想是通过不断地分治和递归来将数组分成两个子数组,然后对子数组进行排序,最后将两个有序的子数组合并成一个有序的数组。归并排序的时间复杂度为 O (nlogn),空间复杂度为 O (n)。
-
堆排序:堆排序是一种高效的排序算法,它的基本思想是通过不断地建堆和取堆顶元素来将数组排序。堆排序的时间复杂度为 O (nlogn),空间复杂度为 O (1)。
STL 实现
在 C++ STL 中,std::sort 函数是一个非常常用的排序函数。根据官方文档,std::sort 函数的内部实现包含了三种排序算法:快速排序、堆排序和插入排序。在默认情况下,std::sort 使用快速排序算法,但是当元素数量较少时,它会切换到插入排序算法,以提高性能。当元素数量较大时,std::sort 会使用堆排序算法,以避免快速排序的最坏情况。
以下是 std::sort 函数的内部实现:
1 | template<class _RanIt, |
在这个实现中,Ideal 参数表示理想的子序列大小,Pred 参数表示比较函数。如果 Ideal 小于 SORTMAXSIZE(16),则使用插入排序算法。如果 Ideal 大于 SORTMAXSIZE 并且元素数量大于 SORTMAXSIZE,则使用快速排序算法。否则,使用堆排序算法。在快速排序算法中,std::sort 使用了一个叫做 unguardedpartition 的函数来进行分区操作。在堆排序算法中,std::sort 使用了一个叫做 Heapsort 的函数来进行排序操作。
Orbit
Orbit 是 Stadia 上的性能分析解决方案。
todo
Profiler
links:
图形学
矩阵三维变换
一般三维变换分为缩放、旋转、平移。
矩阵
M x N:M 宽度 = N 的高度
前面提到的三维变换非常容易可以替换为矩阵运算。这里 1 是为了配合平移变换计算。
曲线拟合
二次曲线拟合
以二次拟合曲线为例,方程可以表示为 y = a*x*x + b*x + c
,其中 a、b 和 c 是待求的系数,x 和 y 分别是点的横坐标和纵坐标,可以使用三个点的坐标来列出三个方程:
1 | y1 = a * x1^2 + b * x1 + c |
将这三个方程化简,可以得到以下矩阵方程:
1 | [ x1^2 x1 1 ] [ a ] [ y1 ] |
可以使用矩阵的逆来求解系数向量 [a, b, c],具体来说,系数向量可以表示为:
1 | [ a ] [ x1^2 x1 1 ]^-1 [ y1 ] |
然后,可以使用 for 循环遍历 3 个点之间的所有横坐标,计算对应的纵坐标,得到绘制拟合曲线上的点。需要注意的是,这个 Godbolt 示例程序只是一个简单的演示,实际的拟合曲线算法可能更加复杂和精确。
密码学
links:
基础工具
编码转换
wstring and string
优先使用 wcsrtombs/mbsrtowcs, 参考示例.
框架库
STL
C++ 标准库实现.
links:
abseil
简介
Abseil 位于 github 包含了对标准库的补充,并提供了一些常用的开发基础能力,快速开始。
主要包括 algorithm, cleanup, container, crc, debugging, flags(handle command flags), hash, iterator, logging, memory, profiling 等工具
hotels-template-library
htl 包含了用于使开发更安全、有效的工具模板。
kicad
kicad 属计算机辅助设计 CAD (Computer Aided Design)
Boost.Geometry
如果想学习 Boost.Geometry,可以从以下几个方面入手:
-
了解 C++ 模板元编程和泛型编程的基本概念和技术,这是使用 Boost.Geometry 的前提条件。
-
学习 Boost.Geometry 的基本数据类型和算法,如点、线、多边形、圆等,以及它们之间的关系,如相交、包含等。
-
学习 Boost.Geometry 的高级功能,如几何图形集合、几何图形索引等,这些功能可以提高几何算法的效率和精度。
-
了解 Boost.Geometry 与其他 C++ 库的集成方式,如 STL、Boost 等。
-
参考 Boost.Geometry 的官方文档和示例代码,这些资源可以帮助更好地理解和使用 Boost.Geometry。
如果想深入学习 Boost.Geometry,可以参考以下资源:
-
Boost.Geometry 的官方文档:https://www.boost.org/doc/libs/1_76_0/libs/geometry/doc/html/index.html
-
Boost.Geometry 的 GitHub 仓库:https://github.com/boostorg/geometry
-
Boost.Geometry 的示例代码:https://www.boost.org/doc/libs/1_76_0/libs/geometry/doc/html/geometry/examples.html
-
《Boost.Geometry 入门与实践》一书,该书详细介绍了 Boost.Geometry 的基本概念、数据类型和算法,以及如何使用 Boost.Geometry 解决实际问题。
single file libs
Boost
Boost 是 C++ 必学的三方库。
dynamix
dynamix 是 C++ 在多态编程支持健壮的 Mixin 库。
proxy
c++20 支持的运行时多态代理
links:
Gstreamer
Gstreamer 是一个功能强大的多媒体框架,可以用于音频和视频处理。它是一个基于管道的框架,可以将多个元素(例如音频解码器、视频编码器、过滤器等)连接在一起,以实现所需的处理。以下是一些有关 Gstreamer 的基本信息:
-
Gstreamer 是一个开源框架,可以在 Linux、Windows 和 macOS 等操作系统上运行。
-
Gstreamer 使用 C 语言编写,但也提供了许多其他语言的绑定,例如 Python、Java 和 C# 等。
-
Gstreamer 提供了许多插件,可以用于处理各种音频和视频格式,例如 MP3、H.264 等。
-
Gstreamer 还提供了许多工具,例如 gst-launch 和 gst-inspect 等,可以帮助构建和调试管道。
开发环境
推荐使用 vcpkg 配置:
1 | vcpkg install gstreamer |
demo 示例
完整的 Gstreamer 示例 demo,可以在 GitHub 上找到许多开源项目。以下是一些示例项目:
-
gst-examples:官方 Gstreamer 示例项目,包含许多使用 Gstreamer 的示例管道。
-
gst-plugins-good:官方 Gstreamer 插件项目,包含许多有用的插件,例如音频解码器、视频编码器等。
-
gst-python-tutorials:使用 Python 编写的 Gstreamer 示例项目,适用于初学者。
-
gst-rtsp-server:使用 Gstreamer 实现的 RTSP 服务器,可以用于流媒体传输。
Eigen
Eigen3 ,它是一个高性能的线性代数库,提供了各种矩阵和向量的操作。Eigen3 中的矩阵是通过模板类 Matrix 实现的,可以使用静态或动态大小。以下是一些常见的操作:
-
创建矩阵:Matrix mat; MatrixXd mat (3, 3);
-
矩阵乘法:mat1 * mat2
-
矩阵转置:mat.transpose ()
-
矩阵求逆:mat.inverse ()
-
矩阵行列式:mat.determinant ()
-
矩阵特征值:mat.eigenvalues ()
-
矩阵特征向量:mat.eigenvectors ()
更多详细信息可以参考 Eigen3 官方文档。
如果需要更多关于 Eigen3 的信息,可以参考以下资源:
gRPC
gRPC 是一种高性能、开源和通用的远程过程调用(RPC)框架。它可以在任何地方运行,从而使客户端和服务器应用程序能够透明地相互通信。gRPC 使用 Protocol Buffers 作为其默认的序列化机制,这使得它非常高效。它还支持多种编程语言,包括 C++、Java、Python、Go、Ruby、C# 和 Objective-C 等。
网络库
wxwidget
cpp 跨平台 ui 框架啊
links:
wfrest
Web Framework RESTful API in C++.
links:
bpf
links: