不要过多使用实现继承,组合通常更合适一些。努力做到只在“是一个”("is-a",译者注, 其他"has-a"情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,才令
必要的话,令析构函数为 virtual,必要是指,如果该类具有虚函数,其析构函数应该为虚函数。 |
译者注:至于子类没有额外数据成员,甚至父类也没有任何数据成员的特殊情况下,析 构函数的调用是否必要是语义争论,从编程设计规范的角度看,在含有虚函数的父类中, 定义虚析构函数绝对必要。 |
限定仅在子类访问的成员函数为 protected,需要注意的是数据成员应始终为私有。 |
当重定义派生的虚函数时,在派生类中明确声明其为 virtual。根本原因:如果遗漏 virtual, 阅读者需要检索类的所有祖先以确定该函数是否为虚函数(译者注,虽然不影响其为虚函数的 本质)。 |
7. 多重继承(Multiple Inheritance) |
真正需要用到多重实现继承(multiple implementation inheritance)的时候非常少,只有 |
当最多一个基类中含有实现,其他基类都是以 Interface为后缀的纯接口类时才会使用多重 继承。 |
定义:多重继承允许子类拥有多个基类,要将作为纯接口的基类和具有实现的基类区别开来。 优点:相比单继承,多重实现继承可令你重用更多代码。 |
缺点:真正需要用到多重实现继承的时候非常少,多重实现继承看上去是不错的解决方案,通 常可以找到更加明确、清晰的、不同的解决方案。 |
结论:只有当所有超类(superclass)除第一个外都是纯接口时才能使用多重继承。为确保它 们是纯接口,这些类必须以 Interface为后缀。 |
注意:关于此规则,Windows下有种例外情况(译者注,将在本译文最后一篇的规则例外中 阐述)。 |
接口是指满足特定条件的类,这些类以 Interface为后缀(非必需)。 定义:当一个类满足以下要求时,称之为纯接口: |
1) 只有纯虚函数("=0")和静态函数(下文提到的析构函数除外); 2) 没有非静态数据成员; |
3) 没有定义任何构造函数。如果有,也不含参数,并且为 protected; 4) 如果是子类,也只能继承满足上述条件并以 Interface为后缀的类。 |
接口类不能被直接实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数(作为第1条规则的例外,析构函数不能是纯虚函数)。具体细节可 |
参考 Stroustrup的《TheC++Programming Language, 3rd edition》第12.4节。
优点:以 Interface为后缀可令他人知道不能为该接口类增加实现函数或非静态数据成员,这 一点对于多重继承尤其重要。另外,对于 Java程序员来说,接口的概念已经深入人心。 |
缺点:Interface后缀增加了类名长度,为阅读和理解带来不便,同时,接口特性作为实现细 节不应暴露给客户。 |
结论:。只有在满足上述需要时,类才以 Interface结尾,但反过来,满足上述需要的类未必 一定以 Interface结尾。 |
9. 操作符重载(Operator Overloading) |
定义:一个类可以定义诸如+、/等操作符,使其可以像内建类型一样直接使用。 |
优点:使代码看上去更加直观,就像内建类型(如 int)那样,重载操作符使那些 Equals()、Add()等黯淡无光的函数名好玩多了。为了使一些模板函数正确工作,你可能需要 定义操作符。 |
缺点:虽然操作符重载令代码更加直观,但也有一些不足 |
1) 混淆直觉,让你误以为一些耗时的操作像内建操作那样轻巧; 2) 查找重载操作符的调用处更加困难,查找 Equals()显然比同等调用==容易的多; |
3) 有的操作符可以对指针进行操作,容易导致 bugs,Foo + 4做的是一件事,而&Foo + 4 可能做的是完全不同的另一件事,对于二者,编译器都不会报错,使其很难调试; |
4) 重载还有令你吃惊的副作用,比如,重载操作符&的类不能被前置声明。 结论: |
一般不要重载操作符,尤其是赋值操作(operator=)比较阴险,应避免重载。如果需要的话, 可以定义类似Equals()、CopyFrom()等函数。 |
然而,极少数情况下需要重载操作符以便与模板或“标准”C++类衔接(如 |
operator<<(ostream&, const T&)),如果被证明是正当的尚可接受,但你要尽可能避免 这样做。尤其是不要仅仅为了在 STL容器中作为 key使用就重载 operator==或 operator<,取而代之,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型。 |
有些 STL算法确实需要重载 operator==时可以这么做,不要忘了提供文档说明原因。 参考拷贝构造函数和函数重载。 |
将数据成员私有化,并提供相关存取函数,如定义变量 foo_及取值函数foo()、赋值函数 set_foo()。 |
11. 声明次序(Declaration Order) |
在类中使用特定的声明次序:public:在 private:之前,成员函数在数据成员(变量)前。 |
定义次序如下:public:、protected:、private:,如果那一块没有,直接忽略即可。 |
每一块中,声明次序一般如下: 1) typedefs和 enums; 2) 常量; |
5) 成员函数,含静态成员函数; 6) 数据成员,含静态数据成员。 |
宏 DISALLOW_COPY_AND_ASSIGN置于 private:块之后,作为类的最后部分。参考拷贝 构造函数。 |
不要将大型函数内联到类的定义中,通常,只有那些没有特别意义的或者性能要求高的,并且 是比较短小的函数才被定义为内联函数。更多细节参考译文第一篇的内联函数。 |
12. 编写短小函数(Write Short Functions) |
长函数有时是恰当的,因此对于函数长度并没有严格限制。如果函数超过 40行,可以考虑在 不影响程序结构的情况下将其分割一下。 |
即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以 发现的 bugs。使函数尽量短小、简单,便于他人阅读和修改代码。 |
在处理代码时,你可能会发现复杂的长函数,不要害怕修改现有代码:如果证实这些代码使用、 调试困难,或者你需要使用其中的一小块,考虑将其分割为更加短小、易于管理的若干函数。 |
______________________________________ 译者:关于类的注意事项,总结一下: |
2. 编译器提供的默认构造函数不会对变量进行初始化,如果定义了其他构造函数,编译 |
器不再提供,需要编码者自行提供默认构造函数;
3. 为避免隐式转换,需将单参数构造函数声明为 explicit; |
4. 为避免拷贝构造函数、赋值操作的滥用和编译器自动生成,可目前声明其为 private 且无需实现; |
6. 组合>实现继承>接口继承>私有继承,子类重载的虚函数也要声明virtual关键字, 虽然编译器允许不这样做; |
7. 避免使用多重继承,使用时,除一个基类含有实现外,其他基类均为纯接口; |
8. 接口类类名以 Interface 为后缀,除提供带实现的虚析构函数、静态成员函数外, 其他均为纯虚函数,不定义非静态数据成员,不提供构造函数,提供的话,声明为 protected; |
9. 为降低复杂性,尽量不重载操作符,模板、标准类中使用时提供文档说明; 10. 存取函数一般内联在头文件中; |
11. 声明次序:public->protected->private; 12. 函数体尽量短小、紧凑,功能单一。 |
Google 有很多自己实现的使 C++代码更加健壮的技巧、功能,以及有异于别处的 C++的使 用方式。 |
如果确实需要使用智能指针的话,scoped_ptr 完全可以胜任。在非常特殊的情况下,例如对 STL 容器中对象,你应该只使用 std::tr1::shared_ptr,任何情况下都不要使用 auto_ptr。 |
“智能”指针看上去是指针,其实是附加了语义的对象。以 scoped_ptr 为例,scoped_ptr 被销毁时,删除了它所指向的对象。shared_ptr 也是如此,而且,shared_ptr 实现了引用计 数(reference-counting),从而只有当它所指向的最后一个对象被销毁时,指针才会被删除。 |
一般来说,我们倾向于设计对象隶属明确的代码,最明确的对象隶属是根本不使用指针,直接 将对象作为一个域(field)或局部变量使用。另一种极端是引用计数指针不属于任何对象,这 样设计的问题是容易导致循环引用或其他导致对象无法删除的诡异条件,而且在每一次拷贝或 赋值时连原子操作都会很慢。 |
虽然不推荐这么做,但有些时候,引用计数指针是最简单有效的解决方案。
译者注:看来,Google 所谓的不同之处,在于尽量避免使用智能指针:D,使用时也尽 量局部化,并且,安全第一。 |
1. 引用参数(Reference Arguments) |
定义:在 C语言中,如果函数需要修改变量的值,形参(parameter)必须为指针,如 int foo(int *pval)。在 C++中,函数还可以声明引用形参:int foo(int &val)。 |
优点:定义形参为引用避免了像(*pval)++这样丑陋的代码,像拷贝构造函数这样的应用也是 必需的,而且不像指针那样不接受空指针NULL。 |
缺点:容易引起误解,因为引用在语法上是值却拥有指针的语义。 结论: |
void Foo(const string &in, string *out); |
事实上这是一个硬性约定:输入参数为值或常数引用,输出参数为指针;输入参数可以是常数 指针,但不能使用非常数引用形参。 |
在强调参数不是拷贝而来,在对象生命期内必须一直存在时可以使用常数指针,最好将这些在 注释中详细说明。bind2nd 和 mem_fun等 STL适配器不接受引用形参,这种情况下也必须 以指针形参声明函数。 |
2. 函数重载(Function Overloading) |
仅在输入参数类型不同、功能相同时使用重载函数(含构造函数),不要使用函数重载模仿缺 省函数参数。 |
定义:可以定义一个函数参数类型为 const string&,并定义其重载函数类型为 const char*。 |
void Analyze(const string &text); |
void Analyze(const char *text, size_t textlen); |
};
优点:通过重载不同参数的同名函数,令代码更加直观,模板化代码需要重载,同时为访问者 带来便利。 |
缺点:限制使用重载的一个原因是在特定调用处很难确定到底调用的是哪个函数,另一个原因 是当派生类只重载函数的部分变量会令很多人对继承语义产生困惑。此外在阅读库的客户端代 码时,因缺省函数参数造成不必要的费解。 |
结论:如果你想重载一个函数,考虑让函数名包含参数信息,例如,使用 AppendString()、AppendInt()而不是 Append()。 |
3. 缺省参数(Default Arguments) |
优点:经常用到一个函数带有大量缺省值,偶尔会重写一下这些值,缺省参数为很少涉及的例 外情况提供了少定义一些函数的方便。 |
缺点:大家经常会通过查看现有代码确定如何使用 API,缺省参数使得复制粘贴以前的代码难 以呈现所有参数,当缺省参数不适用于新代码时可能导致重大问题。 |
结论:所有参数必须明确指定,强制程序员考虑 API 和传入的各参数值,避免使用可能不为程 序员所知的缺省参数。 |
4. 变 长 数 组 和 alloca ( Variable-Length Arrays and alloca()) |
优点:变长数组具有浑然天成的语法,变长数组和 alloca()也都很高效。 |
缺点:变长数组和 alloca()不是标准C++的组成部分,更重要的是,它们在堆栈(stack)上 根据数据分配大小可能导致难以发现的内存泄漏:“在我的机器上运行的好好的,到了产品中 却莫名其妙的挂掉了”。 |
使用安全的分配器(allocator),如 scoped_ptr/scoped_array。 |
通常将友元定义在同一文件下,避免读者跑到其他文件中查找其对某个类私有成员的使用。经 常用到友元的一个地方是将 FooBuilder 声明为 Foo 的友元,FooBuilder 以便可以正确构造 Foo 的内部状态,而无需将该状态暴露出来。某些情况下,将一个单元测试用类声明为待测类 |
的友元会很方便。
友元延伸了(但没有打破)类的封装界线,当你希望只允许另一个类访问某个成员时,使用友 元通常比将其声明为 public 要好得多。当然,大多数类应该只提供公共成员与其交互。 |
1) 异常允许上层应用决定如何处理在底层嵌套函数中发生的“不可能发生”的失败,不像出错 代码的记录那么模糊费解; |
2) 应用于其他很多现代语言中,引入异常使得 C++与Python、Java及其他与C++相近的 语言更加兼容; |
3) 许多 C++第三方库使用异常,关闭异常将导致难以与之结合; |
4) 异常是解决构造函数失败的唯一方案,虽然可以通过工厂函数(factory function)或 Init()方法模拟异常,但他们分别需要堆分配或新的“非法”状态; |
5) 在测试框架(testing framework)中,异常确实很好用。 缺点: |
1) 在现有函数中添加 throw语句时,必须检查所有调用处,即使它们至少具有基本的异常安 全保护,或者程序正常结束,永远不可能捕获该异常。例如:if f() calls g() calls h(),h 抛出被 f捕获的异常,g 就要当心了,避免没有完全清理; |
2) 通俗一点说,异常会导致程序控制流(control flow)通过查看代码无法确定:函数有可能 在不确定的地方返回,从而导致代码管理和调试困难,当然,你可以通过规定何时何地如何使 用异常来最小化的降低开销,却给开发人员带来掌握这些规定的负担; |
3) 异常安全需要 RAII和不同编码实践。轻松、正确编写异常安全代码需要大量支撑。允许使 用异常; |
4) 加入异常使二进制执行代码体积变大,增加了编译时长(或许影响不大),还可能增加地址 空间压力; |
5) 异常的实用性可能会刺激开发人员在不恰当的时候抛出异常,或者在不安全的地方从异常中 恢复,例如,非法用户输入可能导致抛出异常。如果允许使用异常会使得这样一篇编程风格指 南长出很多(译者注,这个理由有点牵强:-()! |
从表面上看,使用异常利大于弊,尤其是在新项目中,然而,对于现有代码,引入异常会牵连 到所有依赖代码。如果允许异常在新项目中使用,在跟以前没有使用异常的代码整合时也是一 个麻烦。因为 Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相 当困难。 |
鉴于 Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用的代价多少要大 一点,迁移过程会比较慢,也容易出错。我们也不相信异常的有效替代方案,如错误代码、断 |
言等,都是严重负担。
我们并不是基于哲学或道德层面反对使用异常,而是在实践的基础上。因为我们希望使用 Google上的开源项目,但项目中使用异常会为此带来不便,因为我们也建议不要在 Google 上的开源项目中使用异常,如果我们需要把这些项目推倒重来显然不太现实。 |
对于 Windows代码来说,这一点有个例外(等到最后一篇吧:D)。 |
译者注:对于异常处理,显然不是短短几句话能够说清楚的,以构造函数为例,很多 C++书籍上都提到当构造失败时只有异常可以处理,Google 禁止使用异常这一点,仅 仅是为了自身的方便,说大了,无非是基于软件管理成本上,实际使用中还是自己决定。 |
7. 运 行 时 类 型 识 别 ( Run-Time Type Information, RTTI) |
定义:RTTI允许程序员在运行时识别 C++类对象的类型。 优点: |
RTTI在某些单元测试中非常有用,如在进行工厂类测试时用于检验一个新建对象是否为期望 的动态类型。 |
缺点:运行时识别类型意味著设计本身有问题,如果你需要在运行期间确定一个对象的类型, 这通常说明你需要重新考虑你的类的设计。 |
除单元测试外,不要使用 RTTI,如果你发现需要所写代码因对象类型不同而动作各异的话, 考虑换一种方式识别对象类型。 |
虚函数可以实现随子类类型不同而执行不同代码,工作都是交给对象本身去完成。 |
如果工作在对象之外的代码中完成,考虑双重分发方案,如 Visitor 模式,可以方便的在对象 本身之外确定类的类型。 |
如果你认为上面的方法你掌握不了,可以使用 RTTI,但务必请三思,不要去手工实现一个貌 似RTTI的方案(RTTI-like workaround),我们反对使用 RTTI,同样反对贴上类型标签的 貌似类继承的替代方案(译者注,使用就使用吧,不使用也不要造轮子:D)。 |
使用 static_cast<>()等 C++的类型转换,不要使用 int y = (int)x 或int y = int(x);。 |
定义:C++引入了有别于 C的不同类型的类型转换操作。
优点:C语言的类型转换问题在于操作比较含糊:有时是在做强制转换(如(int)3.5),有 时是在做类型转换(如(int)"hello")。另外,C++的类型转换查找更容易、更醒目。 |
结论:使用 C++风格而不要使用 C风格类型转换。 |
1) static_cast:和 C风格转换相似可做值的强制转换,或指针的父类到子类的明确的向上转 换; |
3) reinterpret_cast:指针类型和整型或其他指针间不安全的相互转换,仅在你对所做一切了 然于心时使用; |
4) dynamic_cast:除测试外不要使用,除单元测试外,如果你需要在运行时确定类型信息, 说明设计有缺陷(参考 RTTI)。 |
定义:流是 printf()和 scanf()的替代。 |
优点:有了流,在输出时不需要关心对象的类型,不用担心格式化字符串与参数列表不匹配 (虽然在 gcc中使用 printf也不存在这个问题),打开、关闭对应文件时,流可以自动构造、 析构。 |
缺点:流使得 pread()等功能函数很难执行,如果不使用 printf之类的函数而是使用流很 难对格式进行操作(尤其是常用的格式字符串%.*s),流不支持字符串操作符重新定序 (%1s),而这一点对国际化很有用。 |
不要使用流,除非是日志接口需要,使用 printf之类的代替。 使用流还有很多利弊,代码一致性胜过一切,不要在代码中使用流。 拓展讨论: |
对这一条规则存在一些争论,这儿给出深层次原因。回忆唯一性原则(Only One Way):我 们希望在任何时候都只使用一种确定的 I/O 类型,使代码在所有 I/O处保持一致。因此,我们 |
不希望用户来决定是使用流还是 printf + read/write,我们应该决定到底用哪一种方式。 把日志作为例外是因为流非常适合这么做,也有一定的历史原因。 |
流的支持者们主张流是不二之选,但观点并不是那么清晰有力,他们所指出流的所有优势也正 是其劣势所在。流最大的优势是在输出时不需要关心输出对象的类型,这是一个亮点,也是一 个不足:很容易用错类型,而编译器不会报警。使用流时容易造成的一类错误是: |
cout << this; // Prints the address cout << *this; // Prints the contents |
编译器不会报错,因为<<被重载,就因为这一点我们反对使用操作符重载。
有人说 printf的格式化丑陋不堪、易读性差,但流也好不到哪儿去。看看下面两段代码吧, 哪个更加易读? |
cerr << "Error connecting to '" << foo->bar()->hostname.first << ":" << foo->bar()->hostname.second << ": " << strerror(errno); |
fprintf(stderr, "Error connecting to '%s:%u: %s", foo->bar()->hostname.first, foo->bar()->hostname.second, strerror(errno)); |
你可能会说,“把流封装一下就会比较好了”,这儿可以,其他地方呢?而且不要忘了,我们 的目标是使语言尽可能小,而不是添加一些别人需要学习的新的内容。 |
每一种方式都是各有利弊,“没有最好,只有更好”,简单化的教条告诫我们必须从中选择其 一,最后的多数决定是 printf + read/write。 |
10. 前 置 自 增 和 自 减 ( Preincrement and Predecrement) |
对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符。 |
定义:对于变量在自增(++i或i++)或自减(--i或i--)后表达式的值又没有没用到的情 况下,需要确定到底是使用前置还是后置的自增自减。 |
优点:不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高,因为后置 |
的自增自减需要对表达式的值i进行一次拷贝,如果i是迭代器或其他非数值类型,拷贝的代 价是比较大的。既然两种自增方式动作一样(译者注,不考虑表达式的值,相信你知道我在说 什么),为什么不直接使用前置自增呢? |
缺点:C 语言中,当表达式的值没有使用时,传统的做法是使用后置自增,特别是在 for循环 中,有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前。 |
结论:对简单数值(非对象)来说,两种都无所谓,对迭代器和模板类型来说,要使用前置自 增(自减)。 |
11. const 的使用(Use of const) |
我们强烈建议你在任何可以使用的情况下都要使用 const。 |
定义:在声明的变量或参数前加上关键字const用于指明变量值不可修改(如 const int foo),为类中的函数加上 const限定表明该函数不会修改类成员变量的状态(如 class Foo { int Bar(char c) const; };)。 |
优点:人们更容易理解变量是如何使用的,编辑器可以更好地进行类型检测、更好地生成代码。 人们对编写正确的代码更加自信,因为他们知道所调用的函数被限定了能或不能修改变量值。 |
即使是在无锁的多线程编程中,人们也知道什么样的函数是安全的。
缺点:如果你向一个函数传入const变量,函数原型中也必须是 const的(否则变量需要 const_cast类型转换),在调用库函数时这尤其是个麻烦。 |
结论:const变量、数据成员、函数和参数为编译时类型检测增加了一层保障,更好的尽早发 现错误。因此,我们强烈建议在任何可以使用的情况下使用 const: |
1) 如果函数不会修改传入的引用或指针类型的参数,这样的参数应该为 const; |
2) 尽可能将函数声明为 const,访问函数应该总是 const,其他函数如果不会修改任何数据 成员也应该是 const,不要调用非 const函数,不要返回对数据成员的非 const指针或引用; |
3) 如果数据成员在对象构造之后不再改变,可将其定义为 const。 |
然而,也不要对 const过度使用,像const int * const * const x;就有些过了,即 便这样写精确描述了 x,其实写成const int** x 就可以了。 |
关键字mutable可以使用,但是在多线程中是不安全的,使用时首先要考虑线程安全。 const位置: |
有人喜欢 int const *foo形式不喜欢 const int* foo,他们认为前者更加一致因此可读 性更好:遵循了 const总位于其描述的对象(int)之后的原则。但是,一致性原则不适用于 此,“不要过度使用”的权威抵消了一致性使用。将 const放在前面才更易读,因为在自然语 言中形容词(const)是在名词(int)之前的。 |
这是说,我们提倡const在前,并不是要求,但要兼顾代码的一致性! |
C++内建整型中,唯一用到的是 int,如果程序中需要不同大小的变量,可以使用 <stdint.h>中的精确宽度(precise-width)的整型,如 int16_t。 |
定义:C++没有指定整型的大小,通常人们认为 short是 16位,int是 32位,long 是 32 位,long long 是 64位。 |
缺点:C++中整型大小因编译器和体系结构的不同而不同。 结论: |
<stdint.h>定义了 int16_t、uint32_t、int64_t等整型,在需要确定大小的整型时可 以使用它们代替short、unsigned long long 等,在 C整型中,只使用 int。适当情况下, 推荐使用标准类型如 size_t和 ptrdiff_t。 |
最常使用的是,对整数来说,通常不会用到太大,如循环计数等,可以使用普通的 int。你可 以认为 int至少为 32位,但不要认为它会多于 32位,需要 64位整型的话,可以使用 |
int64_t或uint64_t。
不要使用 uint32_t等无符号整型,除非你是在表示一个位组(bitpattern)而不是一个数 值。即使数值不会为负值也不要使用无符号类型,使用断言(assertion,译者注,这一点很 有道理,计算机只会根据变量、返回值等有无符号确定数值正负,仍然无法确定对错)来保护 数据。 |
有些人,包括一些教科书作者,推荐使用无符号类型表示非负数,类型表明了数值取值形式。 但是,在 C语言中,这一优点被由其导致的 bugs所淹没。看看: |
for (unsigned int i = foo.Length()-1; i >= 0; --i) ... 上述代码永远不会终止!有时 gcc会发现该 bug并报警,但通常不会。类似的 bug还会出现 在比较有符合变量和无符号变量时,主要是 C的类型提升机制(type-promotion scheme,C语言中各种内建类型之间的提升转换关系)会致使无符号类型的行为出乎你的意 料。 |
因此,使用断言声明变量为非负数,不要使用无符号型。 |
13. 64 位下的可移植性(64-bit Portability) |
代码在 64位和 32位的系统中,原则上应该都比较友好,尤其对于输出、比较、结构对齐 (structure alignment)来说: |
1) printf()指定的一些类型在 32位和 64位系统上可移植性不是很好,C99标准定义了一 些可移植的格式。不幸的是,MSVC 7.1并非全部支持,而且标准中也有所遗漏。所以有时我 |
们就不得不自己定义丑陋的版本(使用标准风格要包含文件 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" |
%qu, %llu, %llx %"PRIu64",
%"PRIuS", %"PRIxS" C99指定%zu |
注意宏 PRI*会被编译器扩展为独立字符串,因此如果使用非常量的格式化字符串,需要将宏 的值而不是宏名插入格式中,在使用宏 PRI*时同样可以在%后指定长度等信息。例如, printf("x = %30"PRIuS"\n", x)在 32位Linux上将被扩展为 printf("x = %30" "u" "\n", x),编译器会处理为 printf("x = %30u\n", x)。 |
2) 记住 sizeof(void *) != sizeof(int),如果需要一个指针大小的整数要使用 intptr_t。 |
3) 需要对结构对齐加以留心,尤其是对于存储在磁盘上的结构体。在 64位系统中,任何拥有 |
int64_t/uint64_t成员的类/结构体将默认被处理为 8字节对齐。如果32位和 64位代码共 用磁盘上的结构体,需要确保两种体系结构下的结构体的对齐一致。大多数编译器提供了调整 |
结构体对齐的方案。gcc中可使用__attribute__((packed)),MSVC 提供了#pragma pack()和__declspec(align())(译者注,解决方案的项目属性里也可以直接设置)。 |
4) 创建 64位常量时使用 LL 或ULL 作为后缀,如: |
int64_t my_value = 0x123456789LL; uint64_t my_mask = 3ULL << 48; |
5) 如果你确实需要 32位和 64位系统具有不同代码,可以在代码变量前使用。(尽量不要这 么做,使用时尽量使修改局部化)。 |
14. 预处理宏(Preprocessor Macros) |
使用宏时要谨慎,尽量以内联函数、枚举和常量代替之。 |
宏意味着你和编译器看到的代码是不同的,因此可能导致异常行为,尤其是当宏存在于全局作 用域中。 |
值得庆幸的是,C++中,宏不像C中那么必要。宏内联效率关键代码(performance-critical code)可以内联函数替代;宏存储常量可以 const变量替代;宏“缩写”长变量名可以引用 |
替代;使用宏进行条件编译,这个……,最好不要这么做,会令测试更加痛苦(#define防止 头文件重包含当然是个例外)。 |
宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些 特性(如字符串化(stringifying,译者注,使用#)、连接(concatenation,译者注,使用 ##)等等)。但在使用前,仔细考虑一下能不能不使用宏实现同样效果。 |
译者注:关于宏的高级应用,可以参考 C语言宏的高级应用。 下面给出的用法模式可以避免一些使用宏的问题,供使用宏时参考: |
2) 使用前正确#define,使用后正确#undef;
3) 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称; |
4) 不使用会导致不稳定的 C++构造(unbalanced C++ constructs,译者注)的宏,至少 文档说明其行为。 |
整数用 0,实数用 0.0,指针用 NULL,字符(串)用'\0'。 整数用 0,实数用 0.0,这一点是毫无争议的。 |
对于指针(地址值),到底是用 0 还是 NULL,Bjarne Stroustrup建议使用最原始的 0,我 们建议使用看上去像是指针的 NULL,事实上一些 C++编译器(如 gcc 4.1.0)专门提供了 NULL 的定义,可以给出有用的警告,尤其是 sizeof(NULL)和 sizeof(0)不相等的情况。 |
字符(串)用'\0',不仅类型正确而且可读性好。 16. sizeof(sizeof) |
尽可能用 sizeof(varname)代替sizeof(type)。 |
使用 sizeof(varname)是因为当变量类型改变时代码自动同步,有些情况下 sizeof(type)或许有意义,还是要尽量避免,如果变量类型改变的话不能同步。 |
Struct data; memset(&data, 0, sizeof(data)); memset(&data, 0, sizeof(Struct)); |
定义:Boost库集是一个非常受欢迎的、同级评议的(peer-reviewed)、免费的、开源的 C++库。 |
优点:Boost代码质量普遍较高、可移植性好,填补了 C++标准库很多空白,如型别特性 (typetraits)、更完善的绑定(binders)、更好的智能指针,同时还提供了 TR1(标准库 的扩展)的实现。 |
缺点:某些 Boost库提倡的编程实践可读性差,像元程序(metaprogramming)和其他高 级模板技术,以及过度“函数化”("functional")的编程风格。 |
结论:为了向阅读和维护代码的人员提供更好的可读性,我们只允许使用 Boost特性的一个成 熟子集,当前,这些库包括: |
1) Compressed Pair:boost/compressed_pair.hpp;
2) Pointer Container:boost/ptr_container不包括 ptr_array.hpp和序列化 (serialization)。 |
我们会积极考虑添加可以的 Boost特性,所以不必拘泥于该规则。 ______________________________________ 译者:关于 C++特性的注意事项,总结一下: |
1. 对于智能指针,安全第一、方便第二,尽可能局部化(scoped_ptr); 2. 引用形参加上 const,否则使用指针形参; 3. 函数重载的使用要清晰、易读; |
4. 鉴于容易误用,禁止使用缺省函数参数(值得商榷); 5. 禁止使用变长数组; |
7. 为了方便代码管理,禁止使用异常(值得商榷); 8. 禁止使用 RTTI,否则重新设计代码吧; |
9. 使用 C++风格的类型转换,除单元测试外不要使用 dynamic_cast; 10. 使用流还 printf + read/write,it is a problem; 11. 能用前置自增/减不用后置自增/减; |
13. 使用确定大小的整型,除位组外不要使用无符号型; 14. 格式化输出及结构对齐时,注意 32位和 64位的系统差异; 15. 除字符串化、连接外尽量避免使用宏; |
16. 整数用 0,实数用 0.0,指针用 NULL,字符(串)用'\0'; 17. 用 sizeof(varname)代替sizeof(type); 18. 只使用 Boost中被认可的库。 |
最重要的一致性规则是命名管理,命名风格直接可以直接确定命名实体是:类型、变量、函数、
常量、宏等等,无需查找实体声明,我们大脑中的模式匹配引擎依赖于这些命名规则。 |
命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,所以不管你怎么想,规则 总归是规则。 |
1. 通用命名规则(General Naming Rules) |
函数命名、变量命名、文件命名应具有描述性,不要过度缩写,类型和变量应该是名词,函数 名可以用“命令性”动词。 |
尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要,好的命名选择: |
int num_completed_connections; // Good. 丑陋的命名使用模糊的缩写或随意的字符: |
int n; int nerr; int n_comp_conns; |
// Bad - meaningless. // Bad - ambiguous abbreviation. // Bad - ambiguous abbreviation. |
类型和变量名一般为名词:如 FileOpener、num_errors。 |
函数名通常是指令性的,如 OpenFile()、set_num_errors(),访问函数需要描述的更细 致,要与其访问的变量相吻合。 |
除非放到项目外也非常明了,否则不要使用缩写,例如: |
// These show proper names with no abbreviations. int num_dns_connections; // Most people know what "DNS" stands for. int price_count_reader; // OK, price count. Makes sense. |
// Bad! // Abbreviations can be confusing or ambiguous outside a small group. int wgc_connections; // Only your group knows what this stands for. |
int pc_reader; 不要用省略字母的缩写: |
// Lots of things can be abbreviated "pc". |
int error_count; // Good. int error_cnt; // Bad. |
文件名要全部小写,可以包含下划线(_)或短线(-),按项目约定来。 可接受的文件命名: |
不要使用已经存在于/usr/include下的文件名(译者注,对UNIX、Linux等系统而言), 如 db.h。 |
通常,尽量让文件名更加明确,http_server_logs.h就比logs.h要好,定义类时文件名 一般成对出现,如 foo_bar.h和 foo_bar.cc,对应类 FooBar。 |
内联函数必须放在.h文件中,如果内联函数比较短,就直接放在.h中。如果代码比较长,可 以放到以-inl.h结尾的文件中。对于包含大量内联代码的类,可以有三个文件: |
// The class declaration. // The class definition. |
url_table-inl.h // Inline functions that include lots of code. 参考第一篇-inl.h 文件一节。 |
MyExcitingClass、MyExcitingEnum。 |
所有类型命名——类、结构体、类型定义(typedef)、枚举——使用相同约定,例如: |
// classes and structs class UrlTable { ... |
class UrlTableTester { ... struct UrlTableProperties { ... |
// typedefs typedef hash_map<UrlTableProperties *, string> PropertiesMap; |
// enums enum UrlTableErrors { ... |
变量名一律小写,单词间以下划线相连,类的成员变量以下划线结尾,如 |
my_exciting_local_variable、my_exciting_member_variable_。 |
string table_name; // OK - uses underscore. |
string tablename; // OK - all lowercase.
string tableName; // Bad - mixed case. 类数据成员: |
结构体的数据成员可以和普通变量一样,不用像类那样接下划线: |
struct UrlTableProperties { string name; int num_entries; |
对全局变量没有特别要求,少用就好,可以以 g_或其他易与局部变量区分的标志为前缀。 |
所有编译时常量(无论是局部的、全局的还是类中的)和其他变量保持些许区别,k 后接大写 字母开头的单词: |
const int kDaysInAWeek = 7; |
普通函数(regular functions,译者注,这里与访问函数等特殊函数相对)大小写混合,存取 函数(accessors and mutators)则要求与变量名匹配: |
MyExcitingFunction()、MyExcitingMethod()、my_exciting_member_variable ()、set_my_exciting_member_variable()。 |
函数名以大写字母开头,每个单词首字母大写,没有下划线: |
AddTableEntry() DeleteUrl() |
存取函数要与存取的变量名匹配,这儿摘录一个拥有实例变量 num_entries_的类: |
... int num_entries() const { return num_entries_; } void set_num_entries(int num_entries) { num_entries_ = num_entries; } |
private: int num_entries_; }; |
其他短小的内联函数名也可以使用小写字母,例如,在循环中调用这样的函数甚至都不需要缓 存其值,小写命名就可以接受。 |
译者注:从这一点上可以看出,小写的函数名意味着可以直接内联使用。 |
命名空间的名称是全小写的,其命名基于项目名称和目录结构: |
关于命名空间的讨论和如何命名,参考第二篇命名空间。 |
8. 枚举命名(Enumerator Names) |
枚举值应全部大写,单词间以下划线相连:MY_EXCITING_ENUM_VALUE。 枚举名称属于类型,因此大小写混合:UrlTableErrors。 |
enum UrlTableErrors { OK = 0, |
ERROR_OUT_OF_MEMORY, ERROR_MALFORMED_INPUT, }; |
MY_MACRO_THAT_SCARES_SMALL_CHILDREN。 |
参考第四篇预处理宏,通常是不使用宏的,如果绝对要用,其命名像枚举命名一样全部大写、 使用下划线: |
#define ROUND(x) ... #define PI_ROUNDED 3.0 MY_EXCITING_ENUM_VALUE |
10. 命名规则例外(Exceptions to Naming Rules) |
当命名与现有 C/C++实体相似的对象时,可参考现有命名约定: |
struct或class,参考 pos sparse_hash_map |
LONGLONG_MAX 常量,类似INT_MAX |
______________________________________ |
译者:命名约定就相对轻松许多,在遵从代码一致性、可读性的前提下,略显随意: |
1. 总体规则:不要随意缩写,如果说 ChangeLocalValue 写作ChgLocVal还有情可 原的话,把ModifyPlayerName 写作MdfPlyNm就太过分了,除函数名可适当为动 词外,其他命名尽量使用清晰易懂的名词; |
3. 变量(含类、结构体成员变量)、文件、命名空间、存取函数等使用全部小写+下划 线,类成员变量以下划线结尾,全局变量以 g_开头; |
4. 普通函数、类型(含类与结构体、枚举类型)、常量等使用大小写混合,不含下划线; 5. 参考现有或相近命名约定。 |
注释虽然写起来很痛苦,但对保证代码可读性至为重要,下面的规则描述了应该注释什么、注 释在哪儿。当然也要记住,注释的确很重要,但最好的代码本身就是文档(self- documenting),类型和变量命名意义明确要比通过注释解释模糊的命名好得多。 |
注释是为别人(下一个需要理解你的代码的人)而写的,认真点吧,那下一个人可能就是你! |
//或/* */都可以,//只是用的更加广泛,在如何注释和注释风格上确保统一。
在每一个文件开头加入版权公告,然后是文件内容描述。 法律公告和作者信息: |
1) 版权(copyright statement):如 Copyright 2008 Google Inc.; |
2) 许可版本(license boilerplate):为项目选择合适的许可证版本,如 Apache 2.0、BSD、LGPL、GPL; |
3) 作者(author line):标识文件的原始作者。 |
如果你对其他人创建的文件做了重大修改,将你的信息添加到作者信息里,这样当其他人对该 文件有疑问时可以知道该联系谁。 |
每一个文件版权许可及作者信息后,都要对文件内容进行注释说明。 |
通常,.h文件要对所声明的类的功能和用法作简单说明,.cc文件包含了更多的实现细节或算 法讨论,如果你感觉这些实现细节或算法讨论对于阅读有帮助,可以把.cc中的注释放到.h中, 并在.cc中指出文档在.h中。 |
不要单纯在.h和.cc间复制注释,复制的注释偏离了实际意义。 |
// 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()); |
class GargantuanTable_Iterator { ... }; |
如果你觉得已经在文件顶部详细描述了该类,想直接简单的来上一句“完整描述见文件顶部” 的话,还是多少在类中加点注释吧。 |
如果类有任何同步前提(synchronization assumptions),文档说明之。如果该类的实例可 被多线程访问,使用时务必注意文档说明。 |
4. 函数注释(Function Comments) |
函数声明处注释描述函数功能,定义处描述函数实现。 函数声明: |
注释于声明之前,描述函数功能及用法,注释使用描述式("Opens the file")而非指令式 ("Open the file");注释只是为了描述函数而不是告诉函数做什么。通常,注释不会描述函 数如何实现,那是定义部分的事情。 |
1) inputs(输入)及outputs(输出); |
2) 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数; 3) 如果函数分配了空间,需要由调用者释放; |
5) 是否存在函数使用的性能隐忧(performance implications); |
6) 如果函数是可重入的(re-entrant),其同步前提(synchronization assumptions)是 什么? |
// 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; |
但不要有无谓冗余或显而易见的注释,下面的注释就没有必要加上“returns false otherwise”,因为已经暗含其中了: |
// Returns true if the table cannot hold any more entries. bool IsTableFull(); |
注释构造/析构函数时,记住,读代码的人知道构造/析构函数是什么,所以“destroys this object”这样的注释是没有意义的。说明构造函数对参数做了什么(例如,是否是指针的所有 者)以及析构函数清理了什么,如果都是无关紧要的内容,直接省掉注释,析构函数前没有注 释是很正常的。
每个函数定义时要以注释说明函数功能和实现要点,如使用的漂亮代码、实现的简要步骤、如 此实现的理由、为什么前半部分要加锁而后半部分不需要。 |
不要从.h文件或其他地方的函数声明处直接复制注释,简要说明函数功能是可以的,但重点要 放在如何实现上。 |
5. 变量注释(Variable Comments) |
通常变量名本身足以很好说明变量用途,特定情况下,需要额外注释说明。 类数据成员: |
每个类数据成员(也叫实例变量或成员变量)应注释说明用途,如果变量可以接受 NULL 或-1 等警戒值(sentinel values),须说明之,如: |
// Keeps track of the total number of entries in the table. // Used to ensure we do not Go over the limit. -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; |
6. 实现注释(Implementation Comments) |
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。 代码前注释: |
// 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; |
比较隐晦的地方要在行尾加入注释,可以在代码之后空两格加行尾注释,如:
// 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. |
注意,有两块注释描述这段代码,当函数返回时注释提及错误已经被记入日志。 |
前后相邻几行都有注释,可以适当调整使之可读性更好: |
// Comment here so the comments line up. DoSomethingElseThatIsLonger(); // Comment here so there are two spaces between // the code and the comment. |
向函数传入、布尔值或整数时,要注释说明含义,或使用常量让代码望文知意,比较一下: |
bool success = CalculateSomething(interesting_value, 10, |
false, NULL); // What are these arguments?? |
bool success = CalculateSomething(interesting_value, |
10, // Default base value. false, // Not the first time we're calling |
const int kDefaultBaseValue = 10; |
const bool kFirstTimeCalling = false; Callback *null_callback = NULL; bool success = CalculateSomething(interesting_value, kDefaultBaseValue, |
kFirstTimeCalling, null_callback); |
注意永远不要用自然语言翻译代码作为注释,要假设读你代码的人 C++比你强:D: |
// Now go through the b array and make sure that if i occurs, // the next element is i+1. |
// Geez. What a useless comment.
7. 标 点 、 拼 写 和 语 法 ( Punctuation, Spelling and Grammar) |
留意标点、拼写和语法,写的好的注释比差的要易读的多。 |
注释一般是包含适当大写和句点(.)的完整的句子,短一点的注释(如代码行尾的注释)可以 随意点,依然要注意风格的一致性。完整的句子可读性更好,也可以说明该注释是完整的而不 是一点不成熟的想法。 |
虽然被别人指出该用分号(semicolon)的时候用了逗号(comma)有点尴尬。清晰易读的 代码还是很重要的,适当的标点、拼写和语法对此会有所帮助。 |
8. TODO 注释(TODO Comments) |
对那些临时的、短期的解决方案,或已经够好但并不完美的代码使用 TODO 注释。 |
这样的注释要使用全大写的字符串 TODO,后面括号(parentheses)里加上你的大名、邮件 地址等,还可以加上冒号(colon):目的是可以根据统一的 TODO 格式进行查找: |
// TODO(kl@gmail.com): Use a "*" here for concatenation operator. // TODO(Zeke) change this to use relations. |
如果加上是为了在“将来某一天做某事”,可以加上一个特定的时间("Fix by November 2005")或事件("Remove this code when all clients can handle XML responses.")。 |
______________________________________ |
1. 关于注释风格,很多 C++的 coders更喜欢行注释,C coders或许对块注释依然情 有独钟,或者在文件头大段大段的注释时使用块注释; |
2. 文件注释可以炫耀你的成就,也是为了捅了篓子别人可以找你; |
3. 注释要言简意赅,不要拖沓冗余,复杂的东西简单化和简单的东西复杂化都是要被鄙 视的; |
4. 对于 Chinese coders来说,用英文注释还是用中文注释,it is a problem,但不 管怎样,注释是为了让别人看懂,难道是为了炫耀编程语言之外的你的母语或外语水平 吗; |
5. 注释不要太乱,适当的缩进才会让人乐意看,但也没有必要规定注释从第几列开始 (我自己写代码的时候总喜欢这样),UNIX/LINUX 下还可以约定是使用 tab还是 space,个人倾向于 space; |
6. TODO很不错,有时候,注释确实是为了标记一些未完成的或完成的不尽如人意的 |
地方,这样一搜索,就知道还有哪些活要干,日志都省了
代码风格和格式确实比较随意,但一个项目中所有人遵循同一风格是非常容易的,作为个人未 必同意下述格式规则的每一处,但整个项目服从统一的编程风格是很重要的,这样做才能让所 有人在阅读和理解代码时更加容易。 |
我们也认识到这条规则是存有争议的,但如此多的代码都遵照这一规则,我们感觉一致性更重 要。 |
优点:提倡该原则的人认为强迫他们调整编辑器窗口大小很野蛮。很多人同时并排开几个窗口, 根本没有多余空间拓宽某个窗口,人们将窗口最大尺寸加以限定,一致使用 80列宽,为什么 要改变呢? |
缺点:反对该原则的人则认为更宽的代码行更易阅读,80列的限制是上个世纪 60年代的大型 机的古板缺陷;现代设备具有更宽的显示屏,很轻松的可以显示更多代码。 |
1) 如果一行注释包含了超过 80字符的命令或 URL,出于复制粘贴的方便可以超过 80字符; 2) 包含长路径的可以超出 80列,尽量避免; |
3) 头文件保护(防止重复包含第一篇)可以无视该原则。 |
2. 非 ASCII 字符(Non-ASCII Characters) |
尽量不使用非 ASCII字符,使用时必须使用 UTF-8格式。 |
哪怕是英文,也不应将用户界面的文本硬编码到源代码中,因此非 ASCII字符要少用。特殊情 况下可以适当包含此类字符,如,代码分析外部数据文件时,可以适当硬编码数据文件中作为 分隔符的非 ASCII字符串;更常用的是(不需要本地化的)单元测试代码可能包含非 ASCII 字符串。此类情况下,应使用 UTF-8格式,因为很多工具都可以理解和处理其编码,十六进制 |
编码也可以,尤其是在增强可读性的情况下——如"\xEF\xBB\xBF"是 Unicode 的 zero- |
width no-break space字符,以 UTF-8格式包含在源文件中是不可见的。
3. 空格还是制表位(Spaces vs. Tabs) |
使用空格进行缩进,不要在代码中使用 tabs,设定编辑器将 tab转为空格。 |
译者注:在前段时间的关于Debian 开发学习日记一文中,曾给出针对 C/C++编码使 用的 vim配置。 |
4. 函 数 声 明 与 定 义 ( Function Declarations and Definitions) |
返回类型和函数名在同一行,合适的话,参数也放在同一行。 函数看上去像这样: |
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... |
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { |
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent |
Type par_name2, Type par_name3) { |
DoSomething(); // 2 space indent ... |
1) 返回值总是和函数名在同一行; 2) 左圆括号(open parenthesis)总是和函数名在同一行; 3) 函数名和左圆括号间没有空格; |
4) 圆括号与参数间没有空格;
5) 左大括号(open curly brace)总在最后一个参数同一行的末尾处; 6) 右大括号(close curly brace)总是单独位于函数最后一行; 7) 右圆括号(close parenthesis)和左大括号间总是有一个空格; 8) 函数声明和实现处的所有形参名称必须保持一致; 9) 所有形参应尽可能对齐; |
如果函数为 const的,关键字const应与最后一个参数位于同一行。 |
// Everything in this function signature fits on a single line ReturnType FunctionName(Type par) const { ... |
// This function signature requires multiple lines, but // the const keyword is on the line with the last parameter. ReturnType ReallyLongFunctionName(Type par1, |
如果有些参数没有用到,在函数定义处将参数名注释起来: |
// Always have named parameters in interfaces. class Shape { |
public: virtual void Rotate(double radians) = 0; |
// Always have named parameters in the declaration. class Circle : public Shape { |
public: virtual void Rotate(double radians); |
// Comment out unused named parameters in definitions. void Circle::Rotate(double /*radians*/) {} // Bad - if someone wants to implement later, it's not clear what the // variable means. |
void Circle::Rotate(double) {} |
译者注:关于 UNIX/Linux 风格为什么要把左大括号置于行尾(.cc 文件的函数实现处, 左大括号位于行首),我的理解是代码看上去比较简约,想想行首除了函数体被一对大 括号封在一起之外,只有右大括号的代码看上去确实也舒服;Windows 风格将左大括 |
号置于行首的优点是匹配情况一目了然。
尽量放在同一行,否则,将实参封装在圆括号中。 函数调用遵循如下形式: |
bool retval = DoSomething(argument1, argument2, argument3); |
如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前 不要留空格: |
bool retval = DoSomething(averyveryveryverylongargument1, argument2, argument3); |
如果函数参数比较多,可以出于可读性的考虑每行只放一个参数: |
bool retval = DoSomething(argument1, |
argument2, argument3, argument4); |
如果函数名太长,以至于超过行最大长度,可以将所有参数独立成行: |
DoSomethingThatRequiresALongFunctionName( very_long_argument1, // 4 space indent |
argument2, argument3, argument4); } |
更提倡不在圆括号中添加空格,关键字else另起一行。 |
对基本条件语句有两种可以接受的格式,一种在圆括号和条件之间有空格,一种没有。 |
最常见的是没有空格的格式,那种都可以,还是一致性为主。如果你是在修改一个文件,参考 当前已有格式;如果是写新的代码,参考目录下或项目中其他文件的格式,还在徘徊的话,就 不要加空格了。 |
if (condition) { // no spaces inside parentheses ... // 2 space indent. |
} else { // The else goes on the same line as the closing brace. ... |
if ( condition ) { // spaces inside parentheses - rare ... // 2 space indent. |
} else { // The else goes on the same line as the closing brace. ... |
注意所有情况下 if和左圆括号间有个空格,右圆括号和左大括号(如果使用的话)间也要有 个空格: |
if(condition) if (condition){ // Bad - space missing before {. if(condition){ // Doubly bad. |
// Bad - space missing after IF. |
if (condition) { // Good - proper space after IF and before {. |
有些条件语句写在同一行以增强可读性,只有当语句简单并且没有使用 else子句时使用: |
if (x == kFoo) return new Foo(); if (x == kBar) return new Bar(); |
// Not allowed - IF statement on one line when there is an ELSE clause if (x) DoThis(); else DoThat(); |
通常,单行语句不需要使用大括号,如果你喜欢也无可厚非,也有人要求if必须使用大括号: |
if (condition) DoSomething(); // 2 space indent. |
if (condition) { DoSomething(); // 2 space indent. |
但如果语句中哪一分支使用了大括号的话,其他部分也必须使用: |
// Not allowed - curly on IF but not ELSE if (condition) { |
// Not allowed - curly on ELSE but not IF if (condition) |
// Curly braces around both IF and ELSE required because // one of the clauses used braces. |
if (condition) {
7. 循 环 和 开 关 选 择 语 句 ( Loops and Switch Statements) |
switch语句可以使用大括号分块;空循环体应使用{}或continue。 |
switch语句中的 case块可以使用大括号也可以不用,取决于你的喜好,使用时要依下文所 |
如果有不满足case枚举条件的值,要总是包含一个 default(如果有输入值没有 case去处 理,编译器将报警)。如果default永不会执行,可以简单的使用 assert: |
switch (var) { case 0: { // 2 space indent |
空循环体应使用{}或continue,而不是一个简单的分号: |
while (condition) { // Repeat test until it returns false. } |
for (int i = 0; i < kSomeNumber; ++i) {} // Good - empty body. while (condition) continue; // Good - continue indicates no logic. while (condition); // Bad - looks like part of do/while loop. |
8. 指 针 和 引 用 表 达 式 ( Pointers and Reference Expressions) |
句点(.)或箭头(->)前后不要有空格,指针/地址操作符(*、&)后不要有空格。
在声明指针变量或参数时,星号与类型或变量名紧挨都可以: |
// These are fine, space preceding. char *c; const string &str; |
// These are fine, space following. |
char* c; // but remember to do "char* c, *d, *e, ...;"! const string& str; |
char * c; // Bad - spaces on both sides of * const string & str; // Bad - spaces on both sides of & |
同一个文件(新建或现有)中起码要保持一致。 译者注:个人比较习惯与变量紧挨的方式。 |
9. 布尔表达式(Boolean Expressions) |
如果一个布尔表达式超过标准行宽(80字符),如果断行要统一一下。 下例中,逻辑与(&&)操作符总位于行尾: |
if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another & last_one) { |
两个逻辑与(&&)操作符都位于行尾,可以考虑额外插入圆括号,合理使用的话对增强可读性 是很有帮助的。 |
译者注:个人比较习惯逻辑运算符位于行首,逻辑关系一目了然,各人喜好而已,至于 加不加圆括号的问题,如果你对优先级了然于胸的话可以不加,但可读性总是差了些。
return表达式中不要使用圆括号。 函数返回时不要使用圆括号: return x; // not return(x); |
11. 变 量 及 数 组 初 始 化 ( Variable and Array Initialization) |
string name("Some Name"); string name = "Some Name"; |
12. 预处理指令(Preprocessor Directives) |
即使预处理指令位于缩进代码块中,指令也应从行首开始。 |
// Good - directives at beginning of line if (lopsided_score) { |
#if DISASTER_PENDING DropEverything(); #endif |
// Correct -- Starts at beginning of line |
BackToNormal(); } // Bad - indented directives |
if (lopsided_score) { #if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line DropEverything(); |
// Wrong! Do not indent "#endif" |
}
声明属性依次序是 public:、protected:、private:,每次缩进1个空格(译者注,为什 么不是两个呢?也有人提倡private在前,对于声明了哪些数据成员一目了然,还有人提倡 依逻辑关系将变量与操作放在一起,都有道理:-))。 |
类声明(对类注释不了解的话,参考第六篇中的类注释一节)的基本格式如下: |
class MyClass : public OtherClass { public: // Note the 1 space indent! |
MyClass(); // Regular 2 space indent. 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); |
1) 所以基类名应在 80列限制下尽量与子类名放在同一行; |
2) 关键词 public:、protected:、private:要缩进1个空格(译者注,MSVC多使用 tab缩进,且这三个关键词没有缩进); |
3) 除第一个关键词(一般是 public)外,其他关键词前空一行,如果类比较小的话 也可以不空; |
5) public放在最前面,然后是 protected和 private; 6) 关于声明次序参考第三篇声明次序一节。 |
14. 初始化列表(Initializer Lists) |
构造函数初始化列表放在同一行或按四格缩进并排几行。
// When it all fits on one line: MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { |
// When it requires multiple lines, indent 4 spaces, putting the colon on // the first initializer line: MyClass::MyClass(int var) |
: some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up |
15. 命名空间格式化(Namespace Formatting) |
命名空间内容不缩进。 命名空间不添加额外缩进层次,例如: namespace { |
void foo() { // Correct. No extra indentation within namespace. ... |
// Wrong. Indented when it should not be. void foo() { ... |
16. 水平留白(Horizontal Whitespace) |
水平留白的使用因地制宜。不要在行尾添加无谓的留白。 普通: |
void f(bool b) { // Open braces should always have a space before them. |
... int i = 0; // Semicolons usually have no space before them. int x[] = { 0 }; // Spaces inside braces for array initialization are |
// optional. If you use them, put them on both sides! |
// Spaces around the colon in inheritance and initializer lists. class Foo : public Bar { public: |
// For inline function implementations, put spaces between the braces // and the implementation itself. |
Foo(int b) : Bar(), baz_(b) {} // No spaces inside empty braces. void Reset() { baz_ = 0; } // Spaces separating braces from implementation. ... |
添加冗余的留白会给其他人编辑时造成额外负担,因此,不要加入多余的空格。如果确定一行 代码已经修改完毕,将多余的空格去掉;或者在专门清理空格时去掉(确信没有其他人在使 用)。 |
// Space after the keyword in conditions and loops. // Spaces around else. |
while (test) {} // There is usually no space inside parentheses. switch (i) { for (int i = 0; i < 5; ++i) { |
switch ( i ) { // Loops and conditions may have spaces inside if ( test ) { // parentheses, but this is rare. Be consistent. for ( int i = 0; i < 5; ++i ) { |
for ( ; i < 5 ; ++i) { // For loops always have a space after the // semicolon, and may have a space before the // semicolon. |
// No space before colon in a switch case. |
case 2: break; // Use a space after a colon if there's code after it. |
// Assignment operators always have spaces around // them. |
// No spaces separating unary operators and their // arguments. |
if (x && !y) ... v = w * x + y / z; // Binary operators usually have spaces around them, |
v = w*x + y/z; v = w * (x + z); |
// but it's okay to remove spaces around factors. // Parentheses should have no spaces inside them. |
// No spaces inside the angle |
y = static_cast<char*>(x); // brackets (< and >), before // <, or between >( in a cast. |
// Spaces between type andpointer are
// okay, but be consistent. // C++ requires a space in > >. // You may optionally make use // symmetric spacing in < <. |
set<list<string> > x; set< list<string> > x; |
17. 垂直留白(Vertical Whitespace) |
这不仅仅是规则而是原则问题了:不是非常有必要的话就不要使用空行。尤其是:不要在两个 函数定义之间空超过 2行,函数体头、尾不要有空行,函数体中也不要随意添加空行。 |
基本原则是:同一屏可以显示越多的代码,程序的控制流就越容易理解。当然,过于密集的代 码块和过于疏松的代码块同样难看,取决于你的判断,但通常是越少越好。 |
函数头、尾不要有空行: void Function() { |
// Unnecessary blank lines before and after |
while (condition) { // Unnecessary blank line after |
// Unnecessary blank line before |
if (condition) { // Some lines of code too small to move to another function, // followed by a blank line. |
} else { // Another block of code |
______________________________________ |
译者:首先说明,对于代码格式,因人、因系统各有优缺点,但同一个项目中遵循同一标准还 是有必要的: |
1. 行宽原则上不超过 80列,把22寸的显示屏都占完,怎么也说不过去; |
2. 尽量不使用非 ASCII字符,如果使用的话,参考 UTF-8格式(尤其是
UNIX/Linux下,Windows下可以考虑宽字符),尽量不将字符串常量耦合到代码中, 比如独立出资源文件,这不仅仅是风格问题了; |
3. UNIX/Linux 下无条件使用空格,MSVC的话使用 Tab也无可厚非; |
4. 函数参数、逻辑条件、初始化列表:要么所有参数和函数名放在同一行,要么所有参 数并排分行; |
5. 除函数定义的左大括号可以置于行首外,包括函数/类/结构体/枚举声明、各种语句 的左大括号置于行尾,所有右大括号独立成行; |
6. ./->操作符前后不留空格,*/&不要前后都留,一个就可,靠左靠右依各人喜好; 7. 预处理指令/命名空间不使用额外缩进,类/结构体/枚举/函数/语句使用缩进; 8. 初始化用=还是()依个人喜好,统一就好; |
前面说明的编码习惯基本是强制性的,但所有优秀的规则都允许例外。 1. 现有不统一代码(Existing Non-conformant Code) 对于现有不符合既定编程风格的代码可以网开一面。 |
当你修改使用其他风格的代码时,为了与代码原有风格保持一致可以不使用本指南约定。如果 不放心可以与代码原作者或现在的负责人员商讨,记住,一致性包括原有的一致性。 |
1. Windows 代码(Windows Code) |
Windows 程序员有自己的编码习惯,主要源于 Windows 的一些头文件和其他 Microsoft 代码。 我们希望任何人都可以顺利读懂你的代码,所以针对所有平台的 C++编码给出一个单独的指 导方案。 |
如果你一直使用 Windows 编码风格的,这儿有必要重申一下某些你可能会忘记的指南(译者 |
注,我怎么感觉像在被洗脑:D)
1) 不要使用匈牙利命名法(Hungarian notation,如定义整型变量为iNum),使用 Google 命名约定,包括对源文件使用.cc扩展名; |
2) Windows定义了很多原有内建类型的同义词(译者注,这一点,我也很反感),如 DWORD、HANDLE 等等,在调用 Windows API时这是完全可以接受甚至鼓励的,但还是尽量 使用原来的 C++类型,例如,使用 const TCHAR *而不是 LPCTSTR; |
3) 使用 Microsoft Visual C++进行编译时,将警告级别设置为 3 或更高,并将所有 warnings 当作errors 处理; |
4) 不要使用#pragma once;作为包含保护,使用 C++标准包含保护,包含保护的文件路 径包含到项目树顶层(译者注,#include<prj_name/public/tools.h>); |
5) 除非万不得已,否则不使用任何不标准的扩展,如#pragma和__declspec,允许使用 __declspec(dllimport)和__declspec(dllexport),但必须通过 DLLIMPORT 和 DLLEXPORT 等宏,以便其他人在共享使用这些代码时容易放弃这些扩展。 |
在 Windows上,只有很少一些偶尔可以不遵守的规则: |
1) 通常我们禁止使用多重继承,但在使用 COM和 ATL/WTL类时可以使用多重继承,为了 执行COM或ATL/WTL类及其接口时可以使用多重实现继承; |
2) 虽然代码中不应使用异常,但在 ATL和部分STL(包括 Visual C++的 STL)中异常被广 |
泛使用,使用 ATL时,应定义_ATL_NO_EXCEPTIONS 以屏蔽异常,你要研究一下是否也屏蔽 掉 STL的异常,如果不屏蔽,开启编译器异常也可以,注意这只是为了编译STL,自己仍然不 要写含异常处理的代码; |
3) 通常每个项目的每个源文件中都包含一个名为 StdAfx.h或precompile.h的头文件方便 头文件预编译,为了使代码方便与其他项目共享,避免显式包含此文件(precompile.cc除 外),使用编译器选项/FI 以自动包含; |
4) 通常名为 resource.h、且只包含宏的资源头文件,不必拘泥于此风格指南。 |
编辑代码时,花点时间看看项目中的其他代码并确定其风格,如果其他代码 if语句中使用空 格,那么你也要使用。如果其中的注释用星号(*)围成一个盒子状,你也这样做: |
/********************************** * Some comments are here. * There may be many lines. |
**********************************/ |
编程风格指南的使用要点在于提供一个公共的编码规范,所有人可以把精力集中在实现内容而 不是表现形式上。我们给出了全局的风格规范,但局部的风格也很重要,如果你在一个文件中 新加的代码和原有代码风格相去甚远的话,这就破坏了文件本身的整体美观也影响阅读,所以 |
要尽量避免 |
|
|
|