cpp 实践

开发社区

搜集的 C++ 问答社区

相关资源

C++ 比较庞杂,开源社区资源,整理如下:

资源 组织 类型
ISO CPP isocpp 规范
C++ Core Guidelines isocpp 规范
C++ Note TOMO-CAT 基本语法
C++ Tips Abseil 及 Google 规范
Google C++ Style Guide Google 规范
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
2
3
4
5
6
7
8
9
10
11
12
"name": "C++",
"build": {
"dockerfile": "Dockerfile.dev",
"args": {
// Update 'VARIANT' to pick an Ubuntu OS version: ubuntu-22.04, ubuntu-21.04, ubuntu-20.04, ubuntu-18.04
// Use Ubuntu 18.04 or Ubuntu 21.04 on local arm64/Apple Silicon
"VARIANT": "ubuntu-22.04",
// User in container
"USERNAME": "vscode"
},
"context": "./.."
},

其中, Dockerfile.dev 中基础镜像选择 mcr.microsoft.com/devcontainers/cpp 进行扩展:

1
2
ARG VARIANT=ubuntu-22.04
FROM mcr.microsoft.com/vscode/devcontainers/cpp:${VARIANT}

Extsension

用于 Cpp 开发的常用 VSCode 插件:

  • C/C++:官方提供的 C++ 插件,提供了代码补全、语法高亮、调试等功能。

  • C++ Intellisense:提供了更强大的代码补全功能,支持头文件、宏定义等。

  • C++ TestMate:提供了一个测试资源管理器,可以方便地运行和调试 C++ 测试。

  • C++ Insights:提供了一个侧边栏,可以方便地查看 C++ 代码的编译器输出。

  • CMake Tools:提供了一个集成的 CMake 工具,可以方便地生成和构建 C++ 项目。

1
2
3
4
5
6
7
8
9
"extensions": [
"ms-vscode.cpptools",
"austin.code-gnu-global",
"mitaki28.vscode-clang",
"twxs.cmake",
"vector-of-bool.cmake-tools",
"llvm-vs-code-extensions.vscode-clangd",
"mjbvz.vscode-lldb",
],

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 配置区分可分为单配置类型生成器和多配置类型生成器

配置内置变量说明

这静态指定将在此构建树中构建的构建类型(配置)。可能的值为空、 DebugReleaseRelWithDebInfoMinSizeRel... 这个变量只对单配置生成器(例如 Makefile GeneratorsNinja )有意义,即那些在 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_TYPERelease 。该变量配置类型总是存在。

该类型在单配置类型是总是为空。否则,按照生成器默认的类型生成 DebugReleaseRelWithDebInfoMinSizeRel 等类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# multi config setting
get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL
PROPERTY GENERATOR_IS_MULTI_CONFIG)
message(STATUS "GENERATOR_IS_MULTI_CONFIG ${GENERATOR_IS_MULTI_CONFIG}")
message(STATUS "CMAKE_CONFIGURATION_TYPES ${CMAKE_CONFIGURATION_TYPES}")
message(STATUS "CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")

if(${GENERATOR_IS_MULTI_CONFIG})
set(multi_config_path
"${CMAKE_BUILD_TYPE}"
CACHE STRING "Multi Config Path" FORCE)
endif()
# write a json file indicating the multi config mode
file(
WRITE ${CMAKE_BINARY_DIR}/multi_config.json
"
{
\"multi_config\": ${GENERATOR_IS_MULTI_CONFIG}
}")

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
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
{
"version": 3,
"configurePresets": [
{
"name": "common-base",
"hidden": true,
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}"
},
{
"name": "linux-base",
"hidden": true,
"inherits": "common-base",
"generator": "Ninja",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"architecture": {
"value": "x64",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
"hostOS": [
"Linux"
]
},
"microsoft.com/VisualStudioRemoteSettings/CMake/1.0": {
"sourceDir": "$env{HOME}/.vs/$ms{projectDirName}"
}
}
},
{
"name": "linux-x64-debug",
"inherits": "linux-base",
"displayName": "x64 Debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": false
}
},
{
"name": "linux-x64-Release",
"inherits": "linux-base",
"displayName": "x64 Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"USE_SANITIZER": false
}
},
{
"name": "linux-x64-asan",
"inherits": "linux-base",
"displayName": "x64 sanitize=address",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "Address"
}
},
{
"name": "linux-x64-tsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=thread",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "Thread"
}
},
{
"name": "linux-x64-lsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=leak",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "Leak"
}
},
{
"name": "linux-x64-ubsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=undefined",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "Undefined"
}
},
{
"name": "windows-base",
"hidden": true,
"inherits": "common-base",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl"
},
"architecture": {
"value": "x64",
"strategy": "external"
},
"toolset": {
"value": "host=x64",
"strategy": "external"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows-x64-debug",
"inherits": "windows-base",
"displayName": "x64 Debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": false
}
},
{
"name": "windows-x64-Release",
"inherits": "windows-base",
"displayName": "x64 Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"USE_SANITIZER": false
}
},
{
"name": "windows-x64-asan",
"inherits": "windows-base",
"displayName": "x64 sanitize=address",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "Address"
}
}
],
"testPresets": [
{
"name": "common-base",
"description": "Basic shared test settings",
"hidden": true,
"execution": {
"noTestsAction": "error",
"stopOnFailure": false
},
"output": {
"outputOnFailure": true
}
},
{
"name": "linux-base",
"inherits": "common-base",
"hidden": true
},
{
"name": "windows-base",
"inherits": "common-base",
"hidden": true
},
{
"name": "linux-x64-debug",
"inherits": "linux-base",
"displayName": "x64 Debug",
"configurePreset": "linux-x64-debug"
},
{
"name": "linux-x64-Release",
"inherits": "linux-base",
"displayName": "x64 Release",
"configurePreset": "linux-x64-Release"
},
{
"name": "linux-x64-asan",
"inherits": "linux-base",
"displayName": "x64 sanitize=address",
"configurePreset": "linux-x64-asan"
},
{
"name": "linux-x64-tsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=thread",
"configurePreset": "linux-x64-tsan"
},
{
"name": "linux-x64-lsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=leak",
"configurePreset": "linux-x64-lsan"
},
{
"name": "linux-x64-ubsan",
"inherits": "linux-base",
"displayName": "x64 sanitize=undefined",
"configurePreset": "linux-x64-ubsan"
},
{
"name": "windows-x64-debug",
"inherits": "windows-base",
"displayName": "x64 Debug",
"configurePreset": "windows-x64-debug"
},
{
"name": "windows-x64-Release",
"inherits": "windows-base",
"displayName": "x64 Release",
"configurePreset": "windows-x64-Release"
},
{
"name": "windows-x64-asan",
"inherits": "windows-base",
"displayName": "x64 sanitize=address",
"configurePreset": "windows-x64-asan"
}
]
}
模板二
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
{
"version": 3,
"configurePresets": [
{
"name": "base",
"displayName": "Basic Config",
"description": "Basic build using Ninja generator",
// The generator is always ninja because of cross-platform performance
"generator": "Ninja",
// It enables hidden when presets present
"hidden": true,
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}"
},
{
// x64 architecture conf
"name": "x64",
"architecture": {
"value": "x64",
"strategy": "external"
},
"hidden": true
},
{
"name": "x86",
"architecture": {
"value": "x86",
"strategy": "external"
},
"hidden": true
},
{
// Debug conf
"name": "Debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
// Custom definition variable
"USE_SANITIZER": false
},
"hidden": true
},
{
"name": "Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"USE_SANITIZER": false
},
"hidden": true
},
{
"name": "MSVC",
"hidden": true,
// MSVC compiler
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl"
},
// MSVC toolset conf
"toolset": {
"value": "host=x64",
"strategy": "external"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "Clang",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"toolset": {
"value": "host=x64",
"strategy": "external"
}
},
{
"name": "GNUC",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"toolset": {
"value": "host=x64",
"strategy": "external"
}
},
{
"name": "x64-Debug-MSVC",
"description": "MSVC for x64 (Debug)",
"inherits": [
"base",
"x64",
"Debug",
"MSVC"
]
},
{
"name": "x64-Release-MSVC",
"description": "MSVC for x64 (Release)",
"inherits": [
"base",
"x64",
"Release",
"MSVC"
]
},
{
"name": "x64-Debug-Clang",
"description": "Clang/LLVM for x64 (Debug)",
"inherits": [
"base",
"x64",
"Debug",
"Clang"
]
},
{
"name": "x64-Release-Clang",
"description": "Clang/LLVM for x64 (Release)",
"inherits": [
"base",
"x64",
"Release",
"Clang"
]
},
{
"name": "x64-Debug-GNUC",
"description": "GNUC for x64 (Debug)",
"inherits": [
"base",
"x64",
"Debug",
"GNUC"
]
},
{
"name": "x64-Release-GNUC",
"description": "GNUC for x64 (Release)",
"inherits": [
"base",
"x64",
"Release",
"GNUC"
]
},
{
"name": "x64-Debug-MSVC-asan",
"inherits": "x64-Debug-MSVC",
"description": "x64-Debug-MSVC with /fsanitize=address",
"cacheVariables": {
"USE_SANITIZER": "Address"
}
},
{
"name": "x64-Debug-GNUC-asan",
"inherits": "x64-Debug-GNUC",
"cacheVariables": {
"USE_SANITIZER": "Address"
}
},
{
"name": "x64-Debug-GNUC-tsan",
"inherits": "x64-Debug-GNUC",
"cacheVariables": {
"USE_SANITIZER": "Thread"
}
},
{
"name": "x64-Debug-GNUC-lsan",
"inherits": "x64-Debug-GNUC",
"cacheVariables": {
"USE_SANITIZER": "Leak"
}
},
{
"name": "x64-Debug-GNUC-ubsan",
"inherits": "x64-Debug-GNUC",
"cacheVariables": {
"USE_SANITIZER": "Undefined"
}
}
],
"testPresets": [
{
"name": "x64-Debug-MSVC",
"configurePreset": "x64-Debug-MSVC"
},
{
"name": "x64-Release-MSVC",
"configurePreset": "x64-Release-MSVC"
},
{
"name": "x64-Debug-Clang",
"configurePreset": "x64-Debug-Clang"
},
{
"name": "x64-Release-Clang",
"configurePreset": "x64-Release-Clang"
},
{
"name": "x64-Debug-GNUC",
"configurePreset": "x64-Debug-GNUC"
},
{
"name": "x64-Release-GNUC",
"configurePreset": "x64-Release-GNUC"
},
{
"name": "x64-Debug-MSVC-asan",
"configurePreset": "x64-Debug-MSVC-asan"
}
]
}
模板三
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
42
43
44
45
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"patch": 0
},
"configurePresets": [
{
"name": "default",
"displayName": "Default Config",
"binaryDir": "${sourceDir}/build",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"VCPKG_OVERLAY_PORTS": "${sourceDir}/deps",
"CMAKE_INSTALL_PREFIX": "out",
"ASIO_GRPC_BUILD_TESTS": "TRUE",
"ASIO_GRPC_DISCOVER_TESTS": "TRUE"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default",
"configuration": "Debug"
}
],
"testPresets": [
{
"name": "default",
"configurePreset": "default",
"configuration": "Debug",
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error",
"timeout": 180,
"jobs": 8
}
}
]
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# unless specified version, read the file pyproject.toml version information
if(PROJECT_VERSION)
message(STATUS "explicit the version ${PROJECT_VERSION}")
else()
file(
STRINGS "${CMAKE_SOURCE_DIR}/pyproject.toml" ver_read
ENCODING "UTF-8"
REGEX [[^version = "(.+)"]])
string(REGEX MATCH [[^version = "([^-]+)-?(.+)?"]] matches_out "${ver_read}")
message(
STATUS
"Read the project version ${CMAKE_MATCH_0} from the file pyproject.toml ${ver_read}"
)
if("${CMAKE_MATCH_1}" STREQUAL "")
message(FATAL_ERROR "we need a version specified.")
endif()
set(CMAKE_PROJECT_VERSION ${CMAKE_MATCH_1})
if(CMAKE_MATCH_2)
set(VERSION_SUFFIX ${CMAKE_MATCH_2})
message(STATUS "VERSION_SUFFIX:${VERSION_SUFFIX}")
endif()
endif()
变量
模块
命令行
  • 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
2
3
4
5
6
# GNU install standard dirs
include(GNUInstallDirs)
# Configure uniform output directory for Runtime,Libraries,archives
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

配置 _DEBUG 宏

通常 Windows 下调试时会配置 _DEBUG 宏,但在 Linux 下需要手动增加配置。

1
2
3
4
# Debug configuration
if(CMAKE_BUILD_TYPE STREQUAL Debug)
add_definitions(-D_DEBUG)
endif()

追加 CMake 脚本路径

通常会将工程的 cmake 脚本放到一个 cmake 的目录,然后统一在 CMAKE_MODULE_PATH 中导入,方便后续调用相应模块。

1
2
# CMake module tools import custom cmake modules
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

CMake 中注释

#[[注释可以跨越多行,并且不会被编译器解释为代码。

[!CAUTION]
多行注释不会被 cmake-format 格式化。

1
2
3
4
#[[ This is
a multi-line
comment ]]
# This is a single-line comment
1
2
3
4
5
6
7
8
9
10
#[=======================================================================[.rst:
Title
-----

The first line takes up 80 spaces with rst style syntax declaration.The last line takes up 75 spaces.

Variables
^^^^^^^^^

#]=======================================================================]

将选项通过目标链接

其它目标依赖通过将编译或链接选项生成一个 INTERFACE 目标,进而传递该目标包含的各种配置选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Add a common compile setting interface
add_library(common-compile-settings INTERFACE)

target_compile_features(common-compile-settings INTERFACE cxx_std_11)

target_compile_options(common-compile-settings INTERFACE
# clang-like or GNU flags
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
-Wall -Wextra -Wno-unused-parameter -Wno-type-limits -Werror>
# msvc flags
$<$<CXX_COMPILER_ID:MSVC>:/W0 /D_WIN32_WINNT=0x0A00 /EHsc>
)

# Use the interface with PRIVATE link mode so that the settings would
# not be propergated to a target depending on the target other_tgt
add_library(other_tgt
src/some.cpp
)

target_link_libraries(
other_tgt
PRIVATE
common-compile-settings
)

搜索文件

方式一: file(GLOB_RECURSE ...) 搜索文件的方式可以方便地获取指定目录下的所有文件,包括子目录中的文件。然而,这种方式并不推荐在大型项目中使用,因为它会导致以下问题:

  • 性能问题:由于搜索整个目录树,因此会导致性能问题,特别是在大型项目中。

  • 不稳定性:由于该命令依赖于文件系统,因此可能会受到文件系统的限制,例如文件名长度限制。

  • 可读性问题:使用该命令可能会使 CMakeLists.txt 文件变得难以阅读和维护。

因此,对于大型项目,建议手动列出所有源文件,以便更好地控制构建过程。如果仍然想使用该命令,请确保的项目规模较小,并且已经测试了该命令的性能和稳定性。

方式二:aux_source_directory(<dir> <variable>) 是一个旧的 CMake 命令,用于将指定目录中的所有源文件添加到一个变量中。然而,这个命令已经被弃用了,因为它不能正确地处理源文件的依赖关系。相反,应该使用 file(GLOB_RECURSE ...) 命令来获取源文件列表,然后将它们添加到目标中。

导出符号

控制 cpp 符号导出在动态或插件链接库是非常必要的步骤,在 windows 中符号默认都不导出,linux 中默认都导出。

通常为了跨平台实现编译链接,需要使用针对性处理。

1
2
3
#  only export all symbols on windows but no static symbols
set_target_properties(Tgt PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON)

links:

禁用 CMakeLists.txt 路径构建

对构建目录存在 CMakeLists.txt 脚本目录的情况报错,其目的是强制将构建缓存和构建脚本分离。

1
2
3
4
5
6
7
8
9
# Error if building out of a build directory
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH)
if(EXISTS "${LOC_PATH}")
message(
FATAL_ERROR
"You cannot build in a source directory (or any directory with "
"CMakeLists.txt file). Please make a build subdirectory. Feel free to "
"remove CMakeCache.txt and CMakeFiles.")
endif()

符号可见性

CMake 中 Visibility Control 标准的库符号导出,需要严格控制。windows 下默认不导出,linux 默认全导出。这里通过 CMake 控制符号导出能减小这部分工作量。

通常尽可能预设将所有符号隐藏,然后使用 generate_export_header 生成符号导出修饰前缀。

1
2
3
4
5
6
7
8
9
10
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)

add_library(MyTgt ...)

# 生成合适的 mytgt_export.h
# 确保 MyTgt_EXPORT 定义
# 添加符号导出宏 MyTgt_EXPORT 添加到目标 MyTgt
include(GenerateExportHeader)
generate_export_header(MyTgt)

库版本控制

对于动态库,需要做 Library Version 时,CMake 也提供了工具。

1
2
3
4
5
6
7
8
9
10
add_library(Example ...)

# libExample.so -> libExample.so.2.4.7 NAME LINK
# libExample.so.2 -> libExample.so.2.4.7 SONAME
# libExample.so.2.4.7 REAL LIBRARY
set_target_properties(
Example PROPERTIES
SOVERSION 2
VERSION 2.4.7
)

[!WARNING]
如果缺失 SOVERSION,如果 Example 只做了一个简单的内部修改,然后增加了 patch 为 2.4.8,对于依赖于 Example 的 2.4.7 版本的项目来说,简单更新 Example 库就不再合适,而是需要重新编译链接指向 2.4.8,但这实际上是不必要的。所以都建议加上 SOVERSION

包版本控制,需要导出版本 cmake 脚本。

1
2
# require AConfig.cmake AConfigVersion.cmake
find_package(A 2.3)

使用工具自动生成

1
2
3
4
5
6
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
AConfigVersion.cmake
VERSION 2.4.7
COMPATIBILITY SameMajorVersion
)

links:

打包库

配置安装规则 (Installing Rules)。安装规则包括安装运行库、头文件、文档等等,以下是不断对安装规则不断改进的过程。

links:

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
# Version 1: 只是安装对应工件
include(GNUInstallDis) # GNU dir-installed layout
install(TARGETS Example
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
# CMake >=3.14 same as [[install(TARGETS Example)]] as the above


# Version 2:通常安装包会区分运行库(Runtime)和开发库(Development)
install(TARGETS Example
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT Runtime
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT Runtime
NAMELINK_COMPONENT Development # CMake >= 3.12
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT Development
)

# Version 3: 通常加上 SomeProj_ 前缀避免安装规则被其它项目通过 add_subdirctory 的集成的安装规则干扰
install(TARGETS Example
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT SomeProj_Runtime
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT SomeProj_Runtime
NAMELINK_COMPONENT SomeProj_Development
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT SomeProj_Development
)

构建库及依赖的 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
2
3
4
5
6
7
8
9
10
# Simple rpath configuration

# on Apple are there different keyword @loader_path
# check env variables first
# recursive searching like DT_RPATH
if(NOT APPLE)
# $ORIGIN means the location of the binary requiring the dependency
set(CMAKE_INSTALL_RPATH $ORIGIN)
endif()
add_library(Example ...)

[!NOTE]
当使用动态链接器(ld.so)加载共享库时,$ORIGIN 可以用于指定相对于可执行文件的路径的搜索目录。这使得共享库可以相对于可执行文件的位置进行定位,而不是使用绝对路径或者在系统范围内搜索共享库。

@loaderpath(macOS)$ORIGIN(Unix) 都是用于在可执行文件和共享库中指定动态链接器搜索依赖项的路径的特殊字符串,指向包含当前可执行文件或共享库的目录。它们的主要区别在于它们的作用范围和解析方式。

@loaderpath 不能在环境变量中使用。

$ORIGIN 的解析是在运行时进行的,而不是在链接时进行的。这意味着 $ORIGIN 可以用于在不同的环境中运行相同的可执行文件或共享库,而不需要修改它们的路径。例如,如果可执行文件或共享库位于 /path/to/myapp/bin/myapp 中,则 $ORIGIN 将被解析为 /path/to/myapp/bin/,即可执行文件所在的相对路径,无论在哪个目录中运行它。

另见 stackoverflowld.

以下是一个示例,其中使用了 $ORIGIN 变量来加载共享库:

假设有一个可执行文件 /home/user/myapp,它需要加载一个共享库 libfoo.so,该共享库位于可执行文件所在目录的 lib 子目录中。可以使用以下命令来加载共享库:

1
2
3
4
5
cd /home/user
mkdir lib
cp /path/to/libfoo.so lib/
export LD_LIBRARY_PATH=$ORIGIN/lib
./myapp

在这个例子中,将共享库复制到可执行文件所在目录的 lib 子目录中,并使用 $ORIGIN/lib 来指定共享库的搜索目录。这使得动态链接器可以相对于可执行文件的位置找到共享库。

根据之前的讨论,为 bin executable 配置 rpath 为当前路径lib 所在路径如下:

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
#[=======================================================================[.rst:
Runpath Configuration
------------------

This scripts setup a runpath properly when use add_library to generate
a shared library. The path of shared libraries will be pointed to lib
directory by variable CMAKE_INSTALL_BIN.

#]=======================================================================]

# use, i.e. don't skip the full RPATH for the build tree
set(CMAKE_SKIP_BUILD_RPATH FALSE)

# when building, don't use the install RPATH already (but later on when
# installing)
set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF)

# Prepare RPATH
file(RELATIVE_PATH _rel ${CMAKE_INSTALL_FULL_BINDIR}
${CMAKE_INSTALL_FULL_LIBDIR})
message(STATUS "_rel:${_rel}")
if(APPLE)
set(_rpath "@loader_path/${_rel}")
else()
set(_rpath "$ORIGIN/${_rel}")
endif()
message(STATUS "_rpath:${_rpath}")

# add auto dly-load path such as
list(APPEND CMAKE_INSTALL_RPATH ${_rpath};$ORIGIN)
message(STATUS "CMAKE_INSTALL_RPATH:${CMAKE_INSTALL_RPATH}")

# add the automatically determined parts of the RPATH which point to directories
# outside the build tree to the install RPATH
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

完整的安装规则配置 demo,参考仓库如下:

FetchContent

FetchContent 参考应用示例:

配置库 libConfig.cmake

为了为静态库 lib 编写 CMake 配置文件,需要创建一个名为 libConfig.cmake 的文件。在该文件中,可以设置库的导入路径、链接库和其他相关设置。

以下是一个示例 libConfig.cmake 文件的模板,集成使用 include 该模块即可

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
# 设置lib的导入路径
set(lib_INCLUDE_DIRS "<path_to_lib_include_directory>")

# 设置lib的链接库
set(lib_LIBRARIES "<path_to_lib_library_file>")

# 导出lib的配置
set(lib_FOUND TRUE)
set(lib_VERSION_MAJOR <major_version>)
set(lib_VERSION_MINOR <minor_version>)
set(lib_VERSION_PATCH <patch_version>)
set(lib_VERSION "<version_string>")

# 将lib相关的配置信息添加到全局变量中
set(_IMPORT_PREFIX "<path_to_lib_directory>")
set(_IMPORT_PREFIX_Release "<path_to_lib_directory>")

# 添加lib的目标
add_library(lib INTERFACE IMPORTED)

# 设置lib的包含路径和链接库
set_target_properties(lib PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${lib_INCLUDE_DIRS}"
INTERFACE_LINK_LIBRARIES "${lib_LIBRARIES}"
INTERFACE_COMPILE_DEFINITIONS "LIB_VERSION_MAJOR=${lib_VERSION_MAJOR};LIB_VERSION_MINOR=${lib_VERSION_MINOR};LIB_VERSION_PATCH=${lib_VERSION_PATCH};LIB_VERSION=${lib_VERSION}"
)

# 导入其他依赖
include(${_IMPORT_PREFIX}/lib1/lib1Targets.cmake)
include(${_IMPORT_PREFIX}/lib2/lib2Targets.cmake)

需要根据您的库的实际情况修改上述模板中的路径和其他相关设置。确保将 <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
2
3
4
5
6
7
8
9
10
11
# 获取以.specific_suffix为后缀的文件
file(GLOB_RECURSE list_depends
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/suffix_specific_folder"
"${CMAKE_CURRENT_SOURCE_DIR}/suffix_specific_folder/*.specific_suffix")
# 将list以字符串以`;`分隔替换为\$<SEMICOLON>
string(REPLACE ";" "\\$<SEMICOLON>" list_depends "${list_depends}")

# 用于以;分隔的字符串宏展开
target_compile_options(
${target_name}
PRIVATE -DLIST_DEPENDS="${list_depends}")

CMake 中处理 rpath

links:

CMake install imported lib

links:

Platform check

cmake 提供对平台功能的模块。

1
2
3
4
include(CheckIncludeFile)
include(CheckSymbolExists)
include(CheckTypeSize)
include(CheckFunctionExists)

links:

CMake ctest

ctest 可执行文件是 CMake 测试驱动程序。CMake 生成的构建树是为使用 enable_testing () 和 add_test () 命令的项目创建的,它具有测试支持。该程序将运行测试并报告结果。

links:

cdash

cdash 用于发布 CMake 运行 ctest 的测试结果。

links:

vcpkg

vcpkg 是 C++ 比较成熟的包管理器,覆盖大部分平台。

links:

Integration

vcpkg 集成模式包含 Classic modeManifest 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
2
3
4
5
6
vcpkg format-manifest ports/mypackage/vcpkg.json
git add ports
git commit -m "update ports"
vcpkg x-add-version mypackage --overlay-ports=./ports --x-builtin-registry-versions-dir=./versions/ --x-builtin-ports-root=./ports
git add versions
git commit --amend --no-edit

vcpkg 加速

使用代理加速

  • clash 或其它代理方式加速访问 github

  • 国内私域加速(企业加速等)

搭建私域缓存

参考 vcpkg 构建缓存

国内公开缓存

常用命令

export

将包导出作为缓存开发环境,但是不推荐

1
2
# 使用原始格式导出,可以搭建 vcpkg 离线构建环境,只需要在 CMakeLists 中设置 CMAKE_TOOLCHAIN_FILE 为导出的离线 vcpkg.cmake 即可。
vcpkg export abseil:x64-windows --raw --output=$PWD/path/to/project/3rdparty

links:

install

1
2
3
4
5
# libredwg 包名,tools 功能feature,x64 架构,windows 平台
vcpkg install libredwg[tools]:x64-windows

# download and unpack sources for debugging, see https://github.com/microsoft/vcpkg/discussions/33259
vcpkg install --only-downloads

Github Actions

links:

vcpkg-registry

create registry

links:

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
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# .clang-format
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
---
Language: Cpp
# 访问说明符(public、protected、private)不偏移
AccessModifierOffset: -4
# 开括号(开圆括号、开尖括号、开方括号)后的参数换行需要对齐
AlignAfterOpenBracket: Align
# 连续赋值时,不对齐所有等号
AlignConsecutiveAssignments: false
# 连续声明时,不对齐所有声明的变量名
AlignConsecutiveDeclarations: false
# 对齐连续宏定义的样式
AlignConsecutiveMacros: true
# 将转义的换行符(\)尽可能向左对齐
AlignEscapedNewlines: Left
# 水平对齐二元和三元表达式的操作数
AlignOperands: true
# 对齐连续的尾部注释
AlignTrailingComments: true
# 允许函数的所有参数放到下一行
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
# 不允许短的代码块放在同一行,如 while 代码块
AllowShortBlocksOnASingleLine: false
# 不允许短的 case 标签代码块放在同一行
AllowShortCaseLabelsOnASingleLine: false
# 不允许在类外定义的非空函数放在同一行
AllowShortFunctionsOnASingleLine: Inline
# 不允许短的 if 语句放在同一行
AllowShortIfStatementsOnASingleLine: false
# 不允许短的循环代码块放在同一行
AllowShortLoopsOnASingleLine: false
# 不要主动在函数定义返回类型处换行,除非超过行字符数限制必须换行
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
# 不允许在多行 string 前换行
AlwaysBreakBeforeMultilineStrings: false
# 总是在 template 声明后换行
AlwaysBreakTemplateDeclarations: Yes
# 不要求函数实参换行时必须每个参数各自一行
BinPackArguments: false
# 不要求函数形参换行时必须每个参数各自一行
BinPackParameters: false
# 配置大括号是否换行
BraceWrapping:
# case 标签后面大括号不换行
AfterCaseLabel: false
# class 定义后面大括号不换行
AfterClass: false
# 控制语句后面大括号不换行
AfterControlStatement: false
# enum 定义后面大括号不换行
AfterEnum: false
# 函数定义后面大括号不换行
AfterFunction: false
# 命名空间定义后面大括号不换行
AfterNamespace: false
# struct 定义后面大括号不换行
AfterStruct: false
# union 定义后面大括号不换行
AfterUnion: false
# extern 块后面大括号不换行
AfterExternBlock: false
# catch 之前需要换行
BeforeCatch: true
# else 之前需要换行
BeforeElse: true
# lambda 大括号不换行
BeforeLambdaBody: false
# while 之前需要换行
BeforeWhile: true
# 不要缩进大括号
IndentBraces: false
# 空函数体可以在同一行
SplitEmptyFunction: false
# 空语句可以在同一行
SplitEmptyRecord: false
# 空命名空间可以在同一行
SplitEmptyNamespace: false
# 需要换行时在二元操作符前换行,除了赋值操作符
BreakBeforeBinaryOperators: NonAssignment
# 在 BraceWrapping 中自定义
BreakBeforeBraces: Custom
# 继承列表换行时逗号不换行
BreakBeforeInheritanceComma: false
# 继承列表换行时冒号换行,逗号不换行
BreakInheritanceList: BeforeColon
# 在三元运算操作符之前换行
BreakBeforeTernaryOperators: true
# 构造函数初始化列表换行时逗号不换行
BreakConstructorInitializersBeforeComma: false
# 构造函数初始化列表换行时冒号换行,逗号不换行
BreakConstructorInitializers: BeforeColon
# 允许断开字符串文字
BreakStringLiterals: true
# 每行字符数限制
ColumnLimit: 120
CommentPragmas: "^ IWYU pragma:"
# 不允许不同的命名空间定义在同一行
CompactNamespaces: false
# 构造函数初始化列表要么都在一行,要么每个变量一行
ConstructorInitializerAllOnOneLineOrOnePerLine: true
# 构造函数初始化列表需要 4 个缩进
ConstructorInitializerIndentWidth: 4
# 换行后下一行需要 4 个 缩进
ContinuationIndentWidth: 4
# 变量初始化列表前后不需要空格
Cpp11BracedListStyle: true
DerivePointerAlignment: true
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
# 自动为命名空间结尾处添加注释
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
- Regex: "StdAfx.h"
Priority: -1
- Regex: '^<.*\.h>'
Priority: 1
- Regex: "^<.*"
Priority: 2
- Regex: ".*"
Priority: 3
IncludeIsMainRegex: "([-_](test|unittest))?$"
# switch 里的 case 需要缩进
IndentCaseLabels: true
# extern 里的代码块需要缩进
IndentExternBlock: Indent
# 预处理指令不能有缩进
IndentPPDirectives: None
# 使用 4 个缩进
IndentWidth: 4
# 函数声明、定义时在返回类型处换行时,下一行不要缩进
IndentWrappedFunctionNames: false
# 代码块开始处不要有空行
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ""
MacroBlockEnd: ""
# 不能有连续2个及以上的空行
MaxEmptyLinesToKeep: 1
# 命名空间内的内容不要缩进
NamespaceIndentation: None
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
# * & 紧挨类型名
PointerAlignment: Left
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- "c++"
- "C++"
CanonicalDelimiter: ""
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
CanonicalDelimiter: ""
BasedOnStyle: google
# 允许对注释重新排版
ReflowComments: true
# 允许对 #include 排序
SortIncludes: true
# 允许对 using 声明排序
SortUsingDeclarations: true
# c 风格类型转换括号后不要有空格
SpaceAfterCStyleCast: false
# template 关键字后面需要一个空格
SpaceAfterTemplateKeyword: true
# 赋值运算符两边需要空格
SpaceBeforeAssignmentOperators: true
# 变量初始化列表大括号前不要有空格
SpaceBeforeCpp11BracedList: false
# 构造函数初始化冒号前需要空格
SpaceBeforeCtorInitializerColon: true
# 继承语句冒号前需要一个空格
SpaceBeforeInheritanceColon: true
# 控制语句后面需要一个空格(if/for/while...)
SpaceBeforeParens: ControlStatements
# 基于范围循环的冒号前需要一个空格
SpaceBeforeRangeBasedForLoopColon: true
# 不要在空的圆括号内添加空格
SpaceInEmptyParentheses: false
# 同行尾随注释 // 前面需要一个空格
SpacesBeforeTrailingComments: 1
# 尖括号前后没有空格
SpacesInAngles: false
# c 风格类型转换的括号中前后没有空格
SpacesInCStyleCastParentheses: false
# 圆括号前后没有空格
SpacesInParentheses: false
# 方括号前后没有空格
SpacesInSquareBrackets: false
Standard: Auto
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 4
UseTab: Never

WSL

WSL 是 Windows 上的 Linux 子系统。

安装

  • 在 Store 中安装 Windows subsystem for Linux。

  • 配置非 root 用户登录密码提示。

1
2
echo $USER ALL=\(root\) NOPASSWD:ALL | tee /etc/sudoers.d/$USER \
&& chmod 0440 /etc/sudoers.d/$USER
  • 打开 systemd

1
echo \[boot\]\nsystemd=true  >> /etc/wsl.conf

重启 wsl,wsl --shutdown <your linux distro>

1
2
# 验证 systemd
systemctl list-unit-files --type=service

文件共享

  • 通过路径 /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 找到最终的调用实现。如下图:

image

符号可见性

默认符号不到处,除非使用以下声明

  • __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
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
& "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC/14.34.31933/bin/Hostx64/x64/dumpbin.exe" /dependents xxx.dll
Microsoft (R) COFF/PE Dumper Version 14.34.31942.0
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file abseil_dll.dll

File Type: DLL

Image has the following dependencies:

KERNEL32.dll
ADVAPI32.dll
clang_rt.asan_dbg_dynamic-x86_64.dll
MSVCP140D.dll
dbghelp.dll
bcrypt.dll
VCRUNTIME140D.dll
VCRUNTIME140_1D.dll
ucrtbased.dll

Summary

161000 .data
14000 .pdata
557000 .rdata
38000 .reloc
1000 .rsrc
293000 .text

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)

# Vcpkg build environment
message(STATUS "VCPKG_ROOT:$ENV{VCPKG_ROOT}")
set(VCPKG_ROOT "$ENV{VCPKG_ROOT}")

# set cmake tool chain
set(CMAKE_TOOLCHAIN_FILE ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)

set(VCPKG_VERBOSE
ON
CACHE BOOL "Vcpkg VCPKG_VERBOSE")

project(fitting)

find_package(Eigen3 CONFIG REQUIRED)
if(CMAKE_HOST_WIN32)
add_definitions(-D_AFXDLL)
set(CMAKE_MFC_FLAG 2) # shared link
# WIN32 correctly set Visual Studio linker flag /SUBSYSTEM in CMAKE
add_executable(${PROJECT_NAME} WIN32 main.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/)
target_link_libraries(${PROJECT_NAME} PRIVATE Eigen3::Eigen)
endif()

在 vcpkg 中配置使用依赖 Eigen3

1
2
3
4
5
6
7
8
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "fitting",
"version": "1.0.0",
"dependencies": [
"eigen3"
]
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include "Eigen/Dense"
#include <afxwin.h>
#include <cmath>
#include <vector>

#define WindowWidth 640
#define WindowHeight 480
#define WindowLeft 0
#define WindowTop 0
#define WindowRight (WindowLeft + WindowWidth)
#define WindowBottom (WindowTop + WindowHeight)
#define ViewLeft 0
#define ViewBottom 0
#define PixelUnit 1
#define ViewZoomLevel 1

// 获取所给点的矩阵,返回结果的点拟合矩阵
template <size_t row, size_t col>
Eigen::Matrix<double, row, 1> GetFitting(Eigen::Matrix<double, row, col> m,
Eigen::Matrix<double, row, 1> y) {
return m.inverse() * y;
}

// 获取拟合结果
template <size_t row>
double FittingReuslt(Eigen::Matrix<double, row, 1> fa, double x) {
// product
return Eigen::Matrix<double, 1, 3>{x * x, x, 1} * fa;
}

class CMyWnd : public CFrameWnd {
public:
CMyWnd() {
Create(NULL, _T("My Window"), WS_OVERLAPPEDWINDOW,
CRect(WindowLeft, WindowTop, WindowWidth, WindowHeight));
SetTimer(1, 40, NULL); // 设置定时
}

protected:
// 绘制回调
afx_msg void OnPaint() {

CPaintDC dc(this);

dc.SetMapMode(MM_ANISOTROPIC);
dc.SetWindowExt(WindowWidth + 50, WindowHeight + 50);
dc.SetViewportExt(WindowWidth, WindowHeight);
dc.SetViewportOrg(0, 0);

const HPEN pen = CreatePen(PS_SOLID, 0, RGB(0, 255, 0));
const HPEN oldpen = static_cast<HPEN>(SelectObject(dc, pen));
dc.MoveTo(0, 0);
dc.LineTo(WindowWidth, WindowHeight);
dc.MoveTo(0, WindowHeight);
dc.LineTo(WindowWidth, 0);
if (m_points.size() >= 3) {
// 计算拟合参数
CPoint p1 = m_points[0];
CPoint p2 = m_points[1];
CPoint p3 = m_points[2];
Eigen::Matrix<double, 3, 3> m;
Eigen::Matrix<double, 3, 1> y;
m << p1.x * p1.x, p1.x, 1, p2.x * p2.x, p2.x, 1, p3.x * p3.x, p3.x, 1;
y << p1.y, p2.y, p3.y;
auto fa = GetFitting<3, 3>(m, y);

// 绘制拟合结果
dc.MoveTo(p1);
for (int x = p1.x; x <= p3.x; x++) {
dc.LineTo((int)x, (int)FittingReuslt<3>(fa, x));
}
}
SelectObject(dc, oldpen);
DeleteObject(pen);
}

// 鼠标左击事件回调
afx_msg void OnLButtonDown(UINT nFlags, CPoint point) {
if (m_points.size() < 3) {
m_points.push_back(point);
Invalidate();
}
}

// 定时回调,重绘
afx_msg void OnTimer(UINT_PTR nIDEvent) { Invalidate(); }

DECLARE_MESSAGE_MAP()

private:
std::vector<CPoint> m_points{};
};

// 注册消息回调映射
BEGIN_MESSAGE_MAP(CMyWnd, CFrameWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_TIMER()
END_MESSAGE_MAP()

class CMyApp : public CWinApp {
public:
virtual BOOL InitInstance() {
CMyWnd *pWnd = new CMyWnd();
m_pMainWnd = pWnd;
pWnd->ShowWindow(SW_SHOW);
pWnd->UpdateWindow();
return TRUE;
}
};

CMyApp theApp;

LLVM on Windows

links:

Unix

编译和链接

links:

通常比较成熟的 IDE 会将这个过程封装,对用户无感。

  • 编译

简单来说,将用户代码,编译成机器可执行的指令,一般指对单个文件进行编译。

1
2
3
4
5
6
# 获取 ellf 的头部信息
readelf -h main.o
# 获取 elf 区块
readelf -S m
# .text 编译好的代码
# .data 数据区

如果代码中有调用其它的库或对象,在代码编译成单个对象文件时,调用符号时会将对应位置的反汇编先设置为 0,然后在链接这一步组装(生成调用地址)。

在代码块中,可以找到需要为调用函数生成地址的重定位表(.reloc)。

1
2
# 获取重定位表,在 .text 可以找到需要生成调用地址的函数
objdump -r main.o
  • 链接

将编译之后的所有目标文件,库(动态库、静态库)组合拼装成一个独立的可执行文件。

这里,链接器,会根据目标文件、静态库中的重定位表找到需要修正地址的函数及全局变量。

如果,在链接时没有提供需要的库,链接时会警告 undefined reference


以上编译及链接过程通常使用 IDE 或脚本自动完成。比如 makefile 管理依赖的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义一个目标 main 依赖树
all: main
# main 依赖 main.o math.o,以及生成指令
main: main.o math.o
gcc main.o math.o -o main

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

math.o: math.c
gcc -c math.c

clean:
rm main main.o math.o

可执行文件或对象文件的节表和内存映射

对象文件或可执行文件的节表和内存布局是两个不同的概念,它们描述了程序在不同层次上的组织和存储方式。

节表(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 表中的偏移量来计算全局符号的地址,然后进行访问。

image

通常建议所有动态库都使用 PIC, 但 - fPIC 并没有作为隐式传递和 - shared 一起传递使用.

image

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 中存储的实际地址,而不需要再执行解析函数。这样可以提高函数调用的效率。

image

符号可见性

默认符号都可见,需要手动隐藏不需要的符号,这可以一定程度优化链接和加载动态库可维护性。

隐藏符号:

  • -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 节保存动态链接信息. 如下

image

不同于 windows, linux 下动态链接信息是分开保存的,链接的 lib 保存在 .dynamic 节中,链接的符号保存在 .dynsym 节中.

动态链接在数据段 .data 预留一个叫全局偏移表 got(Global Offset Table)区域保存专用于保存全局变量和函数跳转地址。在 .code 中调用动态库时搜先查找 GOT,GOT 中的地址会在动态库加载时替换为真正的动态库中的地址。这里利用了 GOT 和代码段相对位置是固定的,可以利用 CPU 相对寻址实现。这样 GOT 在每个进程中都保留一部分很小的副本,但可以实现代码被所有进程共享,这种方式称为 PIC (地址无关代码)。一般为减小函数符号重定位开销,操作系统会用程序链接表 PLT (Procedure Linkage Table) 进行延迟到函数第一次调用的时候,因为绝大多数函数不会被调用。一般这个过程会在代码段用一段桩代码,在第一次调用时去查询真正的跳转地址并更新 GOT,具体会更新复杂。

在 Linux 下,动态库是通过内存映射的方式加载到进程的地址空间中的。当多个进程加载同一个动态库时,它们会共享同一个物理内存页,这样可以节省内存空间并提高系统性能。具体来说,当一个进程加载一个动态库时,动态链接器会将动态库的代码段、数据段等内容映射到进程的地址空间中。如果另一个进程也需要加载同一个动态库,动态链接器会检查该动态库是否已经在其他进程中加载过。如果已经加载过,动态链接器会将该动态库的内存映射到新进程的地址空间中,并且新进程和原进程会共享同一个物理内存页。这样,多个进程就可以共享同一个动态库的代码和数据,从而节省内存空间并提高系统性能。需要注意的是,如果多个进程需要修改同一个动态库的数据,就需要使用线程同步机制来保证数据的一致性。否则,不同进程之间对同一个动态库的数据修改可能会相互影响,导致程序出现错误。

1
2
3
4
5
6
# (-shared)指令生成位置无关(-fPIC)的动态库,
# (-Wl,-soname,libexample.so.1)指定生成库文件名为(libexample.so.1),并在当前路径生成符号链接(libexample.so)指向(libexample.so.1)
# (-Wl,-rpath=/path/to/rpath)指定运行时查找库(example_dep)的路径
gcc -shared -fPIC example.c -o libexample.so.1 -L. -lexample_dep -Wl,-soname,libexample.so -Wl,-rpath=/path/to/rpath
# 在当前路径(-L.)链接(-lexample)动态库 libexample.so,可以加入 -Wl,-rpath=/path/to/rpath 指定运行时查找依赖动态库 example 路径
gcc -o main main.c -L. -lexample -Wl,-rpath=$(pwd)

查看依赖的动态库

1
2
3
# 查看 main 依赖的动态库
readelf -d main | grep NEEDED
objdump -x main | grep NEEDED

查看库提供的实现符号

1
2
3
4
# 查看 libc 的提供的动态符号
nm -D /lib/x86_64-llinux-gnu/libc.so.6
# 查看 libc 的提供的符号,包括静态和动态
nm /lib/x86_64-llinux-gnu/libc.so.6

查看系统能搜索到的库

1
ldconfig -p | grep libc

生成动态库常用指令

  • -soname 选项用于在动态库文件中指定一个名为 soname 的标记,该标记指定了动态库的名称,通常用于版本区分。

1
2
# 编译器将生成名为libexample.so.1的动态库文件,并在其中指定一个名为soname的标记,该标记的值为libexample.so。此外,编译器还将在当前目录中创建一个名为libexample.so的符号链接,该符号链接指向libexample.so.1。这样,当其他程序链接到该动态库时,它们将使用libexample.so作为动态库的名称。
gcc -shared -fPIC example.c -o libexample.so.1 -Wl,-soname,libexample.so
  • -rpath 选项用于指定动态库文件的搜索路径。

  • -Wl 将逗号分隔的选项列表传递给链接器。

1
2
3
4
# 编译器将使用-rpath=/path/to/rpath选项将rpath加入链接选项,
# 使用-Bstatic选项将后续的静态库链接到程序中,使用-lstatic_dep选项指定静态库的名称,
# 使用-Bdynamic选项将后续的动态库链接到程序中,使用-ldynamic_dep选项指定动态库的名称。
gcc -o main main.c -L. -lexample_dep -Wl,-rpath=/path/to/rpath,-Bstatic,-lstatic_dep,-Bdynamic,-ldynamic_dep

查看及控制动态库加载

  • 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
2
3
4
5
6
7
8
9
10
11
12
# 将二进制文件的动态链接库路径设置为当前目录
patchelf --set-rpath . /path/to/binary

# 将二进制文件的运行权限设置为仅限用户
patchelf --set-interpreter /lib/ld-linux.so.2 --set-rpath . --set-soname new_name.so /path/to/binary
chmod 700 /path/to/binary

# 列出二进制文件的所有动态链接库依赖项
patchelf --print-interpreter /path/to/binary

# 列出二进制文件依赖的所有动态链接库及其版本
patchelf --print-needed /path/to/binary

[!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
2
3
# 使用链接器查看可搜索范围
ldconfig -v | grep python3
ldd $(which python3)

配置非标准位置动态库加载路径

1
2
3
4
# 找到运行库所在路径
find /usr -name 'libpython3.7m.so*'
# 新增运行库路径,并刷新运行库搜索环境
echo "/opt/python/lib" > /etc/ld.so.conf.d/opt_python.conf && ldconfig
相对共享库路径

如下,指定了 python 的运行库安装路径,并指定了对应的运行库的相对路径所在 run path。

1
2
3
./configure --enable-shared \
--prefix=/opt/python \
LDFLAGS=-Wl,-rpath=@ORIGIN/../lib # 指定 path/bin/python3 的共享库搜索路径为 lib 路径

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
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
#
# STL GDB evaluators/views/utilities - 1.03
#
# The new GDB commands:
# are entirely non instrumental
# do not depend on any "inline"(s) - e.g. size(), [], etc
# are extremely tolerant to debugger settings
#
# This file should be "included" in .gdbinit as following:
# source stl-views.gdb or just paste it into your .gdbinit file
#
# The following STL containers are currently supported:
#
# std::vector<T> -- via pvector command
# std::list<T> -- via plist or plist_member command
# std::map<T,T> -- via pmap or pmap_member command
# std::multimap<T,T> -- via pmap or pmap_member command
# std::set<T> -- via pset command
# std::multiset<T> -- via pset command
# std::deque<T> -- via pdequeue command
# std::stack<T> -- via pstack command
# std::queue<T> -- via pqueue command
# std::priority_queue<T> -- via ppqueue command
# std::bitset<n> -- via pbitset command
# std::string -- via pstring command
# std::widestring -- via pwstring command
#
# The end of this file contains (optional) C++ beautifiers
# Make sure your debugger supports $argc
#
# Simple GDB Macros written by Dan Marinescu (H-PhD) - License GPL
# Inspired by initial work of Tom Malnar,
# Tony Novac (PhD) / Cornell / Stanford,
# Gilad Mishne (PhD) and Many Many Others.
# Contact: dan_c_marinescu@yahoo.com (Subject: STL)
#
# Modified to work with g++ 4.3 by Anders Elton
# Also added _member functions, that instead of printing the entire class in map, prints a member.
1
2
3
4
5
6
(gdb) pvector cur
elem[0]: $5 = 3
elem[1]: $6 = 9
Vector size = 2
Vector capacity = 2
Element type = std::allocator<int>::pointer

gdb 配合 vscode 调试 / 附加

linux 下 gdb 针对 core 复现及错误现场跟踪是一个非常强大的工具。配合 vscode 能够可视化的调试错误问题。

前置条件:

  • C/C++ MS plugin

  • GDB

在 launch.json 文件中配置如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Attach",
"type": "cppdbg",
"request": "attach",
// attach program
"program": "/usr/local/bin/python",
// select gdb tool
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
// for selecting process to attach in a moment
"processId": "${command:pickProcess}",
},
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "/usr/local/bin/python",
"args": [ ],
"stopAtEntry": false,
// project folder
"cwd": "${workspaceFolder}",
"environment": [ ],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
// core dump path
"coreDumpPath": "/workspace/xxxx/core.29991"
}

]
}
1
2
3
4
5
# 模板中 core 文件调试方式,即相当于:
gdb /usr/local/bin/python core.29991
# 模板中 attach 方式,即相当于:
cd /project
gdb -p xxxx 或 gdb xxxx 3598

links:

gdb 搜索源码位置

由于源码可能会变动,所以可能需要手动指定源码位置,方便调试时显示源码。

相关命令:

1
2
3
4
5
6
7
8
# 查看编译源码x信息位置
info source
# 查看源码搜索目录
show dir
# gdb 指定源码移动到相应位置
set substitute-path from-path to-path
# 将目录加入到搜索源码路径
dir path-to-set

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
    #include <signal.h>
    #include <stdio.h>
    #include <iosfwd>
    #include <iostream>
    #include <memory>
    #include <sstream>

    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
2
3
4
5
6
7
mamba create -n cling
mamba install xeus-cling -c conda-forge
mamba install xeus -c conda-forge
mamba activate cling

jupter notebook
jupter lab

Docs

sphinx

links:

Doxygen

links:

注释特殊标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 标记代码
\code{.unparsed}
Show this as-is please
\endcode

\code{.sh}
echo "This is a shell script"
\endcode

\code{.py}
class Python:
pass
\endcode

\code{.cpp}
class Cpp {};
\endcode

Coverage

对 C/C++ 项目进行代码覆盖率的度量

links:

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
2
3
4
5
6
7
8
9
10
# Install lcov(genhtml in inclusion)
apt/yum install lcov
# Run gcov and generate coverage file *.gcov
gcov <sources> -s <some exclusion dir>
# 根据 *.gcov 生成 coverage.info 数据文件
# -rc genhtml_branch_coverage=1 打开默认禁用的分支覆盖率计算
lcov --rc lcov_branch_coverage=1 --capture --directory <*.gcov file dir> --output-file coverage.info.tmp
lcov --rc lcov_branch_coverage=1 -e coverage.info.tmp "*src*" -o coverage.info
# 根据 coverage.info 这个数据文件生成 html 报告
genhtml -rc genhtml_branch_coverage=1 coverage.info --output-directory out
  • GNU 方式二,单独依赖 lcov(间接使用 gcov )

1
2
3
4
5
6
7
8
9
10
11
# Install lcov(genhtml in inclusion)
apt/yum install lcov
# Lcov initial/baseline lcov
lcov --no-external --rc lcov_branch_coverage=1 --capture --initial --directory <source dir> --output-file coverage_base.info
# Run test and lcov again after tests/checks complete
lcov --no-external --rc lcov_branch_coverage=1 --capture --directory <source dir> --output-file coverage_test.info
# Combine lcov tracefiles
lcov --add-tracefile coverage_base.info --add-tracefile coverage_test.info --output-file coverage.info
# Remove / filter out remaining unwanted stuff from tracefile
lcov --remove coverage.info '/usr/include/*' '/usr/lib/*' ...<some other dirs> -o coverage_final.info
genhtml -rc genhtml_branch_coverage=1 coverage_final.info --output-directory out
  • Clang

1
2
3
4
5
6
7
8
# Run test and generate *.profraw
./executable_test
# Merge *.profraw to on merged.profraw
llvm-profdata merge -sparse *.profraw -o merged.profdata
# Generate coverage
llvm-cov show --instr-profile=merged.profdata -object=./executable_test -show-line-counts-or-regions -output-dir=./all-merged -format="html"-ignore-filename-regex='/usr/include/*'
# report to console
llvm-cov report --instr-profile=merged.profdata -object=./executable_test -show-line-counts-or-regions -output-dir=./all-merged -format="html"-ignore-filename-regex='/usr/include/*'
  • Windows

1
vstest.console.exe *.dll /EnableCodeCoverage /Collect:"Code Coverage;Format=Xml" /ResultsDirectory:"<coverage result dir>"

集成三方

测试集成三方

CMake 示例

links:

Program

这张图片展示了 ELF 加载到内存的一个数据结构关系。
elf loaded to the memory

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 instructions push rbp and mov 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, and mov 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 instruction mov 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 instruction mov 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; 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:
global _start

section .text

_start:
; Move 5 into EAX
MOV EAX, 5

; Add EBX to EAX
ADD EAX, EBX

; Exit program with EAX as return value
; The reason we use the following code to exit a program in assembly language is because it is a convention that has been established for Linux systems. The INT instruction with the value 0x80 is used to make a system call to the kernel. The kernel then interprets the value in the EAX register as the system call number and performs the appropriate action. In this case, the system call number is 1, which corresponds to the exit system call. The value in the EBX register is used as the exit status of the program. By convention, a value of 0 indicates success, while any other value indicates an error. Therefore, the MOV EBX, 0 instruction is used to set the EBX register to 0 before exiting the program with a return value of 0 in the EAX register.
MOV EBX, 0
; For notifying the kernel to end the system call
MOV EAX, 1
INT 0x80
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
42
43
44
; 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:
global _start

section .text

_start:
; Compare EAX and EBX
CMP EAX, EBX

; Jump to label1 if EAX is greater than EBX
JG label1

; Jump to label2 if EAX is less than EBX
JL label2

; Jump to label3 if EAX is equal to EBX
JE label3

label1:
; Code to execute if EAX is greater than EBX
...

; Jump to end of program
JMP end

label2:
; Code to execute if EAX is less than EBX
...

; Jump to end of program
JMP end

label3:
; Code to execute if EAX is equal to EBX
...

; Jump to end of program
JMP end

end:
; Exit program with EAX as return value
MOV EBX, 0
MOV EAX, 1
INT 0x80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; To write an assembly program that adds two numbers, you can use the ADD instruction.
section .data
; define three variables in the .data section: num1, num2, and result. The dd directive is used to reserve space for each variable and initialize it to a specific value. num1 is initialized to 10, num2 is initialized to 20, and result is initialized to 0.
num1 dd 10
num2 dd 20
result dd 0

section .text
global _start

_start:
; Move num1 into EAX
mov eax, [num1]

; Add num2 to EAX
add eax, [num2]

; Move the result into result variable
mov [result], eax

; Exit the program with a return value of 0
mov ebx, 0
mov eax, 1
int 0x80

arm assembly

links:

C

C 内存管理

API

C 语言主要提供 mallocrealloccallocallocaaligned_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 为指向先前由 malloccallocrealloc 函数所返回的内存指针,而参数 size 为新分配的内存大小,其值可比原内存大或小。其中:

  • 如果 size 值比原分配的内存空间小,内存内容不会改变(即新内存保持原内存的内容),且返回的指针为原来内存的首地址(即 ptr)。

  • 如果 size 值比原分配的内存空间大,则 realloc 不一定会返回原来的指针,原内存的内容保持不变,但新多出的内存则设为初始值。

  • 最后,如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL

alloca

1
void * alloca (size_t size);

相对与 malloccallocrealloc 函数,函数 alloca从栈中分配内存空间,内存分配大小为 size。如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL。也正因为函数 alloca 是从栈中分配内存空间,因此它会自动释放内存空间,而无需手动释放。

aligned_alloc

1
void * aligned_alloc (size_t alignment,size_t size);

该函数属于 C11 标准提供的新函数,用于边界对齐的动态内存分配。该函数按照参数 alignment 规定的对齐方式为对象进行动态存储分配 sizesize_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
2
3
4
5
6
char *p = (char *)malloc(100);
if (!p)
{
/*处理内存分配错误,并返回错误状态*/
return -1;
}

除通过使用 “if(p==NULL)” 或者 “if(p!=NULL)” 语句进行简单防错处理之外,如果指针 p 是函数的参数,那么还可以在函数的入口处用 assert(p !=NULL) 进行检查,从而避免发生内存分配未成功却使用了它的情况。

内存资源的分配与释放

内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行,在 C 语言中,如果内存的分配和释放在不同的模块或抽象层内,不仅会加大程序员追踪内存块生命周期的负担,而且可能会导致内存泄漏、内存双重释放(double-free)、非法访问已经释放的内存、写入已释放或未分配的内存区域等问题。看下面一段示例代码:

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
#define MIN_MEM_SIZE 10
int CompareMemorySize(char *p, size_t size)
{
if (size < MIN_MEM_SIZE)
{
free(p);
p = NULL;
return -1;
}
return 0;
}
void AllocMemory(size_t size)
{
char *p = (char *)malloc(size);
if (p == NULL)
{
/*...*/
}
if (CompareMemorySize(p, size) == -1)
{
free(p);
p = NULL;
return;
}
/*...*/
free(p);
p = NULL;
}

在上面的示例代码中,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
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
#define MIN_MEM_SIZE 10
int CompareMemorySize(size_t size)
{
if (size < MIN_MEM_SIZE)
{
return -1;
}
return 0;
}
void AllocMemory(size_t size)
{
char *p = (char *)malloc(size);
if (p == NULL)
{
/*...*/
}
if (CompareMemorySize(size) == -1)
{
free(p);
p = NULL;
return;
}
/*...*/
free(p);
p = NULL;
}

现在,函数 CompareMemorySizeAllocMemory 的职责很清楚了。其中,CompareMemorySize 函数只负责检查内存分配的长度,而内存的分配与释放都放在 AllocMemory 函数内进行。这样不仅不会导致内存的双重释放,而且完全遵从 “内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行” 原则。

返回指针进行强制类型转换

在 C 语言中,“void” 被称为 “无类型”,而 “void*” 则被称为 “无类型指针”。之所以称 “void*” 为 “无类型指针”,是因为它可以指向任何数据类型。因此,对于任何类型 “T” 都可以转换为 “void”,而 “`void 也可以转换为任何类型 “T*”。

也正是因为 “void” 的这个特征,它常被用在如下两个方面:

  • 对函数返回的限定,即如果函数没有返回值,那么应将其声明为 void 类型。

  • 对函数参数的限定,即如果函数无参数,那么声明函数参数为 void

当然,内存管理函数也不例外,如 mallocrealloccallocallocaaligned_alloc 函数的返回都是 void* 类型。但需要特别注意的是,在使用这些内存管理函数进行内存分配时,必须将返回类型 void* 强制转换为指向被分配类型的指针。如下面的代码所示:

1
char *p = (char *)malloc(10 * sizeof(char));

当然,为了能够简单调用,也可以将 malloc 函数使用 define 定义成如下形式:

1
2
3
#define MALLOC(type) ((type *)malloc(sizeof(type)))
/*或者*/
#define MALLOC(number,type) ((type *)malloc((number) * sizeof(type)))

现在,调用就简单多了,如下面的代码所示:

1
2
3
char *p = MALLOC(char);
/*或者*/
char *p = MALLOC(10, char);

下面的宏为大家提供了更多方便:

1
2
3
4
5
6
7
8
9
10
/*malloc*/
#define MALLOC_ARRAY(number, type) \((type *)malloc((number)* sizeof(type)))
#define MALLOC_FLEX(stype, number, etype) \((stype *)malloc(sizeof(stype) \
+ (number)* sizeof(etype)))
/*calloc*/
#define CALLOC(number, type) \((type *)calloc(number, sizeof(type)))
/*realloc*/
#define REALLOC_ARRAY(pointer, number, type) \((type *)realloc(pointer, (number)* sizeof(type)))
#define REALLOC_FLEX(pointer, stype, number, etype) \((stype *)realloc(pointer, sizeof(stype) \
+ (number)* sizeof(etype)))

指向一块合法的内存

在 C 语言中,只要是指针变量,那么在使用它之前必须确保该指针变量的值是一个有效的值,它能够指向一块合法的内存,并从根本上避免未分配内存或者内存分配不足的情况发生。

看下面一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct phonelist
{
int number;
char *name;
char *tel;
}list,*plist;

int main(void)
{
list.number = 1;
strcpy(list.name, "Abby");
strcpy(list.tel, "13511111111");
/*...*/
return 0;
}

对于上面的代码片段,在定义结构体变量 list 时,并未给结构体 phonelist 内部的指针变量成员 “char*name” 与 “char*tel” 分配内存。这时候的指针变量成员 “char*name” 与 “char*tel” 并没有指向一个合法的地址,从而导致其内部存储的将是一些未知的乱码。

因此,在调用 strcpy 函数时,如 “strcpy(list.name,"Abby")” 语句会将字符串 "Abby" 向未知的乱码所指的内存上拷贝,而这块内存 name 指针根本就无权访问,从而导致程序出错。

既然没有给指针变量成员 “char*name” 与 “char*tel” 分配内存,那么解决的办法就是为指针变量成员分配内存,使其指向一个合法的地址,如下面的示例代码所示:

1
2
3
4
list.name = (char*)malloc(20*sizeof(char));
strcpy(list.name, "Abby");
list.tel = (char*)malloc(20*sizeof(char));
strcpy(list.tel, "13511111111");

除此之外,下面的错误也是大家经常容易忽视的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct phonelist
{
int number;
char *name;
char *tel;
}list, *plist;
int main(void)
{
plist = (struct phonelist*)malloc(\
sizeof(struct phonelist));
if (plist)
{
plist->number = 1;
strcpy(plist->name, "Abby");
strcpy(plist->tel, "13511111111");
/*...*/
}
/*...*/
free(plist);
plist = NULL;
return 0;
}

不难发现,上面的代码片段虽然为结构体指针变量 plist 分配了内存,但是仍旧没有给结构体指针变量成员 “char*name” 与 “char*tel” 分配内存,从而导致结构体指针变量成员 “char*name” 与 “char*tel” 并没有指向一个合法的地址。因此,应该做如下修改:

1
2
3
4
plist->name = (char*)malloc(20*sizeof(char));
strcpy(plist->name, "Abby");
plist->tel = (char*)malloc(20*sizeof(char));
strcpy(plist->tel, "13511111111");

由此可见,对结构体来说,仅仅是为结构体指针变量分配内存还是不够的,还必须为结构体成员中的所有指针变量分配足够的内存。

分配足够的内存空间

对于上面的结构体指针变量 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void f(size_t len)
{
long *p;
if (len == 0 || len > SIZE_MAX / sizeof(long))
{
/*溢出处理*/
}
p = (long *)malloc(len * sizeof(int));
if (p == NULL)
{
/*...*/
}
/*...*/
free(p);
p = NULL;
}

在上面的示例代码中,内存分配语句 “p=(long*)malloc(len*sizeof(int))” 使用了 “sizeof(int)” 来计算内存的大小,而不是 sizeof(long),这显然是不对的,应该修改成 sizeof(long),当然,也可以用 “sizeof(*p)”。

除此之外,对于数组对象尤其要注意内存分配的问题,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define ARRAY_SIZE 10
struct datalist
{
size_t number;
int data[];
};
int main(void)
{
struct datalist list;
list.number = ARRAY_SIZE;
for (size_t i = 0; i < ARRAY_SIZE; ++i)
{
list.data[i] = 0;
}
/*...*/
return 0;
}

对于上面的示例,当一个结构体中包含数组成员时,其数组成员的大小必须添加到结构体的大小中。因此,上面示例的正确内存分配方法应该按照如下方式进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define ARRAY_SIZE 10
struct datalist
{
size_t number;
int data[];
};
int main(void)
{
struct datalist *plist;
plist = (struct datalist *)malloc(sizeof(struct datalist)+sizeof(int) * ARRAY_SIZE);
if (plist == NULL){
/*...*/
}
plist->number = ARRAY_SIZE;
for (size_t i = 0; i < ARRAY_SIZE; ++i)
{
plist->data[i] = 0;
}
/*...*/
return 0;
}

由上面的几个示例代码片段可见,对于 malloccallocreallocaligned_alloc 内存分配函数中长度参数的大小,必须保证有足够的范围来表示对象要存储的大小。如果长度参数不正确或者可能被攻击者所操纵,将可能会出现缓冲区溢出。与此同时,不正确的长度参数、不充分的范围检查、整数溢出或截断都会导致分配长度不足的缓冲区。因此,一定要确保内存分配函数的长度参数能够合法地分配足够数量的内存。

禁止执行零长度的内存分配

根据 C99 规定,如果在程序中试图调用 malloccalloc 与 realloc 等系列内存分配函数分配长度为 0 的内存,那么其行为将是由具体编译器所定义的(如可能返回一个 null 指针,又或者是长度为非零的值等),从而导致产生不可预料的结果。

因此,为了保证不会将 0 作为长度参数值传给 malloc、calloc 与 realloc 等系列内存分配函数,应该对这些内存分配函数的长度参数进行合法性检查,以保证它的合法取值范围。

如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
size_t len;
/*初始化len变量*/
if (len == 0)
{
/* 处理长度为0的错误 */
}
int *p = (int *)malloc(len);
if (p == NULL)
{
/*...*/
}
/*...*/

避免大型的堆栈分配

C99 标准引入了对变长数组的支持,如果变长数组的长度传入未进行任何检查和处理,那么将很容易被攻击者用来实施攻击,如常见的 DOS 攻击。看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int CopyFile(FILE *src, FILE *dst, size_t bufsize)
{
char buf[bufsize];
while (fgets(buf, bufsize, src))
{
if (fputs(buf, dst) == EOF)
{
/*...*/
}
}
/*...*/
return 0;
}

在上面的示例代码中,数组 “char buf[bufsize]” 的长度将根据 CopyFile 函数的 bufsize 参数来决定,这显然不符合要求的。对于这种情况,可以通过一个 malloc 调用来替换掉这个变长数组。与此同时,如果 malloc 函数内存分配失败,还可以对返回值进行检查,从而防止程序异常终止等情况发生。如下面的示例代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int CopyFile(FILE *src, FILE *dst, size_t bufsize)
{
if (bufsize == 0)
{
/*...*/
}
char *buf = (char *)malloc(bufsize);
if (buf == NULL)
{
/*...*/
}
while (fgets(buf, bufsize, src))
{
if (fputs(buf, dst) == EOF)
{
/*...*/
}
}
/* ... */
free(buf);
buf = NULL;
return 0;
}

避免内存分配成功,但并未初始化

在通常情况下,导致这种错误的主要原因有两个:

  • 没有初始化的观念。

  • 误以为内存的默认初值全部为零,从而导致引用初值错误(如数组)。

其实,内存的默认初值究竟是什么并没有统一的标准。如 malloc 函数分配得到的内存空间就是未初始化的,而它所分配的内存空间里可能包含出乎意料的值。因此,一般在使用该内存空间时,就需要调用函数 memset 来将其初始化为全 0。如下面的示例代码所示:

1
2
3
4
5
6
7
8
int * p = NULL;
p = (int*)malloc(sizeof(int));
if (p == NULL)
{
/*...*/
}
/*初始化为0*/
memset(p, 0, sizeof(int));

对于 realloc 函数,同样需要使用 memset 函数对其内存进行初始化。而对于数组,也别忘了赋初值,即便是赋零值也不可省略,千万不要嫌麻烦。

C 宏(macro)

特殊宏

1
2
3
4
5
6
7
__DATE__     : 在原文件中插入当前的编写日期
__TIME__ : 在源文件中插入当前的编辑时间
__cplusplus__: 在源文件中编写c++程序时该标识被定义
__FILE__ :文件名
__LINE__ :行数
__func__ :所在函数
__FUNCTION__ :

__VA_ARGS__

用于实现变参函数,将函数宏的形参列表最后的参数用省略号(…)表示即实现了变参函数。__VA_ARGS__用于在宏替换部分中,表明省略号代表什么。

例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define PR(...) printf(__VA__ARGS__); //Note:需编译器支持__VA__ARGS__
int main(void)
{
int age = 26;
int weight = 75;
float BMI = 26.4;
PR("oh mashuai");
PR("Age:%d, weight:%d,BMI:%d",age,weight,BMI);
}

##__VA_ARGS__ 宏前面加上##的作用在于,当可变参数的个数为 0 时,这里的##起到把前面多余的 "," 去掉的作用,否则会编译出错。

一般这个用在调试信息上多一点.

#

表示将参数转换为字符串输出:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define P(A) printf("%s:%d.\r\n",#A,A);

int main()
{
int a=1;
int b=2;
P(a);
P(b);
}

运行结果为:

1
2
a:1
b:2

##

用于类函数宏的替换部分,也可以用于对象宏的替换部分。主要用于将两个语言符号组成单个语言符号,为宏扩展提供一种连接实际变元的手段;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

#define XNAME(n) X##n //将定元X和变元n组合,列出X1, X2, X3 ...
#define PRINT_XN(n) printf("X"#n"=%d\n",X##n);

int main(void)
{
int XNAME(1)=10;
int XNAME(2)=20;
int X3=30;

PRINT_XN(1);
PRINT_XN(2);
PRINT_XN(3);
}

定义一个宏,求两个数中的最大数

  • 合格

    1
    #define  MAX(x,y)  x > y ? x : y
  • 中等

    1
    #define MAX(x,y) (x) > (y) ? (x) : (y)
  • 良好

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #define MAX(x,y)({ \
    int _x = x; \
    int _y = y; \
    _x > _y ? _x : _y; \
    })
    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
    #define MAX(type,x,y)({ \
    type _x = x; \
    type _y = y; \
    _x > _y ? _x : _y; \
    })
    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
    #define max(x, y) ({    \
    typeof(x) _x = (x); \
    typeof(y) _y = (y); \
    (void) (&_x == &_y);\
    _x > _y ? _x : _y; })

    在这个宏定义中,使用了 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
    23
    max(x, _x)

    #define max(x, y) ({ \
    typeof(x) _max1 = (x); \
    typeof(y) _max2 = (y); \
    (void) (&_max1 == &_max2); \
    _max1 > _max2 ? _max1 : _max2; })

    #define __max(t1, t2, max1, max2, x, y) ({ \
    t1 max1 = (x); \
    t2 max2 = (y); \
    (void) (&max1 == &max2); \
    max1 < max2 ? max1 : max2; })

    #define ___PASTE(a,b) a##b
    #define __PASTE(a,b) ___PASTE(a,b)

    #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

    #define max(x, y) \
    __max(typeof(x), typeof(y), \
    __UNIQUE_ID(max1_), __UNIQUE_ID(max2_), \
    x, y)

    在新版的宏中,内部的临时变量不再由程序员自己定义,而是让编译器生成一个独一无二的变量,这样就避免了同名冲突的风险。宏 **__UNIQUE_ID** 的作用就是生成了一个独一无二的变量,确保了临时变量的唯一性。关于它的使用,可以参考文章,写的很好。

  • 参考

函数指针

指向普通函数的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int int_add(int a, int b)
{
return (a+b);
}
int int_sub(int a, int b)
{
return (a-b);
}
int (*int_operator)(int, int) = int_add;

int main(int argc, char* argv[])
{
cout<<int_operator(4, 5)<<endl; // output 9
int_operator = int_sub;
cout<<int_operator(4, 5)<<endl; // output -1
return 0;
}

上例中,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
2
typedef int(*pFunc)();
pFunc pFuncArray[10];

如果需要调用其中的第三个函数,那么调用方式为:

1
pFuncArray[2]();

指向‘函数指针数组’的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int cmp_len(const char *str1, const char *str2)
{return ((int)strlen(str1) - (int)strlen(str2));}

int cmp_str(const char *str1, const char *str2)
{return strcmp(str1, str2);}

typedef int(*PCMP_FUNC)(const char*, const char*);

PCMP_FUNC pCmpFuncs[2] =
{
cmp_len,
cmp_str,
};
// 声明指向pCmpFuncs的指针
PCMP_FUNC (*ppCmps)[2] = &pCmpFuncs;

声明分解说明如下:

  • (*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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AClass
{
public:
void Add(int a){m_iValue += a;}
int m_iValue;
};

int main(int argc, char* argv[])
{
AClass a;
// 声明并指向AClass的一个成员变量的指针
int AClass::*pValue = &AClass::m_iValue;
// 或者如下方式:
// int AClass::*pValue;// 指针变量声明
// pValue = &AClass::m_iValue;// 指向A的m_iValue成员
a.*pValue = 4; // 使用方式,赋值
cout<<a.m_iValue<<endl; // 输出4
return 0;
}

指向类成员函数的指针

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
class AClass
{
public:
void Add(int a){m_iValue += a;}
int m_iValue;
};
// 指向类 AClass 成员函数的指针的声明方式
typedef void (AClass::*pAFunc)(int);
// 声明一个指向 AClass::Add 的函数指针
pAFunc pAAdd = &(AClass::Add);

int main(int argc, char* argv[])
{
AClass a;
// 声明并指向AClass的一个成员变量的指针
int AClass::*pValue = &AClass::m_iValue;
// 或者如下方式:
// int AClass::*pValue;// 指针变量声明
// pValue = &AClass::m_iValue;// 指向A的m_iValue成员
a.*pValue = 4; // 使用方式,赋值
cout<<a.m_iValue<<endl; // 输出
(a.*pAAdd)(5); // 指向成员函数指针的调用方式
cout<<a.m_iValue<<endl; // 输出
return 0;
}

指向类静态成员的指针

类的静态成员属于该类的全局对象和函数,并不需要 this 指针;因此指向类静态成员的指针声明方式和普通指针相同。

类指针和普通指针的声明和调用方式完全相同;唯一的不同就是设置指向的对象时,仍然需要类信息,这一点和指向普通成员的指针相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AClass
{
public:
static void Add(int a){m_iValue += a;}
static int m_iValue;
};
int AClass::m_iValue;

typedef void(*pAAdd)(int);

int _tmain(int argc, _TCHAR* argv[])
{
// 声明并指向AClass的一个静态成员变量的指针
int *pValue = &AClass::m_iValue;
// 或者如下方式:
// int *pValue;// 指针变量声明
// pValue = &AClass::m_iValue;// 指向A的m_iValue成员
*pValue = 4; // 使用方式同普通指针,赋值
cout<<*pValue<<endl; // 输出
pAAdd p = &AClass::Add;
p(5); // 调用方式同普通函数指针
cout<<*pValue<<endl; // 输出
return 0;
}

OOP In C

多线程

todo

pthreads

Unix/Linux thread standard 基于 wiki 示例。

CPP

links:

CPP Project

项目布局遵循规范:

现代 C++ 工程布局通常使用成熟的工具生成,比如 cmake-init示例

1
2
3
pip3 install cmake-init
# 使用 cmake-init 创建模板,根据提示输出选项即可
cmake-init libhello

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

Template

static assert

  • static assert for equality type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 std::is_same, std::is_same_v, decltype, declval, std::decy, std::decy_t
// \tparam R 一个模板类型
// \tparam Policy 另一个模板类型
// \tparam Args a parameter pack 参数包
static_assert(
std::is_same_v< // 比较类型是否相等 trait
std::decay_t<R>, // 获取对应无限定符修饰的类型
std::decay_t< // 获取表达式无限定符修饰类型
decltype(
static_cast<Policy*>(this)->PolicyImpl(
std::forward<Args>(args)...
)
)
>
>,
"R must be same as the return type of PolicyImpl");

Fold expressions

Fold expressions 折叠表达式在 c++17 引入,用于避免需要为模板写多个版本以使用可变模板参数及编译效率。

1
2
3
4
5
6
7
8
9
10
template<>
auto SumCpp11Version(){ // no parameter version
return 0;
}

template<typename T1, typename... T>
auto SumCpp11Version(T1 s, T... ts){ // recursive version for every param
return s + SumCpp11Version(ts...);
}

使用折叠表达式,书写更简便,且有利于编译器模板实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename ...Args>
auto sum(Args ...args)
{
return (args + ... + 0); // with initial value 0
}

// or even:

template<typename ...Args>
auto sum2(Args ...args)
{
return (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
2
3
4
5
6
7
8
9
template <typename... Args>
std::vector<int> foo_first(bool apply_all = false, Args&&... args) {
std::vector<int> ret;
ret.reserve(sizeof...(args)); // performance with reservation capacity
(... || [&ret, &apply_all](auto&& v) {
return foo_is_even(v) ? ret.push_back(v), apply_all ? false : true : false;
}(std::forward<Args>(args))); // expanding with a lambda and perfect-forwarding way
return ret;
}

相关参考

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// quick sort
template <typename RandomIt>
void quicksort(RandomIt first, RandomIt last) {
if (first == last) return;
auto pivot = *std::next(first, std::distance(first,last)/2);
RandomIt middle1 = std::partition(first, last, [pivot](const auto& em){ return em < pivot; });
RandomIt middle2 = std::partition(middle1, last, [pivot](const auto& em){ return !(pivot < em); });
quicksort(first, middle1);
quicksort(middle2, last);
}

template <typename RandomIt>
void sort(RandomIt first, RandomIt last) {
quicksort(first, last);
}

容器 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
2
3
4
5
6
7
8
9
10
// bad
void print_data(unsigned value) {
fmt::print("Distance is {} meters\n", value);
}
auto distance_mk=3;
print_data(distance_km); // 歧义调用,这里原意只让打印 meters
print_data(distance_km*1000);

// good
void print_distance(unsigned distance_in_meters);

Use strong types

使用健壮的类型,尽量保证在编译或运行给出更多信息,健壮的类型不完全检测包括:

1
2
3
4
5
6
7
8
9
10
11
12
#include<NamedType/named_type.hpp>
using Meter= fluent::NamedType<unsgined, struct MeterTag>;

void print_distance(Meter distance) {
fmt::print("Distance is {} meters\n", distance.get());
}
// ...

auto auto distance_meters=3000;
print_distance(Meter(distance_meters)); // ok

print_distance(3000); // won't compile

Avoid easily swappable params

避免两个类型相等的参数很容易错用,注释不如实际类型使用安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// good 每个参数都能清楚表达应有的含义
struct visitor {/**/}

enum class Direction {Forward,Backward };

struct Config{
bool ignore_x {false};
bool ignore_y {false};
};

struct Graph{
// ...
void walk(visitor v, Direction direction,Config config){}
}
visitor my_visitor;
Config config;
config.ignore_y = true;
Graph().walk(my_visitor, Direction::Backward, config);

// bad 存在 3 个 bool 参数,可能造成调用错误且造成 api 不容易使用
// void walk(visitora v,bool is_backward, bool ignore_x, bool ignore_y){}
// Graph().walk(my_visitor, false, true, false);

避免相同类型设置为函数签名的参数,除了惯用表达外。更健全的类型能在让编译器更易报错

1
2
3
4
5
6
7
8
9
10
// Enum class to the rescue
enum class truncate {off,on};
enum class pad {off,on};
enum class fill {off,on};

void print(std::string_view str, truncate truncate_, pad pad_, fill fill_);

main(){
print("hello", truncate::on, pad::on, fill::on);
}

fopen API 合理重写

1
2
3
4
using FilePtr = std:unique_ptr<FILE,decltype([](FILE *f){fclose(f);})>

[[nodiscard]] FilePtr fopen(const std:filesystem:path &path,
std:string view mode);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle;

template<typename DrawStrategy> // <--- SRP
class Circle : public Shape {
public:
Circle(double rad)
: radius{rad}
, //..Remaining data members
{}

double getRadius() const noexcept;
// ... getCenter(),getRotation(),...
void draw(/*...*/) const override {
DrawStrategy{}(this, /*...*/);
}
void serialize(/*...*/)const override;
// ...
private:
double radius;
// ... Remaining data members
};
  • a callback function(eg. std::function)(c++11+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle;
using DrawCircleStrategy = std::function<void(Circle const &)>; // <--- SRP

class Circle : public Shape {
public:
Circle(double rad, DrawCircleStrategy strategy) // <--- Dependency Injection
: radius{rad}, //..Remaining data members
drawing{std::move(strategy)} {}

double getRadius() const noexcept;
// ... getCenter(),getRotation(),...
void draw(/*...*/) const override {
drawing(this, /*...*/);
}
void serialize(/*...*/)const override;
// ...
private:
double radius;
// ... Remaining data members
DrawCircleStrategy drawing;
};

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
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
class PersistenceInterface {
public:
PersistenceInterface();
virtual ~PersistenceInterface();

bool write( const Blob& blob ) { // <--- NVI
LOG_INFO( "PersistenceInterface::write( Blob ), name = " <<
blob.name() << ": starting..." );
if ( blob.name().empty() ) {
LOG_ERROR( "PersistenceInterface::write( Blob ): Attempt to"
" write unnamed blob failed" );
return false;
}
const auto start = std::chrono::high_resolution_clock()::now();
const bool success = doWrite( blob ); // call the actual implementation
const uint32_t time = std::chrono::high_resolution_clock::now() - start;
LOG_INFO( "PersistenceInterface::write( Blob ), name = " <<
blob.name() << ": Writing blob of size " << blob.size() <<
" bytes " << ( success ? "succeeded" : "failed" ) << " in"
"duration = " << time.count() << "ms" );
return success;
}

// ...
private: // protected?
virtual bool doWrite( const Blob& blob ) = 0;
// ...
}

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
2
3
4
5
6
template<
class T,
class Allocator = std::allocator<T>
> class vector;
template< class ForwardIt >
constexpr void destroy( ForwardIt first, ForwardIt last ); // isolated scopes

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.

Data Member Initialization

Obeying to Cpp Core Guideline

Prefer Free Function

设计功能接口在大多数时候应该使用非成员函数(free function

接口设计基本规则遵循:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(f needs to be virtual)
make f a member function of C;
else if (f is operator >> or operator<< or
f needs type conversion on its left-most argument)
{
make f a non-member function;
if(f needs access to non-public members of C)
make f a friend of C;
}
else if(f can be-implemented via C's public interface)
make f a non-member function;
else
make f a member function of C;

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class server{
public:
struct config{
uint16_t port;
bool tcp = true;
std::string_view address = "0.0.0.0";
std::optional<std::string_view> multicast = std::nullopt;
bool nonblocking = true
};
server(config);
};

main(){
// require c++20
server s({.port = 1666,
.tcp = false,
.address = "127.0.0.1",
.nonblocking = false});
}

Must be initialized

C++20 提供了结构体命名委托初始化器,若没有传参数,gcc -Wextra 给出警告,clang/msvc 不做检查,所以有必要对类型设置必须初始化约束.

links:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename T>
class must_init{
public:
// no default constructor
must_init(T t): value(std::move(t)) {}
operator T&() { return value; }
operator const T&() const { return value; }
private:
T value;
};

class server{
public:
struct config{
must_init<uint16_t> port; // failure to init is now a compiler error
bool tcp = true;
std::string_view address = "0.0.0.0";
std::optional<std::string_view> multicast = std::nullopt;
bool nonblocking = true
};
server(config);
};

Create Unique type with enum for Integer types

1
2
enum class server_id: int {};
enum class client_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
2
3
4
5
template <class Policy1, class Policy2, class Policy3>
class PolicyBasedClass : public Policy1, public Policy2, public Policy3 {
public:
PolicyBasedClass(){};
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template
<
class T,
template< class > class ReadPolicy,
template< class > class WritePolicy
>
class ResourceManager
:
public ReadPolicy< T >,
public WritePolicy< T >
{
public:
void Read();
void Write(XmlElement*);
void Write(DataSource*);
};

void main()
{
ResourceManager< AnimationEntity, BinaryReader, BinaryWriter > ResMgr1;
ResourceManager< ScriptEntity, TextReader, TextWriter > ResMgr2;
}

上述的 class T 即是一个 Template Template Parameter,这使得 Policy Class 更具扩展性与弹性,能够处理各种类型的实体(instance)。

  • Hello World 简单示例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
template<
typename output_policy,
typename language_policy
>
class HelloWorld
: public output_policy,
public language_policy
{
using output_policy::Print;
using language_policy::Message;

public:

//behaviour method
void Run()
{
//two policy methods
Print( Message() );
}

};


#include <iostream>

class HelloWorld_OutputPolicy_WriteToCout
{
protected:

template< typename message_type >
void Print( message_type message )
{
std::cout << message << std::endl;
}

};

#include <string>

class HelloWorld_LanguagePolicy_English
{
protected:

std::string Message()
{
return "Hello, World!";
}

};

class HelloWorld_LanguagePolicy_German{
protected:

std::string Message()
{
return "Hallo Welt!";
}

};

int main()
{

/* example 1 */

typedef
HelloWorld<
HelloWorld_OutputPolicy_WriteToCout,
HelloWorld_LanguagePolicy_English
>
my_hello_world_type;

my_hello_world_type hello_world;
hello_world.Run(); //returns Hello World!


/* example 2
* does the same but uses another policy, the language has changed
*/

typedef
HelloWorld<
HelloWorld_OutputPolicy_WriteToCout,
HelloWorld_LanguagePolicy_German
>
my_other_hello_world_type;

my_other_hello_world_type hello_world2;
hello_world2.Run(); //returns Hallo Welt!
}

后续可以更容易的撰写其他的 Output policy, 单靠创造更新的 Policy class 并实现 print 于其中。

Pimpl - Pointer to implementation

Pimpl 是一种广泛使用的削减编译依赖项的技术,具体讨论参考,看下面例子可能就明白了:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// cppreference 官方示例
#include <iostream>
#include <memory>
#include <experimental/propagate_const>

// ---------------
// 接口(widget.h)
class widget
{
class impl; // 实现类的前置声明
std::experimental::propagate_const< // 转发 const 的指针包装器
std::unique_ptr< // 唯一所有权的不透明指针
impl>> pImpl; // 指向前置声明的实现类
public: // 公开成员
void draw() const; // 公开 API,将被转发给实现
void draw();
bool shown() const { return true; } // 公开 API,实现必须调用它

widget(int);
~widget(); // 在实现文件中定义,其中 impl 是完整类型
widget(widget&&); // 在实现文件中定义
// 注意:在被移动的对象上调用 draw() 是未定义行为
widget(const widget&) = delete;
widget& operator=(widget&&); // 在实现文件中定义
widget& operator=(const widget&) = delete;
};

// -----------------
// 实现(widget.cpp)
class widget::impl
{
int n; // 私有数据
public:
void draw(const widget& w) const
{
if(w.shown()) // 对公开成员函数的此调用要求回溯引用
std::cout << "正在绘制 const 组件 " << n << '\n';
}

void draw(const widget& w)
{
if(w.shown())
std::cout << "正在绘制非 const 组件 " << n << '\n';
}

impl(int n) : n(n) {}
};

void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;

// ---------------
// 用户(main.cpp)
int main()
{
widget w(7);
const widget w2(8);
w.draw();
w2.draw();
}

此技巧用于构造拥有稳定 ABI 的 C++ 库接口,及减少编译时依赖。

links:

CRTP -The curiously recurring template pattern

CRTP (奇异递归模板模式)是一种在编译期实现多态方法,是对运行时多态一种优化,多态是个很好的特性,但是动态绑定比较慢,因为要查虚函数表。而使用 CRTP,完全消除了动态绑定,降低了继承带来的虚函数表查询开销。

CRTP cpp reference 示例 包含:

  • 从模板类继承,

  • 使用派生类本身作为基类的模板参数。

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
#include <cstdio>

#ifndef __cpp_explicit_this_parameter // Traditional syntax
// see https://cppinsights.io/s/82b200f2 from C++ insight result
template <class Derived>
struct Base { void name() { (static_cast<Derived*>(this))->impl(); } };
struct D1 : public Base<D1> { void impl() { std::puts("D1::impl()"); } };
struct D2 : public Base<D2> { void impl() { std::puts("D2::impl()"); } };

void test() {
Base<D1> b1; b1.name();
Base<D2> b2; b2.name();
D1 d1; d1.name();
D2 d2; d2.name();
}

#else // C++23 alternative syntax from https://godbolt.org/z/s1o6qTMnP

struct Base { void name(this auto&& self) { self.impl(); } };
struct D1 : public Base { void impl() { std::puts("D1::impl()"); } };
struct D2 : public Base { void impl() { std::puts("D2::impl()"); } };

void test() {
D1 d1; d1.name();
D2 d2; d2.name();
}

#endif

int main() {
test();
}

这样做的目的是在基类中使用派生类。从基础对象的角度来看,派生对象本身就是对象,但是是向下转换的对象。因此,基类可以通过将 static_cast 自身放入派生类来访问派生类.

实现简单策略调用示例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 定义引线识别模板
* @note 子类需要定义 PolicyImpl
*
* @tparam Policy 策略子类实现
* @tparam R 返回类型约束
*/
template <typename Policy, typename R>
class LeadBasePolicy {
public:
LeadBasePolicy(const DataPtr& data, const AiTextPtr& ref_text)
: ref_text(ref_text), data(data) {}

template <typename... Args>
decltype(auto) // 保留返回修饰限定
DoPolicy(Args&&... args) {
static_assert(
std::is_same_v<
std::decay_t<R>,
std::decay_t<
decltype(
static_cast<Policy*>(this)->PolicyImpl(
std::forward<Args>(args)...
)
)
>
>,
"R must be same as the return type of PolicyImpl");
return static_cast<Policy*>(this)->PolicyImpl(std::forward<Args>(args)...);
}

protected:
const AiTextPtr& ref_text{};
const DataPtr& data{};
};

/**
* 描述文本查找相对引线策略
*
* @code {.cpp}
* auto line_ptr = TextLinePolicy(data,text).DoPolicy();
* assert(line_ptr);
* @endcode
*
*/
class TextLinePolicy : public LeadBasePolicy<TextLinePolicy, AiLinePtr> {
friend LeadBasePolicy;
public:
using LeadBasePolicy::LeadBasePolicy;
protected:
/**
* @brief 策略实际实现的文本关联线段搜索
* PolicyImpl 可被子类重写,进行调用分发
*/
AiLinePtr PolicyImpl() {
auto line_ptr = RangeSearch();
return line_ptr ? line_ptr : NearSearch();
}

/**
* @brief 策略1,文本范围附近引线线段搜索
*/
AiLinePtr RangeSearch();

/**
* @brief 策略2,距离文本附近最近的三个条线搜索
*/
AiLinePtr NearSearch();
};

HAP - Handler on Aggregation with Polymorphism

HAP 提供了一种方法如何在使用多态和虚方法时保持通用性和良好的性能,以及如何使用聚合。

  • 虚方法对集合整体分发一次。

  • 使用基于类策略模式简化策略 Handler 的调用。

  • 处理器 Handler 仅在运行时可知。

NVI - Non Virtual Interface

仅暴露非虚方法接口,避免调用歧义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct HandlerBase {
void handle(std::span<Element> elements) {
for(Element e : elements) {handle(std::move(e)); }
}

private:

virtual void handle(Element element) = 0;
};

struct Handler : public HandlerBase {
private:
void handle(Element element) override{}
};

MutextProtected

这里提供了一种 C++ 方式的并行访问数据加锁模式,通常通过 RAII 实现。

另见,Andreas Kling - MutextProtectedSynchronized - follyMutextProtected - reddit

TEBI - Type erease better than inheritance

在解耦,类型擦除更优于继承,避免了多重继承,导致依赖过多。

参考示例:

links:

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:

Native API

jni

java 提供 invocation api 供三方调用 jvm native api

QA:

links:

Weekly Practice

Void Pointer Arithmetic

避免指针运算有利于提高安全及代码通用性

links:

std::expected/std::optional

c++23 提出的有力的返回值 / 错误处理工具。向后兼容实现 tl-expected

core guideline

Development Utilities

这里搜集了一些用于增强 cpp 开发效率的工具。

links:

Optimization

perf

links:

Guidelines

links:

测试

依赖非代码文件

当测试代码依赖于非代码文件时,需要慎重考虑,是否能拆成更小的测试用例,或者使用 mock 对象。

如果无法避免,可以使用合理的资源管理机制。

links:

GTest Main

当 Gtest 开启在 cmake 中配置时,可以忽略在测试文件中 main 定义

1
2
include(GoogleTest)
gtest_discover_tests(targets)

通常省略以下定义

1
2
3
4
int main(int argc, char const* argv[]) {
testing::InitGoogleTest(&argc, const_cast<char**>(argv));
return RUN_ALL_TESTS();
}

语义优化(语言相关)

减少 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<class _RanIt,
class _Diff,
class _Pr> inline
void _Sort(_RanIt _First, _RanIt _Last, _Diff _Ideal, _Pr& _Pred)
{ // order [_First, _Last), using _Pred
if (_Ideal < _SORT_MAX_SIZE) // _SORT_MAX_SIZE == 16
_Insertion_sort(_First, _Last, _Pred);
else if (_SORT_MAX_SIZE < _Last - _First)
{ // divide and conquer by quicksort
_RanIt _Mid = _Unguarded_partition(_First, _Last, _Pred);
_Sort(_First, _Mid, _Ideal >> 1, _Pred);
_Sort(_Mid, _Last, _Ideal >> 1, _Pred);
}
else
_Heap_sort(_First, _Last, _Pred);
}

在这个实现中,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 的高度
M x N

前面提到的三维变换非常容易可以替换为矩阵运算。这里 1 是为了配合平移变换计算。

二维空间变换

三维空间变换

曲线拟合

二次曲线拟合

以二次拟合曲线为例,方程可以表示为 y = a*x*x + b*x + c,其中 a、b 和 c 是待求的系数,x 和 y 分别是点的横坐标和纵坐标,可以使用三个点的坐标来列出三个方程:

1
2
3
y1 = a * x1^2 + b * x1 + c
y2 = a * x2^2 + b * x2 + c
y3 = a * x3^2 + b * x3 + c

将这三个方程化简,可以得到以下矩阵方程:

1
2
3
[ x1^2  x1  1 ]   [ a ]   [ y1 ]
[ x2^2 x2 1 ] * [ b ] = [ y2 ]
[ x3^2 x3 1 ] [ c ] [ y3 ]

可以使用矩阵的逆来求解系数向量 [a, b, c],具体来说,系数向量可以表示为:

1
2
3
[ a ]   [ x1^2  x1  1 ]^-1    [ y1 ]
[ b ] = [ x2^2 x2 1 ] * [ y2 ]
[ c ] [ x3^2 x3 1 ] [ y3 ]

然后,可以使用 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,可以参考以下资源:

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: