赞
踩
注意: 本人现在已将博客都移动到个人博客,查看更多请点击此处
所有头文件要能够自给自足。即:用户和重工具不需要为特别场合而包含额外的头文件。
所有头文件都应该使用#define来防止头文件被多重包含,命名格式是<PROJECT>_<PATH>_<FILE>_H
。
为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径。
尽可能地避免使用前置声明。使用#include
包含需要的头文件即可。
所谓前置声明(forward declaration)
是类、函数和模板的纯粹声明,没伴随着其定义。
#include
会迫使编译器展开更多的文件,处理更多的输入。#inclue
使代码因头文件中无关的改动而被重新编译多次。std::
的symbol时,其行为未定义。#include
。极端情况下,用前置声明代替inclues
甚至都会暗暗地改变代码的含义。include
冗长。#include
#include
只有当函数只有10行甚至更少时才将其定义为内联函数。
当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。
只要内联函数体较小,内联函数可以令目标代码更加高效。对于存取函数以及其他函数体比较短,性能关键的函数,鼓励使用内联。
滥用内联函数导致程序变得更慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小,但内联一个相当大得函数将戏剧性的增加代码大小。现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。
switch
语句的函数常常是得不偿失。使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库、C++库,其他库的.h,本项目内的.h。
项目头文件应该按照项目源代码目录树结构排列,避免使用unix特殊的快捷目录.
(当前目录)或..
(上级目录)。
鼓励在.cc
文件使用匿名命名空间或static
声明。使用具名的命名空间时,其名称可基于项目名或相对路径。精致使用using表示(using-directive)。禁止使用内联命名空间(inline namespace)。
命名空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突。
虽然类已经提供了命名轴线,命名空间在这基础上又封装了一层。
内联命名空间会自动把内部的标识符放到外层作用域。
命名空间具有迷惑性,因为它们使得区分两个相同命名所指代的定义更加困难。
内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。
在头文件中使用匿名空间导致违背C++的唯一定义原则(One Definition Rule(ODR))。
std
内声明任何东西,包括标准库的类前置声明。在std
命名空间声明实体是未定义的行为,会导致如不可移植。声明标准库下的实体,需要包含对应的头文件。在.cc
文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static
。但是不要在.h
文件中这么做。
所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为static
拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
推荐、鼓励在.cc
中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h
中使用。
匿名命名空间的声明和具名的格式相同,在最后注释上namespace
:
namespace{
...
} //namespace
使用静态成员函数或命名空间内的非成员函数,尽量不要用裸的全局函数。将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关。
某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在命名空间内可避免污染全局作用域。
将非成员函数和静态成员函数作为新类的成员函数作为新类的成员或许更有意义。当它们需要访问外部资源或具有重要的依赖关系时更是如此。
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类。
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内.
如果必须定义非成员函数, 又只是在.cc
文件中使用它, 可使用匿名命名空间
或static
链接关键字 (如static int Foo() {...}
) 限定其作用域.
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化。
C++允许在函数的任何位置声明变量。我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好。 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。
int j = g(); //初始化声明
vector<int> v;
v.push_back(1); //用花括号初始化更好
v.push_back(1);
std::vector<int> v = {1, 2};
属于if
,while
和for
语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了。
while (const char* p = strchr(str, '/')) strr = p + 1;
**注:**有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低。
禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。
禁止使用类的静态储存周期
变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过constexpr
变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在C++中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化POD变量,除非该函数(比如getenv()
或getpid()
)不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从main()
返回还是对exit()
的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。
改善以上析构问题的办法之一是用quick_exit()
来代替exit()
并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行atexit()
所绑定的任何handlers. 如果您想在执行quick_exit()
来中断时执行某handler(比如刷新log),您可以把它绑定到_at_quick_exit()
. 如果您想在exit()
和quick_exit()
都用上该handler, 都绑定上去。
综上所述,我们只允许POD类型的静态变量,即完全禁用vector
(使用C数组替代)和string
(使用 const char []
)。
如果您确实需要一个class
类型的静态或全局变量,可以考虑在main()
函数或pthread_once()
内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
类是C++中代码的基本单元。
不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化。
在构造函数中可以进行各种初始化操作。
const
类型,也能更方便地被标准容器或算法使用。bool isValid()
或类似这样的机制才能检查出来, 然而这是一个十分容易被疏忽的方法。构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式。否则, 考虑用Init()
方法或工厂函数。
构造函数不得调用虚函数, 或尝试报告一个非致命错误。如果对象需要进行有意义的(non-trivial)初始化, 考虑使用明确的Init()
方法或使用工厂模式. Avoid Init() methods on objects with no other states that affect which public methods may be called(此类形式的半构造对象有时无法正确工作)。
不要定义隐式类型转换。对于转换运算符和单参数构造函数,请使用explicit
关键字。
隐式类型转换允许一个某种类型(源类型)的对象被用于需要另一种类型(目的类型)的位置。
除了语言所定义的隐式类型转换,用户还可以通过在类定义中添加合适的成员定义自己需要的转换。在源类型中定义的隐式类型转换,可以通过目的的类型名的类型转换运算符实现(operator bool()
)。在目的类型中定义隐式类型转换,泽通过以源类型作为其唯一参数(或唯一无默认值的参数)的构造函数实现。
explicit
关键字可以用于构造函数或类型转换运算符,以保证只有当目的类型在调用点被显示写明时才能进行类型转换。例如cast
。这不仅用于隐式类型转换,还能作用于C++11的初始化语法:
class Foo{
explicit Foo(int x, double y);
}
void Func(Foo, f);
explicit
关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是作者忘了加上explicit
标记。在类型定义中, 类型转换运算符和单参数构造函数都应当用explicit
进行标记。一个例外是, 拷贝和移动构造函数不应当被标记为explicit
, 因为它们并不执行类型转换。对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的。这时应当联系项目组长并说明特殊情况.
不能以一个参数进行调用的构造函数不应当加上explicit
。接受一个std::initializer_list
作为参数的构造函数也应当省略,explicit
以便支持拷贝初始化(例如MyType m = {1, 2};
)。
如果你的类型需要,就让它们指出拷贝/移动,否则,就把隐式产生的拷贝和移动函数禁用。
可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值。对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义. string
类型就是一个可拷贝类型的例子。
可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的)。std::unique_ptr<int>
就是一个可移动但不可复制的对象的例子。对于用户定义的类型, 移动操作一般是通过移动构造函数和移动赋值操作符实现的。
拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如, 通过传值的方式传递对象。
可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得API更简单, 更安全也更通用。 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确。这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护。这样的对象可以和需要传值操作的通用API一起使用, 例如大多数容器。
拷贝/移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如Clone()
, CopyFrom()
or Swap()
, 更容易定义, 因为它们能通过编译器产生, 无论是隐式的还是通过= default
。 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时, 对于类似 省略不必要的拷贝 这样的优化它们也更加合适。
移动操作允许隐式且高效地将源数据转移出右值对象. 这有时能让代码风格更加清晰。
许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 单件类型 (Registerer
), 与特定的作用域相关的类型 (Cleanup
), 与其他对象实体紧耦合的类型 (Mutex
) 从逻辑上来说都不应该提供拷贝操作. 为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成 对象切割。默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误。
拷贝构造函数是隐式调用的, 也就是说, 这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时, 这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题。
如果需要就让你的类型可拷贝/可移动. 作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝。如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然。如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义。如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作。
如果定义了拷贝/移动操作, 则要保证这些操作的默认实现是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的。
由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone()
和一个 protected
的拷贝构造函数以供派生类实现。
如果你的类不需要拷贝/移动操作, 请显式地通过在 public
域中使用 = delete
或其他手段禁用之。
仅当只有数据成员时使用struct
,其它一概使用calss
。
在C++中struct
和calss
关键字几乎含义一样。我们为这两个关键字添加我们自己的语义理解,以便为定义的数据类型选择合适的关键字。
struct
用来定义包含数据的被动式对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能。并且存取功能是通过直接访问位域,而非函数调用。除了构造函数、析构函数,Initialize
、Reset
,Validate
等类似的用于设定数据成员的函数,不能提供其它功能的函数。
如果需要更多的函数功能,class
更适合。如果拿不准,就用class
。
为了和STL保持一致,对于仿函数等特性可以不用class
而使用struct
。
注意:类和结构体的成员变量使用不同的命名规则。
使用组合常常比使用继承更合适。如果使用继承的话,定义为public
继承。
当子类继承基类时,子类包含了父基类所有的数据以及操作的定以。C++实践中,继承主要用于两种场合:实现继承,子类继承父类的实现代码;接口继承,子类仅继承父类的方法名称。
实现继承通过原封不动的复用基类代码减少了代码量。由于继承是在编译时声明,程序员和编译器都可以理解相应操作并发现错误。从编程角度而言,接口继承是用来强制类输出特定的API。在类没有实现API中某个必须的方法,编译器同样会发现并报告错误。
对于实现继承,由于子类的实现代码散布在父类和子类之间,要理解其实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改其实现。基类也可能定义了一些函数成员,因此还必须区分基类的实现布局。
所有继承必须是public
。如果你想要使用私有继承,你应该替换成把基类的实例作为成员对象的方式。
不要过度使用实现继承。组合常常更适合一些,尽量做到只在“是一个”(”is-a”)的情况下使用继承:如果bar
的确“是一种”Foo
,bar
才能继承Foo
。
必要的话,析构函数声明为virtual
。如果你的类有虚函数,则析构函数也应该为虚函数。
对于可能被子类访问的成员函数,不要过度使用protected
关键字。注意,数据成员都必须是私有的。
对于重载的虚函数或析构函数,使用override
,或final
关键字显示地进行标记。较早的代码可能使用virtual
关键字作为不得已的选项。因此,在声明重载时,请使用override
、final
、virtual
中其中之一进行标记。标记为override
或final
的析构函数如果不是对基类虚函数的重载的话,编译会报错。这有助于捕获常见的错误。这些标记起到了文档的作用,因为如果省略这些关键字,代码阅读者不得不检查所有父类,以判断该杉树是否是虚函数。
真正需要到多重实现继承的情况少之又少。只在一下情况下才允许多重继承:最多只有一个基类是非抽象类;其他基类都是以Interface
后缀的纯接口类。
多重继承允许子类拥有多个基类。要将作为纯接口的基类和具有实现的基类区别开来。
相比单继承,多重实现继承可以复用更多的代码。
真正需要用到多重实现继承的情况少之又少。有时多重实现继承看上去是不错的解决方案,但这是你通常可以找到一个更明确、更清晰的不同解决方案。
只有当所有父类除第一个外都是纯接口类时,才允许使用多重继承。为确保它们是纯接口,这些类必须以Interface
为后缀。
注意
关于该规则,Windows下有个特例。
接口是指满足特定条件的类,这些类以Interface
为后缀(不强制)。
当一个类满足以下要求时,称之为纯接口:
=0
”)和静态函数(除了析构函数)。protected
。Interface
为后缀的类继承。接口类不能直接被实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明析构函数。
以Interface
为后缀可以提醒其他人不要为该接口类增加杉树实现或非静态数据成员。这一点对于多重继承尤其重要。另外。对于Java程序员来说,接口的概念已是深入人心。
Interface
后缀增加了类名长度,为阅读和理解带来不便。同时,接口属性作为实现细节不应暴露给用户。
只有在满足上述条件时,类才以Interface
结尾,但反过来,满足上述需要类未必一定以Interface
结尾。
除少数特定环境外,不要重载运算符。也不要创建用户定义字面量。
C++允许用户通过使用operator
关键字对内建运算符进行重新定义,只要其中一个参数是用户定义的类型。operator
关键字还允许用户使用operator""
定义新的字面运算符,并且定义类型转换函数,例如operator bool()
。
重载运算符可以让代码更简洁易懂,也使得用户定义的类型和内建类型拥有相似的行为。重载运算符对于某些运算来说是符合符合语言习惯的名称(例如==
、<
, =
, <<
),遵循这些语言约定可以让用户定义的类型更易读,也能更好地和需要这些重载运算符的函数库进行交互操作。
对于创建用户定义的类型的对象来说,用户定义字面量是一种非常简洁的标记。
foo < bar
执行的是一个行为,而&foo < &bar
执行的就是完全不同的另一个行为了。&
会导致同样的代码有完全不同的含义,这取决于重载的声明对某段代码而言是否是可见的。重载诸如&&
, ||
和,
会导致运算顺序和内建运算的顺序不一致。只有在意义明显,不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符。例如,|
要作为位或或逻辑或来使用,而不是作为shell中的管道。
只有对用户自己定义的类型重载运算符。更准确地说,将它们和它们所操作的类型定义在同一个头文件中,.cc
中和命名空间中。这样做无论类型在哪里都能够使用定义的运算符,并且最大程度上避免了多重定义的风险。如果可能的话,请避免将运算符定义为模板,因为此时它们必须对任何模板参数都能够作用。如果你定义了一个运算符,请将其相关且有意义的运算符都进行定义,并且保证这些定义的语义是一致的。例如,如果你重载了<
,那么请将所有的比较运算符都进行重载,并且保证对于同一组参数,<
和>
不会同时返回true
。
建议不要将不进行修改的二元运算符定义为成员函数。如果一个二元运算符被定义为类成员,这时隐式转换会作用域右侧的参数却不会作用于左侧。这时会出现a < b
能够通过编译而b < a
不能的情况,这是很让人迷惑的。
不要为了避免重载操作符而走极端。比如说,应当定义 ==
, =
, 和<<
而不是Equals()
, CopyFrom()
和PrintTo()
。反过来说,不要只是为了满足函数库需要而去定义运算符重载。比如说,如果你的类型没有自然顺序,而你要将它们存入std::set
中,最好还是定义一个自定义的比较运算符而不是重载 <
。
不要重载&&
, ||
,,
或一元运算符&
。不要重载 operator""
,也就是说,不要引入用户定义字面量。
类型转换运算符在隐式类型转换一节有提及。=
运算符在可拷贝类型和可移动类型一节有提及。 运算符<<
在流一节有提及。同时请参见函数重载一节,其中提到的的规则对运算符重载同样适用。
将所有数据成员声明为private
,除非是static const
类型成员(遵循常量命名规则)。处于技术上的原因,在使用Google Test时我们允许测试固件类中的数据成员为protected
。
将相似的声明放在一起,将public
部分放在最前。
类定义一般应在public:
开始,后跟protected:
,最后是private:
。省略空部分。
在各个部分中,建议将类似的声明放在一起,并且建议以如下顺序:类型(包括typedef
、using
和嵌套的结构体与类),常量,工厂函数、构造函数、赋值运算符、析构函数、其他函数,数据成员。
不要将大段的函数定义内联在类定义中。通常,只有那些普通的,或性能关键且短小的函数可以内联在类定义中。参考内联函数一节。
函数的参数顺序为:输入参数在先,后跟输出参数。
C/C++中的函数参数或者函数的输入,或者是函数的输出,或兼而有之。输入参数通常是值参或const
引用,输出参数或者输入/输出参数则一般为非const
指针。在排列参数顺序时,将所有的输入参数置于输出参数之前。特别注意,在加入新参数时不要因它们是新参数就置于参数列表最后,是仍要按照前述的规则,即将新的输入参数也置于输出参数之前。
这并非一个硬性规定。输入/输出参数(通常是类或结构体)让这个问题变得复杂,并且,有时候为了其他函数保持一致,你可能不得不有所变通。
我们倾向于编写简短、凝练的函数。
我们承认长函数有时是合理的,因此并不硬性限制函数的长度。如果函数超过40行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。
即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的 bug。使函数尽量简短,以便于他人阅读和修改代码。
在处理代码时,你可能会发现复杂的长函数。不要害怕修改现有代码:如果证实这些代码使用/调试起来很困难,或者你只需要使用其中的一小段代码,考虑将其分割为更加简短并易于管理的若干函数。
所有按引用传递的参数必须加上const
。
在C语言中,如果函数需要修改变量的值,参数必须为指针,如int foo(int *pval)
。在C++中,函数还可以声明为引用参数:int foo(Interface &val)
。
定义引用参数可以防止出现(*pval)++
这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是需要的。同时也更明确地不接受空指针。
容易引起误解,因为引用在语法上是值变量却拥有指针的语义。
函数参数列表中,所有引用参数都必须是const
:
void Foo(const string &in, string *out);
事实上这在Google Code是一个硬性约定:输入参数是值参或const
引用,输出参数为指针。输入参数可以是const
指针,但绝不能是非const
的引用参数,除非特殊要求,比如swap
。
有时候,在输入形参中用const T*
指针比const T&
更明智。比如:
总而言之,大多时候输入形参往往是const T&
。若用const T*
则说明输入另有处理。所以若要使用const T*
,则应给出相应的理由,否则会使得读者感到迷惑。
若要使用函数重载,则必须能让读一看调用点就胸有成竹,而不用花心思猜测调用的重载函数到底是哪一种,这样一规则也适用于构造函数。
你可以编写一个参数类型为const string&
的函数,然后用另一个参数类型为const char*
的函数对齐进行重载:
class MyClass{
public:
void Analyze(const string &text);
void Analyze(const char* text, size_t textlen);
}
};
通过重载参数不同的同名参数,可以令代码更加直观,模版化代码需要重载,这同时也能为使用这带来便利。
如果函数单靠不同的参数类型而重载,读者就得十分熟悉C++五花八门的匹配规则,以了解匹配过程具体到底如何。另外,如果派生类只重载了某个函数的部分变体,继承语义的容易令人困惑。
如果打算重载一个函数,可以试试改在函数名里加上参数信息。例如,用AppendString()
和AppendInt
等, 而不是一口气重载多个Append()
。如果重载函数的目的是为了支持不同数量的同一类型参数,则优先考虑使用std::vector
以便使用者可以用列表初始化指定参数。
只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致。缺省参数与函数重载遵循同样的规则。一般情况下建议使用函数重载,尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下。
有些函数一般情况下使用默认参数,但有时需要又使用非默认的参数。缺省参数为这样的情形提供了便利,使程序员不需要为了极少的例外情况编写大量的函数。和函数重载相比,缺省参数的语法更简洁明了,减少了大量的样板代码,也更好地区别了“必要参数”和“可选参数”。
缺省参数实际上是函数重载语义的另一种实现方式,因此所有不应当使用函数重载的理由也都适用于缺省参数。
虚函数调用的缺省参数取决于目标对象的静态类型,此时无法保证给定函数的所有重载声明的都是同样的缺省参数。
缺省参数是在每个调用点都要进行重新求值的,这会造成生成的代码迅速膨胀。作为读者,一般来说也更希望缺省的参数在声明时就已经被固定了,而不是在每次调用时都可能会有不同的取值。
缺省参数会干扰函数指针,导致函数签名与调用点的签名不一致。而函数重载不会导致这样的问题。
对于虚函数,不允许使用缺省参数,因为在虚函数中缺省参数不一定能正常工作。如果在每个调用点缺省参数的值都有可能不同,在这种情况下缺省函数也不允许使用。(例如,不要写像void f(int n = counter++);这样的代码。)
在其他情况下,如果缺省参数对可读性的提升远远超过了以上提及的缺点的话,可以使用缺省参数。 如果仍有疑惑,就使用函数重载。
只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法。
C++现在允许两种不同的函数声明方式。以往的写法是将返回类型置于函数名之前。例如:
int foo(int x);
C++11引入了这一新的形式. 现在可以在函数名前使用auto
关键字, 在参数列表之后后置返回类型. 例如:
auto foo(int x) -> int;
后置返回类型为函数作用域。对于像int
这样简单的类型,两种写法没有区别。但对于复杂的情况,例如类域中的类型声明或者以函数参数的形式书写的类型,写法的不同会造成区别。
后置返回类型是显式地指定Lambda表达式的返回值的唯一方式。某些情况下,编译器可以自动推导出Lambda表达式的返回类型,但并不是在所有的情况下都能实现。即使编译器能够自动推导,显式地指定返回类型也能让读者更明了。
有时在已经出现了的函数参数列表之后指定返回类型,能够让书写更简单,也更易读,尤其是在返回类型依赖于模板参数时。例如:
template <class T, class U> auto add(T t, U u) -> decltype(t + u);
对比下面的例子:
template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);
后置返回类型相对来说是非常新的语法,而且在C和Java中都没有相似的写法,因此可能对读者来说比较陌生。
在已有的代码中有大量的函数声明,你不可能把它们都用新的语法重写一遍。因此实际的做法只能是使用旧的语法或者新旧混用。在这种情况下,只使用一种版本是相对来说更规整的形式。
在大部分情况下,应当继续使用以往的函数声明写法,即将返回类型置于函数名前。只有在必需的时候(如Lambda表达式)或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法。但是后一种情况一般来说是很少见的,大部分时候都出现在相当复杂的模板代码中,而多数情况下不鼓励写这样复杂的模板代码。
最重要的一致性规则是命名管理。命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义:类型、变量、函数、常量、宏等等,甚至,我们大脑中的模式匹配引擎非常依赖这些命名规则。
命名规则具有一定随意性。但相比按个人喜好命名,一致性更重要,所以无论你认为它们是否重要,规则总是规则。
函数命名、变量命名,文件命名要有描述性;少用缩写。
尽可能使用描述性的命名,别心疼空间,毕竟相比之下代码易于新读者理解更重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉一个字母来缩写单词。
int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么
int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.
注意,一些特定的广为人知的缩写是允许的,例如用i
和用T
表示模版参数。
模板参数的命名应当遵循对应的分类:类型模板参数应当遵循类型命名的规则,而非类型模板应当遵循变量命名的规则。
文件名要全部小写,可以包含下划线(_
)和连字符(-
),依照项目约定,如果没有约定,那么“_
”更好。
可接受的文件命名实例:
my_userful_class.cc
my-userful-class.cc
myuserfulclass.cc
myuserfulclass_test.cc
// _unittest.cc
和_regtest.cc
以弃用。C++文件要以.cc
结尾,头文件以.h
结尾。专门插入文本的文件则以.inc
结尾,参见头文件自足。
不要使用已经存在于/usr/include
下的文件名。如:db.h
通常应尽量让文件名更加明确。http_servr.h
就比logs.h
要好。定义类时文件名一般成对出现,如:foo_bar.h
和foo_bar.cc
,对应于类
FooBar
。
内联函数必须放在.h
文件中,如果内联函数比较短,就直接放在.h
。
类型名称的每个单词首字母均大写,不包好下划线:MyExcitingClass
,MyExcitingEnum
。
所有类型命名——类、结构体、类型定义(typedef
)、枚举、类型模板参数——均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。例如:
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;
// 枚举
enum UrlTableErrors { ...
变量(包含函数参数)和数据成员名一律小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体就不用。比如:a_local_variable
,a_struct_data_member
,a_class_data_member
。
普通变量命名
举例:
string table_name; // 好 - 用下划线.
string tablename; // 好 - 全小写.
string tableName; // 差 - 混合大小写
类数据成员
不管是静态的还是非静态的,类数据成员都可以和普通变量一样,但要接下划线。
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
string tablename_; // 好.
static Pool<TableInfo>* pool_; // 好.
};
结构体变量
不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样,不要像类那样接下划线。
struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};
结构体与类的使用讨论,参考结构体和类。
声明为constexpr
或const
的变量,或在程序运行期间其值始终保持不变的,命名时以“K”开头,大小写混合。例如:
const int kDaysInAWeek = 7;
所有具有静态存储类型的变量(例如静态变量或全局变量,参见存储类型) 都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。
常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:MyExcitingFunction()
,MyExcitingMethod()
, my_exciting_member_variable()
, set_my_exciting_member_variable()
。
一般来说,函数名的每个单词首字母大写(即“驼峰变量名”或“帕斯卡变量名”),没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写(例如,写作StartRpc()
而非StartRPC()
)。
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
(同样的命名规则同时适用于类作用域与命名空间作用域的常量,因为它们是作为API的一部分暴露对外的,因此应当让它们看起来像是一个函数,因为在这时,它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节。)
取值和设值函数的命名与变量一致。一般来说它们的名称与实际的成员变量对应,但并不强制要求.。例如sint count()
与void set_count(int count)
。
命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。要注意避免嵌套命名空间的名字之间和行间的顶级命名空间的名字之间发生的冲突。
顶级命名空间的名称应当是项目名称或是该命名空间中的代码所属的团队的名字。命名空间中的代码,应该存放于和命名空间的名字匹配的文件夹或其子文件夹中。
注意不使用缩写作为名称的规则同样适用于命名空间。命名空间中的代码极少需要设计命名空间的名称,因为没有必要在命名空间中使用缩写。
要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突。由于名称查找规则的存在,命名空间之间的冲突完全有可能导致编译失败。尤其是,不要创建嵌套的std
m命名空间。建议使用更独特的项目标识符(websearch::index
、websearch::index_util
),而非常见的极易发生冲突的名名称(比如:websearch::util
)。
对于internal
命名空间,要当心加入到同一internal
命名空间的代码之间发生冲突(由于内部维护人员通常来自于同一个团队,因此常有可能导致冲突)。在这样的情况下,请使用文件名以使得内部名称独一无二(例如对于frobber.h
,使用websearch::index::frobber_internal
)。
枚举的命名应当和常量或宏一致:kEnumName
或是ENUM_NAME
。
单独的枚举值应该优先采用常量的命名方式。但宏方式的命名也可以接受,枚举名UrlTableErrors
(以及AlternateUrlTableErrors
)是类型,所以要用大小写混合的方式。
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
2009年1月之前,我们一直建议采用宏的方式命名枚举值。由于枚举值和宏之间的命名冲突,直接导致了很多问题。由此,这里改为优先选择常量风格的命名方式。新代码应该尽可能优先使用常量风格。但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期问题。
你并不打算[使用宏],对吧?如果你一定要用,就像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN
。
参考预处理宏;通常,不应该使用宏,如果不得不用,其命名像枚举命名一样全部大写,使用下划线:
#define ROUND(x) ...
#define PI_ROUNDED 3.0
如果你命名的实体与已有C/C++实体相似,可参考现有命名策略。
bigopen()
:函数名,参照open()
的形式。
uint
:typedef
bigpos
:struct
或class
,参照pos
的形式。
sparse_hash_map
:STL型实体;参照STL命名约定。
UrlTableErrors
常量,如同INT_MAX
注释虽然写起来很痛苦,但对保证代码可读性至关重要。下面的规则描述了如何注释以及在哪儿注释。当然也要记住:注释固然很重要,但最好的代码应当本身就是文档。有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字。
你写的注释是给代码读者看的,也就是下一个需要理解你的代码的人。所以慷慨些吧,下一个读者可能就是你!
使用//
或/* */
,统一就好。
虽然//
或/* */
都可以,但//
更常用。更在如何注释以及注释风格上确保统一。
在每一个文件开头加入版权公告。
文件注释描述了该文件的内容。如果一个文件只声明,或实现,或测试了一个对象,并且这个对象已经在它的声明处进行了详细的注释,那么就没必要再加上文件注释。除此之外的其他文件都需要文件注释。
法律公告和作者信息
每个文件都应该包含许可证引用。为项目选择合适的许可证版本。(比如,Apache 2.0、BSD、LGPL,、GPL)。
如果你对原始作者的文件做了重大修改。请考虑删除原作者信息。
文件内容
如果一个.h
文件声明了多个概念,则文件注释应当对文件的内容做一个大致的说明,同时说明各概念之间的联系。一个一到两行的文件注释就足够了,对于每个概念的详细文档应当放在各个概念中,而不是文件注释中。
不要在.h
和.cc
之间复制注释,这样的注释偏离了注释的实际意义.
每个类的定义都要附带一份注释,描述类的功能和用法,除非它的功能相当明显。
// Iterates over the contents of a GargantuanTable.
// Example:
// GargantuanTableIterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
// delete iter;
class GargantuanTableIterator {
...
};
类注释应当为读者理解如何使用与何时使用类提供足够的信息,同时应当提醒读者在正确使用此类时应当考虑的因素。如果类有任何同步前提,请用文档说明。如果该类的实例可被多线程访问,要特别注意文档说明多线程环境下相关的规则和常量使用。
如果你想用一小段代码演示这个类的基本用法或通常用法,放在类注释里也非常合适。
如果类的声明和定义分开了(例如分别放在了.h
和.cc
文件中),此时,描述类用法的注释应当和接口定义放在一起,描述类的操作和实现的注释应当和实现放在一起。
函数声明处的注释描述函数功能;定义处的注释描述函数实现。
函数声明
基本上每个函数声明处前都应当加上注释,描述函数的功能和用途。只有在函数的功能简单而明显时才能省略这些注释(例如,简单的取值和设值函数)。注释使用叙述式(“Opens the file”)而非指令式 (“Open the file”);注释只是为了描述函数,而不是命令函数做什么。通常,注释不会描述函数如何工作。那是函数定义部分的事情。
函数声明处注释的内容:
举例如下:
// Returns an iterator for this table. It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
// Iterator* iter = table->NewIterator();
// iter->Seek("");
// return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;
但也要避免罗罗嗦嗦,或者对显而易见的内容进行说明。下面的注释就没有必要加上“否则返回 false”, 因为已经暗含其中了:
// Returns true if the table cannot hold any more entries.
bool IsTableFull();
注释函数重载时,注释的重点应该是函数中被重载的部分,而不是简单的重复被重载的函数的注释。多数情况下,函数重载不需要额外的文档,因此也没有必要加上注释。
注释构造/析构函数时,切记读代码的人知道构造/析构函数的功能,所以“销毁这一对象”这样的注释是没有意义的。你应当注明的是注明构造函数对参数做了什么(例如,是否取得指针所有权)以及析构函数清理了什么。如果都是些无关紧要的内容,直接省掉注释。析构函数前没有注释是很正常的。
不要从.h
文件或其他地方的函数声明处直接复制注释。简要重述函数功能是可以的,但注释重点要放在如何实现上。
通常变量名本身足以很好说明变量用途。某些情况下,也需要额外的注释说明。
类数据成员
每个类数据成员(也叫实例变量或成员变量)都应该用注释说明用途。如果有非变量的参数(例如特殊值,数据成员之间的关系、生命周期等)不能够用类型与变量名明确表达,则应当加上注释。然而,如果变量类型与变量名已经足以描述一个变量,那么就不再需要加上注释。
特别地,如果变量可以接受NULL
或-1
等警戒值,须加以说明。比如:
private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;
全局变量
和数据成员一样,所有全局变量也要注释说明含义及用途,以及作为全局变量的原因。比如:
// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;
对于代码中巧妙的,晦涩的,有趣的,重要的地方加以注释。
代码前注释
巧妙或复杂的代码段前要加注释。比如:
// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
行注释
比较隐晦的地方要在行尾加入注释。在行尾空两格进行注释。比如:
// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.
注意,这里用了两段注释分别描述这段代码的作用,和提示函数返回时错误已经被记入日志。
如果你需要连续进行多行注释,可以使之对齐获得更好的可读性:
DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
// thus the comment lines up with the following comments and code.
DoSomethingElse(); // Two spaces before line comments normally.
}
std::vector<string> list{
// Comments in braced lists describe the next element...
"First item",
// .. and should be aligned appropriately.
"Second item"};
DoSomething(); /* For trailing block comments, one space is fine. */
函数参数注释
如果函数参数的意义不明显,考虑用下面的方式进行弥补:
bool
类型的参数变为enum
类型,这样可以让这个参数的值表达其意义。比如下面的示例的对比:
// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
和
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options, /*completion_callback=*/nullptr);
哪个更清晰一目了然。
不允许的行为
不要描述显而易见的现象,永远不要用自然语言翻译代码作为注释,除非即使对深入理解C++的读者来说代码的行为都是不明显的。要假设读代码的人C++水平比你高,即便他/她可能不知道你的用意:
你所提供的注释应当解释代码为什么要这么做和代码的目的,或者最好是让代码自文档化。
比较这样的注释:
// Find the element in the vector. <-- 差: 这太明显了!
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}
和这样的注释:
// Process "element" unless it was already processed.
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}
自文档化的代码根本就不需要注释。上面例子中的注释对下面的代码来说就是毫无必要的:
if (!IsAlreadyProcessed(element)) {
Process(element);
}
注意标点,拼写和语法;写的好的注释比差的要易读的多。
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句。大多数情况下,完整的句子比句子片段可读性更高。短一点的注释,比如代码行尾注释,可以随意点。但依然要注意风格的一致性.
虽然被别人指出该用分号时却用了逗号多少有些尴尬,但清晰易读的代码还是很重要的。正确的标点、拼写和语法对此会有很大帮助。
对那些临时的、短期的解决方案,或已经够好但仍不完美的代码使用TODO
注释。
TODO
注释要使用全大写的字符串TODO
,在随后的圆括号里写上你的名字、邮件地址、bug ID或其它身份标识和与这一TODO
相关的issue。主要目的是让添加注释的人(也是可以请求提供更多细节的人)可根据规范的TODO
格式进行查找。添加TODO
注释并不意味着你要自己来修正,因此当你加上带有姓名的TODO
时,一般都是写上自己的名字。
// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature
如果加TODO
为了在“将来某一天做某事”,可以附上一个非常明确的时间 “Fix by November 2005”),或者一个明确的事项(“Remove this code when all clients can handle XML responses.”)。
通过弃用注释(DEPRECATED comments)以标记某接口点已弃用。
您可以写上包含全大写的DEPRECATED的注释,以标记某接口为弃用状态。 注释可以放在接口声明前或者同一行。
在DEPRECATED一词后,在括号中留下您的名字,邮箱地址以及其他身份标识。
弃用注释应当包涵简短而清晰的指引,以帮助其他人修复其调用点。在 C++ 中,你可以将一个弃用函数改造成一个内联函数,这一函数将调用新的接口。
仅仅标记接口为DEPRECATED并不会让大家不约而同地弃用,您还得亲自主动修正调用点(callsites)或是找个帮手。
修正好的代码应该不会再涉及弃用接口点了,着实改用新接口点。如果您不知从何下手,可以找标记弃用注释的当事人一起商量。
每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规则需要一定时间的适应,但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人轻松地阅读和理解代码。
为了帮助你正确的格式化代码,我们写了一个emacs配置文件。
每一行代码字符数不超过80。
我们也认识到这条规则是有争议的,但很多已有代码都遵照这一规则,因此我们感觉一致性更重要。
提倡该原则的人认为强迫他们调整编辑器窗口大小是很野蛮的行为。很多人同时并排开几个代码窗口,根本没有多余的空间拉伸窗口。大家都把窗口最大尺寸加以限定并且80列宽是传统标准。那么为什么要改变呢?
反对该原则的人则认为更宽的代码行更易阅读。80列的限制是上个世纪60年代的大型机的古板缺陷;现代设备具有更宽的显示屏,可以很轻松地显示更多代码。
如果无法在不伤害易读性的条件下进行断行,那么注释行可以超过80个字符,这样可以方便复制粘贴。例如,带有命令示例或URL的行可以超过80个字符。
包含长路径的#include
语句可以超出80列。
头文件保护
可以无视该原则.
尽量不使用非ASCII字符,使用时必须使用UTF-8编码。
即使是英文,也不应将用户界面的文本硬编码到源代码中,因此非ASCII字符应当很少被用到。特殊情况下可以适当包含此类字符。例如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非 ASCII字符串;更常见的是(不需要本地化的)单元测试代码可能包含非ASCII字符串。此类情况下,应使用UTF-8编码,因为很多工具都可以理解和处理UTF-8编码。
十六进制编码也可以,能增强可读性的情况下尤其鼓励 —— 比如"\xEF\xBB\xBF"
,或者更简洁地写作u8"\uFEFF"
,在Unicode中是零宽度 无间断的间隔符号,如果不用十六进制直接放在UTF-8格式的源文件中,是看不到的。
使用u8前缀把带uXXXX
转义序列的字符串字面值编码成UTF-8。不要用在本身就带UTF-8字符的字符串字面值上,因为如果编译器不把源代码识别成UTF-8,输出就会出错。
别用C++11的char16_t
和char32_t
,它们和UTF-8文本没有关系,wchar_t
同理,除非你写的代码要调用Windows API,后者广泛使用了wchar_t
。
只使用空格,每次缩进2个空格。
我们使用空格缩,不要在代码中使用制表符。你应该设置编辑器将制表符转为空格。
返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参分行,分行方式与函数调用一致。
函数看上去像这样:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}
如果同一行文本太多,放不下所有参数:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}
甚至连第一个参数都放不下:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4 space indent
Type par_name2,
Type par_name3) {
DoSomething(); // 2 space indent
...
}
注意以下几点:
未被使用的参数,或者根据上下文很容易看出其用途的参数,可以省略参数名:
class Foo {
public:
Foo(Foo&&);
Foo(const Foo&);
Foo& operator=(Foo&&);
Foo& operator=(const Foo&);
};
未被使用的参数如果其用途不明显的话,在函数定义处将参数名注释起来:
class Shape {
public:
virtual void Rotate(double radians) = 0;
};
class Circle : public Shape {
public:
void Rotate(double radians) override;
};
void Circle::Rotate(double /*radians*/) {}
/ 差 - 如果将来有人要实现, 很难猜出变量的作用.
void Circle::Rotate(double) {}
属性和展开为属性的宏,写在函数声明或定义的最前面,即返回类型之前:
MUST_USE_RESULT bool IsOK();
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());
要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。
函数调用遵循如下形式:
bool retval = DoSomething(argument1, argument2, argument3);
如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:
bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
参数也可以放在次行,缩进四格:
if (...) {
...
...
if (...) {
DoSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}
把多个参数放在同一行以减少函数调用所需的行数,除非影响到可读性。有人认为把每个参数都独立成行,不仅更好读而且方便编辑参数。不过,比起所谓的参数编辑,我们更看重可读性,且后者比较好办。
如果一些参数本身就是略复杂的表达式,且降低了可读性,那么可以直接创建临时变量描述该表达式,并传递给函数:
int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);
或者放着不管,补充上注释:
bool retval = DoSomething(scores[x] * y + bases[x], // Score heuristic.
x, y, z);
如果某参数独立成行,对可读性更有帮助的话,那也可以如此做。参数的格式处理应当以可读性而非其他作为最重要的原则。
此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式:
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
您平时怎么格式化函数调用,就怎么格式化列表初始化。
如果列表初始化伴随着名字,比如类型或变量名,格式化时将将名字视作函数调用名,*{}*视作函数调用的括号。如果没有名字,就视作名字长度为零。
// 一行列表初始化示范. 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}};
倾向于不在圆括号内使用空格。关键字if
和else
另起一行。
对基本条件语句有两种可以接受的格式。一种在圆括号和条件之间有空格,另一种没有。
最常见的是没有空格的格式,哪一种都可以,最重要的是保持一致,如果你是在修改一个文件,参考当前已有格式。如果是写新的代码,参考目录下或项目中其它文件。还在犹豫的话,就不要加空格了。
if (condition) { // 圆括号里没有空格.
... // 2 空格缩进.
} else if (...) { // else 与 if 的右括号同一行.
...
} else {
...
}
如果你更喜欢在圆括号内部加空格:
if ( condition ) { // 圆括号与空格紧邻 - 不常见
... // 2 空格缩进.
} else { // else 与 if 的右括号同一行.
...
}
注意所有情况下if
和左圆括号间都有个空格。右圆括号和左大括号之间也要有个空格:
if(condition) // 差 - IF 后面没空格.
if (condition){ // 差 - { 前面没空格.
if(condition){ // 变本加厉地差.
if (condition) { // 好 - IF 和 { 都与空格紧邻.
如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用else
子句时使用:
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();
如果语句有else
分支则不允许:
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();
通常,单行语句不需要使用大括号,如果你喜欢用也没问题;复杂的条件或循环语句用大括号可读性会更好。也有一些项目要求if
必须总是使用大括号:
if (condition)
DoSomething(); // 2 空格缩进.
if (condition) {
DoSomething(); // 2 空格缩进.
}
但如果语句中某个if-else
分支使用了大括号的话,其它分支也必须使用:
// 不可以这样子 - IF 有大括号 ELSE 却没有.
if (condition) {
foo;
} else
bar;
// 不可以这样子 - ELSE 有大括号 IF 却没有.
if (condition)
foo;
else {
bar;
}
// 只要其中一个分支用了大括号, 两个分支都要用上大括号.
if (condition) {
foo;
} else {
bar;
}
switch
语句可以使用大括号分段,以表明font color=red>case
之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用font color=red>{}
或font color=red>switch
。
switch
语句中的case
块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。
如果有不满足case
条件的枚举值,switch
应该总是包含一个default
匹配(如果有输入值没有case
去处理,编译器将给出warning)。如果default
应该永远执行不到,简单的加条assert
:
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}
在单语句循环里,括号可用可不用:
for (int i = 0; i < kSomeNumber; ++i)
printf("I love you\n");
for (int i = 0; i < kSomeNumber; ++i) {
printf("I take it back\n");
}
空循环体应使用{}
或continue
,而不是一个简单的分号。
while (condition) {
// 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体.
while (condition) continue; // 可 - contunue 表明没有逻辑.
while (condition); // 差 - 看起来仅仅只是 while/loop 的部分之一.
句点或箭头前后不要有空格。指针/地址操作符(*,&
)之后不能有空格。
下面是指针和引用表达式的正确使用范例:
x = *p;
p = &x;
x = r.y;
x = r->y;
注意:
*
或&
后没有空格.在声明指针变量或参数时,星号与类型或变量名紧挨都可以:
// 好, 空格前置.
char *c;
const string &str;
// 好, 空格后置.
char* c;
const string& str;
int x, *y; // 不允许 - 在多重声明中不能使用 & 或 *
char * c; // 差 - * 两边都有空格
const string & str; // 差 - & 两边都有空格.
在单个文件内要保持风格一致,所以,如果是修改现有文件,要遵照该文件的风格。
如果一个布尔表达式超过标准行宽,断行方式要统一一下。
下例中,逻辑与(&&
)操作符总位于行尾:
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
注意,上例的逻辑与(&&
)操作符均位于行尾。这个格式在Google里很常见,虽然把所有操作符放在开头也可以。可以考虑额外插入圆括号,合理使用的话对增强可读性是很有帮助的。此外,直接用符号形式的操作符,比如&&
和~
,不要用词语形式的and
和compl
。
不要在return
表达式里加上非必须的圆括号。
只有在写x = expr
要加上括号的时候才在return expr;
里使用括号。
return result; // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
another_condition);
return (value); // 毕竟您从来不会写 var = (value);
return(result); // return 可不是函数!
用=
,()
和{}
均可。
您可以用=
,()
和{}
,以下的例子都是正确的:
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); // 内容为 100 个 1 的向量.
vector<int> v{100, 1}; // 内容为 100 和 1 的向量.
此外,列表初始化不允许整型类型的四舍五入,这可以用来避免一些类型上的编程失误。
int pi(3.14); // 好 - pi == 3.
int pi{3.14}; // 编译错误: 缩窄转换.
预处理指令不要缩进,从行首开始。
即使预处理指令位于缩进代码块中,指令也应从行首开始。
// 好 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
# if NOTIFY // 非必要 - # 后跟空格
NotifyClient();
# endif
#endif
BackToNormal();
}
// 差 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - ”#endif“不要缩进
BackToNormal();
}
访问控制块的声明依次序是public:,protected:,private:,每个都缩进 1 个空格。
类声明(下面的代码中缺少注释,参考类注释)的基本格式如下:
class MyClass : public OtherClass { public: // 注意有一个空格的缩进 MyClass(); // 标准的两空格缩进 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_; };
注意事项:
构造函数初始化列表放在同一行或按四格缩进并排多行。
下面两种初始值列表方式都可以接受:
// 如果所有变量能放在同一行: MyClass::MyClass(int var) : some_var_(var) { DoSomething(); } // 如果不能放在同一行, // 必须置于冒号后, 并缩进 4 个空格 MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { DoSomething(); } // 如果初始化列表需要置于多行, 将每一个成员放在单独的一行 // 并逐行对齐 MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up DoSomething(); } // 右大括号 } 可以和左大括号 { 放在同一行 // 如果这样做合适的话 MyClass::MyClass(int var) : some_var_(var) {}
命名空间内容不缩进。
命名空间不要增加额外的缩进层次,例如:
namespace {
void foo() { // 正确. 命名空间内没有额外的缩进.
...
}
} // namespace
不要在命名空间内缩进:
namespace {
// 错, 缩进多余了.
void foo() {
...
}
} // namespace
声明嵌套命名空间时,每个命名空间都独立成行。
namespace foo {
namespace bar {
水平留白的使用根据在代码中的位置决定,永远不要在行尾添加没意义的留白。
通用
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;
// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
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;
垂直留白越少越好.
这不仅仅是规则而是原则问题了:不在万不得已,不要使用空行。尤其是: 两个函数定义之间的空行不要超过2行,函数体首尾不要留空行,函数体中也不要随意添加空行。
基本原则是: 同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。但通常是垂直留白越少越好。
下面的规则可以让加入的空行更有效:
前面说明的编程习惯基本都是强制性的。但所有优秀的规则都允许例外,这里就是探讨这些特例。
对于现有不符合既定编程风格的代码可以网开一面。
当你修改使用其他风格的代码时,为了与代码原有风格保持一致可以不使用本指南约定。如果不放心, 可以与代码原作者或现在的负责人员商讨。记住,一致性也包括原有的一致性。
Windows程序员有自己的编程习惯,主要源于Windows头文件和其它Microsoft代码。我们希望任何人都可以顺利读懂你的代码,所以针对所有平台的C++编程只给出一个单独的指南。
如果你习惯使用Windows编码风格,这儿有必要重申一下某些你可能会忘记的指南:
iNum
)。使用 Google命名约定,包括对源文件使用.cc
扩展名。DWORD
、HANDLE
等等。在调用Windows API时这是完全可以接受甚至鼓励的。即使如此,还是尽量使用原有的C++类型,例如使用const TCHAR *
而不是 LPCSTR
。#pragma once
;而应该使用Google的头文件保护规则。 头文件保护的路径应该相对于项目根目录。#program
和__declspec
。使用__declspec(dllimport)
和__declspec(dllxeport)
是允许的,但必须通过宏来使用,比如 DLLIMPORT
和DLLEXPORT
,这样其他人在分享使用这些代码时可以很容易地禁用这些扩展。然而,在Windows上仍然有一些我们偶尔需要违反的规则:
_ATL_NO_EXCEPTIONS
以禁用异常。你需要研究一下是否能够禁用STL的异常,如果无法禁用,可以启用编译器异常。(注意这只是为了编译STL,自己的代码里仍然不应当包含异常处理)。StdAfx.h
或precompile.h
的文件。为了使代码方便与其他项目共享,请避免显式包含此文件(除了在precompile.cc
中),使用/FI
编译器选项以自动包含该文件.resource.h
且只包含宏,这一文件不需要遵守本风格指南。Google用了很多自己实现的技巧/工具使C++代码更加健壮,我们使用C++的方式可能和你在其它地方见到的有所不同。
动态分配出的对象最好有单一且固定的所有主,并通过智能指针传递所有权。
所有权是一种登记/管理动态内存和其它资源的技术。动态分配对象的所有主是一个对象或函数,后者负责确保当前者无用时就自动销毁前者。所有权有时可以共享,此时就由最后一个所有主来负责销毁它。甚至也可以不用共享,在代码中直接把所有权传递给其它对象。
智能指针是一个通过重载*
和->
运算符以表现得如指针一样的类。智能指针类型被用来自动化所有权的登记工作,来确保执行销毁义务到位。std::unique_ptr是C++11新推出的一种智能指针类型,用来表示动态分配出的对象的独一无二的所有权;当std::unique_ptr离开作用域时,对象就会被销毁。std::unique_ptr
不能被复制,但可以把它移动(move)给新所有主。std::unique_ptr](http://en.cppreference.com/w/cpp/memory/unique_ptr)同样表示动态分配对象的所有权,但可以被共享,也可以被复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。
std::unique_ptr
的所有权传递原理是C++11的move语法,后者毕竟是刚刚推出的,容易迷惑程序员。如果必须使用动态分配,那么更倾向于将所有权保持在分配者手中。如果其他地方要使用这个对象,最好传递它的拷贝,或者传递一个不用改变所有权的指针或引用。倾向于使用std::unique_ptr
来明确所有权传递,例如:
std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);
如果没有很好的理由,则不要使用共享所有权。这里的理由可以是为了避免开销昂贵的拷贝操作,但是只有当性能提升非常明显,并且操作的对象是不可变的(比如说std::shared_ptr<const Foo>
)时候,才能这么做。如果确实要使用共享所有权, 建议于使用std::shared_ptr
.
不要使用std::auto_ptr
,使用std::unique_ptr
代替它.
使用cpplint.py
检查风格错误。
cpplint.py
是一个用来分析源文件,能检查出多种风格错误的工具,它不并完美,甚至还会漏报和误报,但它仍然是一个非常有用的工具。在行尾加//NOLINT
或在上一行加// NOLINTNEXTLINE
,可以忽略报错。
某些项目会指导你如何使用他们的项目工具运行 cpplint.py
。如果你参与的项目没有提供,你可以单独下载cpplint.py。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。