Google C++ 开源风格指南

打印 上一主题 下一主题

主题 1012|帖子 1012|积分 3036

前言



  • Google C++ Style Guide
  • Google 开源项目风格指南 - 中文版
Google C++风格进行了总结,主要依照上面的中文版内容,此外我增加了一些旁注,方便阅读时理解,部分内容我只进行了很简单的呈现,详见上面的Google官网和中文翻译官网。阅读Google C++ Style可以学会更好的使用C++,阅读时也能再次比力深入的理解C++的特性,如何写出好的C++推荐Effective C++。
这里附一张大佬做的一张图总结Google C++编程规范(Google C++ Style Guide)。

1. 头文件

每个.cc文件应有一个配套的.h文件,单位测试和仅有main()函数的.cc文件是常见破例。
正确使用头文件会大大改善代码的可读性和执行文件的大小、性能。
1.1 自给自足的头文件

头文件应该自给自足 (可以独立编译),并以 .h 为扩展名. 头文件要有头文件防护符 (1.2** #define 防护符),并导入它所需的所有其它头文件。
若头文件声明了内联函数或模版,而且头文件的使用者必要实例化这些组件时,头文件必须直接或通过导入的文件间接提供这些组件的实现 。
   内联函数的这种替换举动是在编译阶段发生的,编译器必要知道内联函数的完备界说才能进行替换操作。
  模板的实例化是由编译器根据模板的使用情况在编译时完成的。和内联函数类似,编译器必要知道模板的完备界说才能进行实例化。
  1.2 #define 防护符

所有头文件都应该用 #define 防护符来防止重复导入防护符的格式是: 项目>_<路径>_<文件名>_H_ 。
为了包管符号的唯一性,防护符的名称应该基于该文件在项目目次中的完备文件路径。例如,foo 项目中的文件 foo/src/bar/baz.h 应该有如下防护:
  1. #ifndef FOO_BAR_BAZ_H_
  2. #define FOO_BAR_BAZ_H_
  3. ...
  4. #endif  // FOO_BAR_BAZ_H_
复制代码
1.3 导入你的依靠

若代码文件或头文件引用了其他地方界说的符号,该文件应该直接导入提供该符号的声明大概界说的头文件。不应该为了其他原因而导入头文件。
不要依靠间接导入. 如许,人们删除不再必要的 #include 语句时,才不会影响使用者. 此规则也实用于配套的文件:若 foo.cc 使用了 bar.h 的符号,就必要导入 bar.h,即使 foo.h 已经导入了 bar.h。
1.4 前向声明

界说:前向声明 (forward declaration) 是没有对应界说的声明
  1. // 在 C++ 源码文件中:
  2. class B; // 前向声明
  3. void FuncInB();
  4. extern int variable_in_b;
  5. ABSL_DECLARE_FLAG(flag_in_b);
复制代码
优点:节省编译时间、制止不必要的重复编译。
缺点:隐藏依靠关系、自动化工具识别困难、限制库的修改与维护、对std::命名空间的符号进行前向声明可能引发未界说举动、使用判断困难及代码含义改变风险、代码冗长与性能及复杂度题目。
尽量制止使用前向声明(弊大于利),应导入所需头文件。
1.5 内联函数

只把 10 行以下的小函数界说为内联 (inline)。
优点:只要内联函数体积较小,内联函数可以令目标代码更加高效。鼓励对存取函数用于获取类中成员变量值的函数)、变异函数(用于修改类中成员变量值的函数)和其它短小且影响性能的函数使用内联展开。
缺点:滥用内联将拖慢程序。根据函数体积,内联可能会增加或减少代码体积。通常,内联展开非常短小的存取函数会减少代码大小,但内联一个巨大的函数将显着增加代码大小。在当代处理惩罚器上,通常代码越小执行越快,因为指令缓存使用率高。
注意:审慎对待析构函数。析构函数往往比外貌上更长,因为会暗中调用成员和基类的析构函数!虚函数和递归函数通常不会被内联。
1.6 #include 的路径及顺序

推荐按照以下顺序导入头文件:配套的头文件,C 语言系统库头文件,C++ 尺度库头文件,其他库的头文件,本项目的头文件。
头文件的路径应相对于项目源码目次,不能出现 UNIX 目次别名 . (当前目次) 或 .. (上级目次). 例如,应该按如下方式导入 google-awesome-project/src/base/logging.h:
  1. #include "base/logging.h"
复制代码
注意 C 语言头文件 (如 stddef.h) 和对应的 C++ 头文件 (cstddef) 是等效的。两种风格都可以接受,但是最好和现有代码保持一致。
举例来说,google-awesome-project/src/foo/internal/fooserver.cc 的导入语句如下:
  1. #include "foo/server/fooserver.h"
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <string>
  5. #include <vector>
  6. #include "base/basictypes.h"
  7. #include "foo/server/bar.h"
  8. #include "third_party/absl/flags/flag.h"
复制代码
**破例:**有时平台相干的代码必要有条件地导入,此时可以在其他导入语句后放置条件导入语句。 当然,尽量保持平台相干的代码简便且影响范围小。 例如:
  1. #include "foo/public/fooserver.h"
  2. #include "base/port.h"  // 为了 LANG_CXX11.
  3. #ifdef LANG_CXX11
  4. #include <initializer_list>
  5. #endif  // LANG_CXX11
复制代码
2. 作用域

2.1 命名空间

**除了少数特别情况,都应该在命名空间内放置代码。**命名空间应该有独一无二的名字,此中包含项目名称,也可以选择性地包含文件路径。禁止使用 using 指令 (例如 using namespace foo)。 禁止使用内联命名空间。
界说:命名空间可以将全局作用域 分别为独立的、著名字的作用域,因此可以有效防止全局作用域中的命名冲突。
优点:在大型程序里,不同项目或模块可能会界说同名的符号(好比都界说了名为Foo的类),若没有命名空间进行区分,在编译或运行时就会产生冲突。而将代码放置在各自的命名空间下(如project1::Foo和project2::Foo),就能让这些本来同名的符号成为不同作用域下的独立符号,制止了命名冲突,保障程序顺利运行。
内联命名空间会自动把此中的标识符置入外层作用域,好比:
  1. namespace outer {
  2. inline namespace inner {
  3.     void foo();
  4. }  // namespace inner
  5. }  // namespace outer
复制代码
此时表达式 outer::inner::foo() 与 outer::foo() 等效. 内联命名空间的主要用途是保持不同 ABI 版本之间的兼容性。
缺点


  • 理解难度增加:命名空间的存在使得代码阅读和理解变得相对困难,因为要确定一个标识符对应的界说,必要清晰它地点的命名空间,可能涉及多层嵌套等情况,查找起来不如全局作用域下直观。
  • 内联命名空间特别题目:内联命名空间更是加大了理解复杂度,其内部标识符不仅出现在自身声明的命名空间内,还会被置入外层作用域,导致代码逻辑的追踪变得更复杂,而且它主要用途局限于作为版本控制策略的一部分,实用场景较窄。
  • 代码冗长题目:在部分场景下,为了正确引用符号,必要使用完全限定名称,若命名空间存在多层嵌套,书写起来会让代码显得冗长,低落代码的简便性。
使用建议


  • 根本使用规范:遵照命名空间命名规则,而且像示例那样通过解释注明命名空间名字,方便阅读代码时快速知晓。在导入语句、gflags声明 / 界说以及其他命名空间的类的前向声明完成后,用命名空间包裹整个源代码文件,无论是.h文件中的声明部分照旧.cc文件中的函数界说部分,都放置在对应的命名空间内,保持代码结构清晰且符合规范。
  1. // .h 文件
  2. namespace mynamespace {
  3. // 所有声明都位于命名空间中.
  4. // 注意没有缩进.
  5. class MyClass {
  6.     public:
  7.     ...
  8.     void Foo();
  9. };
  10. }  // namespace mynamespace
  11. // .cc 文件
  12. namespace mynamespace {
  13. // 函数定义位于命名空间中.
  14. void MyClass::Foo() {
  15.     ...
  16. }
  17. }  // namespace mynamespace
复制代码


  • 与尺度库相干:明确禁止在std命名空间内声明任何东西,也不要对尺度库的类进行前向声明。
  • 命名空间别名使用限制:一般不允许头文件引入命名空间别名,除非是显着标注为内部使用的命名空间,
  • 内联命名空间禁用:明确禁止使用内联命名空间,制止引入不必要的代码维护和理解题目。
  • 特定命名空间标识:如果命名空间名称包含 “internal”,意味着这属于内部使用的 API,外部用户不应使用相干符号,以此来区分公开和内部的代码逻辑。
  1. // Absl 以外的代码不应该使用这一内部符号.
  2. using ::absl::container_internal::ImplementationDetail;
复制代码


  • 嵌套命名空间声明鼓励形式:鼓励新代码采用单行的嵌套命名空间声明方式,这种形式相对简便明了,符合代码书写的简便性和规范性趋势。
  1. namespace foo::bar {
  2. ...
  3. }  // namespace foo::bar
复制代码
2.2 内部链接

若其他文件不必要使用 .cc 文件中的界说,这些界说可以放入匿名命名空间或声明为 static,以实现内部链接。但是不要在 .h 文件中使用这些手段。
   内部链接与声明为static类似。它们都将所修饰的实体(函数大概变量)的作用域限制在一个特定的编译单位(通常是一个.cc文件)内。
  界说:所有放入匿名命名空间中的声明都会内部链接。声明为 static 的函数和变量也会内部链接。这意味着其他文件不能访问你声明的任何事物。即使另一个文件声明了一模一样的名称,这两个实体也都是相互独立的。
结论:建议 .cc 文件中所有不必要外部使用的代码采用内部链接。不要在 .h 文件中使用内部链接.匿名命名空间的声明应与具名命名空间的格式相同。在末尾的解释中,不用填写命名空间名称:
  1. namespace {
  2. ...
  3. }  // namespace
复制代码
2.3 非成员函数、静态成员函数和全局函数

建议将非成员函数放入命名空间。尽量不要使用完全全局的函数。不要仅仅为了给静态成员分组而使用类。类的静态方法应当和类的实例或静态数据精密相干。
优点:非成员函数和静态成员函数在某些情况下有效。若将非成员函数放在命名空间内,可以制止命名冲突,不会污染全局命名空间。
缺点:有时非成员函数和静态成员函数更得当成为一个新的类的成员,尤其是当它们必要访问外部资源或有显着的依靠关系时。
   在良好的面向对象设计中,类应该是对具有精密关联的属性和举动的封装。如果只是因为函数必要访问外部资源或有依靠关系就将非成员函数或静态成员函数随意纳入一个新类,可能会导致类的职责不明确。
  从代码阅读者的角度来看,当一个类包含了很多本来作为非成员函数或静态成员函数添加进来的操作时,会使类的功能变得难以理解。
  结论:有时我们必要界说一个和类的实例无关的函数,如许的函数可以界说为静态成员函数或非成员函数。非成员函数不应该依靠外部变量,且大部分情况下应该位于命名空间中。不要仅仅为了给静态成员分组而创建一个新类,这相当于给所著名称添加一个公共前缀,而如许的分组通常是不必要的。如果你界说的非成员函数仅供本 .cc 文件使用,请用内部链接限制其作用域。
2.4 局部变量

应该尽可能缩小函数变量的作用域,并在声明的同时初始化。
尽可能缩小变量的作用域,且声明离第一次使用的位置越近越好。更容易找到声明,相识变量的类型和初始值. 特别地,应该直接初始化变量而非先声明再赋值,好比:
  1. int i;
  2. i = f();     // 不好: 初始化和声明分离.
  3. int i = f(); // 良好: 声明时初始化。
复制代码
  1. int jobs = NumJobs();
  2. // 更多代码...
  3. f(jobs);      // 不好: 初始化和使用位置分离.
复制代码
  1. int jobs = NumJobs();
  2. f(jobs);      // 良好: 初始化以后立即 (或很快) 使用.
复制代码
  1. vector<int> v;
  2. v.push_back(1);  // 用花括号初始化更好.
  3. v.push_back(2);
复制代码
  1. vector<int> v = {1,2}; // 良好: 立即初始化 v.
复制代码
通常应该在语句内声明用于 if、while 和 for 语句的变量,如许会把作用域限制在语句内. 例如:
  1. while (const char* p = strchr(str,'/')) str = p + 1;
复制代码
必要注意的是,如果变量是一个对象,那么它每次进入作用域时会调用构造函数,每次退出作用域时都会调用析构函数.
  1. // 低效的实现:
  2. for (int i = 0; i < 1000000; ++i) {
  3.     Foo f;  // 调用 1000000 次构造函数和析构函数.
  4.     f.DoSomething(i);
  5. }
复制代码
在循环的作用域外面声明这类变量更高效:
  1. Foo f;  // 调用 1 次构造函数和析构函数.
  2. for (int i = 0; i < 1000000; ++i) {
  3.     f.DoSomething(i);
  4. }
复制代码
2.5 静态和全局变量

禁止使用静态储存周期的变量,除非它们可以平凡地析构。
   平凡初始化:简单直接的初始化方式,通常在编译阶段就能完成大部分工作,不必要复杂的运行时操作。这种初始化方式主要涉及根本数据类型,如int、char、float等。
  不平凡初始化:涉及复杂的操作,通常必要在运行时执行,而且可能包括动态内存分配、调用复杂的构造函数、从外部资源获取数据等来初始化对象。如String类的构造函数中,必要动态分配内存(new char[length + 1])。
  平凡析构:简单的析构函数,它不执行任何用户界说的、具有实质性影响的操作。
  不平凡析构:在类中进行了资源分配(如动态内存分配、获取文件句柄、获取网络套接字等),在析构函数中就必要开释这些资源,以制止资源泄漏。
  界说:每个对象都有与生命周期相干的储存周期。静态储存周期对象的存活时间是从程序初始化开始,到程序结束为止。这些对象可能是命名空间作用域内的变量 (全局变量)、类的静态数据成员大概用 static 修饰符声明的函数局部变量。对于函数局部静态变量,初始化发生在在控制流第一次经过声明时,所有其他对象会在程序启动时初始化。程序退出时会销毁所有静态储存周期的对象 (这发生在未汇合 (join) 的线程终止前)。
初始化过程可以是动态的,也就是初始化过程中有不平凡的操作。其他初始化都是静态初始化。二者并非水火不容:静态储存周期的变量 一定 会静态初始化 (初始化为指定常量或给所有字节清零),必要时会随后再次动态初始化。
优点:静态储存周期的变量(特别是全局或静态变量)在很多场景下都能发挥作用,例如可充当具名常量方便代码中常量的使用和理解;作为编译单位内部的辅助数据结构来辅助实现某些功能;用于存储下令行旗标以控制程序不同的运行举动;记录日记信息;实现注册机制方便模块注册管理;搭建背景基础办法等,对程序团体的功能实现和结构组织有一定帮助。
缺点


  • 代码复杂度及错误风险:使用动态初始化大概具有非平凡析构函数的全局和静态变量,会显着增加代码的复杂度。因为不同编译单位中这类变量的动态初始化顺序是不确定的,相应地析构顺序也不确定(只知道是初始化顺序的逆序),这就容易引发一些难以察觉的错误。
  • 生命周期相干题目:若静态变量的初始化代码引用了另一个静态储存周期的变量,可能出现访问时间在另一变量的生命周期开始前(导致访问未初始化的变量)或生命周期结束后(产生非法访问)的情况。而且在程序结束时,若存在未汇合的线程,这些线程可能在静态变量析构后继续尝试访问它们,进而引发程序错误。
平凡的析构函数不受执行顺序影响。其他析构函数则有风险,可能访问生命周期已结束的对象。因此,只有拥有平凡析构函数的对象才能采用静态储存周期。根本类型 (例如指针和 int) 可以平凡地析构,可平凡析构的类型所构成的数组也可以平凡地析构。注意,用 constexpr 修饰的变量可以平凡地析构。
   constexpr是 C++ 11 引入的一个关键字,用于指定一个表达式或函数在编译期可以求值。它主要用于常量表达式的盘算,通过使用constexpr,可以让编译器在编译阶段尽可能地盘算出表达式的值,而不是比及运行时再盘算,从而提高程序的性能而且可以用于一些必要在编译期确定值的场景。
  关于析构
  1. const int kNum = 10;  // 允许
  2. struct X { int n; };
  3. const X kX[] = {{1},{2},{3}};  // 允许
  4. void foo() {
  5.   static const char* const kMessages[] = {"hello","world"};  // 允许
  6. }
  7. // 允许: constexpr 可以保证析构函数是平凡的.
  8. constexpr std::array<int,3> kArray = {1,2,3};
复制代码
  1. // 不好: 非平凡的析构.
  2. const std::string kFoo = "foo";
  3. // 和上面相同的原因,即使 kBar 是引用 (该规则也适用于生命周期被延长的临时对象).
  4. const std::string& kBar = StrCat("a","b","c");
  5. void bar() {
  6.   // 不好: 非平凡的析构.
  7.   static std::map<int,int> kData = {{1,0},{2,0},{3,0}};
  8. }
复制代码
注意,引用不是对象,因此它们的析构函数不受限。但是,它们仍需遵守动态初始化的限制。特别地,我们允许形如 static T& t = *new T; 的函数内局部静态引用。
关于初始化
初始化是更复杂的话题,因为我们不仅必要考虑构造函数的执行过程,也要考虑初始化表达式的求值过程。
  1. int n = 5;    // 可以
  2. int m = f();  // ? (依赖 f)
  3. Foo x;        // ? (依赖 Foo::Foo)
  4. Bar y = g();  // ? (依赖 g 和 Bar::Bar)
复制代码
除了第一行语句以外,其他语句都会受到不确定的初始化顺序影响。
我们所需的概念在 C++ 尺度中的正式称谓是常量初始化。这意味着初始化表达式是常量表达式,而且如果要用构造函数进行初始化,则该构造函数也必须声明为 constexpr:
  1. struct Foo { constexpr Foo(int) {} };
  2. int n = 5;  // 可以,5 是常量表达式.
  3. Foo x(2);   // 可以,2 是常量表达式且被选中的构造函数也是 constexpr.
  4. Foo a[] = { Foo(1),Foo(2),Foo(3) };  // 可以
复制代码
可以自由使用常量初始化。应该用 constexpr 或 constinit 标记静态变量的常量初始化过程. 应该假设任何没有这些标记的静态变量都是动态初始化的,并审慎地检查这些代码。
   constinit是 C++ 20 引入的一个关键字,用于声明具有静态存储期或线程存储期的变量的初始化是常量初始化。它主要用于确保变量在编译期进行初始化,而且这种初始化方式更加明确和严格,相比于其他初始化方式,constinit强调初始化的常量性和确定性。
  作为反例,以下初始化过程有题目:
  1. // 下文使用了这些声明.
  2. time_t time(time_t*);      // 不是 constexpr!
  3. int f();                   // 不是 constexpr!
  4. struct Bar { Bar() {} };
  5. // 有问题的初始化.
  6. time_t m = time(nullptr);  // 初始化表达式不是常量表达式.
  7. Foo y(f());                // 同上
  8. Bar b;                     // 被选中的构造函数 Bar::Bar() 不是 constexpr.
复制代码
我们不建议且通常禁止动态地初始化全局变量。不过,如果这一初始化过程不依靠于其他初始化过程的顺序,则可以允许。若满意这一要求,则初始化的顺序变化不会产生任何区别。例如:
  1. int p = getpid();  // 若其他静态变量不会在初始化过程中使用 p,则允许.
复制代码
允许动态地初始化静态局部变量 (这是常见的)。
常用的语法结构


  • 全局字符串:如果必要具名的全局或静态字符串常量,可以采用constexpr修饰的string_view变量、字符数组大概指向字符串字面量的字符指针,因为字符串字面量本身具有静态储存周期,往往能满意相应需求。
  • 动态容器:对于像字典和聚集等动态容器,若要以静态变量储存不变的数据,不建议使用尺度库的动态容器(因其拥有非平凡的析构函数),可以考虑用平凡类型的数组替代(如int数组的数组用于模拟字典、数对的数组等),对于少量数据使用线性搜索并借助absl/algorithm/container.h中的工具实现常见操作,必要时保持数据有序采用二分查找法;若确实必要使用尺度库动态容器,建议使用函数内局部静态指针的方式来处理惩罚。
   Abseil 是一个由谷歌开发的开源 C++ 库。它的目的是为 C++ 开发提供一系列高质量、可复用的组件,包括算法、容器、字符串处理惩罚、内存管理等方面的工具。这些组件帮助 C++ 程序员更高效地编写代码,同时遵照最佳实践。
  Abseil 是对尺度 C++ 库的增补。它并不是要替代尺度库,而是在尺度库的基础上提供了更多的功能和优化。
  

  • 智能指针:智能指针(如std::unique_ptr和std::shared_ptr)由于在析构时有开释资源的操作,属于非平凡析构,不能作为静态变量使用,这种情况下可以思考是否实用其他模式,简单的解决办法是用裸指针指向动态分配的对象且永不删除它。
  • 自界说类型的静态变量:对于自界说类型的静态数据或常量数据,应给该类型设置平凡的析构函数以及constexpr修饰的构造函数,使其符合作为静态储存周期变量的条件;若上述各种建议都不实用,可以采用函数内局部静态指针或引用,通过动态分配一个对象且永不删除的方式来处理惩罚(如static const auto& impl = *new T(args...);)。
2.6 thread_local 变量

必须使用编译期常量初始化在函数外界说的 thread_local 变量,且必须使用 ABSL_CONST_INIT 属性来逼迫执行这一规则。优先采用 thread_local,而非其他界说线程内局部数据的方法。
   thread_local是 C++ 11 引入的一个存储期说明符。它用于声明一个变量具有线程存储期,这意味着每个线程都有该变量的一个独立副本。与全局变量(整个程序只有一个副本)和局部变量(在函数执行期间存在于栈上)不同,thread_local变量为每个线程提供了专属的数据存储。
  可以用 thread_local 修饰符声明变量,其能在命名空间内、函数内或类的静态成员内声明,但不能在类的普通成员内声明。例如
  1. thread_local Foo foo =...;
复制代码
展示了根本的声明形式,且不同线程访问该变量时,会访问各自独立的对象,从本质上来说它相当于一组不同的对象分布在各个线程中。
thread_local 实例与静态变量的初始化过程有相似之处,不过 thread_local 变量是在每个线程启动时初始化,而静态变量是在程序启动时初始化。正因如此,函数内的 thread_local 变量是线程安全的,制止了多线程同时访问同一变量可能引发的数据竞争题目。但在访问其他 thread_local 变量时,和静态变量类似存在初始化顺序题目,且由于线程的复杂性,这个题目可能更严重。
在析构方面,线程终止时,thread_local 变量的销毁顺序是初始化顺序的逆序,这一点和 C++ 中其他相干规则一致。但如果在 thread_local 变量的析构过程中访问了该线程中已销毁的其他 thread_local 变量,就容易出现难以调试的开释后使用(野指针)题目,必要格外注意。
优点


  • 防止竞态条件与助力并行化:线程的局部数据通过 thread_local 变量来界说,可以从根本上防止竞态条件,因为通常只有一个线程访问本身对应的 thread_local 变量副本,以是在多线程并行处理惩罚任务时,能有效制止因多个线程同时读写共享数据导致的题目,从而有助于程序的并行化实现,提升团体执行服从。
  • 语法尺度支持优势:在创建线程局部数据的各种方法中,thread_local 是由语法尺度支持的唯一方法,这意味着它具有规范性和通用性,使用它符合 C++ 语言尺度规范,不用担心因使用非尺度或不规范的方式带来的兼容性、可维护性等题目。
缺点


  • 运行时不确定性:在线程启动或首次使用 thread_local 变量时,可能触发很多难以猜测、运行时间不可控的其他代码。这会给程序的性能分析、调试等带来困难,因为无法正确预估这部分代码对团体运行时间等方面的影响。
  • 类似全局变量的缺点:只管 thread_local 变量在每个线程中有独立副本实现了线程安全,但本质上它类似全局变量,依然具备全局变量除线程安全外的其他缺点,好比可能导致代码结构不够清晰、模块间耦合性增加等题目,不利于代码的模块化设计和维护。
  • 内存占用题目:在最坏情况下,thread_local 变量占用的内存与线程数目成正比,若线程数目浩繁,其占用量可能非常巨大,对系统内存资源是个较大挑衅,必要公道评估和控制线程数目以及 thread_local 变量的使用规模。
  • 析构相干风险:成员数据若要声明为 thread_local 必须是静态的,而且若 thread_local 变量拥有复杂的析构函数,可能会遇到野指针题目,尤其是析构函数不能(直接或间接地)访问任何有可能已被销毁的其他 thread_local 变量,而实际中很难检查这一规则是否被遵守,增加了程序出现错误的风险。
  • 资源泄漏风险:那些用于全局 / 静态变量预防野指针的方法不实用于 thread_local 变量。对于全局或局部变量,即使跳过析构函数,随着程序终止其生命周期结束,操作系统能较快回收泄露的内存和其他资源;但对于 thread_local 变量,若跳过析构函数,资源泄漏量会和程序运行期间创建的线程数目成正比,这是个不容忽视的潜在风险。
建议:


  • 位于类或命名空间中的 thread_local 变量只能用真正的编译时常量来初始化,也就是禁止动态初始化,必须用 ABSL_CONST_INIT 修饰(也可使用 constexpr 修饰,但不常见)来包管初始化符合要求,以此确保变量初始化的规范性和确定性,制止因不当初始化引发后续题目。
  • 函数中的 thread_local 变量不存在初始化的顾虑,但在线程退出时有开释后使用的风险。可以通过用静态方法暴露函数内的 thread_local 变量来模拟类或命名空间中的 thread_local 变量,不过要特别注意线程退出时变量的析构情况,如果析构函数使用了任何其他(可能已经销毁的) thread_local 变量,就会遇到难以调试的野指针题目,以是建议使用平凡的类型,大概析构函数中没有自界说代码的类型,以减少访问其他 thread_local 变量的可能性,低落出现野指针等错误的风险。
3. 类

类 (class) 是 C++ 中最根本的代码单位。因此,类在 C++ 中被广泛使用。
3.1 构造函数的内部操作

构造函数中禁止调用虚函数,应制止在无错误处理惩罚机制下进行可能失败的初始化。
构造函数可完全初始化对象,使其可作为const类型并方便在尺度容器或算法中使用,但存在一些缺点,如调用虚函数的隐患、难以报告错误、初始化失败导致对象异常状态等。
若初始化可能失败,可界说Init()方法或工厂函数,确保从对象状态可知公用方法可用性。
3.2 隐式类型转换

不应界说隐式类型转换,类型转换运算符和单参数构造函数应标记为explicit,但拷贝和移动构造函数除外。
隐式类型转换使类型更易用、简化函数重载、列表初始化简便,但可能掩盖类型不匹配错误、低落代码可读性、导致调用歧义等题目。
对于某些可互换类型,若隐式类型转换必要且恰当,可申请豁免。接受多个参数或单个std::initializer_list参数的构造函数可省略explicit标记。
3.3 可拷贝类型和可移动类型

类的公有接口应明确指明是否支持拷贝和移动操作,支持的话应正确界说相干函数,不支持则应显式删除。
可移动和可拷贝类型使 API 更简单、安全、通用,相干函数易于确保正确性和高效性,但某些类型不应支持拷贝,拷贝构造函数隐式调用可能导致题目。
若类的拷贝或移动操作易被误解或故不测开销,应设计为不可拷贝或不可移动,可拷贝类型的移动操作应在高效时界说,同时应检查默认实现正确性,制止对象切割风险,基类最好为抽象类。
3.4 结构体照旧类

仅用struct界说储存数据的被动对象,包含常量成员,所有成员为公共,无不变式关系,可含构造函数等,但不能要求或实现不变式。
若需更多功能、不变式束缚或结构体用途广泛且会更新,应使用类。不确定时优先选类,无状态类型可使用结构体以与 STL 保持一致,类和结构体成员变量命名规则不同。
3.5 结构体、数对照旧元组

若能给成员起故意义名字,优先用结构体,其比数对和元组更可读,只管使用数对(pair)和元组(tuple)可节流编写代码时间,但它们得当通用代码或与现有代码 / API 交互。
   

  • std::pair和std::tuple在 C++11 中引入。
  • 元组是数对的扩展,可以存储恣意数目的值,每个值可以是不同类型的。它界说在<tuple>头文件中,通过std::tuple模板类实现。例如,std::tuple<int,double,std::string>可以用来存储一个整数、一个双精度浮点数和一个字符串。
  • 元组中的值可以通过std::get函数模板按照索引或类型来访问。例如:
  1. std::tuple<int,double,std::string> myTuple(42,3.14,"World");
  2. int value = std::get<0>(myTuple);  // 访问第一个值(42)
  3. double number = std::get<1>(myTuple);  // 访问第二个值(3.14)
  4. std::string word = std::get<2>(myTuple);  // 访问第三个值("World")
  5. // 也可以根据类型访问(前提是类型在元组中是唯一的)
  6. std::string text = std::get<std::string>(myTuple);  
复制代码
3.6 继承

通常组合比继承更合适,继承应使用public访问权限,制止过度使用实现继承,尽量只在 “is-a” 关系时使用。
实现继承可复用代码、减少代码量,接口继承可逼迫公开特定 API,但实现继承使子类实现难理解,多重继承题目更严重,应明确使用override或final关键字限定虚函数或虚析构函数,允很多重继承但制止多重实现继承。
3.7 运算符重载

审慎使用运算符重载,禁止自界说字面量,重载运算符应意义明确、符合常理且与内置运算符举动一致,只为自界说类型界说且制止在类外不同文件界说同一运算。
重载运算符可使自界说类型举动类似内置类型,代码更简便直观,但实现难度大,易引起困惑和错误,过度使用会使代码难理解,某些运算符重载危险,自界说字面量有诸多题目。
3.8 访问控制

类的所有数据成员应声明为私有,除非是常量,使用 Google Test 时,测试夹具类数据成员在.cc文件中可声明为受保护,若声明在.h文件中则应为私有。
3.9 声明序次

声明应按相似性分组,公有部分放最前面,各部分建议按特定顺序声明,制止在类界说中放置大段函数界说,只有简单、对性能重要且简短的方法可声明为内联函数。
建议使用以下顺序:

  • 类型和类型别名 (typedef,using,enum,嵌套结构体和类,友元类型)
  • (可选,仅实用于结构体) 非静态数据成员
  • 静态常量
  • 工厂函数 (factory function)
  • 构造函数和赋值运算符
  • 析构函数
  • 所有其他函数 (包括静态与非静态成员函数,另有友元函数)
  • 所有其他数据成员 (包括静态和非静态的)
4. 函数

4.1 输入和输出

返回值倾向于按值返回,如果失败按引用返回,制止返回原始指针(除非可为空)。
非可选输入参数用值或 const 引用,非可选输出和输入 / 输出参数用引用(不能为空),可选参数按情况使用 std:ptional、const 指针或非 const 指针。
将仅输入参数置于输出参数之前,但不是硬性规定,需考虑实际情况。
4.2 编写简短函数

倾向于简短凝练的函数,虽不硬性限制长度,但超过 40 行可考虑分割,便于阅读和修改,不要害怕修改复杂长函数。
4.3 函数重载

如果计划重载一个函数,可以试试改在函数名里加上参数信息。如用 AppendString() 和 AppendInt() 等
  1. class MyClass {
  2.     public:
  3.     void Analyze(const string &text);
  4.     void Analyze(const char *text,size_t textlen);
  5. };
复制代码
优点:使代码直观,模板化代码必要。
缺点:读者需认识匹配规则,派生类重载部分变体时继承语义易混淆。
4.4 缺省参数

只用于非虚函数,值须始终一致,与函数重载规则相同,一般建议用函数重载。
优点:处理惩罚函数参数默认与非默认情况便利,语法简便。
缺点:与函数重载类似,虚函数调用缺省参数有题目,在调用点重新求值致代码膨胀,干扰函数指针。
4.5 函数返回类型后置语法

C++11 允许在函数名前用 auto,参数列表后后置返回类型,如auto foo (int x) -> int。
优点:显式指定 Lambda 表达式返回值,依靠模板参数时书写和阅读可能更简单。
缺点:语法新,读者可能陌生,新旧混用不规整。
5. 来自Google的奇技

5.1 所有权与智能指针

**所有权是用于登记和管理动态内存及其他资源的一种技术。**对于动态分配的对象,其所有主可以是一个对象或函数,这个所有主负担着在该对象不再有效时自动将其销毁的责任。而且所有权存在不同情况,既可以是单一固定的,也可以在多个主体间共享,共享时由末了一个持有所有权的主体负责销毁对象,还可以在代码中直接将所有权传递给其他对象。
智能指针本质上是一个类,它通过重载 * 和 -> 运算符,使其表现得如同普通指针一样。其重要作用在于自动化所有权的登记工作,确保销毁义务能正确执行。例如,std::unique_ptr 是 C++ 11 引入的一种智能指针类型,用于表现对动态分配对象独一无二的所有权,一旦 std::unique_ptr 离开其作用域,对应的对象就会被销毁,而且它不能被复制,只能通过 C++ 11 的 move 语法移动给新的所有主;而 std::shared_ptr 同样表现动态分配对象的所有权,不过它可以被共享、被复制,对象的所有权由所有复制者共同拥有,直到末了一个复制者被销毁时,对象才随之被销毁。
优点


  • 有效管理内存:在处理惩罚动态分配的内存时,通过明确所有权归属,能很好地把控动态内存的生命周期,制止内存泄漏等题目。
  • 低落开销与简化操作:传递对象所有权的开销相较于复制对象(如果对象可复制的话)通常更小,而且相比于通过 “借用” 指针或引用来管理对象生命周期(这必要协调多个使用者对对象生命周期的操作),传递所有权更为简单直接,能省去不少协调工作。
  • 提升代码可读性与简化代码:当所有权逻辑清晰、有文档记录且不杂乱时,代码的可读性会显着提升,同时借助智能指针还能自动完成所有权登记工作,大大简化了代码编写过程,减少因手动管理所有权可能出现的诸多错误。
  • 对 const 对象更友好:对于 const 对象而言,使用智能指针操作简单方便,而且比深度复制这类操作更具服从优势,能在包管对象不可变的同时高效地处理惩罚其所有权相干事件。
缺点


  • 指针语义复杂性:无论是智能指针照旧原生指针,使用指针来表现和传递所有权,其指针语义远比值语义复杂得多,尤其在 API 设计中,不仅要考虑所有权题目,还必要顾及别名、生命周期、可变性等诸多方面的题目,增加了代码理解和编写的难度。
  • 性能与可读性衡量题目:固然传递所有权在某些情况下能低落开销,但实际上值语义的开销常被高估,以是有时间所有权传递带来的性能提升可能并不足以增补其导致的可读性降落以及代码复杂度增加的损失,必要衡量利弊来决定是否采用所有权传递的方式。
  • 限制客户端内存管理模子:如果 API 依靠所有权的传递,那么客户端就只能遵照这一单一的内存管理模子,限制了客户端在内存管理方面的机动性,可能给客户端代码的编写和集成带来不便。
  • 资源开释位置不直观:使用智能指针时,资源开释详细发生的位置变得不那么显着了,不像手动管理内存那样清晰可查,这在调试和理解代码执行过程中可能造成一定困扰。
  • 语法迷惑性与系统重构题目:std::unique_ptr 的所有权传递依靠 C++ 11 的 move 语法,而这个语法相对较新,容易让程序员产生迷惑。而且如果本来的所有权设计已经比力完善,后续若要引入所有权共享机制(好比使用 std::shared_ptr),可能不得不对整个系统进行重构,成本较高。
  • 运行时开销与特别情况题目:所有权共享机制(如 std::shared_ptr)的登记工作是在运行时进行的,会产生一定的开销,在某些极端情况下(好比存在循环引用时),被共享所有权的对象可能永远不会被销毁,导致资源泄漏等题目。此外,智能指针并不能完全替代原生指针,原生指针在一些特定场景下仍有其不可替代的作用。
结论
如果必须进行动态分配,更倾向于让分配者保持对象的所有权。当其他地方必要使用该对象时,优先考虑传递它的拷贝(如果可行的话),大概传递一个不改变所有权的指针或引用,以此来尽量简化所有权管理,制止不必要的复杂性。
  1. std::unique_ptr<Foo> FooFactory();
  2. void FooConsumer(std::unique_ptr<Foo> ptr);
复制代码
倾向于使用 std::unique_ptr 来明确所有权的传递,例如通过函数返回 std::unique_ptr 以及在函数参数中吸收 std::unique_ptr 的方式,清晰地展现所有权的转移过程。而对于共享所有权的情况,不要容易使用,只有在有很好的理由时才考虑,好比为了制止开销昂贵的拷贝操作,而且要确保性能提升非常显着,同时操作的对象是不可变的(如 std::shared_ptr<const Foo> 这种形式),如果确实必要使用共享所有权,建议使用 std::shared_ptr。另外,明确指出不要使用 std::auto_ptr,而是用 std::unique_ptr 来替代它,以遵照更好的编程规范和制止潜在题目。
5.2 Cpplint

使用 cpplint.py 检查风格错误。
cpplint.py 是一个用来分析源文件,能检查出多种风格错误的工具。它不并完美,乃至还会漏报和误报,但它仍然是一个非常有效的工具. 在行尾加 // NOLINT,或在上一行加 // NOLINTNEXTLINE,可以忽略报错。
某些项目会指导你如何使用他们的项目工具运行 cpplint.py。如果你参与的项目没有提供,你可以单独下载 cpplint.py。
6. 其他C++特性

6.1 右值引用

仅在界说移动构造函数与移动赋值操作时使用。
优点:可实现移动语义,提升性能,支持通用函数封装和可移动但不可拷贝类型,是使用某些尺度库类型的必需操作。
缺点:是相对较新的特性,规则复杂,尚未被广泛理解。
6.2 函数重载

若要重载函数,可试试在函数名中参加参数信息。
优点:使代码更直观,方便模板化代码,为使用者带来便利。
缺点:读者需认识匹配规则,派生类重载部分变体时继承语义易令人困惑。
6.3 缺省参数

除特定情况外,不允许使用,应改用函数重载,如AppendString() 和 AppendInt() 等。
优点:可修改缺省参数,语法清晰,能区分必选和可选参数。
缺点:会干扰函数指针,造成代码臃肿,在调用点有重复。
  
  1. void func(int a,int b);
  2. // 改成下面情况
  3. void func(int a,int b = 0);
  4. void (*funcPtr)(int,int) = &func; // 会报错
复制代码
会出现题目,因为现在 func 的函数签名改变了,funcPtr 所盼望的函数签名与修改后的 func不匹配了,再通过这个函数指针去调用函数时,编译器可能会报错,代码的逻辑就被破坏了。
  6.4 变长数组和 alloca ()

不允许使用,改用更安全的分配器,std::vector 或 std::unique_ptr<T[]>。
优点:语法天然,执行高效。
缺点:不是尺度 C++ 组成部分,可能导致内存越界错误。
   alloca()是一个函数,用于在栈上动态分配内存。它的主要特点是分配的内存是从栈空间获取的,当调用alloca()的函数返回时,分配的内存会自动开释,无需像使用malloc()等函数分配堆内存那样必要手动开释。
  6.5 友元

允许公道使用,通常应界说在同一文件内。
优点:扩大了类的封装界限,是处理惩罚特定情况的较好选择。
6.6 异常

不使用 C++ 异常。
优点:允许高层处理惩罚底层失败,与其他语言更一致,在测试框架中好用,是处理惩罚构造函数失败的唯一途径,有些第三方库依靠异常。
缺点:添加异常语句时需检查所有调用点,扰乱执行流程,增加二进制文件数据,延伸编译时间,可能鼓励滥用,与现有代码整合困难。
6.7 运行时类型识别(RTTI)

禁止使用,尽量制止,单位测试中可使用,有公道用途但易被滥用。
优点:尺度替代方案可能必要修改类层级,在单位测试和管理对象关系中有效。
缺点:运行时判断类型通常意味着设计题目,使代码难以维护,基于类型的判断树难以修改。
   RTTI(Run - Time Type Information)即运行时类型信息,是 C++ 语言的一个特性,它允许程序在运行时获取对象的类型信息。在 C++ 中,通过typeid运算符和dynamic_cast运算符来实现 RTTI 功能。
  6.8 类型转换

使用 C++ 的类型转换,如 static_cast<>() 等,不使用 C 风格转换。
优点:C++ 类型转换机制更清晰,查找更夺目。
缺点:语法较复杂。
   用 static_cast 替代 C 风格的值转换,或某个类指针必要明确的向上转换为父类指针时。
  用 const_cast 去掉 const 限定符。
  用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。仅在你对所做统统了然于心时使用。
  6.9 流

只在记录日记时使用。
优点:打印时无需关心对象类型,构造和析构函数自动处理惩罚文件。
缺点:使某些功能函数难执行,部分格式化操作性能低,不支持字符串操作符重新排序。
6.10 前置自增和自减

对迭代器和模板对象使用前缀形式。
优点:通常服从更高,可以制止一次不必要的拷贝。
缺点:C 开发中传统做法是使用后置自增,部分人认为后置自增更易懂。
6.11 const 用法

在可能的情况下使用 const,有时改用 constexpr 更好,保持代码一致性。
优点:加强代码理解、类型检测和安全性。
缺点:具有入侵性,调用库函数时较贫苦。
6.12 constexpr 用法

用于界说真正的常量和常量初始化,制止复杂函数界说。
优点:可界说多种常量,实现 C++ 常量机制。
缺点:过早优化为 constexpr 变量后修改贫苦,界说限制可能导致方法模糊。
6.13 整型

C++ 内建整型中只使用 int,根据情况使用 <stdint.h> 中精确大小的整型。
优点:保持声明同一。
缺点:整型大小因编译器和体系结构而异。
6.14 64 位下的可移植性

代码应对 64 位和 32 位系统友好,注意打印、比力、结构体对齐等题目。
优点:确保程序在不同系统上的兼容性。
缺点:需处理惩罚不同系统下的差异,如格式化指示符、结构体对齐等。
6.15 预处理惩罚宏

审慎使用,尽量用内联函数、枚举和常量取代,遵照特定用法模式。
优点:可实现一些其他技术无法实现的功能。
缺点:可能导致异常举动,测试困难。
6.16 0、nullptr 和 NULL

指针使用 nullptr,字符使用 ‘\0’。
优点:包管指针类型安全,提高代码可读性。
6.17 sizeof

尽可能用 sizeof (varname) 取代 sizeof (type)。
优点:变量类型改变时自动更新。
6.18 auto

用于绕过烦琐类型名,提高局部变量声明的便利性,但需注意可读性,不用于局部变量之外。
优点:简化复杂类型声明,方便使用中间变量。
缺点:类型不显着时影响代码可读性,需区分 auto 和 const auto&,与列表初始化结合时易混淆,在接口中使用可能导致 API 变化。
6.19 列表初始化

在考虑可移植性下,可以使用列表初始化,C++03 中聚合类型可用,C++11 中推广到任何对象类型。
优点:提供了同一方便的初始化方式,实用于多种类型,可简化代码编写,如在初始化容器、自界说类型等场景中,使代码更简便直观,加强可读性。
6.20 Lambda 表达式

适当使用 lambda 表达式。当 lambda 将转移当前作用域时,首选显式捕捉。
仅在 lambda 生存期显着短于潜在捕捉时使用默认引用捕捉,仅用默认按值捕捉绑定少量变量且不捕捉this,制止用捕捉引入新名称或改变现著名称含义,使用时遵照相干格式要求,考虑团队成员对代码的理解和维护本领。
优点:是创建匿名函数对象的简易途径,在传函数对象给 STL 算法时最简易且可读性好,适当使用默认捕捉可消除冗余,与std::functions和std::bind可搭配成通用回调机制,方便编写吸收有界函数为参数的函数。
缺点:变量捕捉可能导致悬空指针错误,按值默认捕捉可能产生误导,捕捉语法与常规变量声明不同,初始化捕捉依靠类型推导有与auto类似缺点且语法不提示推导,lambda 使用过度会使代码难理解。
6.21 模板编程

不要使用复杂的模板编程。
优点:能实现机动类型安全的接口和极好性能,如 Google Test、std::tuple、std::function和 Boost.Spirit 等工具依靠模板实现。
缺点:技巧艰涩难懂,复杂模板代码难读懂、调试和维护,编译出错信息不友好,大量使用会使重构工具难以发挥作用。
6.22 Boost 库

只使用 Boost 中被认可的库。
优点:代码质量高、可移植性好,弥补 C++ 尺度库空白,如提供更好的智能指针、型别特性等。
缺点:部分库的编程实践可读性差,如元编程和高级模板技术等。
6.23 C++11

适当使用库和语言扩展,使用前考虑项目可移植性,除个别情况外<ratio>、<cfenv>和<fenv.h>头文件、默认 lambda 捕捉等特性最好不用。
优点:是官方尺度,被大多编译器支持,尺度化了很多扩展,简化操作,改善性能和安全。
缺点:相对于前身更复杂,开发者可能不认识,部分扩展对可读性有害或与原有机制冲突,带来困惑和迁移代价。
7. 命名约定

**最重要的一致性规则是命名管理。**命名的风格能让我们在不必要去查找类型声明的条件下快速地相识某个名字代表的含义:类型,变量,函数,常量,宏,等等,乃至. 我们大脑中的模式匹配引擎非常依靠这些命名规则。
命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,以是无论你认为它们是否重要,规则总归是规则。
7.1 通用命名规则

函数命名,变量命名,文件命名要有描述性; 少用缩写。
尽可能使用描述性的命名,别心疼空间,毕竟相比之下让代码易于新读者理解更重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词。
  1. int price_count_reader;    // 无缩写
  2. int num_errors;            // "num" 是一个常见的写法
  3. int num_dns_connections;   // 人人都知道 "DNS" 是什么
  4. int n;                     // 毫无意义.
  5. int nerr;                  // 含糊不清的缩写.
  6. int n_comp_conns;          // 含糊不清的缩写.
  7. int wgc_connections;       // 只有贵团队知道是什么意思.
  8. int pc_reader;             // "pc" 有太多可能的解释了.
  9. int cstmr_id;              // 删减了若干字母.
复制代码
注意,一些特定的广为人知的缩写是允许的,例如用 i 表现迭代变量和用 T 表现模板参数。
模板参数的命名应当遵照对应的分类:类型模板参数应当遵照类型命名的规则,而非类型模板应当遵照变量命名的规则。
7.2 文件命名

文件名要全部小写,可以包含下划线 (_) 或连字符 (-),依照项目的约定。如果没有约定,那么 “_” 更好。
可接受的文件命名示例:


  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest 和 _regtest 已弃用.
C++ 文件要以 .cc 结尾,头文件以 .h 结尾。专门插入文本的文件则以 .inc 结尾。
不要使用已经存在于 /usr/include 下的文件名 ,如 db.h。
通常应尽量让文件名更加明确。http_server_logs.h 就比 logs.h 要好。界说类时文件名一般成对出现,如 foo_bar.h 和 foo_bar.cc,对应于类 FooBar。
内联函数界说必须放在 .h 文件中。如果内联函数比力短,就直接将实现也放在 .h 中。
7.3 类型命名

类型名称的每个单词首字母均大写,不包含下划线:MyExcitingClass,MyExcitingEnum。
所有类型命名 —— 类,结构体,类型界说 (typedef),枚举,类型模板参数 —— 均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。例如:
  1. // 类和结构体
  2. class UrlTable { ...
  3. class UrlTableTester { ...
  4. struct UrlTableProperties { ...
  5. // 类型定义
  6. typedef hash_map<UrlTableProperties *,string> PropertiesMap;
  7. // using 别名
  8. using PropertiesMap = hash_map<UrlTableProperties *,string>;
  9. // 枚举
  10. enum UrlTableErrors { ...
复制代码
7.4 变量命名

变量 (包括函数参数) 和数据成员名划一小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体的就不用,如:a_local_variable,a_struct_data_member,a_class_data_member_。
普通变量命名
举例:
  1. string table_name;  // 好 - 用下划线.
  2. string tablename;   // 好 - 全小写.
  3. string tableName;  // 差 - 混合大小写
复制代码
类数据成员
不管是静态的照旧非静态的,类数据成员都可以和普通变量一样,但要接下划线。
  1. class TableInfo {
  2.   ...
  3. private:
  4.   string table_name_;  // 好 - 后加下划线.
  5.   string tablename_;   // 好.
  6.   static Pool<TableInfo>* pool_;  // 好.
  7. };
复制代码
结构体变量
不管是静态的照旧非静态的,结构体数据成员都可以和普通变量一样,不用像类那样接下划线:
  1. struct UrlTableProperties {
  2.   string name;
  3.   int num_entries;
  4.   static Pool<UrlTableProperties>* pool;
  5. };
复制代码
7.5 常量命名

声明为 constexpr 或 const 的变量,或在程序运行期间其值始终保持不变的,命名时以 “k” 开头,大小写混淆。例如:
  1. const int kDaysInAWeek = 7;
复制代码
所有具有静态存储类型的变量都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。
7.6. 函数命名

常规函数使用大小写混淆,取值和设值函数则要求与变量名匹配:MyExcitingFunction(),MyExcitingMethod(),my_exciting_member_variable(),set_my_exciting_member_variable()。
一般来说,函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”),没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写 (例如,写作 StartRpc() 而非 StartRPC())。
  1. AddTableEntry()
  2. DeleteUrl()
  3. OpenFileOrDie()
复制代码
同样的命名规则同时实用于类作用域与命名空间作用域的常量,因为它们是作为 API 的一部分暴露对外的,因此应当让它们看起来像是一个函数,因为在这时,它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节。
取值和设值函数的命名与变量一致。一般来说它们的名称与实际的成员变量对应,但并不逼迫要求. 例如 int count() 与 void set_count(int count)。
7.7 命名空间命名

命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。要注意制止嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突。
顶级命名空间的名称应当是项目名大概是该命名空间中的代码所属的团队的名字。命名空间中的代码,应当存放于和命名空间的名字匹配的文件夹或其子文件夹中。
注意不使用缩写作为名称的规则同样实用于命名空间。命名空间中的代码极少必要涉及命名空间的名称,因此没有必要在命名空间中使用缩写。
要制止嵌套的命名空间与常见的顶级命名空间发生名称冲突。由于名称查找规则的存在,命名空间之间的冲突完全有可能导致编译失败。尤其是,不要创建嵌套的 std 命名空间. 建议使用更独特的项目标识符 (websearch::index,websearch::index_util) 而非常见的极易发生冲突的名称 (好比 websearch::util).
对于 internal 命名空间,要当心参加到同一 internal 命名空间的代码之间发生冲突 (由于内部维护职员通常来自同一团队,因此常有可能导致冲突)。在这种情况下,请使用文件名以使得内部名称独一无二 (例如对于 frobber.h,使用 websearch::index::frobber_internal)。
7.8 枚举命名

枚举的命名应当和常量或宏一致:kEnumName 或是 ENUM_NAME。
单独的枚举值应该优先采用常量的命名方式。但宏方式的命名也可以接受。枚举名 UrlTableErrors (以及 AlternateUrlTableErrors) 是类型,以是要用大小写混淆的方式。
  1. enum UrlTableErrors {
  2.     kOK = 0,
  3.     kErrorOutOfMemory,
  4.     kErrorMalformedInput,
  5. };
  6. enum AlternateUrlTableErrors {
  7.     OK = 0,
  8.     OUT_OF_MEMORY = 1,
  9.     MALFORMED_INPUT = 2,
  10. };
复制代码
2009 年 1 月之前,我们一直建议采用宏的方式命名枚举值。由于枚举值和宏之间的命名冲突,直接导致了很多题目。由此,这里改为优先选择常量风格的命名方式。新代码应该**尽可能优先使用常量风格。**但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期题目。
7.9 宏命名

你并不计划使用宏,对吧? 如果你一定要用,像如许命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN。
参考预处理惩罚宏; 通常 不应该 使用宏。如果不得不用,其命名像枚举命名一样全部大写,使用下划线:
  1. #define ROUND(x) ...
  2. #define PI_ROUNDED 3.0
复制代码
7.10 命名规则特例

如果你命名的实体与已有 C/C++ 实体相似,可参考现有命名策略.
bigopen(): 函数名,参照 open() 的形式。
uint: typedef
bigpos: struct 或 class,参照 pos 的形式。
sparse_hash_map: STL 型实体; 参照 STL 命名约定。
LONGLONG_MAX: 常量,如同 INT_MAX。
8. 解释

解释固然写起来很痛苦,但对包管代码可读性至关重要。下面的规则描述了如何解释以及在哪儿解释。当然也要记住:解释固然很重要,但最好的代码应当本身就是文档。故意义的类型名和变量名,要远胜过要用解释解释的暗昧不清的名字。
你写的解释是给代码读者看的,也就是下一个必要理解你的代码的人。以是慷慨些吧,下一个读者可能就是你!
8.1 解释风格

使用 // 或 /* */,同一即可。
// 或 /* */ 都可以; 但 // 常用. 要在如何解释及解释风格上确保同一。
8.2 文件解释

每个文件开头应参加版权公告和允许证引用,庞大修改时可考虑删除原作者信息。
如果.h文件声明了多个概念,需对文件内容做大抵说明及概念联系,不必要在.h和.cc间复制解释。
8.3 类解释

除非功能显着,每个类界说都要附带解释,描述功能、用法、同步条件(多线程相干),可包含使用示例,声明和界说分开时,解释也应分开。
  1. // Iterates over the contents of a GargantuanTable.
  2. // Example:
  3. //    GargantuanTableIterator* iter = table->NewIterator();
  4. //    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
  5. //      process(iter->key(),iter->value());
  6. //    }
  7. //    delete iter;
  8. class GargantuanTableIterator {
  9.   ...
  10. };
复制代码
8.4 函数解释

函数声明处解释描述功能、用途、输入输出、调用相干信息(如参数保持引用、空间分配、参数空指针情况、性能隐患、可重入同步条件等),制止啰嗦,函数重载重点解释重载部分,构造 / 析构函数注明特别操作,简单显着的可省略解释。
函数界说处若实现奥妙,需解释编程技巧、步骤或理由,不要复制声明处解释,重点在实现。
8.5 变量解释

类数据成员需解释用途、特别值、关系、生命周期等,全局变量解释含义、用途及作为全局变量的原因。
8.6 实现解释

代码前解释用于解释奥妙或复杂代码段,行解释用于隐晦地方,可连续多行解释并对齐,函数参数意义不显着时,优先改进代码(如用常量名、更改参数类型、使用类或结构体、用具名变量),末了才考虑在调用点解释。
制止描述显着征象和翻译代码,应解释原因和目的,寻求自文档化代码。
8.7 标点,拼写和语法

解释用正确大小写和结尾句号,完备叙述性语句可读性更高,短解释可随意但保持风格一致,注意标点、拼写和语法。
8.8 TODO 解释

用于临时、短期解决方案或不完美代码,格式为TODO(标识),可附明确时间或事项,添加者一般写本身名字。
8.9 弃用解释

用DEPRECATED标记弃用接口,注明姓名、邮箱等,包含指引帮助修复调用点,需自动修正调用点或找人帮助,修正后不再使用弃用接口。
9. 格式

通过同一的格式规则,可以提高代码的可读性和可维护性,便于团队协作开发。
9.1 行长度

每行代码字符数不超 80,解释、包含长路径的#include语句、头文件保护可破例。
9.2 非 ASCII 字符

尽量不用,用则需 UTF - 8 编码,特别情况如分析外部数据文件或单位测试代码中可包含,可使用十六进制编码加强可读性,制止使用char16_t、char32_t和wchar_t(调用 Windows API 除外)。
9.3 空格照旧制表位

只用空格,每次缩进 2 个空格,编辑器应将制表符转为空格。
9.4 函数声明与界说

返回类型和函数名偕行,参数尽量偕行,放不下则按函数调用方式分行,注意各种格式细节,如括号、空格、大括号位置,参数名使用规则,未使用或用途显着参数可省略参数名,不显着的在界说处解释,属性写在返回类型前。
函数看上去像如许:
  1. ReturnType ClassName::FunctionName(Type par_name1,Type par_name2) {
  2.   DoSomething();
  3.   ...
  4. }
复制代码
如果同一行文本太多,放不下所有参数:
  1. ReturnType ClassName::ReallyLongFunctionName(Type par_name1,Type par_name2,
  2.                                              Type par_name3) {
  3.   DoSomething();
  4.   ...
  5. }
复制代码
乃至连第一个参数都放不下:
  1. ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
  2.     Type par_name1, // 4 space indent
  3.     Type par_name2,
  4.     Type par_name3) {
  5.   DoSomething();  // 2 space indent
  6.   ...
  7. }
复制代码
注意以下几点:


  • 使用好的参数名。
  • 只有在参数未被使用大概其用途非常显着时,才能省略参数名。
  • 如果返回类型和函数名在一行放不下,分行。
  • 如果返回类型与函数声明或界说分行了,不要缩进。
  • 左圆括号总是和函数名在同一行。
  • 函数名和左圆括号间永远没有空格。
  • 圆括号与参数间没有空格。
  • 左大括号总在末了一个参数同一行的末尾处,不另起新行。
  • 右大括号总是单独位于函数末了一行,大概与左大括号同一行。
  • 右圆括号和左大括号间总是有一个空格。
  • 所有形参应尽可能对齐。
  • 缺省缩进为 2 个空格。
  • 换行后的参数保持 4 个空格的缩进。
9.5 Lambda 表达式

Lambda 表达式对形参和函数体的格式化和其他函数一致; 捕捉列表同理,表项用逗号隔开。
若用引用捕捉,在变量名和 & 之间不留空格。
  1. int x = 0;
  2. auto add_to_x = [&x](int n) { x += n; };
复制代码
短 lambda 就写得和内联函数一样。
  1. std::set<int> blacklist = {7,8,9};
  2. std::vector<int> digits = {3,9,1,8,4,7,1};
  3. digits.erase(std::remove_if(digits.begin(),digits.end(),[&blacklist](int i) {
  4.                return blacklist.find(i) != blacklist.end();
  5.              }),
  6.              digits.end());
复制代码
9.6 函数调用

要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,好比把多个参数适本地放在同一行里。
函数调用遵照如下形式:
  1. bool retval = DoSomething(argument1,argument2,argument3);
复制代码
如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:
  1. bool retval = DoSomething(averyveryveryverylongargument1,
  2.                           argument2,argument3);
复制代码
参数也可以放在次行,缩进四格:
  1. if (...) {
  2.   ...
  3.   ...
  4.   if (...) {
  5.     DoSomething(
  6.         argument1,argument2, // 4 空格缩进
  7.         argument3,argument4);
  8.   }
复制代码
把多个参数放在同一行以减少函数调用所需的行数,除非影响到可读性。有人认为把每个参数都独立成行,不仅更好读,而且方便编辑参数。不过,比起所谓的参数编辑,我们更看重可读性,且后者比力好办:
如果一些参数本身就是略复杂的表达式,且低落了可读性,那么可以直接创建临时变量描述该表达式,并传递给函数:
  1. int my_heuristic = scores[x] * y + bases[x];
  2. bool retval = DoSomething(my_heuristic,x,y,z);
复制代码
大概放着不管,增补上解释:
  1. bool retval = DoSomething(scores[x] * y + bases[x], // Score heuristic.
  2.                           x,y,z);
复制代码
如果某参数独立成行,对可读性更有帮助的话,那也可以如此做。参数的格式处理惩罚应当以可读性而非其他作为最重要的原则。
此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式:
  1. // 通过 3x3 矩阵转换 widget.
  2. my_widget.Transform(x1,x2,x3,
  3.                     y1,y2,y3,
  4.                     z1,z2,z3);
复制代码
9.7 列表初始化格式

参照函数调用格式,若著名字则将名字视作函数调用名,{}视作括号,无名字则视为长度为零,断行时按相应规则处理惩罚。
  1. // 一行列表初始化示范.
  2. return {foo,bar};
  3. functioncall({foo,bar});
  4. pair<int,int> p{foo,bar};
  5. // 当不得不断行时.
  6. SomeFunction(
  7.     {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字.
  8.     some_other_function_parameter);
  9. SomeType variable{
  10.     some,other,values,
  11.     {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字.
  12.     SomeOtherType{
  13.         "Very long string requiring the surrounding breaks.", // 非常长的字符串,前后都需要断行.
  14.         some,other values},
  15.     SomeOtherType{"Slightly shorter string", // 稍短的字符串.
  16.                   some,other,values}};
  17. SomeType variable{
  18.     "This is too long to fit all in one line"};  // 字符串过长,因此无法放在同一行.
  19. MyType m = {  // 注意了,您可以在 { 前断行.
  20.     superlongvariablename1,
  21.     superlongvariablename2,
  22.     {short,interior,list},
  23.     {interiorwrappinglist,
  24.      interiorwrappinglist2}};
复制代码
9.8 条件语句

倾向于不在圆括号内使用空格。关键字 if 和 else 另起一行。
对根本条件语句有两种可以接受的格式。一种在圆括号和条件之间有空格,另一种没有。
最常见的是没有空格的格式。哪一种都可以,最重要的是 保持一致。如果你是在修改一个文件,参考当前已有格式。如果是写新的代码,参考目次下或项目中其它文件。还在犹豫的话,就不要加空格了。
  1. if (condition) {  // 圆括号里没有空格.
  2.   ...  // 2 空格缩进.
  3. } else if (...) {  // else 与 if 的右括号同一行.
  4.   ...
  5. } else {
  6.   ...
  7. }
复制代码
如果你更喜好在圆括号内部加空格:
  1. if ( condition ) {  // 圆括号与空格紧邻 - 不常见
  2.   ...  // 2 空格缩进.
  3. } else {  // else 与 if 的右括号同一行.
  4.   ...
  5. }
复制代码
注意所有情况下 if 和左圆括号间都有个空格。右圆括号和左大括号之间也要有个空格:
  1. if(condition)     // 差 - IF 后面没空格.
  2. if (condition){   // 差 - { 前面没空格.
  3. if(condition){    // 变本加厉地差.
  4. if (condition) {  // 好 - IF 和 { 都与空格紧邻.
复制代码
如果能加强可读性,简短的条件语句允许写在同一行。只有当语句简单而且没有使用 else 子句时使用:
  1. if (x == kFoo) return new Foo();
  2. if (x == kBar) return new Bar();
复制代码
如果语句有 else 分支则不允许:
  1. // 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
  2. if (x) DoThis();
  3. else DoThat();
复制代码
通常,单行语句不必要使用大括号,如果你喜好用也没题目; 复杂的条件或循环语句用大括号可读性会更好。也有一些项目要求 if 必须总是使用大括号:
  1. if (condition)
  2.   DoSomething();  // 2 空格缩进.
  3. if (condition) {
  4.   DoSomething();  // 2 空格缩进.
  5. }
复制代码
但如果语句中某个 if-else 分支使用了大括号的话,其它分支也必须使用:
  1. // 不可以这样子 - IF 有大括号 ELSE 却没有.
  2. if (condition) {
  3.   foo;
  4. } else
  5.   bar;
  6. // 不可以这样子 - ELSE 有大括号 IF 却没有.
  7. if (condition)
  8.   foo;
  9. else {
  10.   bar;
  11. }
  12. // 只要其中一个分支用了大括号,两个分支都要用上大括号.
  13. if (condition) {
  14.   foo;
  15. } else {
  16.   bar;
  17. }
复制代码
9.9 循环和开关选择语句

switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {} 或 continue。
switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。
如果有不满意 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理惩罚,编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert:
  1. switch (var) {
  2.   case 0: {  // 2 空格缩进
  3.     ...      // 4 空格缩进
  4.     break;
  5.   }
  6.   case 1: {
  7.     ...
  8.     break;
  9.   }
  10.   default: {
  11.     assert(false);
  12.   }
  13. }
复制代码
在单语句循环里,括号可用可不用:
  1. for (int i = 0; i < kSomeNumber; ++i)
  2.   printf("I love you\n");
  3. for (int i = 0; i < kSomeNumber; ++i) {
  4.   printf("I take it back\n");
  5. }
复制代码
空循环体应使用 {} 或 continue,而不是一个简单的分号.
  1. while (condition) {
  2.   // 反复循环直到条件失效.
  3. }
  4. for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循环体.
  5. while (condition) continue;  // 可 - contunue 表明没有逻辑.
  6. while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一.
复制代码
9.10 指针和引用表达式

句点或箭头前后不要有空格。指针/地址操作符 (*,&) 之后不能有空格。
下面是指针和引用表达式的正确使用范例:
  1. x = *p;
  2. p = &x;
  3. x = r.y;
  4. x = r->y;
复制代码
注意:


  • 在访问成员时,句点或箭头前后没有空格。
  • 指针操作符 * 或 & 后没有空格。
在声明指针变量或参数时,星号与类型或变量名紧挨都可以:
  1. // 好,空格前置.
  2. char *c;
  3. const string &str;
  4. // 好,空格后置.
  5. char* c;
  6. const string& str;
  7. int x,*y;  // 不允许 - 在多重声明中不能使用 & 或 *
  8. char * c;  // 差 - * 两边都有空格
  9. const string & str;  // 差 - & 两边都有空格.
复制代码
在单个文件内要保持风格一致,以是,如果是修改现有文件,要遵照该文件的风格。
9.11 布尔表达式

如果一个布尔表达式超过尺度行宽,断行方式要同一一下。
下例中,逻辑与 (&&) 操作符总位于行尾:
  1. if (this_one_thing > this_other_thing &&
  2.     a_third_thing == a_fourth_thing &&
  3.     yet_another && last_one) {
  4.   ...
  5. }
复制代码
注意,上例的逻辑与 (&&) 操作符均位于行尾。这个格式在 Google 里很常见,固然把所有操作符放在开头也可以。可以考虑额外插入圆括号,公道使用的话对加强可读性是很有帮助的。此外,直接用符号形式的操作符,好比 && 和 ~,不要用词语形式的 and 和 compl。
9.12 函数返回值

不要在 return 表达式里加上非必须的圆括号。
只有在写 x = expr 要加上括号的时间才在 return expr; 里使用括号。
  1. return result;                  // 返回值很简单,没有圆括号.
  2. // 可以用圆括号把复杂表达式圈起来,改善可读性.
  3. return (some_long_condition &&
  4.         another_condition);
  5. return (value);                // 毕竟您从来不会写 var = (value);
  6. return(result);                // return 可不是函数!
复制代码
9.13 变量及数组初始化

用 =,() 和 {} 均可。
您可以用 =,() 和 {},以下的例子都是正确的:
  1. int x = 3;
  2. int x(3);
  3. int x{3};
  4. string name("Some Name");
  5. string name = "Some Name";
  6. string name{"Some Name"};
复制代码
请务必小心列表初始化 {...} 用 std::initializer_list 构造函数初始化出的类型。非空列表初始化就会优先调用 std::initializer_list,不过空列表初始化除外,后者原则上会调用默认构造函数。为了逼迫禁用 std::initializer_list 构造函数,请改用括号。
  1. vector<int> v(100,1);  // 内容为 100 个 1 的向量.
  2. vector<int> v{100,1};  // 内容为 100 和 1 的向量.
复制代码
此外,列表初始化不允许整型类型的四舍五入,这可以用来制止一些类型上的编程失误。
  1. int pi(3.14);  // 好 - pi == 3.
  2. int pi{3.14};  // 编译错误: 缩窄转换.
复制代码
9.14 预处理惩罚指令

预处理惩罚指令不要缩进,从行首开始。
即使预处理惩罚指令位于缩进代码块中,指令也应从行首开始。
  1. // 好 - 指令从行首开始
  2.   if (lopsided_score) {
  3. #if DISASTER_PENDING      // 正确 - 从行首开始
  4.     DropEverything();
  5. # if NOTIFY               // 非必要 - # 后跟空格
  6.     NotifyClient();
  7. # endif
  8. #endif
  9.     BackToNormal();
  10.   }
  11. // 差 - 指令缩进
  12.   if (lopsided_score) {
  13.     #if DISASTER_PENDING  // 差 - "#if" 应该放在行开头
  14.     DropEverything();
  15.     #endif                // 差 - "#endif" 不要缩进
  16.     BackToNormal();
  17.   }
复制代码
9.15 类格式

访问控制块的声明依序次是 public:,protected:,private:,每个都缩进 1 个空格。
类声明的根本格式如下:
  1. class MyClass : public OtherClass {
  2. public:      // 注意有一个空格的缩进
  3.   MyClass();  // 标准的两空格缩进
  4.   explicit MyClass(int var);
  5.   ~MyClass() {}
  6.   void SomeFunction();
  7.   void SomeFunctionThatDoesNothing() {
  8.   }
  9.   void set_some_var(int var) { some_var_ = var; }
  10.   int some_var() const { return some_var_; }
  11. private:
  12.   bool SomeInternalFunction();
  13.   int some_var_;
  14.   int some_other_var_;
  15. };
复制代码
注意事项:


  • 所有基类名应在 80 列限制下尽量与子类名放在同一行。
  • 关键词 public:,protected:,private: 要缩进 1 个空格。
  • 除第一个关键词 (一般是 public) 外,其他关键词前要空一行。如果类比力小的话也可以不空。
  • 这些关键词后不要保留空行。
  • public 放在最前面,然后是 protected,末了是 private。
9.16 构造函数初始值列表

构造函数初始化列表放在同一行或按四格缩进并排多行。
下面两种初始值列表方式都可以接受:
  1. // 如果所有变量能放在同一行:
  2. MyClass::MyClass(int var) : some_var_(var) {
  3.   DoSomething();
  4. }
  5. // 如果不能放在同一行,
  6. // 必须置于冒号后,并缩进 4 个空格
  7. MyClass::MyClass(int var)
  8.     : some_var_(var),some_other_var_(var + 1) {
  9.   DoSomething();
  10. }
  11. // 如果初始化列表需要置于多行,将每一个成员放在单独的一行
  12. // 并逐行对齐
  13. MyClass::MyClass(int var)
  14.     : some_var_(var),            // 4 space indent
  15.       some_other_var_(var + 1) {  // lined up
  16.   DoSomething();
  17. }
  18. // 右大括号 } 可以和左大括号 { 放在同一行
  19. // 如果这样做合适的话
  20. MyClass::MyClass(int var)
  21.     : some_var_(var) {}
复制代码
9.17 命名空间格式化

命名空间内容不缩进。
命名空间不要增加额外的缩进层次,例如:
  1. namespace {
  2. void foo() {  // 正确. 命名空间内没有额外的缩进.
  3.   ...
  4. }
  5. }  // namespace
复制代码
不要在命名空间内缩进:
  1. namespace {
  2.   // 错,缩进多余了.
  3.   void foo() {
  4.     ...
  5.   }
  6. }  // namespace
复制代码
声明嵌套命名空间时,每个命名空间都独立成行.
  1. namespace foo {
  2. namespace bar {
复制代码
9.18 水平留白

水平留白的使用根据在代码中的位置决定。永远不要在行尾添加没意义的留白。
  1. void f(bool b) {  // 左大括号前总是有空格.
  2.   ...
  3. int i = 0;  // 分号前不加空格.
  4. // 列表初始化中大括号内的空格是可选的.
  5. // 如果加了空格,那么两边都要加上.
  6. int x[] = { 0 };
  7. int x[] = {0};
  8. // 继承与初始化列表中的冒号前后恒有空格.
  9. class Foo : public Bar {
  10. public:
  11.   // 对于单行函数的实现,在大括号内加上空格
  12.   // 然后是函数实现
  13.   Foo(int b) : Bar(),baz_(b) {}  // 大括号里面是空的话,不加空格.
  14.   void Reset() { baz_ = 0; }  // 用空格把大括号与实现分开.
  15.   ...
复制代码
添加冗余的留白会给其他人编辑时造成额外负担。因此,行尾不要留空格。如果确定一行代码已经修改完毕,将多余的空格去掉; 大概在专门清算空格时去掉(尤其是在没有其他人在处理惩罚这件事的时间)。
循环和条件语句
  1. if (b) {          // if 条件语句和循环语句关键字后均有空格.
  2. } else {          // else 前后有空格.
  3. }
  4. while (test) {}   // 圆括号内部不紧邻空格.
  5. switch (i) {
  6. for (int i = 0; i < 5; ++i) {
  7. switch ( i ) {    // 循环和条件语句的圆括号里可以与空格紧邻.
  8. if ( test ) {     // 圆括号,但这很少见. 总之要一致.
  9. for ( int i = 0; i < 5; ++i ) {
  10. for ( ; i < 5 ; ++i) {  // 循环里内 ; 后恒有空格,;  前可以加个空格.
  11. switch (i) {
  12.   case 1:         // switch case 的冒号前无空格.
  13.     ...
  14.   case 2: break;  // 如果冒号有代码,加个空格.
复制代码
操作符
  1. // 赋值运算符前后总是有空格.
  2. x = 0;
  3. // 其它二元操作符也前后恒有空格,不过对于表达式的子式可以不加空格.
  4. // 圆括号内部没有紧邻空格.
  5. v = w * x + y / z;
  6. v = w*x + y/z;
  7. v = w * (x + z);
  8. // 在参数和一元操作符之间不加空格.
  9. x = -5;
  10. ++x;
  11. if (x && !y)
  12.   ...
复制代码
模板和转换
  1. // 尖括号(< and >) 不与空格紧邻,< 前没有空格,> 和 ( 之间也没有.
  2. vector<string> x;
  3. y = static_cast<char*>(x);
  4. // 在类型与指针操作符之间留空格也可以,但要保持一致.
  5. vector<char *> x;
复制代码
9.19 垂直留白

垂直留白越少越好。
这不仅仅是规则而是原则题目了: 不在万不得已,不要使用空行. 尤其是:两个函数界说之间的空行不要超过 2 行,函数体首尾不要留空行,函数体中也不要随意添加空行。
根本原则是: 同一屏可以表现的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样丢脸,这取决于你的判断。但通常是垂直留白越少越好。
下面的规则可以让参加的空行更有效:


  • 函数体内开头或结尾的空行可读性微乎其微。
  • 在多重 if-else 块里加空行大概有点可读性。



namespace foo {
namespace bar {
  1. ## 9.18 水平留白水平留白的使用根据在代码中的位置决定。永远不要在行尾添加没意义的留白。```c++void f(bool b) {  // 左大括号前总是有空格.
  2.   ...
  3. int i = 0;  // 分号前不加空格.
  4. // 列表初始化中大括号内的空格是可选的.
  5. // 如果加了空格,那么两边都要加上.
  6. int x[] = { 0 };
  7. int x[] = {0};
  8. // 继承与初始化列表中的冒号前后恒有空格.
  9. class Foo : public Bar {
  10. public:
  11.   // 对于单行函数的实现,在大括号内加上空格
  12.   // 然后是函数实现
  13.   Foo(int b) : Bar(),baz_(b) {}  // 大括号里面是空的话,不加空格.
  14.   void Reset() { baz_ = 0; }  // 用空格把大括号与实现分开.
  15.   ...
复制代码
添加冗余的留白会给其他人编辑时造成额外负担。因此,行尾不要留空格。如果确定一行代码已经修改完毕,将多余的空格去掉; 大概在专门清算空格时去掉(尤其是在没有其他人在处理惩罚这件事的时间)。
循环和条件语句
  1. if (b) {          // if 条件语句和循环语句关键字后均有空格.
  2. } else {          // else 前后有空格.
  3. }
  4. while (test) {}   // 圆括号内部不紧邻空格.
  5. switch (i) {
  6. for (int i = 0; i < 5; ++i) {
  7. switch ( i ) {    // 循环和条件语句的圆括号里可以与空格紧邻.
  8. if ( test ) {     // 圆括号,但这很少见. 总之要一致.
  9. for ( int i = 0; i < 5; ++i ) {
  10. for ( ; i < 5 ; ++i) {  // 循环里内 ; 后恒有空格,;  前可以加个空格.
  11. switch (i) {
  12.   case 1:         // switch case 的冒号前无空格.
  13.     ...
  14.   case 2: break;  // 如果冒号有代码,加个空格.
复制代码
操作符
  1. // 赋值运算符前后总是有空格.
  2. x = 0;
  3. // 其它二元操作符也前后恒有空格,不过对于表达式的子式可以不加空格.
  4. // 圆括号内部没有紧邻空格.
  5. v = w * x + y / z;
  6. v = w*x + y/z;
  7. v = w * (x + z);
  8. // 在参数和一元操作符之间不加空格.
  9. x = -5;
  10. ++x;
  11. if (x && !y)
  12.   ...
复制代码
模板和转换
  1. // 尖括号(< and >) 不与空格紧邻,< 前没有空格,> 和 ( 之间也没有.
  2. vector<string> x;
  3. y = static_cast<char*>(x);
  4. // 在类型与指针操作符之间留空格也可以,但要保持一致.
  5. vector<char *> x;
复制代码
9.19 垂直留白

垂直留白越少越好。
这不仅仅是规则而是原则题目了: 不在万不得已,不要使用空行. 尤其是:两个函数界说之间的空行不要超过 2 行,函数体首尾不要留空行,函数体中也不要随意添加空行。
根本原则是: 同一屏可以表现的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样丢脸,这取决于你的判断。但通常是垂直留白越少越好。
下面的规则可以让参加的空行更有效:


  • 函数体内开头或结尾的空行可读性微乎其微。
  • 在多重 if-else 块里加空行大概有点可读性。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表