🗓️ 2025-03-26 🎖️ 环境配置专栏 🗂️ 环境配置 🏷️ #c++

现代c++环境配置指南

引言

作为一个高强度冲浪的cpper,经常能在各种地方看到各式各样的大家诟病cpp的地方,其中比较多(但是相对温和)的一条就是,环境配置麻烦。这个问题有多少人说呢?最直观的一条佐证是,迄今为止,我的阅读、点赞、收藏量最多的一篇博文,居然就是随手写的一篇很粗糙的 —— vscode搭配clangd配置现代c++环境-CSDN博客 ,这背后又有多少被这编程第一关折磨的旁友……

本人的CPP水平一言难尽,平常喜欢的就是精通各类语言的hello world,以及配置各种有的没的环境,那么闲话少叙,下面就对这个粗糙的环境配置的坑进行补档。

Goal
  1. 所谓CPP环境,具体指的是什么?
    • 高亮、跳转、补全、错误提示、依赖管理、构建、调试、运行、发布
  2. 具体开发环境搭建
    • vscode、vim/nvim、sublime,or clion、vs
提示

本文会对我所认知范围内的,C++的环境做全流程的介绍。也会介绍具体的工具、环境该如何配置,但囿于篇幅和本人的水平以及耐心,可能不会特别详细,更无法兼顾到所有的conner case。

比如我会说编译器是g++,构建工具是cmake,但是gcc和cmake如何安装到你的电脑上这种就没有介绍了。不过我想既然看官能够在茫茫互联网中找到我这篇博文,那么继续搜索搜索解决这些小问题自然不在话下。网上有相当多的且优质的关于某一个工具、软件如何下载安装配置的教程,如果你不清楚,直接STFW即可!

至于本文,如果您能只凭本文顺着就把环境配好了,那实在是您天赋异禀,天生就是干C嘎嘎的料子。如果不能,那么直接搜索不清楚的内容就好。我希望的是本文能给各位刚接触C++的朋友们一个稍微完整一点的,关于C++开发环境的认识,知道有哪些工具,分别是做什么用的。然后根据自己的需要去按图索骥!

CPP开发环境都包括哪些?

什么是CPP开发环境呢?其实就是,你现在想阅读C++代码,或者想写C++代码,或者想把一个C++代码/项目跑起来,或者你想修改一个C++代码/项目,但是直接用记事本看/写你有不乐意,干看呢代码也不会自己运行。所以你需要在你的电脑上安装一系列的东西,来让这个看代码写代码改代码的过程,首先是能走通,然后是走得更舒服、更规范,这个就是配C++环境。

那么C++程序需要什么环境呢?看你的需求。

  1. 如果你想看代码,那么有个高亮、点击函数能自动跳转就可以了(这个需要lsp(不用管是啥,反正就是一个能理解你代码,还能让你跳转的东西));
  2. 如果你想编辑代码,那么最好还要有自动补全(这个也是需要lsp),写的过程中还得有错误提示(这个动作叫做lint),告诉你这样写不对/不规范,写着写着还想格式化一下(format),不然看着不舒服;
  3. 如果你还想把代码跑起来,那你害得安装个编译器(就是gcc、clang、msvc之类的),编译器就是负责把你看到的.cpp或者.c这种文件,转变成.exe的可执行程序的东西(你看代码用的vscode/记事本这些,叫做编辑器);
  4. 如果你想跑的代码,还不是单个文件,是一个正儿八经的项目,那么一般就需要安装下构建工具了(也就是makefile、cmake、xmake、bazel这些,具体是啥看你的项目用的啥)
  5. 如果一跑发现,哎呀出错了,那这个时候就需要调试了。调试就是找到程序中哪里写的不对,然后改正。你可以瞪眼调试,也可以通过一些调试器来做(也就是gdb,或者你用什么clion、visual studio来做)
  6. 如果是你自己写项目,标准库不够你用了,你迫切地想使用别人写好的一些库,可是怎么整到自己的项目中来呢?从哪整进来呢?整进来了怎么运行呢?(这也算是构建的内容,但也有一部分包管理的部分。c++虽然以没有统一的包管理而饱受诟病,但不代表它就没有,比如用查cmake,或者专业的vcpkg、conan)
  7. 还是你写的项目,千辛万苦,程序终于跑起来了!但是跑起来了就算没问题了吗?会不会只是暂时没有执行到有错的地方,跑一会儿或者多跑几次就出错了呢?当然会,比如大名鼎鼎的错误:内存泄漏、指针问题等。那么这个时候还需要用到一些工具来帮助你扫描这些潜在的漏洞(也就是valgrind、sanitizer、cppcheck等等等)
  8. 正确性满足了,但是程序只是能正确地跑,跑得快不快呢?跑得慢具体是哪一部分拖慢了程序呢?有追求的化,还要对性能进行优化,找到性能的瓶颈,这时候可能会用到一些性能分析工具(比如perf)
  9. 终于,一切就绪了,你写好了,也跑好了,现在就差拿去给别人用了。但是别人一用,缺少库文件!格式不兼容!或者我怎么编译不通过!!这时候你发现:哎早知道不学C嘎嘎了。。。

下面对各个步骤的推荐工具配置进行注意介绍:

高亮、补全、跳转

TLDR

这三类可以统一由一个LSP提供,也是最推荐的——clangd,此外就是用IDE自带的,或者VSCODE-CPP插件包。(还有一个在更新中的项目 clice-project/clice 有望超过clangd,不过目前还处于不可用状态。)

介绍与使用

clangd是一个语言服务器,提供代码高亮、补全、跳转的功能,但是不能直接用,因为既然是服务器,那么就要配合客户端使用,使用不同的IDE或编辑器对应的客户端不同。

配置

clangd也可以进行配置,参考 官方手册 ,比如可以指定在后台索引、指定对待源文件的方式、指定语法检查的工具Diagnostics(就是后文中 clang-tidy 的配置)等等。

配置文件的路径,Linux在 $HOME/.config/clangd/config.yaml,windows在 %APPDATALOCAL%/clangd/config.yaml。(如果有不对的,可以直接打开vscode,安装clangd插件,然后 ctrl+shift+p 搜索 clangd open user config 即可找到)

这里给出一份我的配置

Index:
  Background: Build

CompileFlags:
  Add: [-xc++, -Wall, -std=c++20]
  Compiler: clang++

Diagnostics:
  ClangTidy:
    Add: ["*"]
    Remove: [
        abseil*,
        fuchsia*,
        llvmlib*,
        zircon*,
        altera*,

        bugprone-easily-swappable-parameters, #相邻同类型参数容易混淆
        cppcoreguidelines-avoid-c-arrays, # 不要用 C 数组
        cppcoreguidelines-avoid-do-while,
        cppcoreguidelines-avoid-magic-numbers, # 不要用魔法数字
        cppcoreguidelines-macro-usage, # 宏定义用法,不要定义常数之类的
        cppcoreguidelines-non-private-member-variables-in-classes, # 所有成员变量都得是private的
        cppcoreguidelines-owning-memory, # 用gsl::owner<> 来表示指针拥有对象的所有权
        cppcoreguidelines-pro-bounds-array-to-pointer-decay, # 数组传参退化为指针
        cppcoreguidelines-pro-bounds-pointer-arithmetic, # 不要使用指针运算
        cppcoreguidelines-pro-bounds-constant-array-index, # 数组index当为常量
        google-build-using-namespace, # 不要用using namesapce xxx;
        google-readability-todo,
        hicpp-avoid-c-arrays, # 禁用c风格数组
        hicpp-braces-around-statements,
        hicpp-no-array-decay, # 防止数组传参退化为指针
        misc-no-recursion, #递归
        misc-non-private-member-variables-in-classes, # public成员变量,或许不该用,但是想用
        modernize-avoid-c-arrays, # 同上
        modernize-use-nodiscard, # 推荐使用 [[nodiscard]]
        modernize-use-trailing-return-type, # 不要每个都加上尾返回值类型
        readability-braces-around-statements,
        readability-convert-member-functions-to-static, # 转为静态成员函数
        readability-identifier-length, # 不检查变量名长度
        readability-implicit-bool-conversion, # 隐式布尔类型转换
        readability-magic-numbers, #同上
      ]

格式化

推荐 clangd-format

简介与使用

可能看一些开源项目的时候应该就见过了,很多项目中都会有一个.clang-format文件,这个就是clang-format需要使用的配置文件,可以在里面指定样式。

你可以直接从认可的开源项目中clone一份来用,也可以找一个基础版本在上面修改,具体的选项参考 官方文档

使用时,在编辑器/IDE里面可以配合相应的格式化插件使用,编辑文件的时候实时格式化一下。

也可以直接使用命令行对文件格式化。比如使用如下命令 fd -e cpp -e cc -e h -x clang-format -i 可以做到对项目中所有的头文件源文件按照当前.clang-format文件指定的格式重新格式化一下。

配置

这里给出一个我经常用的clang-format配置:

DisableFormat: false # 关闭格式化
BasedOnStyle: Google
Language: Cpp
Standard: Latest
ColumnLimit: 120
# 在大括号前换行: Attach(始终将大括号附加到周围的上下文), Linux(除函数、命名空间和类定义,与Attach类似),
#   Mozilla(除枚举、函数、记录定义,与Attach类似), Stroustrup(除函数定义、catch、else,与Attach类似),
#   Allman(总是在大括号前换行), GNU(总是在大括号前换行,并对于控制语句的大括号增加额外的缩进), WebKit(在函数前换行), Custom
#   注:这里认为语句块也属于函数
BreakBeforeBraces: Custom
BraceWrapping:
  # case标签后面
  AfterCaseLabel: false
  # class定义后面
  AfterClass: false
  # 控制语句后面
  AfterControlStatement: Never
  # enum定义后面
  AfterEnum: false
  # 函数定义后面
  AfterFunction: false
  # 命名空间定义后面
  AfterNamespace: false
  # ObjC定义后面
  AfterObjCDeclaration: false
  # struct定义后面
  AfterStruct: false
  # union定义后面
  AfterUnion: false
  #ExternBlock定义后面
  AfterExternBlock: false
  # catch之前
  BeforeCatch: false
  # else之前
  BeforeElse: false
  # lambda块之前
  BeforeLambdaBody: false
  # while之前
  BeforeWhile: false
  # 缩进大括号
  IndentBraces: false
  # 分割空函数
  SplitEmptyFunction: true
  # 分割空记录
  SplitEmptyRecord: true
  # 分割空命名空间
  SplitEmptyNamespace: true

# 在 @property 后面添加空格, \@property (readonly) 而不是 \@property(readonly).
ObjCSpaceAfterProperty: true
# 访问说明符(public、private等)的偏移
AccessModifierOffset: -4

# 开括号(开圆括号、开尖括号、开方括号)后的对齐: Align, DontAlign, AlwaysBreak(总是在开括号后换行)
AlignAfterOpenBracket: Align
# 连续赋值时,对齐所有等号
AlignConsecutiveAssignments: Consecutive
# 连续声明时,对齐所有声明的变量名
AlignConsecutiveDeclarations: Consecutive
# 连续宏声明时,对齐空格
AlignConsecutiveMacros: Consecutive
# 对齐连接符: DontAlign(不对齐), Left(左对齐), Right(右对齐)
AlignEscapedNewlines: Left
# 水平对齐二元和三元表达式的操作数
AlignOperands: Align
# 对齐连续的尾随的注释
AlignTrailingComments: true
# 允许函数调用的所有参数在放在下一行
AllowAllArgumentsOnNextLine: true
# 允许函数声明的所有参数在放在下一行
AllowAllParametersOfDeclarationOnNextLine: true
# 允许短的块放在同一行
AllowShortBlocksOnASingleLine: Empty
# 允许短的case标签放在同一行
AllowShortCaseLabelsOnASingleLine: true
# 允许短的枚举放在同一行
AllowShortEnumsOnASingleLine: true
# 允许短的函数放在同一行: None, InlineOnly(定义在类中), Empty(空函数), Inline(定义在类中,空函数), All
AllowShortFunctionsOnASingleLine: Inline
# 允许短的if语句保持在同一行
AllowShortIfStatementsOnASingleLine: Never
# 允许短的Lambdas语句保持在同一行
AllowShortLambdasOnASingleLine: All
# 允许短的循环保持在同一行
AllowShortLoopsOnASingleLine: true
# 总是在返回类型后换行: None, All, TopLevel(顶级函数,不包括在类中的函数),
#   AllDefinitions(所有的定义,不包括声明), TopLevelDefinitions(所有的顶级函数的定义)
# (clang-format-19: Renamed to BreakAfterReturnType)
AlwaysBreakAfterReturnType: None
# 总是在多行string字面量前换行
AlwaysBreakBeforeMultilineStrings: false
# 总是在template声明后换行(clang-format-19: Renamed to BreakTemplateDeclarations)
AlwaysBreakTemplateDeclarations: Yes
# false表示函数实参要么都在同一行,要么都各自一行
BinPackArguments: true
# false表示所有形参要么都在同一行,要么都各自一行
BinPackParameters: true
# 在二元运算符前换行: None(在操作符后换行), NonAssignment(在非赋值的操作符前换行), All(在操作符前换行)
BreakBeforeBinaryOperators: NonAssignment
# 在三元运算符前换行
BreakBeforeTernaryOperators: true
# 在构造函数的初始化列表的逗号前换行
BreakConstructorInitializers: BeforeColon
# 在类声明继承列表的逗号前换行
BreakInheritanceList: BeforeColon
# 允许中断长字符串
BreakStringLiterals: true
# 描述具有特殊意义的注释的正则表达式,它不应该被分割为多行或以其它方式改变
CommentPragmas: "^ ONE[ ]?LINE"
# 允许连续的名称空间声明将在同一行
CompactNamespaces: false
# 构造函数的初始化列表的缩进宽度
ConstructorInitializerIndentWidth: 4
# 延续的行的缩进宽度
ContinuationIndentWidth: 4
# 去除C++11的列表初始化的大括号{后和}前的空格
Cpp11BracedListStyle: true
# 继承最常用的指针和引用的对齐方式
DerivePointerAlignment: true
# 修饰符后放置空行
EmptyLineAfterAccessModifier: Never
# 修饰符前放置空行
EmptyLineBeforeAccessModifier: LogicalBlock
# 修正命名空间注释
FixNamespaceComments: true
# 需要被解读为foreach循环而不是函数调用的宏
ForEachMacros:
  - foreach
  - Q_FOREACH
  - BOOST_FOREACH
# 需要解读为if的函数
IfMacros:
  - KJ_IF_MAYBE
# 对#include进行排序,匹配了某正则表达式的#include拥有对应的优先级,匹配不到的则默认优先级为INT_MAX(优先级越小排序越靠前),
#   可以定义负数优先级从而保证某些#include永远在最前面
IncludeBlocks: Regroup
IncludeCategories:
  - Regex: '^<ext/.*\.h>'
    Priority: 2
    SortPriority: 0
    CaseSensitive: false
  - Regex: '^<.*\.h>'
    Priority: 1
    SortPriority: 0
    CaseSensitive: false
  - Regex: "^<.*"
    Priority: 2
    SortPriority: 0
    CaseSensitive: false
  - Regex: ".*"
    Priority: 3
    SortPriority: 0
    CaseSensitive: false
# include块排序
IncludeIsMainRegex: "([-_](test|unittest))?$"
# 缩进修饰符
IndentAccessModifiers: false
# 缩进case块
IndentCaseBlocks: false
# 缩进case标签
IndentCaseLabels: true
# 缩进goto标签
IndentGotoLabels: false
# 预处理缩进
IndentPPDirectives: None
# 缩进extern块
IndentExternBlock: AfterExternBlock
# 缩进宽度
IndentWidth: 4
# 函数返回类型换行时,缩进函数声明或函数定义的函数名
IndentWrappedFunctionNames: false
# 添加尾部注释
InsertTrailingCommas: None
# Lambda块缩进
LambdaBodyIndentation: Signature
# 开始一个块的宏的正则表达式
MacroBlockBegin: ""
# 结束一个块的宏的正则表达式
MacroBlockEnd: ""
# 连续空行的最大数量
MaxEmptyLinesToKeep: 1
# 命名空间的缩进: None, Inner(缩进嵌套的命名空间中的内容), All
NamespaceIndentation: Inner
# 使用的包构造函数初始化式样式
PackConstructorInitializers: NextLine
# 在call(后对函数调用换行的penalty
PenaltyBreakBeforeFirstCallParameter: 19
# 在一个注释中引入换行的penalty
PenaltyBreakComment: 300
# 第一次在<<前换行的penalty
PenaltyBreakFirstLessLess: 120
# 在一个字符串字面量中引入换行的penalty
PenaltyBreakString: 1000
# 对于每个在行字符数限制之外的字符的penalty
PenaltyExcessCharacter: 1000000
# 将函数的返回类型放到它自己的行的penalty
PenaltyReturnTypeOnItsOwnLine: 200
# 指针和引用的对齐: Left, Right, Middle
PointerAlignment: Left
# 预处理的缩进
PPIndentWidth: -1
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
      - ParseTestProto
      - ParsePartialTestProto
    CanonicalDelimiter: pb
    BasedOnStyle: google
# 引用对齐
ReferenceAlignment: Pointer
# 允许重新排版注释
ReflowComments: true
# 允许排序#include
SortIncludes: CaseSensitive
# 允许排序声明
SortUsingDeclarations: true
# 单独的定义块
SeparateDefinitionBlocks: Always
# 在C风格类型转换后添加空格
SpaceAfterCStyleCast: false
# 在赋值运算符之前添加空格
SpaceBeforeAssignmentOperators: true
# 在逻辑非操作符之后插入一个空格
SpaceAfterLogicalNot: false
# 在' template '关键字之后会插入一个空格
SpaceAfterTemplateKeyword: true
# 在用于初始化对象的c++ 11带括号的列表之前(在前面的标识符或类型之后)将插入一个空格
SpaceBeforeCpp11BracedList: false
# 构造函数初始化式冒号前的空格是否删除
SpaceBeforeCtorInitializerColon: true
# 在继承冒号前添加空格
SpaceBeforeInheritanceColon: true
# 控制括号前的单独空格。
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
  AfterControlStatements: true
  AfterForeachMacros: true
  AfterFunctionDefinitionName: false
  AfterFunctionDeclarationName: false
  AfterIfMacros: true
  AfterOverloadedOperator: false
  BeforeNonEmptyParentheses: false
# 在基于冒号的范围循环之前 添加空格
SpaceBeforeRangeBasedForLoopColon: true
# 在尾随的评论前添加的空格数(只适用于//)
SpacesBeforeTrailingComments: 2
# 在尖括号的<后和>前添加空格
SpacesInAngles: Never
# 在容器(ObjC和JavaScript的数组和字典等)字面量中添加空格
SpacesInContainerLiterals: true
# 在C风格类型转换的括号中添加空格
SpacesInCStyleCastParentheses: false
# 在方括号的[后和]前添加空格,lambda表达式和未指明大小的数组的声明不受影响
SpacesInSquareBrackets: false
# tab宽度
TabWidth: 4
# 使用tab字符: Never, ForIndentation, ForContinuationAndIndentation, Always
UseTab: Never
WhitespaceSensitiveMacros:
  - STRINGIZE
  - PP_STRINGIZE
  - BOOST_PP_STRINGIZE
  - NS_SWIFT_NAME
  - CF_SWIFT_NAME

错误告警(Lint)

推荐 clang-tidy

简介与使用

看这个又是clang开头的应该就知道了,clang-tidy也是llvm项目的一部分,使用方法大同小异,都是在当前项目下放一个配置文件,这个叫.clang-tidy,然后就可以在里面指定你想要的检查项目。使用clangd时会自动对相关项进行检查与告警。

不过在前面clangd的部分中提到过,在clangd的配置文件中也可以指定你的默认检查项,具体的可以返回参考相关部分

配置

一开始我是恨不得开启所有的检查项,以求代码的干净。这样确实能快速学到很多更加规范的写法,所以也可以尝试一下。

但是如果指定的检查项太多了的话,打开一个项目全是波浪线,也有点难受。因此学一阵子之后,还是建议回归到开启最关键的和常用的即可。仍然是可以从认可的开源项目中抓一个下来用,也可以参考官方手册进行修改。

这里给出我的一个比较精简的配置:

# REFERENCE https://blog.csdn.net/stallion5632/article/details/139545885
Checks: "-*,
  clang-analyzer-core.*,
  clang-analyzer-cplusplus.*,
  modernize-redundant-void-arg,
  modernize-use-bool-literals,
  modernize-use-equals-default,
  modernize-use-nullptr,
  modernize-use-override,
  google-explicit-constructor,
  ; google-readability-casting,
  readability-braces-around-statements,
  readability-identifier-naming.ClassCase,
  readability-identifier-naming.StructCase,
  readability-identifier-naming.TypedefCase,
  readability-identifier-naming.EnumCase,
  readability-non-const-parameter,
  cert-dcl21-cpp,
  bugprone-undelegated-constructor,
  bugprone-macro-parentheses,
  bugprone-macro-repeated-side-effects,
  bugprone-forward-declaration-namespace,
  bugprone-bool-pointer-implicit-conversion,
  bugprone-misplaced-widening-cast,
  cppcoreguidelines-narrowing-conversions,
  misc-unconventional-assign-operator,
  misc-unused-parameters"
WarningsAsErrors: ""
HeaderFilterRegex: ""
CheckOptions:
  # 现代化(Modernize)
  - key: modernize-redundant-void-arg
    value: "true" # 检查并移除函数声明中冗余的 void 参数。
  - key: modernize-use-bool-literals
    value: "true" # 建议使用布尔字面量 true 和 false 代替整数值 0 和 1。
  - key: modernize-use-equals-default
    value: "true" # 建议在默认构造函数、复制构造函数和赋值运算符中使用 = default,以简化代码。
  - key: modernize-use-nullptr
    value: "true" # 建议使用 nullptr 代替 NULL 或 0 来表示空指针。
  - key: modernize-use-override
    value: "true" # 建议在覆盖基类虚函数时使用 override 关键字,以增加代码的清晰性和安全性。

  # Google 代码风格(Google)
  - key: google-explicit-constructor
    value: "true" # 检查并建议在单参数构造函数中使用 explicit 关键字,以防止隐式转换。
  - key: google-readability-casting
    value: "true" # 检查并建议使用 C++ 风格的类型转换(如 static_cast、dynamic_cast、const_cast 和 reinterpret_cast)代替 C 风格的类型转换。

  # 可读性(Readability)
  - key: readability-braces-around-statements
    value: "true" # 建议在单行语句周围添加大括号,以提高代码的可读性和一致性。
  - key: readability-identifier-naming.ClassCase
    value: "CamelCase" # 类名应使用 CamelCase 风格,例如 MyClassName。
  - key: readability-identifier-naming.StructCase
    value: "CamelCase" # 结构体名应使用 CamelCase 风格,例如 MyStructName。
  - key: readability-identifier-naming.TypedefCase
    value: "CamelCase" # 类型定义应使用 CamelCase 风格,例如 MyTypeDef。
  - key: readability-identifier-naming.EnumCase
    value: "CamelCase" # 枚举名应使用 CamelCase 风格,例如 MyEnumName。
  - key: readability-non-const-parameter
    value: "true" # 检查并标识非 const 参数,以提高代码的可读性和安全性。

  # CERT 安全编码标准(CERT)
  - key: cert-dcl21-cpp
    value: "true" # 检查并标识在头文件中不应包含无命名空间的 using 声明和指令,以防止命名空间污染。

  # Bug 检测(Bugprone)
  - key: bugprone-undelegated-constructor
    value: "true" # 检查并标识未委托的构造函数,以确保构造函数的正确性。
  - key: bugprone-macro-parentheses
    value: "true" # 检查并建议在宏定义中使用括号,以防止潜在的错误。
  - key: bugprone-macro-repeated-side-effects
    value: "true" # 检查并标识宏中重复的副作用,以防止潜在的错误。
  - key: bugprone-forward-declaration-namespace
    value: "true" # 检查并标识命名空间前向声明的潜在问题。
  - key: bugprone-bool-pointer-implicit-conversion
    value: "true" # 检查并标识布尔指针的隐式转换,以防止潜在的错误。
  - key: bugprone-misplaced-widening-cast
    value: "true" # 检查并标识错误的宽化转换,以防止潜在的错误。

  # 杂项(Miscellaneous)
  - key: misc-unconventional-assign-operator
    value: "true" # 检查并标识不常见的赋值操作符重载,以确保代码的一致性和可维护性。
  - key: misc-unused-parameters
    value: "true" # 检测未使用的参数。

  # C++ 核心指南(CppCoreGuidelines)
#   - key: cppcoreguidelines-narrowing-conversions
#     value: "true" # 检查并标识可能导致数据丢失的窄化转换。

构建

单文件(编译选项)

构建,对于单个文件来说其实就是编译嘛,直接用 gcc/g++ 或者 clang/clang++ 加上一堆编译选项就可以了。但是编译选项比较多,很多比较偏的我也不甚精通,所以就列出几个常用的,能把代码跑起来就行,后面的还需进一步学习。比如菜鸟上有一个很直观的介绍编译选项的教程 GCC 参数详解 | 菜鸟教程

最简单的命令就是 g++ demo.cpp -o demo,得到的demo就是可执行文件(-o, output,用于指定输出文件的名字)。这背后有很多过程,如果想了解,或者编译出错了,那么就再进一步去了解相关的编译选项。

编译流程相关

编译的流程是: 源文件 -> 预处理 -> 汇编 -> 编译 -> 链接,这些流程都有对应的编译选项,也就是说你可以只编译到任意的中间步骤去查看整成什么样子了。

头文件库文件相关

当然有时候需要处理一些跟头文件库文件相关的内容

调试优化告警标准

然后是,调试、优化、告警、标准这些,还算常用,但不知道怎么归类了,就叫做风格化吧:

项目依赖查看

有时候需要看看项目的依赖,看看g++编译的时候到底都给你找了什么东西引用上了链接上了

性能优化相关

性能优化的值得单开一部分

项目构建

构建工具概要介绍

项目构建就要使用到构建工具了,常见的有 Make,Ninja,CMake,XMake,QMake, Bazel,VSProject …

下面就简单介绍下Make、CMake、Bazel,是真的简单介绍,深入学习的话还是要进一步深入学习的。

Make
Reference

简单来说makefie就是指定目标,指定目标的依赖,然后指定怎么从依赖得到目标的一个脚本,比如:

rebuild:
    @rm -rf build && cd build && cmake .. -G"Ninja"

这里我指定了目标是rebuild,依赖是空,得到目标的办法是执行那一长串命令。这实际上不是编译,只是给那一长串命令起了个名字叫 make rebuild。makefile不只可以用来构建,也可以当作脚本启动器。

Warning

不过需要注意的是,makefile中的命令是新启动一个shell来做的,你可以指定使用shell还是使用bash,但你没法指定它不启动一个新的。所以想在makefile中设置当前的环境变量是不可以的

当然它的主业还是构建,简单介绍一些特性,然后直接给几个例子揣摩一下吧

特殊变量

模式替换

$(patsubst <pattern>,<replacement>,<text> )

查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。

这里,可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,以“%”来表示真实含义的“%”字符)

$(patsubst %.c,%.o, a.c b.c)
# 把字串 “a.c b.c” 符合模式[%.c]的单词替换成[%.o],返回结果是 “a.o b.o”

变量替换引用

对于一个已经定义的变量,可以使用“替换引用”将其值中的后缀字符(串)使用指定的字符(字符串)替换。格式为$(VAR:A=B)或者${VAR:A=B}

意思是,替换变量“VAR”中所有“A”字符结尾的字为“B”结尾的字。“结尾”的含义是空格之前(变量值多个字之间使用空格分开)。而对于变量其它部分的“A”字符不进行替换。

foo := a.o b.o c.o
bar := $(foo:.o=.c)
# 注意变量不要带 $
SRCS_NODIR := $(notdir $(wildcard $(SRC_DIR)/*$(SRC_SUFFIX)))
OBJS_NODIR := $(SRCS_NODIR:$(SRC_SUFFIX)=$(OBJ_SUFFIX))

模板1

# 一个适合中小规模的makefile模版,基本上自己按照实际情况指定一下 源文件,目标文件,头文件目录,以及源文件后缀就行了。

# ---------------------------------------------------------------------------
# commands
# ---------------------------------------------------------------------------
CC := gcc
LINK := gcc
RM := rm -rf
MV := mv
TAR := tar
MKDIR := mkdir

# ---------------------------------------------------------------------------
# settings
# ---------------------------------------------------------------------------
SRC_SUFFIX := .c
OBJ_SUFFIX := .o
LIB_SUFFIX := .a
BIN_SUFFIX := .exe
DLL_SUFFIX := .so

INC_PREFIX := -I
LIB_PREFIX := -L

OPT_C := -c
OPT_OUT := -o
OPT_LINKOUT := -o

CFLAGS := $(OPT_C)
LIBFLAGS := -Debug

# ---------------------------------------------------------------------------
# directories
# ---------------------------------------------------------------------------
SRC_DIR := ./src
OBJ_DIR := ./obj
INC_DIR := ./inc
LIB_DIR := ./lib /usr/local/lib /lib /usr/lib

# ---------------------------------------------------------------------------
# common settings
# ---------------------------------------------------------------------------
SRCS := $(wildcard $(SRC_DIR)/*$(SRC_SUFFIX))
OBJS := $(patsubst $(SRC_DIR)/%$(SRC_SUFFIX),$(OBJ_DIR)/%$(OBJ_SUFFIX),$(SRCS))
INCS := $(addprefix $(INC_PREFIX), $(INC_DIR))
LIBS := $(addprefix $(LIB_PREFIX), $(LIB_DIR)) $(LIBFLAGS)
TEMPFILES := core core.* *$(OBJ_SUFFIX) temp.* *.out typescript*

# ---------------------------------------------------------------------------
# make rule
# ---------------------------------------------------------------------------
TARGET := loader

.PHONY: all clean

all: $(TARGET)

clean:
$(RM) $(TARGET)$(BIN_SUFFIX) $(OBJS)

$(TARGET):$(OBJS)
$(LINK) $(OPT_LINKOUT)$(TARGET)$(BIN_SUFFIX) $(LIBS) $(OBJS)

$(OBJS):$(OBJ_DIR)/%$(OBJ_SUFFIX):$(SRC_DIR)/%$(SRC_SUFFIX)
$(CC) $(CFLAGS) $(INCS) $(OPT_OUT)$@ $<

模板2

CXX := g++

SRC_DIR := ./src
OBJ_DIR := ./build
BIN_DIR := ./bin
INC_DIR := ./include

VPATH = $(INC_DIR) $(OBJ_DIR) $(SRC_DIR)
vpath %.h $(INC_DIR)

# 一种搜索源文件的方式
# SRC_DIRS = $(shell find $(SRC_DIR) -maxdepth 3 -type d)
# SRCS = $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.cpp))
# TODO: 这样子出来的目标文件,在jing'tai时就找不到依赖了
# OBJS := $(OBJ_DIR)/$(notdir $(patsubst %.cpp, %.o, $(SRCS)))

SRCS := $(wildcard $(SRC_DIR)/*.cpp)
OBJS := $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS))
INCS := $(addprefix -I, $(INC_DIR))
BUILDING_DIRS := $(OBJ_DIR) $(BIN_DIR)

TARGET := adb_lab2.exe
RUN := run.sh

$(TARGET) : $(BUILDING_DIRS) $(OBJS)
	$(CXX) -o $(BIN_DIR)/$(TARGET) $(OBJS)
	@touch $(RUN)
	@echo "$(BIN_DIR)/$(TARGET)" > $(RUN)

# 这里的前缀不能少。makefile不会自动去VPATH里面找这几个目标,而是直接当成新的目标来对待
$(OBJ_DIR)/BufferPoolManager.o : BufferPoolManager.h LRUReplacer.h
$(OBJ_DIR)/DataStorageManager.o : DataStorageManager.h
$(OBJ_DIR)/LRUReplacer.o : LRUReplacer.h
$(OBJ_DIR)/main.o : BufferPoolManager.h

# 一个创建运行时依赖文件夹的方法
$(BUILDING_DIRS) :
	@mkdir $@

# 这叫 静态模式
$(OBJS) : $(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
	$(CXX) -o $@ -c $< $(INCS)

.PHONY: all clean output
all : $(TARGET)
clean:
	-rm -rf $(BUILDING_DIRS) test.dbf $(RUN)
output:
	@echo $(SRCS)
	@echo --------------
	@echo $(OBJS)

模板3

CC := gcc
CC_INCLUDE_FLAGS := -I ./include/
CC_FLAGS := $(CC_INCLUDE_FLAGS) -g

# 程序执行的参数
ARGS := ~/codes

DIR_SRC := ./src
DIR_OBJ := ./build
DIR_EXE := ./bin

SRCS := $(shell find $(DIR_SRC) -name "*.c")
OBJS := $(patsubst $(DIR_SRC)/%.c, $(DIR_OBJ)/%.o, $(SRCS))
DPTS := $(patsubst %.c, %.d, $(SRCS))
DIRS := $(DIR_OBJ) $(DIR_EXE)

target := $(DIR_EXE)/my_ls_pro

$(target): $(DIRS) $(OBJS)
	$(CC) $(OBJS) -o $@

$(DIRS):
	@mkdir $@

$(DIR_OBJ)/%.o: $(DIR_SRC)/%.c
	$(CC) $(CC_FLAGS) -c $< -o $@

%.d: %.c
	@set -e; \
	rm -f $@; \
	$(CC) -MM $(CC_FLAGS) $< $(CC_INCLUDE_FLAGS) > $@.$$$$.dtmp; \
	sed 's,\(.*\)\.o\:,$*\.o $*\.d\:,g' < $@.$$$$.dtmp > $@;\
	rm -f $@.$$$$.dtmp

-include $(DPTS)

clean:
	rm -f $(OBJS)
	rm -f $(DPTS)

run:
	make
	$(target) $(ARGS)
CMake

这里只说基本的,拷下来一个cmake项目怎么运行。具体的CMake项目怎么写怎么组织,可以参考项目 quick-cmake GitHub

一般来说分为三步:

  1. 项目根目录创建一个build文件夹,然后cd进去。在build文件夹中进行构建,cmake生成的文件就会都在build文件夹内,不会污染源项目,所以十分推荐这种方式。(当然不cd进去直接用 -B build 指定也可以)
  2. 运行cmake,获取到构建文件。是的,cmake只负责生成构建文件,具体的构建还是由make、ninja这些完成的。
  1. 运行构建命令,得到目标

如果需要安装,就再加一步 make install or ninja install or cmake --build . --target=install。是的,install本身也只是一个target而已

Bazel

bazel我还在学,这里只说环境怎么配置就好了。想学习第一手资料还是去看 官网

首先需要安装的有:

vscode的配置如下:

{
    "bazel.executable": "bazel",
    "bazel.buildifierExecutable": "buildifier",
    "bazel.lsp.command": "starpls", // alternatively: "bazel-lsp"
    "bazel.enableCodeLens": true,
}

代码补全

各语言源码补全参考 将 Bazel 与 IDE 集成

具体到 C/C++ 来说一般是生成 compile_commands.json 文件,官方推荐有两种方式:

  1. kiron1/bazel-compile-commands :在 Bazel 工作区中运行 bazel-compile-commands //... 以生成 compile_commands.json 文件。compile_commands.json 文件可让 clang-tidyclangd (LSP) 和其他 IDE 等工具提供自动补全、智能导航、快速修复等功能。该工具使用 C++ 编写,并使用 Bazel 的 Protobuf 输出来提取编译命令。
  2. hedronvision/bazel-compile-commands-extractor :可在各种可扩展的编辑器(包括 VSCode、Vim、Emacs、Atom 和 Sublime)中启用自动补全、智能导航、快速修复等功能。它可让 clangd 和 ccls 等语言服务器以及其他类型的工具利用 Bazel 对 ccobjc 代码编译方式的理解,包括它如何为其他平台配置交叉编译。

依赖管理

有些规模的C++程序,除了标准库外,一般都会依赖一些别的三方库,或者是自己造的轮子库,比如什么日志库、网络库等等。这些依赖大体上可以分为三种,一种是,静态库、动态库,还有一种 header-only 的库,顾名思义就是只有头文件,不用额外让你的程序链接上库文件(它的优劣可以参考这个 c++ - Benefits of header-only libraries - Stack Overflow )。

如何正确防止三方库的位置、处理编译选项,使得程序能够跑起来,似乎是 编译 部分的事。确实是这样,因为很多程序跑不起来的重要的原因之一,就是第三方依赖找不到、找不对、编译不过,而C++在这一问题上尤为严重。主要是相比较其他主流语言如python(pip)、jvav(maven)、rust(cargo)等,C++没有一个主流的、让各方都信服的包管理工具。可能一方面是C++话事委员会不care这个,另一方面或许是,我称之为Cppers的傲慢。都用c++了,轮子不是自己造的,那算什么用c++?(或许这也是一种乐趣吧)。

所以自己写的包,或者peer写的包,就只能通过一种比较原始的方式,集成到项目中来——把他们下载下来、放到指定的位置,然后手动在编译选项中指定它们。但其实也没有那么地原始。从某种程度上来说cmake、bazel这些构建工具也可以算是一种包管理器,比如cmake的find_package() find_library(),也可以相当自动地帮你完成依赖寻找的任务。只不过前提是你用的库按照cmake的方式组织了,写好了Findxxx.cmake这种。不过一般也都会有,毕竟cmake是事实标准。所以实际上,没有包管理器这件事,是有些不方便,但是其实也并没有一些CPP小黑子说得那么地,无药可救。

但是,事实上C++也是有好用的包管理器的,虽然不是官方的,但也并非什么小作坊的作品。如果你就是习惯有个包管理,那还是相当值得一用的。

Attention

由于C++的编译产物跟平台是强相关的,windows还是linux,x86还是arm,之间是完全不兼容的。因此这些个包管理呢实际上并不是给你下载个二进制文件就直接用了,而是把源码下载下来,然后用你机器上的工具,自动的给你编译好,再放到一个可以方便找到的地方。所以使用它和不使用的区别就是,它可能比你更会编译这个库。当然,都用它来做,更方便管理。

vcpkg
Tldr

微软巨硬做的C++包管理,如果不知道用什么,就相信品牌的力量。支持自动下载、编译、管理三方库,与 CMake 深度集成。支持几千个C++库了,基本涵盖所有常用的。详细学习请参考 vcpkg 文档 | Microsoft Learn

使用起来很简单,跟着下面做就行了。

# 安装
git clone https://github.com/microsoft/vcpkg
cd vcpkg

## 执行引导脚本(Windows 使用 .\bootstrap-vcpkg.bat)
./bootstrap-vcpkg.sh

# 添加环境变量(可选)
export VCPKG_ROOT="/path/to/vcpkg"
export DEFAULET_TRIPLET="x64-linux"
export PATH=$VCPKG_ROOT:${PATH}

# vcpkg还有一个一键和 visual studio集成的命令,但我不咋用vs,给搞忘了。有兴趣可以搜一下

然后你就可以使用vcpkg了。需要注意的是,vcpkg有两个比较重要的环境变量,VCPKG_ROOTDEFAULT_TRIPLET,前者用于搜索vcpkg的位置,后者,triplet,三元组,就是描述你平台的三个关键词,比如x64-linux-static,就说明要安装x64版本linux上的静态库。具体的参考 Triplet | Microsoft Learn

# 搜索
vcpkg search spdlog

# 安装
vcpkg install spdlog

# 在cmake中使用
## 法 1,使用vcpkg的toolchain。 注意 toolchain的指定要在 project(xxx) 之前
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
find_package(spdlog REQUIRED)
target_link_libraries(your_target PRIVATE spdlog::spdlog)

## 法2:设置CMAKE_PREFIX_PATH,这样相当于只是告诉cmake了查找库的位置,侵入性更小
list(APPEND CMAKE_PREFIX_PATH "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_DEFAULT_TRIPLET}")
find_package(spdlog REQUIRED)
target_link_libraries(your_target PRIVATE spdlog::spdlog)

## 更详细专业的用法参考 https://learn.microsoft.com/zh-cn/vcpkg/users/buildsystems/cmake-integration
conan

conan是一个去中心化的c++包管理,可以指定具体的库版本(这么说是因为vcpkg在哪个场景来着,只支持使用最新版的库),可以构建自己的库仓库。使用的人也很多,支持的库也有几千个。做这种包管理的,还做出名堂来的,你所了解的常用的库肯定都会支持,所以这方面不必担心。

不过我用的不多,详情还是搜一下。以下内容来自deepseek。

# 安装
pip install conan

# 创建默认 profile(生成 ~/.conan2/profiles/default)
conan profile detect

# 创建配置文件 conanfile.txt
[requires]
fmt/10.1.0

[generators]
CMakeDeps
CMakeToolchain

[layout]
cmake_layout

# 安装依赖并生成配置
mkdir build && cd build
conan install .. --build=missing

# 集成到 CMake
include(${CMAKE_BINARY_DIR}/generators/conan_toolchain.cmake)
find_package(fmt REQUIRED)
target_link_libraries(your_target PRIVATE fmt::fmt)
apt / pacman

严格来说呢,这些个并不是C++的包管理!它们是系统的包管理,但是很多时候你缺少依赖,直接一个sudo apt install libxxx-dev也就解决了(在arc linux,或者在msys2中,可以是pacman -S libxxx,还可能是yum等等等等)。那么,这怎么不算管理呢?(这个下载的是真的二进制文件,没有编译的步骤,跟前面说的vcpkg和connan那种包管理不同)

不过需要注意的是,通过这样安装的库,是全局的。有可能你这个项目安装了这个版本的库,下个项目想用的是另一个版本的,这就会导致冲突。不过对于很多基础的库,像什么boost啦、opencv啦,直接安装是没问题的,也是最最方便的(甚至都可以不用加编译选项)。

调试

程序跑起来有错误是相当正常滴,用各种方法,找到错误在哪并修改,再找再改,直到把程序跑起来的过程就是调试。

当然这里说得更广义一些。有时候程序只是跑起来了,但可能并不那么健康。能跑不一定就对,只是暂时没执行到有错误的逻辑而已。如何查找预防这种隐藏起来的错误,也算调试了。还有就是,程序写好了,性能如何呢?跑得对,但是跑得慢,怎么让它快起来,优化性能,也是一种调试。下面逐个进行简单介绍。

程序调试

找错误一般来说有以下几种方法:

瞪眼法

这应该是最直观也是最常用的。不管你用的什么构建工具,编译出错,它都会告诉你错误是什么。有的报错信息很直观,你直接就能看懂。有的报错信息很冗长,像乱码一样,刚接触你可能看不懂,但是看多了,一看到某种乱码的模式,根据经验,或者捕捉关键词,就能立马明白错误是什么,应该怎么改。读报错信息,然后明白过来哪里错了,这就是瞪眼法

关于冗长的报错信息如何看,可以参考《Effective STL》的 第49条:学会分析与STL相关的编译器诊断信,简单来说就是,C++有很多模板,即使你没用,你用的标准库、三方库中也会用。报错信息看着很长,其实很多都是模板展开后的结果。肉眼排除掉冗余信息(或者写个过滤的小工具 ErrorReducer.py )能够使得错误更直观一些。不过这些可能都是老巴式了,直接复制粘贴到AI中,让ai帮你分析,也是不错的。不过最好分析完了理解一些,不然每次都只会复制粘贴问AI,长进不大。

当小脑袋瓜子栈溢出的时候,反应不过来到底是怎么个错误法了,一般就会选择在代码的某些位置上写上一串神秘字符:printf("【x=%d, y=%s】\n", x, y);或者std::cout<<"========\n"; 或者 LOG("XXXXXXX");这是很直观的,也基本是无师自通的。

不管是printf还是直接读日志,都可以算是print大法了吧,即,在程序的关键位置输出一些有特征的信息,从而判断程序运行情况。有人嘲笑这种太土太Low,不会开个gdb打个断点就不能算是程序员;有人对这种推崇备至,说是真正高级的程序员谁tm打断点,都是看日志。我的看法是,管它高级不高级,有效的最快最对的就是最好的。

Warning

刚学的时候小脑袋瓜子不要装一堆饭圈的东西,这个高级那个老土。有用就行,装杯那是以后的事。程序调完了,丝滑运行了,喝杯茶冲冲浪,看看网友吹B,瘾上来了也下场小装一下,那会才是需要分辨谁High谁Low的时候。

Reference

简单的print太过简单了,可以参考这个 header-only 单头文件库 dgb-macro ,可以帮你更好的print。(灵感来源rust的dbg!宏)

gdb

gdb是一个命令行运行的黑窗口工具,作用是可以让你在程序运行的时候,让它在指定的位置停下,然后还能让你读取停下来的位置的各种变量值是多少,的一个工具。非常强大,但是背后的原理也不算太复杂,通过什么系统调用实现的感兴趣也可以一学,甚至可以一做。

要想使用gdb,在编译程序的时候要添加上一个 编译选项 -g,保留程序的各种符号信息,作用是调试的时候能看到源代码,如果不加也能调试,但是看不到源码(可以硬看汇编)。

调试的工具有很多,但是基本的原理都一样,就是打断点,分析断点,分析变量取值,分析调用栈等等。这里简单罗列一下:

links

单元测试

调试程序,一般你先得知道程序有错误。自己写的小程序,跑一边就知道又没错误了。但是如果是多人合作的大型项目,一来程序分支众多,跑一边不见得会跑到出错误的那种情况;另一方面,你好不容易设计出了一个好东西,或者修改好了一个bug,你当然不想被人再破坏它。保证这种情况的手段就是 单元测试。

或许你听说过一个词叫 面向测试开发,或者什么测试先行。简单来说就是,代码还没写呢,功能还没实现呢,先把测试功能的代码写出来。这个一般说的就是,你先写出单元测试。单测的粒度比较小,比较精细,能够验证某个函数甚至某一个分支逻辑行为是否符合预期。

C++有很多单测框架,著名而全面的有GoogleTest,它还带了一个可以模仿接口的GoogleMock,这个一般是绝对够用了的。你只会嫌弃它太重了而想换到更轻量的,比如catch2doctest等这种单个头文件的。

(这一节其实是后面才想起来,随便写写填个坑了)具体的参考这篇总结 C++ 单测框架Catch2、doctest、CppTest、GTest、CppUnit 和 CppUTest

安全检查

c++程序的安全问题,最出名的当属内存泄,以及指针相关的空指针野指针悬挂指针,此外还有一些除零、越界、溢出等等吧。这一节究竟是按照工具分类,还是按照错误分类呢?纠结了一下,还是直接介绍工具吧。读者知道有这么些个工具,等环境配好了,真个碰到对应场景的时候,再搜相关的问题,见到这些名词有个印象即可。

Valgrind

详情请参考 Valgrind快速上手 | SHUAIKAI’s Blog

简单来说,valgrind提供了如下几个工具。使用方法就是 valgrind --tool=<tool> ./<your_program>。不同的工具可能还会输出一些文件,这些文件具体又怎么可视化,详情直接一搜即可。

Sanitizer

Sanitizer最开始是google做的一个错误检查工具,通过在你的代码中插入一些东西,然后运行你的程序,这些东西就会把你的错误给你报出来。现在三大编译器都已经默认支持这个编译选项了。具体怎么用在上文 性能优化相关 的编译选项中已经介绍过了,直接加编译选项就好:

g++ -fsanitize=address/leak/undefined/thread -o demo demo.cpp

Reference
静态分析

静态分析有很多工具,像 CppcheckClangStaticAnalyzer(CSA)cpplint ,前面说的 clang-tidy 也算是吧。

个人浅显的总结:cppcheck检查逻辑错误,csa能更深入的检查运行时错误,cpplint检查风格是否符合google style,clang-tidy写代码的时候给lint。有很多吧,感兴趣随便搜一下,比如这一篇 C++静态代码检查工具? - 知乎 ,这一篇 开源C++静态代码检测工具clang-tidy、cppcheck和oclint的比较_clang-tidy cppcheck-CSDN博客 ……

CppCheck

Cppcheck 是 C/C++ 代码的静态分析工具,它提供独特的代码分析来检测错误(大概就是说可以检查一些别的检查不出来的错误。只检查编译器检查不出来的bug,不检查语法错误),专注于检测未定义的行为和危险的编码结构。

使用

cppcheck [OPTIONS] [files or paths]

  1. cppcheck . 2> err.txt: 递归检查当前文件夹,并在屏幕上打印进度,将错误写入文件
  2. cppcheck --quiet ../myproject/: 递归检查 ../myproject/,并且不打印进度
  3. cppcheck --enable=all --inconclusive --library=posix test.cpp: 检查test.cpp,启用所有检查
  4. cppcheck -I inc1/ -I inc2/ f.cpp: 检查f.cpp并搜索inc1/和inc2/中的include文件
  5. –enable=all,warning,style,performance,portability,information,unusedFunction,missingInclude启用更多检查。默认情况下只显示错误消息
  6. --platform=unix32,unix64 ,win32A,win32W,win64,avr8,elbrus-e1cp,pic8,pic8-enhanced,pic16,mips32,native,unspecified: 指定平台,精确检查
  7. -i <dir or file> 忽略源文件或源文件目录
  8. --suppress=syntaxError屏蔽该类错误
  9. -j n 启动多线程同时进行检查。
ClangStaticAnalyzer

Clang Static Analyzer 是一个工业级的静态源码检测工具,可以用来发现 C、C++ 和 Objective-C 程序中的 Bug。它既可以作为一个独立工具(scan-build)使用,也可以集成在 Xcode 中使用。Clang Static Analyzer 建立在 ClangLLVM 之上。严格地讲,它是 Clang 的一部分,因此它是完全开源的。Clang Static Analyzer 使用的静态分析引擎被实现为一个 C++ 库,可以在不同的客户端中重用,因此拥有很高的可扩展性。

scan-build 是它自带的命令行工具,可以劫持你的构建工具来用(直接在工具如clang++、cmake前面加上scan-build即可)。找到问题后会给你生成一个一个可视化的报告,非常易读,用scan-view查看。

#(1) 单文件用scan-build

scan-build clang++ demo.cc -o demo

#(2) cmake项目用 scan-build

mkdir build && cd build

## 在你的cmake前面添加上scan-build
scan-build cmake ..

## 然后构建
scan-build cmake --build . -j8

## 可能的输出
# scan-build: Analysis run complete.
# scan-build: 8 bugs found.
# scan-build: Run 'scan-view /tmp/scan-build-2025-04-08-130150-171264-1' to examine bug reports.

## 根据提示命令,用scan-view就可以查看报告
scan-view /tmp/scan-build-2025-04-08-130150-171264-1
形式化验证

形式化验证能够从数学的角度对代码进行分析验证,查找问题。常见的工具有CBMC和ESBMC。可能更偏学术一些,可以了解下有这么种东西。

CBMC

CBMC(C Bounded Model Checker) 是一个有界模型检查器,专门用于C和C++程序。它通过将程序转换为逻辑公式,并使用SAT求解器来验证这些公式,从而检测程序中的错误。CBMC特别适用于验证嵌入式系统和安全关键软件。

使用cbmc [opt] file.cpp,可添加选项如--bounds-check。支持生成验证报告(--xml-ui)。

ESBMC

ESBMC(Efficient SMT-Based Context-Bounded Model Checker) 是一个基于SMT(可满足性模理论)的有界模型检查器,支持C、C++和Java程序。它使用SMT求解器来验证程序的属性,能够处理更复杂的逻辑和数据结构。ESBMC在验证并发程序和实时系统方面表现出色。

使用esbmc [opt] file.cpp,输入C++代码并指定属性(如--memory-leak-check)。通过esbmc file.cpp执行验证,输出反例路径或确认安全性。

性能分析

perf

暂时写不动了,等着后续吧。如果你看到的时候还没有后续,直接STFW得了。

运行

程序编译好了,怎么跑起来,一般不是问题。但偶尔确实有问题,出去系统格式不对这种低级错误之外,最常见的问题应该是动态库找不到。

找不到的原因有很多,可能程序是你自己编译的,但是输出到bin目录,动态库却在build目录、在lib目录;可能程序不是你编译的,你只有可执行文件,没有动态库文件;可能你有动态库文件,但是没放在默认能找到的位置。说来说去,总结一句话:动态库放的位置不对。(没动态库也算位置不对,相当于是放在了人家电脑上,没放自己电脑上)

那么放哪里才算对呢?=> 放系统默认位置,和当前目录下。最快的验证方法就是把动态库拷贝到当前文件夹再运行,一般就能跑起来了。

如何判断缺没缺动态库可以用 ldd 这个工具。ldd [your_excutable] 他会告诉你这个程序依赖哪些动态库,找到的在哪个位置,没找到的又是谁。

如果是安装的库,一般都会安装到默认位置,肯定能找到。如果是自己编译的库,那么不推荐放到系统默认位置,可以通过一个环境变量来指定:LD_LIBRARY_PATH,它是程序运行的时候默认的动态库搜索位置。

# 临时添加路径
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH

# 永久配置(写入 ~/.bashrc 或 /etc/ld.so.conf)
sudo ldconfig  # 刷新缓存

具体编辑器配置

vscode

Reference

vim

nvim

sublime

学习资料推荐

手册类

工具类

其他

Comment