现代C++:工具漫谈:编译、格式化、代码检查、排错各显身手

发布时间:2026/7/24 10:39:30
现代C++:工具漫谈:编译、格式化、代码检查、排错各显身手 现代 C 语言我们讲到这里就告一段落了。今天我们正式开启了实战篇先讲一个轻松些的话题——工具。编译器当然轻松不等于不重要。毕竟工欲善其事必先利其器。我们做 C 开发最基本的工具就是编译器对其有些了解显然也是必要的。我们就先来看看我在专栏开头就提到的三种编译器MSVC、GCC和 Clang。MSVC三种编译器里最老资格的就是 MSVC 了。据微软员工在 2015 年的一篇博客在 MSVC 的代码里还能找到 1982 年写下的注释。这意味着 MSVC 是最历史悠久、最成熟但也是最有历史包袱的编译器。微软的编译器在传统代码的优化方面做得一直不错但对模板的支持则是它的软肋在 Visual Studio 2015 之前尤其不行——之前模板问题数量巨大之后就好多了。而 2018 年 11 月 MSVC 宣布终于能够编译 range-v3 库也成了一件值得庆贺的事。当然这件事情是值得高兴的但考虑我在 2016 年的演讲里就已经用到了 range-v3不能不觉得还是有点晚了。此外我已经提过微软对代码的“容忍度”一直有点太高缺省情况下不使用 /Za 选项能接受 C 标准认为非法的代码这至少对写跨平台的代码而言绝不是一件好事。MSVC 当然也有领先的地方。它对标准库的实现一直不算慢较早就提供了比较健壮的线程、正则表达式等标准库。在并发 [7] 方面微软也是比较领先的并主导了协程的技术规格书。微软一开始支持 C 标准的速度比较慢但慢慢地微软已经把全面支持 C 标准当作了目标并在 2018 年宣布已全面支持 C17 标准虽然同时也承认仍有一些重大问题影响了其编译一些重要的开源 C 项目。MSVC 有一个地方我一直比较喜欢就是代码里可以写出要求链接具体什么库而链接什么库的命令可以是使用的第三方代码里直接给出的。这就使得在命令行上编译使用到第三方库如 Boost的代码变得非常容易。在使用 GCC 和 Clang 时用到什么库就必须在命令行上写出来这就迫使程序员使用更规范、也更麻烦的管理方式了。具体而言对于下面的这个最小的单元测试程序#define BOOST_TEST_MAIN #include boost/test/unit_test.hpp BOOST_AUTO_TEST_CASE(minimal_test) { BOOST_CHECK(1 1 2); }使用 GCC 或 Clang 时你需要输入类似下面这样的命令g -DBOOST_TEST_DYN_LINK test.cpp -lboost_unit_test_framework而 Windows 下使用 MSVC 你只需要输入cl /DBOOST_TEST_DYN_LINK /EHsc /MD test.cpp一下子就简单多了。另外在免费的 C 集成开发环境里Visual Studio Community Edition 恐怕可以算是最好的了至少在 Windows 上是这样。在自动完成功能和调试功能上 Visual Studio 做得特别好为其他的免费工具所不及。如果你开发的 C 程序主要在 Windows 上运行那 MSVC 就应该是首选了。Clang相反在三个编译器里最新的就是 Clang。作为 LLVM 项目的一部分它的最早发布是在 2007 年然后流行程度一路飙升到现在成了一个通用的跨平台编译器。其中有不少苹果的支持——因为苹果对 GCC 的许可要求不满意苹果把 LLVM 开发者 Chris Lattner 招致麾下2005—2017期间他除了为苹果设计开发了全新的语言 SwiftClang 的 C 支持也得到了飞速的发展。作为后来者Clang 在错误信息易用性上做出了极大的改善。Clang 虽然一直在模拟 GCC 的功能和命令行但错误信息的友好性是它的最大亮点。在语言层面Clang 对 C 标准的支持也是飞速正如下面这张图所展示的那样可以看到Clang 在 2011 异军突起对 C11 的支持程度在短时间甚至还超过了原先的领跑者 GCC。由于 Clang/LLVM 的模块化设计在 Clang 上扩展新功能相当容易而且动态库 libclang 直接向开发者暴露了分析 C 代码的接口这也是 Clang 流行的一个主要原因。即使在我主要使用 Windows 工作的时候我在机器上也装了 Clang。我主要不是用它编译而是利用它对 C 的理解做代码的格式化本讲下面会讲和自动完成——对于文件数不多的项目我还是喜欢使用 Vim [11]那机器上能不能用 clang_complete [12] 区别就很大了。有了 clang_complete那 Vim 里也就有个不算太笨的 C 自动完成引擎了。顾名思义clang_complete 主要依赖的就是 Clang 了更精确地说是 libclang。另外当我写出在 MSVC 下编译不过的代码时我也会看看代码能不能在 Clang 下通过。如果能过那我就比较有信心我写出的代码是正确的只不过是 MSVC 处理不了而已。Clang 目前在 macOS 下是默认的 C/C 编译器。在 Linux 和 Windows 下当然也都能安装这种情况下Clang 会使用平台上的主流 C 库也就是在 Linux 上使用 libstdc在 Windows 上使用 MSVC 的 C 运行时。只有在 macOS 上Clang 才会使用其原生 C 库libc。顺便说一句如果你想阅读一下现代 C 标准库的参考实现的话libc 是可读性最好的——不过任何一个软件产品的源代码都不是以可读性为第一考量比起教科书、专栏里的代码例子libc 肯定是要复杂多了。最后一个关于版本号的说明苹果开发工具里带的 Clang 的是苹果自己维护的一个分支版本号和苹果的 Xcode 开发工具版本号一致和开源项目 Clang 的版本号没有关系显得比较乱。目前 Apple Clang 的最新版本是 11 了但功能上落后于官方的 LLVM Clang 9.0 [14]。要想使用最新版本的 Clang最方便的方式是使用 Homebrew 安装 llvmbrew install llvm安装完之后新的 clang 和 clang 工具在 /usr/local/opt/llvm/bin 目录下和系统原有的命令不会发生冲突。你如果需要使用新的工具的话需要改变路径的顺序或者自己创建命令的别名alias。GCCGCC 的第一个版本发布于 1987 年是由自由软件运动的发起人 Richard Stallman常常被缩写为 RMS亲自写的。因而从诞生伊始GCC 就带着很强的意识形态承担着振兴自由软件的任务。在 GNU/Linux 平台上GCC 自然是首选的编译器。自由软件的开发者大部分也选择了 GCC。由于 GCC 是用 GPL 发布的任何对 GCC 的修改都必须以 GPL 协议发布。这就迫使想修改 GCC 的人要为 GCC 做出贡献。这对自由软件当然是件好事但对一家公司来讲就未必了。此外你想拆出 GCC 的一部分来做其他事情比如对代码进行分析也绝不是件容易的事。这些问题实际上就是迫使苹果公司在 LLVM/Clang 上投资的动机了。作为应用最广的自由软件之一GCC 无疑是非常成熟的软件。某些实验性的功能比如对概念的支持也是最早在 GCC 上面出现的。对 C 标准的支持GCC 一直跟得非常紧但是由于自由软件依靠志愿者的工作而非项目经理或产品经理的管理对不同功能的优先级跟商业产品往往不同也造就了 GCC 和 MSVC 上各有不同的着重点优化编译结果哪个性能更高也会依赖于具体的程序。当然 GCC 是跨平台的这点上肯定是 MSVC 不及的。根据 GCC 的方式写出的代码跨平台性就会更好。目前我已知的最主要例外是终端上的多语言支持由于 GCC 在 Windows 上使用了 MSVC 的一个过时的运行库 MSVCRT.DLL到现在为止 GCC 要在终端上显示中文经常会出现问题。初期 GCC 在出错信息的友好程度上一直做得不太好。但 Clang 的出现刺激出了一种和 GCC 之间的良性竞争到今天GCC 的错误信息反而是最友好的了。我如果遇到程序编译出错在 Clang 里看不明白的话我会试着用 GCC 再编译看看在某些情况下可能 GCC 的出错信息会更让人明白一些。在可预见的将来在自由 / 开源软件的开发上GCC 一直会是编译器的标准。格式化工具Clang-Format我上面提到了 Clang 有着非常模块化的设计容易被其他工具复用其代码分析功能。LLVM 团队自己也提供一些工具其中我个人最常用的就是 Clang-Format。在使用 Clang-Format 之前我也使用过一些其他的格式化工具。它们和 Clang-Format 的最大区别是它们不理解 C 代码在对付简单的 C 代码时还行遇到复杂的 C 代码时就很容易出问题。此外Clang-Format 还很智能可以像人一样根据具体情况和剩余空间来格式化比如void func(int arg1, int arg2, int arg3); void long_func_name(int arg1, int arg2, int arg3); void a_very_long_func_name( int arg1, int arg2, int arg3);此外它也提供了完善的配置项你可以根据自己的需要来进行配置如这是我的一个项目使用的格式化选项https://github.com/adah1972/nvwa/blob/master/.clang-formatC 项目里放上这样一个文件代码的格式化问题大家就不用瞎争了——大家确定这个文件的内容就行。目前这个专栏的代码格式化选项也和上面的类似最主要的区别就是行长限制ColumnLimit设成了 36缩进宽度IndentWidth等选项基本减半来适配手机的小显示屏。如果没有 Clang-Format做代码的小屏适配就会累多了。代码检查工具Clang-TidyClang 项目也提供了其他一些工具包括代码的静态检查工具 Clang-Tidy [18]。这是一个比较全面的工具它除了会提示你危险的用法也会告诉你如何去现代化你的代码。默认情况下Clang-Tidy 只做基本的分析。你也可以告诉它你想现代化你的代码和提高代码的可读性clang-tidy --checksclang-analyzer-*,modernize-*,readability-* test.cpp以下面简单程序为例#include iostream #include stddef.h using namespace std; int sqr(int x) { return x * x; } int main() { int a[5] {1, 2, 3, 4, 5}; int b[5]; for (int i 0; i 5; i) { b[i] sqr(a[i]); } for (int i : b) { cout i endl; } char* ptr NULL; *ptr \0; }Clang-Tidy 会报告下列问题前两条我不想听。这种情况下使用配置文件来定制行为就必要了。配置文件叫 .clang-tidy应当放在你的代码目录下或者代码的一个父目录下。Clang-Tidy 会使用最“近”的那个配置文件。下面的配置文件反映了我的偏好Checks: clang-diagnostic-*,clang-analyzer-*,modernize-*,readability-*,-modernize-deprecated-headers,-modernize-use-trailing-return-type世界清静多了我不想听到的唐僧式的啰唣就消失了。使用 Clang-Tidy 还需要注意的地方是额外的命令行参数应当跟在命令行最后的 -- 后面。比如如果我们要扫描一个 C 头文件 foo.h我们就需要明确告诉 Clang-Tidy 这是 C 文件默认 .h 是 C 文件。然后如果我们需要包含父目录下的 common 目录语言标准使用了 C17命令行就应该是下面这个样子clang-tidy foo.h -- -x c -stdc17 -I../common你有没有注意到上面 Clang-Tidy 实际上漏报告了些问题它报告了一些不重要的问题却漏过了真正严重的问题。这似乎是个实现相关的特殊问题因为如果把前面那些行删掉的话后面两行有问题的代码也还是会产生告警的。CppcheckClang-Tidy 还是一个比较“重”的工具。它需要有一定的配置需要能看到文件用到的头文件运行的时间也会较长。而 Cppcheck就是一个非常轻量的工具了。它运行速度飞快看不到头文件、不需要配置就能使用。它跟 Clang-Tidy 的重点也不太一样它强调的是发现代码可能出问题的地方而不太着重代码风格问题两者功能并不完全重叠。有条件的情况下这两个工具可以一起使用。以上面的例子来为例Cppcheck 会干脆地报告代码中最严重的问题——空指针的解引用。它的开销很低却能发现潜在的安全性问题因而我觉得这是个性价比很高的工具。排错工具排错工具当然也有很多种我们今天介绍其中两个Valgrind 和 nvwa::debug_new。ValgrindValgrind算是一个老牌工具了。它是一个非侵入式的排错工具。根据 Valgrind 的文档它会导致可执行文件的速度减慢 20 至 30 倍。但它可以在不改变可执行文件的情况下只要求你在编译时增加产生调试信息的命令行参数-g即可查出内存相关的错误。以下面的简单程序为例int main() { char* ptr new char[20]; }在 Linux 上使用 g -g test.cpp 编译之后然后使用 valgrind --leak-checkfull ./a.out 检查运行结果我们得到的输出会如下所示即其中包含了内存泄漏的信息包括内存是从什么地方泄漏的。Valgrind 的功能并不只是内存查错也包含了多线程问题分析等其他功能。要进一步了解相关信息请查阅其文档。nvwa::debug_new在 nvwa项目里我也包含了一个很小的内存泄漏检查工具。它的最大优点是小巧并且对程序运行性能影响极小缺点主要是不及 Valgrind 易用和强大只能检查 new 导致的内存泄漏并需要侵入式地对项目做修改。需要检测内存泄漏时你需要把 debug_new.cpp 加入到项目里。比如可以简单地在命令行上加入这个文件c test.cpp \../nvwa/nvwa/debug_new.cpp下面是可能的运行时报错Leaked object at 0x100302760 (size 20, 0x1000018a4)*** 1 leaks found在使用 GCC 和 Clang 时可以让它自动帮你找出内存泄漏点的位置。在命令行上需要加入可执行文件的名称并产生调试信息c -D_DEBUG_NEW_PROGNAME\a.out\ \-g test.cpp \../nvwa/nvwa/debug_new.cpp这样我们就可以在运行时看到一个更明确的错误Leaked object at 0x100302760 (size 20, main (in a.out)(test.cpp:3))*** 1 leaks found这个工具的其他用法可以参见文档。网页工具Compiler Explorer编译器都有输出汇编代码的功能在 MSVC 上可使用 /Fa在 GCC 和 Clang 上可使用 -S。不过要把源代码和汇编对应起来就需要一定的功力了。在这点上godbolt.org [22] 可以提供很大的帮助。它配置了多个不同的编译器可以过滤掉编译器产生的汇编中开发者一般不关心的部分并能够使用颜色和提示来帮助你关联源代码和产生的汇编。使用这个网站你不仅可以快速查看你的代码在不同编译器里的优化结果还能快速分享结果。比如下面这个链接就可以展示我们之前讲过的一个模板元编程代码的编译结果https://godbolt.org/z/zPNEJ4网页截图示意如下当然作为一个网站godbolt.org 对代码的复杂度有一定的限制也不能任意使用你在代码里用到的第三方库不过它已经装了不少主流的 C 库如我们后面会讲到的 Boost、Catch2、range-v3 和 cppcoro。要解决这个问题你可以在你自己的机器上本地安装它背后的引擎compiler-explorer。如果你的代码较复杂或者有安全、隐私方面的顾虑的话可以考虑这个方案。C Insights如果你在上面的链接里点击了“CppInsights”按钮的话你就会跳转到 C Insights [24] 网站并且你贴在 godbolt.org 的代码也会一起被带过去。这个网站提供了另外一个编译器目前没有提供、但十分有用的功能展示模板的展开过程。回想我们在模板编程时的痛苦之一来自于我们需要在脑子中想象模板是如何展开的而这个过程非常容易出错。当编译器出错时我们得通过冗长的错误信息来寻找出错原因的蛛丝马迹当编译器成功编译了一段我们不那么理解的模板代码时我们在感到庆幸的同时也往往会仍然很困惑——而使用这个网站你就可以看到一个正确工作的模板是如何展开的。以讨论的 make_index_sequence 为例如果你把代码完整输入到网站上去、然后尝试展开 make_index_sequence5你就会看到 index_sequence_helper 是这样展开的index_sequence_helper5index_sequence_helper4, 4index_sequence_helper3, 3, 4index_sequence_helper2, 2, 3, 4index_sequence_helper1, 1, 2, 3, 4index_sequence_helper0, 0, 1, 2, 3, 4如果我更早一点知道这个工具的话我就会在讲编译期编程的时候直接建议大家用了应该会更有助于模板的理解……内容小结在今天这一讲中我们对各个编译器和一些常用的工具作了简单的介绍。用好工具可以大大提升你的开发效率。