文档教程【免费下载链接】cppbestpracticesCollaborative Collection of C Best Practices. This online resource is part of Jason Turners collection of C Best Practices resources. See README.md for more information.项目地址https://gitcode.com/gh_mirrors/cp/cppbestpractices点击查看免费下载导读本文基于开源协作项目 cppbestpracticesJason Turner 维护的 C 最佳实践合集中的 Style 章节系统梳理一套可直接落地执行的 C 风格体系从命名约定、成员/参数前缀、头文件组织到成员初始化、类型安全与运算符重载原则。读完本文你将能为自己或团队制定并固化一套统一的 C 编码规范配合.clang-format实现风格自动化并避免一系列容易引发未定义行为与可维护性灾难的常见写法。本文是 cppbestpractices 系列的第 3 章完整目录见 00-Table_of_Contents.md系列总览见 README.md写作理念见 01-Preface.md与安全主题的 04-Considering_Safety.md、可维护性主题的 05-Considering_Maintainability.md 互为补充。风格的核心原则一致性优先风格Style最重要的一点是一致性第二重要的是遵循普通 C 程序员习惯阅读的风格。这两点决定了代码的可读性与协作效率不一致的代码让每个读者都感到困惑而过于个性化的风格即使自洽也会让不熟悉它的开发者难以快速理解。C 允许任意长度的标识符名称因此在命名时没有任何理由吝啬。命名应当具有描述性descriptive并且在整个代码库中保持一致。常见的两种风格是CamelCase驼峰式如MyClass、myVariableNamesnake_case下划线式如my_class、my_variable_name其中snake_case有一个独特优势如果需要它可以与拼写检查器spell checker配合工作帮助发现命名拼写错误。用 .clang-format 固化风格无论你制定了怎样的风格指南务必落实一个.clang-format文件来指定团队期望的格式。虽然 clang-format 无法约束命名但它能自动统一缩进、换行、空格、花括号位置等机械性格式这对开源项目尤其重要——开源项目必须维持一致的风格因为贡献者来自世界各地手动保持格式一致几乎不可能。所有主流 IDE 和编辑器都对 clang-format 提供了内置支持或可通过插件轻松安装VSCode微软官方 C/C 扩展Microsoft C/C extension for VS CodeCLion内置 clang-format 作为替代格式化器Visual StudioLLVM 官方 ClangFormat 扩展ReSharper支持使用 clang-format 作为代码风格来源Vimvim-clang-format或vim-autoformat等插件XcodeClangFormat-Xcode插件建议将.clang-format文件提交到仓库根目录并配合编辑器保存时自动格式化或 pre-commit 钩子让格式问题在进入代码评审之前就被机器解决。clang-format 的更多用法可参考仓库 02-Use_the_Tools_Available.md 中关于 ClangFormat 工具的介绍。命名规范一套完整可执行的 C 命名体系常见 C 命名约定速查原文档给出了一套社区广泛认可的基线约定类型Types以大写字母开头MyClass。函数与变量以小写字母开头myMethod。常量全部大写const double PI 3.14159265358979323;。C 标准库以及 Boost 等知名 C 库遵循以下约定可视为事实上的行业标准宏名称使用大写加下划线INT_MAX。模板参数名称使用 PascalCaseInputIterator。其他所有名称使用 snake_caseunordered_map。这套约定之所以值得采用是因为它让看名字即知身份成为可能看到大写开头的标识符即可判断是类型看到全大写即可怀疑是常量或宏看到 snake_case 即可判断是普通函数、变量或标准库实体。区分私有对象数据m_ 前缀为私有数据加上m_前缀以将其与公有数据区分开。m_代表 member成员class PrivateSize { public: int width() const { return m_width; } int height() const { return m_height; } PrivateSize(int t_width, int t_height) : m_width(t_width), m_height(t_height) {} private: int m_width; int m_height; };在成员函数体内m_前缀让读者一眼就能看出某个名字是对象状态成员变量而非局部变量或参数从而避免赋值方向的误读。区分函数参数t_ 前缀函数参数使用t_前缀用于与作用域内其他变量区分。t_可以理解为 the但含义本身是任意的要点是为参数提供一致、可辨识的命名策略struct Size { int width; int height; Size(int t_width, int t_height) : width(t_width), height(t_height) {} };注意任何前缀或后缀都可以根据组织自身选择t_只是一个示例。这一建议存在争议cppbestpractices 仓库的 issue [#11] 对此有过专门讨论——原文档也明确标注了这一争议性因此团队采用前应先达成共识。无论选哪种代码库内部的一致性才是最重要的。永远不要以下划线开头命名如果你这样做就有可能与编译器及标准库实现保留的标识符发生冲突以下划线开头、后跟大写字母或另一个下划线的标识符如_Foo、__foo在全局与局部作用域都被 C 标准保留给实现使用在全局命名空间中以单个下划线开头的标识符如_foo同样被保留命名空间作用域中的_foo形式也可能与标准库实现内部使用的名称冲突。规避方法是私有成员用m_前缀而不是_前缀参数用t_前缀模板参数用 PascalCase——这样既达成了区分目的又不会踩进保留标识符的雷区。一份完整的好风格示例类把上述约定整合起来得到如下自洽的示例class MyClass { public: MyClass(int t_data) : m_data(t_data) { } int getData() const { return m_data; } private: int m_data; };特征总结类型MyClass大写开头构造参数t_data带t_前缀成员m_data带m_前缀getter 命名为getDataconst成员函数明确不修改对象状态。布局与可读性生成文件与源码目录分离启用源代码目录外的构建Out-of-Source-Directory Builds确保构建生成的产物目标文件、可执行文件、中间文件全部输出到与源码分离的目录而不是混入源码树。这样做的好处包括源码目录保持干净、多个构建配置Debug/Release、不同编译器可并行共存、清理构建产物只需删除输出目录。实践中用 CMake 等构建工具时应坚持在独立构建目录中执行配置与编译参见 02-Use_the_Tools_Available.md 中关于构建工具的介绍。使用 nullptr 而非 0 或 NULLC11 引入了nullptr——一个专门表示空指针的值。应当用它代替0或NULL来表示空指针。理由0是整型字面量NULL在 C 中通常是0的宏或0L在重载解析、模板推导等场景中容易与整型混淆而nullptr的类型是std::nullptr_t只能转换为指针类型语义精确、类型安全。注释统一使用 //注释块应使用//而不是/* */。用//的好处是调试时更容易成块注释掉代码。例如// this function does something int myFunc() { }调试时想暂时禁用该函数只需用/* */包住整块/* // this function does something int myFunc() { } */如果函数头的注释本身用的是/* */这样的成块注释会因嵌套注释而失败/*遇到内部的*/会提前结束。因此统一使用//把块注释掉代码的能力留给/* */。代码块强制使用 {}块block的花括号不能省略。省略花括号会导致语义错误// Bad Idea // This compiles and does what you want, but can lead to confusing // errors if modification are made in the future and close attention // is not paid. for (int i 0; i 15; i) std::cout i std::endl; // Bad Idea // The cout is not part of the loop in this case even though it appears to be. int sum 0; for (int i 0; i 15; i) sum; std::cout i std::endl; // 看似在循环内实际不在 // Good Idea // Its clear which statements are part of the loop (or if block, or whatever). int sum 0; for (int i 0; i 15; i) { sum; std::cout i std::endl; }第二个 Bad Idea 展示了典型的陷阱无花括号的循环体只有一条语句后续追加的语句会被踢出循环且编译器在缩进不匹配时若无额外告警不会报错。这与-Wmisleading-indentation等告警相关——GCC 6.0 起可用该选项检测缩进暗示块而实际没有块的情况见 02-Use_the_Tools_Available.md 的告警清单。最稳妥的做法就是任何控制流语句都写花括号。行长度控制保持行的合理长度。对比// Bad Idea // hard to follow if (x y myFunctionThatReturnsBool() caseNumber3 (15 12 || 2 3)) { } // Good Idea // Logical grouping, easier to read if (x y myFunctionThatReturnsBool() caseNumber3 (15 12 || 2 3)) { }许多项目和编码规范有一个软性指南每行尽量少于 80 或 100 个字符。这样的代码通常更易读还能让两个文件并排显示在同一屏幕上而无需缩小字体。折行时按逻辑分组断行便于快速浏览条件构成。本地文件用 包含#include的引号与尖括号有明确分工保留给系统头文件本地文件应使用。// Bad Idea. Requires extra -I directives to the compiler // and goes against standards. #include string #include includes/MyHeader.hpp // Worse Idea // Requires potentially even more specific -I directives and // makes code more difficult to package and distribute. #include string #include MyHeader.hpp // Good Idea // Requires no extra params and notifies the user that the file // is a local file. #include string #include MyHeader.hpp用包含本地文件的好处编译器优先在包含该文件的源文件所在目录中查找不需要额外的-I参数同时从写法上就向读者明确这是一个项目内文件。头文件与编译单元组织头文件中禁止 using namespace在头文件中写using namespace会把该命名空间拉入所有包含此头文件的文件的命名空间中——这污染了命名空间并可能在将来引发名称冲突。而在实现文件.cpp中写using namespace则没有问题因为其影响范围被限制在该编译单元内。Include Guard头文件必须包含命名明确、不易冲突的 include guard以避免同一头文件被多次包含时重复定义并防止与其他项目的头文件冲突。推荐的命名模式是项目名 路径 文件名#ifndef MYPROJECT_MYCLASS_HPP #define MYPROJECT_MYCLASS_HPP namespace MyProject { class MyClass { }; } #endif也可以考虑使用#pragma once指令——它在许多编译器中是准标准quasi-standard支持。它更简短且意图清晰。两种方式都应在项目内统一选择其一。文件扩展名用 .hpp 和 .cpp最终这是偏好问题但.hpp与.cpp被各种编辑器和工具广泛识别因此这是一个务实的选择。具体来说Visual Studio 只自动识别.cpp和.cxx作为 C 源文件Vim 不一定把.cc识别为 C 文件。值得借鉴的一个大型项目OpenStudio的做法是用户生成的文件用.hpp/.cpp工具生成的文件用.hxx/.cxx——两类扩展名都被广泛识别且这种区分很有帮助。永远不要混用 Tab 和空格一些编辑器默认用 Tab 与空格的混合缩进。这会让所有未使用完全相同 Tab 宽度设置的人无法阅读代码。应配置编辑器避免这种情况例如统一空格缩进 2/4 格并开启Tab 键插入空格再配合.clang-format强制执行。成员变量初始化优先使用成员初始化列表用成员初始化列表member initializer list初始化成员。对于 POD 类型初始化列表与手动赋值的性能相同但对于其他类型初始化列表有明显性能优势// Bad Idea class MyClass { public: MyClass(int t_value) { m_value t_value; } private: int m_value; }; // Bad Idea // This leads to an additional constructor call for m_myOtherClass // before the assignment. class MyClass { public: MyClass(MyOtherClass t_myOtherClass) { m_myOtherClass t_myOtherClass; } private: MyOtherClass m_myOtherClass; }; // Good Idea // There is no performance gain here but the code is cleaner. class MyClass { public: MyClass(int t_value) : m_value(t_value) { } private: int m_value; }; // Good Idea // The default constructor for m_myOtherClass is never called here, so // there is a performance gain if MyOtherClass is not is_trivially_default_constructible. class MyClass { public: MyClass(MyOtherClass t_myOtherClass) : m_myOtherClass(t_myOtherClass) { } private: MyOtherClass m_myOtherClass; };第二个 Bad Idea 中m_myOtherClass会先被默认构造一次再被拷贝赋值一次——白白多一次构造调用初始化列表版本则直接以参数构造成员省去中间步骤。默认成员初始化 与 {} 的取舍C11 起可以给每个成员直接赋默认值用或用{}。这保证了任何构造函数都不可能忘记初始化某个成员。使用赋值// ... // private: int m_value 0; // allowed unsigned m_value_2 -1; // narrowing from signed to unsigned allowed // ... //使用花括号初始化{}初始化在编译期禁止缩窄narrowing转换// Best Idea // ... // private: int m_value{ 0 }; // allowed unsigned m_value_2 { -1 }; // narrowing from signed to unsigned not allowed, leads to a compile time error // ... //除非有充分理由否则优先使用{}初始化而不是。忘记初始化成员是未定义行为UB的来源之一这类 bug 往往极难排查。花括号初始化的编译期缩窄检查能在错误发生前就拦下类型隐患。不可变成员标记为 const如果成员变量在初始化之后预期不再改变就把它标记为constclass MyClass { public: MyClass(int t_value) : m_value{t_value} { } private: const int m_value{0}; };注意const成员无法被赋值新值因此这样的类可能没有有意义的拷贝赋值运算符——这是选择const成员前需要意识到的约束。命名空间与类型安全总是使用命名空间几乎没有任何理由把标识符声明在全局命名空间中。函数和类应存在于命名恰当的命名空间内或命名空间内的类中。放在全局命名空间的标识符有与来自其他库主要是没有命名空间的 C 库的标识符冲突的风险。标准库整数类型与 size_t 下溢陷阱标准库对任何与大小相关的东西通常使用std::size_t。size_t的大小是实现定义的。通常使用auto能避免大部分相关问题但并非全部。务必坚持使用正确的整数类型并与 C 标准库保持一致。问题可能不会在你当前的平台上告警但换平台后很可能就会告警。特别要注意对无符号值进行某些运算时会发生整数下溢underflow。例如std::vectorint v1{2,3,4,5,6,7,8,9}; std::vectorint v2{9,8,7,6,5,4,3,2,1}; const auto s1 v1.size(); const auto s2 v2.size(); const auto diff s1 - s2; // diff underflows to a very large number这里s18与s29都是size_t无符号8 - 9下溢成一个极大值而不是负数——这种 bug 在循环索引、边界判断中极易造成内存越界。写法上应避免无符号减法或在计算前显式转换/判断大小关系。防止常见误用不要在 assert() 里放带副作用的代码assert(registerSomeThing()); // make sure that registerSomeThing() returns true上面这段代码在 Debug 构建时能正常工作但在 Release 构建时该调用会被编译器删除导致 Debug 与 Release 行为不一致——因为assert()是宏在 Release 模式下展开为空。正确的做法是把副作用剥离出来[[maybe_unused]] const auto success registerSomeThing(); assert(success);这也与 05-Considering_Maintainability.md 中 Never UseassertWith Side Effects 一节给出的建议完全一致可见该原则在本系列中被反复强调。不要害怕模板模板能帮助你坚守 DRYDont Repeat Yourself原则。模板应优先于宏因为宏不尊重命名空间等作用域规则。将通用逻辑抽象为模板既避免重复代码又能获得编译期类型检查。审慎使用运算符重载运算符重载的初衷是让语法富有表现力两个大整数相加写作a b而不是a.add(b)另一个常见例子是std::string的string1 string2字符串拼接。然而过多或错误地重载运算符很容易产生不可读的表达式。重载运算符时有几条基本规则需要牢记当类管理资源时重载operator()是必须的参见下文 考虑 Rule of Zero对于其他运算符只在它们被普遍用于该上下文时才重载典型场景是用拼接、重载可视为真/假的表达式的取反等始终注意[运算符优先级]尽量避免反直觉的构造不要重载~、%等冷门运算符除非在实现数值类型或遵循某个领域公认的语法永远不要重载operator,()逗号运算符处理流时使用非成员函数operator()和operator()。例如重载operator(std::ostream , MyClass const )实现把类写入流——std::cout、std::fstream或std::stringstream后者常用于生成值的字符串表示更多常见可重载运算符清单可参考公开的 C FAQ 讨论。避免隐式转换单参数构造函数单参数构造函数会在编译期被隐式调用以自动转换类型。这对std::string(const char *)这类场景很方便但一般应避免因为它们可能带来意外的运行时开销。正确做法是把单参数构造函数标记为explicit要求显式调用。转换运算符与单参数构造函数类似转换运算符会被编译器隐式调用并引入意外开销也应标记为explicit// bad idea struct S { operator int() { return 2; } };// good idea struct S { explicit operator int() { return 2; } };explicit转换运算符要求显式写出static_castint(s)避免在if (s)、数值运算等处发生隐式、非预期的类型变换。考虑 Rule of ZeroRule of Zero主张除非你的类在做某种新形式的资源所有权管理否则不要提供任何编译器能自动生成的函数——包括拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。目标在于让编译器提供最优版本并且在后续添加更多成员变量时自动保持正确维护。自己手写这些特殊成员函数意味着每次成员变更都必须同步维护它们极易出错比如漏了深拷贝、移动构造等而遵循 Rule of Zero 的类把这些职责交给编译器也就自动规避了经典的 Rule of Three/Five 资源管理错误。实现 Rule of Zero 的关键技巧是让所有资源都由值语义的 RAII 类型持有如std::vector、std::string、std::unique_ptr等而非裸指针。这与 04-Considering_Safety.md 中避免裸内存访问、使用std::make_unique/std::make_shared的建议互为表里当每个成员都自带正确的拷贝/移动/析构语义时编译器合成的特殊成员函数就是最优且正确的这也是本系列在风格层面给出的最值得贯彻的现代 C 原则之一。总结cppbestpractices 的 Style 章提供的不是唯一正确的风格而是一套自洽、可落地、可被工具固化的基线一致性优先的命名体系m_/t_前缀、PascalCase 模板参数、snake_case 常规标识符、//注释与强制花括号、初始化列表与{}默认值、头文件守卫与命名空间纪律、explicit与 Rule of Zero。将这些规则写进.clang-format与团队评审清单即可在规模化协作中把风格从争论对象变成自动执行的基础设施。赞分享文档教程【免费下载链接】cppbestpracticesCollaborative Collection of C Best Practices. This online resource is part of Jason Turners collection of C Best Practices resources. See README.md for more information.项目地址https://gitcode.com/gh_mirrors/cp/cppbestpractices点击查看免费下载相关推荐流媒体下载保姆级攻略N_m3u8DL-RE 5分钟搞定点播、直播与解密流媒体下载保姆级攻略N_m3u8DL RE 5分钟搞定点播、直播与解密 想把网课存下来、想把公开课的直播留下来只需要一个工具就够了。N_m3u8DL RECLI音视频xiaozhi-esp32 代码风格指南基于 clang-format 的 C/C 格式化规范与实战xiaozhi esp32 代码风格指南基于 clang format 的 C/C 格式化规范与实战 本指南围绕 xiaozhi esp32基于 ESP人工智能大模型语音交互助手嵌入式物联网智能硬件MCP 服务rippled C 编码风格指南从缩进、命名到 clang-format 的完整工程实践rippled C 编码风格指南从缩进、命名到 clang format 的完整工程实践 本篇技术指南以 XRP Ledger 守护进程 rippled区块链上一篇炉石传说HsMod终极指南55项功能一键解锁游戏新体验下一篇Integrity通知系统详解邮件、IRC、Campfire等8种通知方式配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
