【总结】Google Cpp 编程规范学习

Posted on Posted in 计算机2,009 views

一、学习笔记

1.1、头文件

    1、每一个.cc文件都有一个对应的.h文件,(单元测试和只包含main函数的.cc文件除外)。

    2、Self-contained头文件:可以作为第一个头文件被引入的头文件,以.h结尾;至于用来插入文本的文件,说到底它们并不是头文件,所以应以.inc结尾。

    3、如果.h文件声明了一个模板或内联函数,同时也要在该文件加以定义。

    4、#define保护:所有头文件都应该使用#define来防止头文件被多重包含,命名格式当是: <PROJECT>_<PATH>_<FILE>_H_,如:

//  foo/src/bar/baz.h
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_

    5、前置声明:是类、函数和模板的纯粹声明,没伴随着其定义。应尽可能地避免使用。

    6、内联函数:只有当函数只有10行甚至更少时才将其定义为内联函数。当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联。

    7、#include 的路径及顺序

        使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖。顺序如下:

        相关头文件

        C库

        C++库

        其他库的.h

        本项目内的.h

        项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录)。

        有时,平台特定代码需要条件编译,这些代码可以放到其它 includes 之后,整体结构如下:

#include "foo/public/fooserver.h"  // 优先位置
 
#include <sys/types.h>
#include <unistd.h>
 
#include <hash_map>
#include <vector>
 
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
 
#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

1.2、作用域

    1、鼓励在.cc文件内使用匿名名字空间。使用具名名字空间时,可有效防止全局作用域的命名冲突。其名称可基于项目名或相对路径。禁止使用using指示。禁止使用内联命名空间(inline namespace)。

    2、匿名名字空间

        在.cc文件中,允许甚至鼓励使用匿名名字空间,以避免运行时的命名冲突。 匿名空间结束时用注释 // namespace 标识。如下:

namespace {                               // .cc 文件中
// 名字空间的内容无需缩进
enum { kUNUSED, kEOF, kERROR };        // 经常使用的符号
bool AtEof() { return pos_ == kEOF; }   // 使用本名字空间内的符号 EOF
} // namespace

        不要在 .h 文件中使用匿名名字空间。导致违背 C++ 的唯一定义原则。

    3、具名名字空间

// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
    public:
    …
    void Foo();
};

} // namespace mynamespace


// .cc 文件
#include “a.h”

DEFINE_bool(someflag, false, “dummy flag”);

class C;                    // 全局名字空间中类 C 的前置声明
namespace a { class A; }    // a::A 的前置声明

namespace b {

…code for b…                // b 中的代码

} // namespace b

        不要在名字空间std内声明任何东西, 包括标准库的类前置声明。

        最好不要使用 using 指示,以保证名字空间下的所有名称都可以正常使用–污染名字空间。在.cc文件和.h 文件的函数、方法或类内部,可以使用using声明。

        在.cc文件,.h文件的函数,方法或类中,允许使用名字空间别名:

// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用

namespace fbz = ::foo::bar::baz;

// 在 .h 文件里
namespace librarian {
//以下别名在所有包含了该头文件的文件中生效。
namespace pd_s = ::pipeline_diagnostics::sidetable;
inline void my_inline_function() {
  // namespace alias local to a function (or method).
  namespace fbz = ::foo::bar::baz;
  ...
  }
}  // namespace librarian

        注意在 .h 文件的别名对包含了该头文件的所有人可见,所以在公共头文件(在项目外可用)以及它们递归包含的其它头文件里,不要用别名。

    4、嵌套类

        在一个类内部定义另一个类; 嵌套类也被称为 成员类 (member class)。

class Foo {

private:
    // Bar是嵌套在Foo中的成员类
    class Bar {
        …
    };

};

        当嵌套 (或成员) 类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 .cc 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关。

        不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项。

    5、非成员函数、静态成员函数和全局函数

        某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域。

        有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的。但是相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用:ref:namespaces。

        如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名:ref:namespaces`或 “static` 链接关键字 (如 static int Foo() {…}) 限定其作用域。

    6、局部变量

        将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化。

        如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数,所以在循环作用域外面声明这类变量要高效的多。

    7、静态和全局变量

        禁止使用 class 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。

        静态生存周期的对象,包括了全局变量,静态变量,静态类成员变量和函数静态变量。

        静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是不确定的,甚至随着构建变化而变化。不过析构顺序正好与构造函数调用的顺序相反。

        改善析构问题的办法之一是用 quick_exit() 来代替 exit() 并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers。

        我们只允许POD(原生数据类型:int、char和float,以及POD类型的指针、数组和结构体)类型的静态变量,即完全禁用vector(使用C数组替代)和string(使用const char[])。

        如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

1.3、类

    1、构造函数的职责

        不要在构造函数中进行复杂的初始化 (尤其是那些有可能失败或者需要调用虚函数的初始化。

        构造函数中很难上报错误, 不能使用异常。操作失败会造成对象初始化失败,进入不确定状态。

        如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患。

    2、初始化

        new 一个不带参数的类对象时, 会调用这个类的默认构造函数. 用 new[] 创建数组时, 默认构造函数则总是被调用。

        在类成员里面进行初始化是指声明一个成员变量的时候使用一个结构例如 int _count = 17 或者 string _name{"abc"} 来替代 int _count 或者 string _name 这样的形式。

    3、显式构造函数

        对单个参数的构造函数使用 C++ 关键字 explicit。

explicit Foo(string name);

        通常, 如果构造函数只有一个参数,可看成是一种隐式转换。比如你定义了 Foo::Foo(string name),接着把一个字符串传给一个以Foo对象为参数的函数,构造函数 Foo::Foo(string name)将被调用,并将该字符串转换为一个 Foo的临时对象传给调用函数。

        拷贝构造函数可以不声明成 explicit. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明。

        只有 std::initializer_list 的构造函数可以是非 explicit, 以允许你的类型结构可以使用列表初始化的方式进行赋值. 例如:

MyType m = {1, 2};
MyType MakeMyType() 
{ 
    return {1, 2}; 
}
TakeMyType({1, 2});

    4、可拷贝类型和可移动类型

        可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值。

        可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值。所有可拷贝对象也是可移动的。

        如果你的类型需要, 就让它们支持拷贝 / 移动,建议通过 = default 定义拷贝和移动操作。否则, 就把隐式产生的拷贝和移动函数禁用,请显式地通过 = delete 或其他手段禁用之。

    5、委派和继承构造函数

        委派和继承构造函数是由 C++11 引进为了减少构造函数重复代码而开发的两种不同的特性. 通过特殊的初始化列表语法, 委派构造函数允许类的一个构造函数调用其他的构造函数。继承构造函数允许派生类直接调用基类的构造函数, 一如继承基类的其他成员函数, 而无需重新声明. 当基类拥有多个构造函数时这一功能尤其有用。

        在能够减少重复代码的情况下使用委派和继承构造函数。

    6、结构体 VS. 类

        仅当只有数据时使用 struct, 其它一概使用 class。

        如果需要更多的函数功能, class 更适合。如果拿不准, 就用 class。

        为了和 STL 保持一致, 对于仿函数和 trait 特性可以不用 class 而是使用 struct。

    7、继承

        使用组合 (composition, YuleFox 注: 这一点也是 GoF 在 <<Design Patterns>> 里反复强调的) 常常比使用继承更合理。如果使用继承的话, 定义为 public 继承。

        所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式。

        不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个” (“is-a”, YuleFox 注: 其他 “has-a” 情况下请使用组合) 的情况下使用继承。

        必要的话, 析构函数声明为 virtual,如果你的类有虚函数, 则析构函数也应该为虚函数。

        当重载一个虚函数, 在衍生类中把它明确的声明为 virtual. 理论依据: 如果省略 virtual 关键字,代码阅读者不得不检查所有父类,以判断该函数是否是虚函数。

    8、多重继承

        只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 Interface 为后缀。

    9、接口

        接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制)。

        当一个类满足以下要求时, 称之为纯接口: 

            只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数). 

            没有非静态数据成员. 

            没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected. 

            如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.

        接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数。

    10、运算符重载

        除少数特定环境外,不要重载运算符。

        尤其是赋值操作 (operator=) 比较诡异, 应避免重载. 如果需要的话, 可以定义类似 Equals(), CopyFrom() 等函数。

        有些 STL 算法确实需要重载 operator== 或 operator< 时, 你可以这么做, 记得别忘了在文档中说明原因。

    11、存取控制

        将 所有 数据成员声明为 private, 并根据需要提供相应的存取函数。

        特例是, 静态常量数据成员 (一般写做 kFoo) 不需要是私有成员。

        一般在头文件中把存取函数定义成内联函数。

    12、声明顺序

        类的访问控制区段的声明顺序依次为: public:, protected:, private:。

        成员函数在数据成员 (变量) 前。

        每个区段内的声明通常按以下顺序: 

            typedefs 和枚举 

            常量 

            构造函数 

            析构函数 

            成员函数, 含静态成员函数 

            数据成员, 含静态数据成员

        友元声明应该放在 private 区段. 如果用宏 DISALLOW_COPY_AND_ASSIGN 禁用拷贝和赋值, 应当将其置于 private 区段的末尾, 也即整个类声明的末尾。

        .cc 文件中函数的定义应尽可能和声明顺序一致。

    13、编写简短函数

        如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割。

    14、译者 (YuleFox) 笔记

 不在构造函数中做太多逻辑相关的初始化;
 编译器提供的默认构造函数不会对变量进行初始化, 如果定义了其他构造函数, 编译器不再提供, 需要编码者自行提供默认构造函数;
 为避免隐式转换, 需将单参数构造函数声明为 explicit;
 为避免拷贝构造函数, 赋值操作的滥用和编译器自动生成, 可将其声明为 private 且无需实现;
 仅在作为数据集合时使用 struct;
 组合 > 实现继承 > 接口继承 > 私有继承, 子类重载的虚函数也要声明 virtual 关键字, 虽然编译器允许不这样做;
 避免使用多重继承, 使用时, 除一个基类含有实现外, 其他基类均为纯接口;
 接口类类名以 Interface 为后缀, 除提供带实现的虚析构函数, 静态成员函数外, 其他均为纯虚函数, 不定义非静态数据成员, 不提供构造函数, 提供的话,声明为 protected;
 为降低复杂性, 尽量不重载操作符, 模板, 标准类中使用时提供文档说明;
 存取函数一般内联在头文件中;
 声明次序: public -> protected -> private;
 函数体尽量短小, 紧凑, 功能单一;

1.4、来自 Google 的奇技

    1、所有权与智能指针

        动态分配出的对象最好有单一且固定的所有主(onwer), 且通过智能指针传递所有权(ownership)。

        如果必须使用动态分配,倾向于保持分配者的所有权。如果其他地方要使用这个对象,最好传递它的拷贝,或者传递一个不用改变所有权的指针或引用。倾向于使用 std::unique_ptr 来明确所有权传递,例如:

std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);

    2、cpplint

        使用 cpplint.py 检查风格错误。参考第三节。

1.5、其他 C++ 特性

    1、引用参数

        所有按引用传递的参数必须加上 const。

        输入参数是值参或const引用, 输出参数为指针。输入参数可以是const指针, 但决不能是非const的引用参数,除非用于交换,比如 swap()。

        有时候,在输入形参中用const T*指针比const T&更明智,比如:您会传null指针;函数要把指针或对地址的引用赋值给输入形参。

        总之大多时候输入形参往往是 const T&。若用 const T* 说明输入另有处理。所以若您要用 const T*, 则应有理有据,否则会害得读者误解。

    2、右值引用?

        只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用 std::forward 功能函数. 你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象。

    3、函数重载

         若要用好函数重载,最好能让读者一看调用点(call site)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。该规则适用于构造函数。

        如果您打算重载一个函数, 可以试试改在函数名里加上参数信息。例如,用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append()。

    4、缺省参数

        我们不允许使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。

        由于缺点并不是很严重,有些人依旧偏爱缺省参数胜于函数重载。所以除了以下情况,我们要求必须显式提供所有参数:

            其一,位于 .cc 文件里的静态函数或匿名空间函数,毕竟都只能在局部文件里调用该函数了。 

            其二,可以在构造函数里用缺省参数,毕竟不可能取得它们的地址。 

            其三,可以用来模拟变长数组。

    5、变长数组和 alloca()

        不允许使用变长数组和 alloca(),改用更安全的分配器(allocator),就像 std::vector 或 std::unique_ptr<T[]>。

    6、友元

        通常友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类。

        经常用到友元的一个地方是将 FooBuilder 声明为 Foo 的友元, 以便 FooBuilder 正确构造 Foo 的内部状态, 而无需将该状态暴露出来。

        某些情况下, 将一个单元测试类声明成待测类的友元会很方便。

        相对于将类成员声明为 public, 使用友元是更好的选择。

    7、异常

        Google不使用 C++ 异常。

        从表面上看来,使用异常利大于弊, 尤其是在新项目中. 但是对于现有代码, 引入异常会牵连到所有相关代码。

    8、运行时类型识别

        RTTI 允许程序员在运行时识别 C++ 类对象的类型. 它通过使用 typeid 或者 dynamic_cast 完成。

        在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免. 尤其是在新代码中, 使用 RTTI 前务必三思。

        两种替代方案:

            虚函数可以根据子类类型的不同而执行不同代码. 这是把工作交给了对象本身去处理。

            如果这一工作需要在对象之外完成, 可以考虑使用双重分发的方案, 例如使用访问者设计模式. 这就能够在对象之外进行类型判断。

    9、类型转换

        使用 C++ 的类型转换:

            用 static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时。

            用 const_cast 去掉 const 限定符。

            用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换,仅在你对所做一切了然于心时使用。

    10、

        流用来替代 printf() 和 scanf()。

        只在记录日志时使用流。

        流使得 pread() 等功能函数很难执行。

    11、前置自增和自减

        对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符。

        自己补充,尽量使用++i,除了要获取自加之前的值采用i++。

    12、const 用法

        强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。

        在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如 const int foo )。

        为类中的函数加上 const 限定符表明该函数不会修改类成员变量的状态 (如 class Foo { int Bar(char c) const; };)。

    13、constexpr 用法

        在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。

    14、整型

        C++ 内建整型中,仅使用int. 如果程序中需要不同大小的变量,可以使用 <stdint.h> 中长度精确的整型,如 int16_t、uint32_t、int64_t。此外要留意,哪怕您的值并不会超出 int 所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。

        不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补码溢出。

    15、64 位下的可移植性

        代码应该对 64 位和 32 位系统友好. 处理打印、比较、结构体对齐时应切记。

        对于某些类型,printf()的指示符在32位和64位系统上可移植性不是很好,有时需要定义丑陋的版本 (inttypes.h仿标准风格):

// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif

// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);
#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"

        记住 sizeof(void *) != sizeof(int). 如果需要一个指针大小的整数要用 intptr_t。

        如果你确实需要 32 位和 64 位系统具有不同代码, 可以使用 #ifdef _LP64 指令来切分 32/64 位代码. (尽量不要这么做, 如果非用不可, 尽量使修改局部化)

    16、预处理宏

        使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之。

        如果你要宏, 尽可能遵守:

            不要在 .h 文件中定义宏.

            在马上要使用时才进行 #define, 使用后要立即 #undef.

            不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;

            不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.

            不要用 ## 处理函数,类和变量的名字。

    17、nullptr NULL 0

        整数用 0, 实数用 0.0, 指针用 nullptr(C++11) 或 NULL, 字符 (串) 用 '\0'

    18、sizeof

        尽可能用 sizeof(varname) 代替 sizeof(type),是因为当代码中变量类型改变时会自动更新。

    19、auto(C++11)

        用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。

        auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。

    20、列表初始化

        早在 C++03 里,聚合类型(aggregate types)就已经可以被列表初始化了,比如数组和不自带构造函数的结构体:

struct Point { int x; int y; };
Point p = {1, 2};

        C++11 中,该特性得到进一步的推广,任何对象类型都可以被列表初始化。示范如下:

// Vector 接收了一个初始化列表。
vector<string> v{"foo", "bar"};

// 不考虑细节上的微妙差别,大致上相同。
// 您可以任选其一。
vector<string> v = {"foo", "bar"};

// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};

// map 接收了一些 pair, 列表初始化大显神威。
map<int, string> m = {{1, "one"}, {2, "2"}};

// 初始化列表也可以用在返回类型上的隐式转换。
vector<int> test_function() { return {1, 2, 3}; }

// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}

// 在函数调用里用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});

        用户自定义类型也可以定义接收 std::initializer_list<T> 的构造函数和赋值运算符,以自动列表初始化:

class MyType {
 public:
  // std::initializer_list 专门接收 init 列表。
  // 得以值传递。
  MyType(std::initializer_list<int> init_list) {
    for (int i : init_list) append(i);
  }
  MyType& operator=(std::initializer_list<int> init_list) {
    clear();
    for (int i : init_list) append(i);
  }
};
MyType m{2, 3, 5, 7};

        最后,列表初始化也适用于常规数据类型的构造,哪怕没有接收 std::initializer_list<T> 的构造函数。

double d{1.23};
// MyOtherType 没有 std::initializer_list 构造函数,
 // 直接上接收常规类型的构造函数。
class MyOtherType {
 public:
  explicit MyOtherType(string);
  MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不过如果构造函数是显式的(explict),您就不能用 `= {}` 了。
MyOtherType m{"b"};

        千万别直接列表初始化 auto 变量。

    21、Lambda 表达式

        C++11 首次提出 Lambdas,Lambda 表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传,例如:

std::sort(v.begin(), v.end(), [](int x, int y) {
    return Weight(x) < Weight(y);
});

        适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。

    22、模板元编程

    23、Boost 库

        Boost 库集 是一个广受欢迎, 经过同行鉴定, 免费开源的 C++ 库集。

        只使用 Boost 中被认可的库。

    24、C++11

        适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。

1.6、命名约定

    1、通用命名规则

        函数命名,变量命名,文件命名要有描述性;少用缩写。

int price_count_reader;    // 无缩写
int num_errors;            // “num” 本来就很常见
int num_dns_connections;   // 人人都知道 “DNS” 是啥

    2、文件命名

        文件名要全部小写, 可以包含下划线 (_) 或连字符 (-). 按项目约定来. 如果并没有项目约定,”_” 更好。

* my_useful_class.cc
* my-useful-class.cc
* myusefulclass.cc
* muusefulclass_test.cc // ``_unittest`` 和 ``_regtest`` 已弃用。

    3、类型命名

        类型(类、结构体、类型定义[typedef]、枚举)名称的每个单词首字母均大写, 不包含下划线。【个人倾向struct首字母加个S、枚举加E、类型加T】

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// enums
enum UrlTableErrors { ...

    4、变量命名

        变量名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用。

// 普通变量命名
string table_name;  // 可 - 用下划线。
string tablename;   // 可 - 全小写。

// 类数据成员:不管是静态的还是非静态的,类数据成员都可以和普通变量一样, 但要接下划线。
class TableInfo {
  ...
 private:
  string table_name_;  // 可 - 尾后加下划线。
  string tablename_;   // 可。
  static Pool<TableInfo>* pool_;  // 可。
};

// 结构体变量:不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样, 不用像类那样接下划线
struct UrlTableProperties {
    string name;
    int num_entries;
}

// 全局变量:可以用 g_ 或其它标志作为前缀

    5、常量命名

        在全局或类里的常量名称前加 k: kDaysInAWeek. 且除去开头的 k 之外每个单词开头字母均大写。 所有编译时常量, 无论是局部的, 全局的还是类中的, 和其他变量稍微区别一下. k 后接大写字母开头的单词

const int kDaysInAWeek = 7;

    6、函数命名

        常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配

// 常规函数:函数名的每个单词首字母大写, 没有下划线。
AddTableEntry()
DeleteUrl()
OpenFileOrDie()

// 取值和设值函数:取值(Accessors)和设值(Mutators)函数要与存取的变量名匹配
class MyClass {
    public:
        ...
        int num_entries() const { return num_entries_; }
        void set_num_entries(int num_entries) { num_entries_ = num_entries; }

    private:
        int num_entries_;
};

// 其它非常短小的内联函数名也可以用小写字母, 例如. 如果你在循环中调用这样的函数甚至都不用缓存其返回值, 小写命名就可以接受.

    7、名字空间命名

        名字空间用小写字母命名, 并基于项目名称和目录结构。

google_awesome_project

    8、枚举命名

        枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME.

// 单独的枚举值应该优先采用 常量 的命名方式. 但 宏 方式的命名也可以接受.
enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
    OK = 0,
    OUT_OF_MEMORY = 1,
    MALFORMED_INPUT = 2,
};

    9、宏命名

        通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线。

#define ROUND(x) ...
#define PI_ROUNDED 3.0

    10、命名规则的特例

        如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略。

1.7、注释

    1、注释风格

        // 或 /* */ 都可以; 但 // 更 常用. 要在如何注释及注释风格上确保统一。

    2、文件注释

        每个文件都应该包含以下项, 依次是:

            版权声明 (比如, Copyright 2008 Google Inc.)

            许可证. 为项目选择合适的许可证版本 (比如, Apache 2.0, BSD, LGPL, GPL)

            作者: 标识文件的原始作者。

    3、类注释

        如果类有任何同步前提, 文档说明之. 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用。

// Iterates over the contents of a GargantuanTable.  Sample usage:
//    GargantuanTable_Iterator* iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
//    delete iter;
class GargantuanTable_Iterator {
    ...
};

    4、函数注释

        函数声明处注释的内容:

            函数的输入输出.

            对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.

            如果函数分配了空间, 需要由调用者释放.

            参数是否可以为 NULL.

            是否存在函数使用上的性能隐患.

            如果函数是可重入的, 其同步前提是什么?

        注释构造/析构函数时, 切记读代码的人知道构造/析构函数是干啥的。

        每个函数定义时要用注释说明函数功能和实现要点. 比如说说你用的编程技巧, 实现的大致步骤, 或解释如此实现的理由, 为什么前半部分要加锁而后半部分不需要。

    5、变量注释

        每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明。

        所有全局变量也要注释说明含义及用途。

    6、实现注释

        代码前注释:巧妙或复杂的代码段前要加注释。

        行注释: 比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释。

        注意 永远不要 用自然语言翻译代码作为注释。

    7、标点, 拼写和语法

    8、TODO 注释

        对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释。

// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.

    9、弃用注释

        通过弃用注释(DEPRECATED comments)以标记某接口点(interface points)已弃用。

1.8、格式

    1、行长度

        每一行代码字符数不超过 80,特例:

            如果一行注释包含了超过 80 字符的命令或 URL, 出于复制粘贴的方便允许该行超过 80 字符. 

            包含长路径的 #include 语句可以超出80列. 但应该尽量避免. 

            头文件保护 可以无视该原则.

    2、非 ASCII 字符

        尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码。

        "\xEF\xBB\xBF" 通常用作 UTF-8 with BOM 编码标记。

    3、空格还是制表位

        只使用空格, 每次缩进 2 个空格.

    4、函数声明与定义

        返回类型和函数名在同一行, 参数也尽量放在同一行,如果放不下就对形参分行。

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
        Type par_name1,  // 4 空格缩进
        Type par_name2,
        Type par_name3) {
    DoSomething();  // 2 空格缩进
    ...
}

        注意以下几点:

            如果返回类型和函数名在一行放不下,分行。

            如果返回类型那个与函数声明或定义分行了,不要缩进。

            左圆括号总是和函数名在同一行;

            函数名和左圆括号间没有空格;

            圆括号与参数间没有空格;

            左大括号总在最后一个参数同一行的末尾处;

            如果其它风格规则允许的话,右大括号总是单独位于函数最后一行,或者与左大括号同一行。

            右大括号和左大括号间总是有一个空格;

            函数声明和定义中的所有形参必须有命名且一致;

            所有形参应尽可能对齐;

            缺省缩进为 2 个空格;

            换行后的参数保持 4 个空格的缩进;

        如果有些参数没有用到, 在函数定义处将参数名注释起来。

    5、Lambda 表达式

        其它函数怎么格式化形参和函数体,Lambda 表达式就怎么格式化;捕获列表同理。 

// 若用引用捕获,在变量名和 & 之间不留空格。
int x = 0;
auto add_to_x = [&x](int n) { x += n; };

// 短 lambda 就写得和内联函数一样。
std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
                return blacklist.find(i) != blacklist.end();
            }),
            digits.end());

    6、函数调用

        要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。

// 函数调用遵循如下形式
bool retval = DoSomething(argument1, argument2, argument3);

// 如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格
bool retval = DoSomething(averyveryveryverylongargument1,
                             argument2, argument3);

// 参数也可以放在次行,缩进四格
if (...) {
  ...
  ...
  if (...) {
    DoSomething(
        argument1, argument2,  // 4 空格缩进
        argument3, argument4);
  }

// 有人认为把每个参数都独立成行,不仅更好读,而且方便编辑参数。
// 此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3)

    7、列表初始化

        平时怎么格式化函数调用,就怎么格式化:ref:braced_initializer_list。

// 一行列表初始化示范。
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};

// 当不得不断行时。
SomeFunction(
    {"assume a zero-length name before {"},
    some_other_function_parameter);
SomeType variable{
    some, other, values,
    {"assume a zero-length name before {"},
    SomeOtherType{
        "Very long string requiring the surrounding breaks.",
        some, other values},
    SomeOtherType{"Slightly shorter string",
                  some, other, values}};
SomeType variable{
    "This is too long to fit all in one line"};
MyType m = {  // 注意了,您可以在 { 前断行。
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {interiorwrappinglist,
     interiorwrappinglist2}};

    8、条件语句

        倾向于不在圆括号内使用空格. 关键字 if 和 else 另起一行。

if (condition) {  圆括号里没空格紧邻。
  ...  // 2 空格缩进。
} else {  // else 与 if 的右括号同一行。
  ...
}

// 简短的条件语句允许写在同一行. 只有当语句简单并且没有使用 else 子句时
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

// 只要其中一个分支用了大括号,两个分支都要用上大括号。
if (condition) {
  foo;
} else {
  bar;
}

    9、循环和开关选择语句

        switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {} 或 continue.

switch (var) {
  case 0: {  // 2 空格缩进
    ...      // 4 空格缩进
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

        空循环体应使用 {} 或 continue, 而不是一个简单的分号.

while (condition) {
  // 反复循环直到条件失效。
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循环体。
while (condition) continue;  // 可 - contunue 表明没有逻辑。

    10、指针和引用表达式

        句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.

// 好样的,空格前置。
char *c;
const string &str;

// 好样的,空格后置。
char* c;    // 但别忘了 "char* c, *d, *e, ...;"!
const string& str;

    11、布尔表达式

        如果一个布尔表达式超过 标准行宽, 断行方式要统一一下.

// 如逻辑与 (&&) 操作符总位于行尾
if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another & last_one) {
  ...
}

    12、函数返回值

        return 表达式里时没必要都用圆括号。

return result;                  // 返回值很简单,没有圆括号。
// 可以用圆括号把复杂表达式圈起来,改善可读性。
return (some_long_condition &&
        another_condition);

    13、变量及数组初始化

        用 =, () 和 {} 均可.

int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};

        请务必小心列表初始化 {…} 用 std::initializer_list 构造函数初始化出的类型。非空列表初始化就会优先调用 std::initializer_list, 不过空列表初始化除外,后者原则上会调用默认构造函数。为了强制禁用 std::initializer_list 构造函数,请改用括号。

vector<int> v(100, 1);  // A vector of 100 1s.
vector<int> v{100, 1};  // A vector of 100, 1.

        此外,列表初始化不允许整型类型的四舍五入,这可以用来避免一些类型上的编程失误。

int pi(3.14);  // 可 -- pi == 3.
int pi{3.14};  // Compile error: narrowing conversion.

    14、预处理指令

        预处理指令不要缩进, 从行首开始,即使预处理指令位于缩进代码块中。

// 可 - directives at beginning of line
  if (lopsided_score) {
#if DISASTER_PENDING      // 正确 -- 行开头起。
    DropEverything();
#endif
    BackToNormal();
  }

    15、类格式

        访问控制块的声明依次序是 public:, protected:, private:, 每次缩进 1 个空格。

class MyClass : public OtherClass {
 public:      // 注意有 1 空格缩进!
  MyClass();  // 照常,2 空格缩进。
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
  DISALLOW_COPY_AND_ASSIGN(MyClass);
};

        注意事项:

            所有基类名应在 80 列限制下尽量与子类名放在同一行.

            关键词 public:, protected:, private: 要缩进 1 个空格.

            除第一个关键词 (一般是 public) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空.

            这些关键词后不要保留空行.

            public 放在最前面, 然后是 protected, 最后是 private.

            关于声明顺序的规则请参考 声明顺序 一节。

    16、构造函数初始值列表

        构造函数初始值列表放在同一行或按四格缩进并排几行。

// 当全放在一行合适时:
MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) {

// 如果要断成多行,缩进四格,冒号放在第一行初始化句:
MyClass::MyClass(int var)
    : some_var_(var),             // 4 空格缩进
      some_other_var_(var + 1) {  // 对准
  ...
  DoSomething();
  ...
}

    17、名字空间格式化

        名字空间内容不缩进。

namespace {

void foo() {  // 正确。命名空间内没有额外的缩进。
  ...
}

}  // namespace

// 声明嵌套命名空间时,每命名空间都独立成行。
namespace foo {
namespace bar {

    18、水平留白

        水平留白的使用因地制宜. 永远不要在行尾添加没意义的留白.

/* 常规 */
void f(bool b) {  // 左大括号前恒有空格。
  ...
int i = 0;  // 分号前不加空格。
int x[] = { 0 };  // 大括号内部可与空格紧邻也不可,不过两边都要加上。
int x[] = {0};
// 继承与初始化列表中的冒号前后恒有空格。
class Foo : public Bar {
 public:
  // 至于内联函数实现,在大括号内部加上空格并编写实现。
  Foo(int b) : Bar(), baz_(b) {}  // 大括号里面是空的话,不加空格。
  void Reset() { baz_ = 0; }  // 用括号把大括号与实现分开。
  ...

/* 循环和条件语句 */
if (b) {          // if 条件语句和循环语句关键字后均有空格。
} else {          // else 前后有空格。
}
while (test) {}   // 圆括号内部不紧邻空格。
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) {    // 循环和条件语句的圆括号里可以与空格紧邻。
if ( test ) {     // 圆括号,但这很少见。总之要一致。
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) {  // 循环里内 ; 后恒有空格,; 前可以加个空格。
switch (i) {
  case 1:         // switch case 的冒号前无空格。
    ...
  case 2: break;  // 如果冒号有代码,加个空格。

/* 操作符 */
// 赋值操作系统前后恒有空格。
x = 0;

// 其它二元操作符也前后恒有空格,不过对 factors 前后不加空格也可以。
// 圆括号内部不紧邻空格。
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在参数和一元操作符之间不加空格。
x = -5;
++x;
if (x && !y)
  ...

/* 模板和转换 */
// 尖叫括号(< and >) 不与空格紧邻,< 前没有空格,>( 之间也没有。
vector<string> x;
y = static_cast<char*>(x);

// 在类型与指针操作符之间留空格也可以,但要保持一致。
vector<char *> x;
set<list<string>> x;        // 在 C++11 代码里可以这样用了。
set<list<string> > x;       // C++03 中要在 > > 里留个空格。

// 您或许可以在 < < 里加上一对对称的空格。
set< list<string> > x;

    19、垂直留白

        垂直留白越少越好。

        不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行。

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

1.9、规则特例

    1、现有不合规范的代码

        对于现有不符合既定编程风格的代码可以网开一面。当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定。

    2、Windows 代码

        Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南。参见:http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/exceptions/#windows

二、一张图总结Google C++编程规范

2.1、.h 文件

01-一张图总结GoogleCpp编程规范-h文件.png

2.2、.cc 文件

1465894902341018.png

三、Google代码规范工具Cpplint的使用

1、环境介绍

    windows 7 64bit

2、下载脚本

    可以在github上面直接下载Cpplint脚本,地址为:https://github.com/google/styleguide/tree/gh-pages/cpplint

    可以直接放在工程目录,也可以放在固定目录,如D:/cpplint

3、安装python

    在python官网:https://www.python.org/下载,或者直接点击连接:https://www.python.org/ftp/python/2.7.11/python-2.7.11.amd64.msi

    个人安装,在D:/python27 目录

4、配置环境变量

     安装python时没有选择配置path,可以手动配置python的环境变量;

5、关于windows cmd

    windows的CMD下切换盘符是直接盘符即可,不用加cd,查看目录命令是:dir 即:

> D:
> dir

6、使用

    将脚本拷贝到工程;

    运行命令:

>python cpplint.py records.h

    然后按照提示修改不规范的地方即可;

7、其他注意

    A、可以通过linelength选项,来控制每行的最长长度,默认是80字符,如果允许每行最长长度为120,则为:

>python D:/soft/Cpplint/cpplint.py --linelength=120 test.cpp

    B、可以通过counting选项,来显示每种Category有多少个错误,如:

>python D:/soft/Cpplint/cpplint.py --counting=detailed test.cpp

    C、对于发现的每个问题,cpplint都会给出一个位于区间[1, 5]之间的置信度评分,分数越高就代表问题越肯定,可以通过verbose选项控制输出哪些级别,如下,置信度评分为1、2的将不会再输出:

>python D:/soft/Cpplint/cpplint.py --verbose=3 test.cpp

四、参考

1、http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/

2、http://www.open-open.com/lib/view/open1405608079046.html

3、https://github.com/google/styleguide/tree/gh-pages/cpplint

4、http://blog.csdn.net/fengbingchun/article/details/47341765


转载标明出处:https://blog.evanxia.com/2016/06/702