//自己翻译的,本想整理一下,但一直没时间,现在就这样放上来吧,有错就批,别客气,呵呵。
C++编码规范
一、组织与策略问题
If builders built buildings the way programmers wrote programs, then the
first woodpecker that came along would destroy civilization.
----Gerald Weinberg
在C和C++的主要传统中,我们认为是一种零基础的习惯.第0条是一个根本的指示,它涵盖了编码规范中我们认为是最基本的建议.
在这介绍性的一节的其余部分中,我们精心选取了其中的少量问题加以阐述,这些大多与编码本身无直接关系,但却是写出可靠代码的基本工具和技术.
在这一节中我们认为最有价值的是第0条不要因小失大.(或:知道什么不需要规范化.)
0. 不要因小失大. (或:知道什么不需要规范化.)
摘要
仅说必要的话:不要坚持个人品味或陈旧的习惯.
讨论
真只是个人品味且不影响正确性或可读性的结论不属于编码标准.任何专业程序员可以很容易地读写稍微不同于他所习惯的格式的代码.
在每个源文件甚至每个工程中都要用一致的格式,因为在几种不同编码风格的代码片段中跳转是很不协调的.但是不要企图在不同项目或公司中坚持一致地格式.
下面是几条通用的结论,这里重要的不是去制定规则,而只是要保持与你维护的文件的风格的一致:
不必明确指定要缩进多少,但要缩进以突出结构:用你喜欢的任意数量的空格缩进,但至少要在同一文件中保持一致.
不必强行保持特定的行长度,但要保持行具有可读性:用你喜欢的任意长度的行长,但不要太长了.研究表明十个单词以内的宽度对眼睛跟踪是最理想的.
不要过度制定命名规则,但要用一致的命名约定:仅有两点必须要做的:a)决不用"隐秘的"名称,也即以一个下划线开头或包括双下划线的名称;以及b)总是用字母全部大写的单词命名宏并且决不要考虑定义一个常用或缩写单词的宏(包括常用模板参数,比如T和U;以#define T anything定义任何东西是极其不好的做法).此外,要用一致的有意义的名称,并按照文件或模块的约定.(若你不能决定你自己的命名约定,试试这种方式:以各单词首字母大写方式命名类、函数和枚举(LikeThis);命名变量时在前者基础上小写第一个单词的首字母(likeThis);命名私有成员变量时在前者方式之后再加一个下划线(linkThis_);以全大写并用一个下划线连接各单词的方式命名宏(LINK_THIS).)
不要规定注释的风格(除非有工具解析特定格式的注释生成文档),但要写有用的注释:如果可以的话以代码来代替注释(见
第16条).不要在注释中重复代码;它们不能被同步维护.要写解释方法和基本原理的启发性的注释.
最后,不要试图坚持陈旧的规则(见
例3和
例4),即使它们曾在旧编码规范出现过.
例子
例1:括号的放置.下面的几种没有任何可读性上的差异.
void using_k_and_r_style() {
// K&R风格
}
void putting_each_brace_on_its_own_line()
{
// 括号独占一行
}
void or_putting_each_brace_on_its_own_line_indented()
{
// 括号独占一行并缩进
}
任何专业程序员都可以不费力地读写上面所列的任何一种风格的代码.但要保持一致性:不要随意的或以晦涩的嵌套方式放置括号,试着去遵循各文件中已有的风格.在本书中,我们的括号有意识的在排版的约束中以最好的可读性的来放置.
例2: 空格与制表符.由于编辑器对制表符解释的不同,它可能被误用,或被理解成凸出,或无缩进,这种情况下一些团队合理地选择禁用制表符.其他同样有名望的团队则合理地允许制表符,而采用纪律来避免其潜在缺点.只要保持一致性:当团队成员维护其他人的代码时,如果你允许使用制表符,确保不要以代码清晰度和可读性为代价(见第6条).如果你禁用制表符,允许编辑器在读入源文件时将空格转换成制表符,以便用户可以在编辑器中使用制表符;但要确保写回文件的时候将它们还原为空格.
例3: 匈牙利命名法. 将类型信息合并到变量名称中的命名方式为类型不安全的语言(尤其是C)带来了一些混合的作用.但这种命名法在面向对象语言中没有好处(只有坏处),尤其在泛型编程中是根本不可能的.因此,不会有哪个C++编码规范会要求用匈牙利命名法,C++编码规范可能合理地将其排除在外.
例4: 单入口,单出口("SESE"). 历史上,一些编码规范要求每个函数有且仅有一个出口,也即一个返回语句.这样的要求在支持异常与析构函数的语言中是过时的,在这些语言中,函数通常都会有许多隐式的出口.取而代之的是,按照像第5条那样直接提倡更简短的函数的标准,这样会很自然地具有更容易理解代码和把握错误的特性.
参考
[BoostLRG] · [Brooks95] $12 · [Constantine95] $29 · [Keffer95] p. 1 ·
[Kernighan99] $1.1, $1.3, $1.6-7 · [Lakos96] $1.4.1, $2.7 · [McConnell93]
$9, $19 · [Stroustrup94] $4.2-3 · [Stroustrup00] $4.9.3, $6.4, $7.8, $C.1
· [Sutter00] $6, $20 · [SuttHysl01]
1. 以高警告级别干净地编译
摘要
将警告铭记于心:使用你的编译器的最高警告级别.要求干净(无警告)的构建.理解全部的警告,并通过修改代码消除警告,而不是通过降低警告级别.
讨论
编译器是你的好朋友.若它由于一个特定的结构而发出一个警告,通常你的代码含有潜在的问题.
成功构建应该是干净的(无警告的).如若不是这样,你将会很快养成快速浏览输出结果的习惯,进而你将错过真正的问题.(见第2条)
消除警告: a)理解它;然后b)更改你的代码去消除警告并让你想让它所做的事情对人和编译器都更清楚.
一定要做这一步,即使一开始程序看起来正确运行了,或者即使你肯定警告是良性的.即使是良性警告也可以使后面的指出真正危险的警告变得隐晦.
例子
例1: 第三方头文件.你不可能去修改一个引起(或许是良性的)警告的库头文件.因此你要在你自己的头文件中包含原始头文件并仅在这个头文件的作用域内选择性的屏蔽掉这些烦人的警告,然后在你其它的项目文件中包含你的这个包装过的头文件.例如(注意这里的警告控制语法在编译器间是不同的):
// 文件: myproj/my_lambda.h -- 包装Boost的lambda.hpp
// 总是使用这个文件,而不直接使用lambda.hpp.
// 注意: 我们的构建现在自动检查: "grep lambda.hpp ".
// Boost.Lambda产生我们所知道的无害的编译警告.
// 当作者修正它时我们将移除下面的#pragma语句,但是这个头文件仍将存在.
//
#pragma warning(push) // 仅屏蔽这个头文件
#pragma warning(disable:4512)
#pragma warning(disable:4180)
#include
#pragma warning(pop) // 恢复原来的警告级别
例2: "未使用过的函数参数."检查以确保你真的不打算用这个函数参数(例如:它可能是占位符以便将来扩展,或是你代码中从未用到但却是必需的标准化函数签名的一部分).如若它不是必需的,只要删除这个函数参数就行了:
// … 不使用提示的用户自定义分配器内部 …
// 警告: "unused parameter 'localityHint'"
pointer allocate( size_type numObjects, const void *localityHint = 0 ) {
return static_cast( mallocShared( numObjects * sizeof(T) ) );
}
// 新版本: 消除警告
pointer allocate( size_type numObjects, const void * /* localityHint */ = 0 ) {
return static_cast( mallocShared( numObjects * sizeof(T) ));
}
例3: "变量定义了但却从未使用."检查确保你真的不想引用这个变量.(一个基于栈的RAII对象经常引起这样的不合理的警告,请见第13条.)如若它不是必需的,通常你可以插入一个变量自求值的表达式使编译器安静(这一求值不会对运行时速度有影响):
// 警告: "变量'lock'定义了但却从未使用."
void Fun() {
Lock lock;
// …
}
// 新版本: 消除了警告
void Fun() {
Lock lock;
lock;
// …
}
例4: "可能使用了未初始化的变量."那就初始化这个变量(见第19条).
例5: "丢失返回语句."即使你的控制流永远也不可能到达函数的末尾,编译器有时也会要求有一个返回语句(例如无限循环、异常抛出语句以及其它类型的返回语句).这也许是一件好事,因为有时你只是认为控制流不会运行到末尾.例如无default的switch语句没有修改的弹性,因些要有一个default语句执行assert( false ) (另见第68和90条):
// 警告: 丢失"return"
int Fun( Color c ) {
switch( c ) {
case Red: return 2;
case Green: return 0;
case Blue:
case Black: return 1;
}
}
// 新版本: 消除警告
int Fun( Color c ) {
switch( c ) {
case Red: return 2;
case Green: return 0;
case Blue:
case Black: return 1;
default: assert( !"should never get here!" ); // !"string"的值为false
return -1;
}
}
例6: "有符号/无符号不匹配."有符号数与无符号数之间的比较与赋值通常不是必需的.改变参与比较的变量的类型以使满足类型匹配要求.最坏情况下,插入一个显式转换.(由于编译器为你插入这样的转换,也会警告你它所做的,所以你最好不要让它出现.)
例外
有时编译器可能发出一个厌烦的甚至欺骗性的警告(比如纯粹的扰乱信息),但没有可提供的方法去消除它,而且去修改代码去消除它可能是不可实现的或是徒劳的工作.在这些罕见的情况下,作为一个团队决策,除去这个只是无聊的警告的烦人的工作是:仅使特定警告无效,并尽可能是局部性的,并写一个清晰的注释文档说明为什么这样做是必要的.
参考
[Meyers97] $48 · [Stroustrup94] $2.6.2
2. 使用自动构建系统
摘要
按(单个)按钮:使用一个无需用户参与的全自动("一键触发")构建系统.
讨论
一个一键触发式构建方法是基本的.它必须进行可靠的和可重复的转换,将你的源文件转换为可交付的程序包.有很多自动构建工具可以使用,没有理由不去用它.挑选一个,使用它.
我们已见过一些忽视了"一键触发"式要求的组织.一些人认为随处点几下鼠标,就可以运行一些工具来注册COM/CORBA服务,或通过手工定制的一个合理构建过程拷贝一些文件.但是你没有时间和精力可以浪费在一些机器能做得更好更快的事情上.你需要一键触发式的自动化的和可靠的构建.
成功的构建应该是没有任何警告的(见第1条).理想化的构建不产生扰乱信息,而仅是一个日志消息:"构建成功完成."
有两个构建模式:增量构建和完全构建.增量构建仅重建自上自增量构建或完全构建以来被修改过的文件.推论:两个连续的增量构建中的后者应该没有任何输出文件;如果有的话,你可能有一个依赖环(见第22条),或者你的构建系统执行了不必要的操作(例如生成不合理的临时文件而只是丢弃它们).
一个工程可以有不同形式的完全构建.考虑用一系列本质的特征确定你的构建的参数;很可能候选者就是目标式体系结构、调试和发布、或更广(基本文件和全部文件和完全安装).一个构建设置可以创建一个产品的基本的可执行文件和库,另一个可能也创建一些辅助文件,一个完全充实的构建也可能创建一个包含你所有文件、可重发布的第三方库和安装代码的安装程序.
随着工程的进行,没有自动构建的花费也在增长.如果你一开始没有用,你将浪费很多时间和资源.更糟糕的是,随着自动构建成为无法抵抗的需求,你将会有比项目一开始更多的压力.
大项目可能有一个"构建师/主管",他的工作就是照料构建系统.
参考
[Brooks95] $13, $19 · [Dewhurst03] $1 · [GnuMake] · [Stroustrup00] $9.1
3. 使用一个版本控制系统
摘要
好记性比不上烂笔头:使用一个版本控制系统(VCS).决不要让检出的文件保留很长时间.一旦你的更新的单元通过测试就尽快检入.确保检入的代码不会破坏整个构建.
讨论
几乎所有不平凡的工程都需要一个以上的开发者和/或超过一周的工作量.在这样的工程中,你将需要比较同一文件的历史版本以确定变化是什么时候(和/或被谁)引入的.你也将需要控制和管理源代码的变化.
当有多个开发者时,几个开发者很可能会在同一时间对同一文件的不同部分进行并行地更改.你需要工具以自动进行文件的检出和恢复,以及在某些时候对并发编辑的合并.VCS自动操作和控制检出,恢复以及合并.VCS比手工做的更快更准确.而且你不用花时间每天的去摆弄那些重复性的工作,你有软件要写.
不要破坏构建.在VCS中的代码必须总是可以成功构建的.
存在很多的版本控制系统可供选择,没有理由不去用它.最便宜和流行的是cvs(见参考).它是一个灵活的工具,具胡TCP/IP访问特性,可选择性的提高安全性(通过用安全外壳SSH作为后端),卓越的脚本管理,甚至有图形接口.许多其它VCS也将cvs作为标准去效仿,或基于它构建新的功能.
例外
从始至终只花一周左右时间的一个程序员的项目或许可以不需要VCS而生存吧.
参考
[BetterSCM] · [Brooks95] $11, $13 · [CVS]
4. 在代码审阅上作投入
摘要
代码审阅:更多双眼睛将会带来更好的质量.展示你的代码,并阅读他人的.你们都将相互学习或受益.
讨论
一个良好的代码审阅过程在许多方面都对你的团队有好处.它可以:
有益的平等的压力可以增加代码质量.
可以找到bugs、移植性不好的代码以及潜在的度量问题.
通过思想的互补培养形成的设计和实现.
带动新成员和初级者迅速提升能力.
在团队中形成共同价值和团队意识.
增加精英、信心、动机和专业自豪心.
许多开发商既不对代码质量和团队质量进行奖赏,也不投入时间和金钱鼓励他们.希望我们从现在起几年都不会食言,但我们感觉到这种趋势正在慢慢改变,部分原因是由于对安全软件的需求的增长.代码审阅正有助于这种趋势,另外也是一个极好的(和免费的)内部训练方法.
即使你的雇主还不支持代码审阅方法,你也要增加管理知识(提示:要开始,给他们看这本书)以及无论如何要尽你最大努力去安排时间并引导审阅的进行.这时间是值得花的.
将代码审阅作为你的软件开发周期的一项常规程序.如果你和你的队友赞同基于激励(也可能是挫折)的奖惩制度,就会好得多.
不要做得太形式化了,写一封简单的邮件就足够将代码审阅做得很好了.这会使你更容易跟踪自己的进程以及避免重复.
当审阅他人的代码时,你可能想在旁边保留一个供参考的清单.窃以为一个好的清单可能正是你正在读的这本书的目录表.满意吧!
摘要:我们知道我们在给"唱诗班"布道,但是不得不说.你们的自负或自我主义也许讨厌代码审阅,但你们中的少量天才程序员喜欢它,因为它会有成效并使代码更好,使程序更强健.
参考
[Constantine95] $10, $22, $33 · [McConnell93] $24 · [MozillaCRFAQ]