项目组一直没有做代码审查,最近有启动这项计划的打算,因此提前复习一下《C++编程规范》,并做一些笔记。我们做任何事通常都先从简单的入手,循序渐进,持续改进,那么做代码审查也不例外,《C++编程规范》又很多,如果一下子突然引入,会对代码编写提出过高的要求,对开发人员的打击比较大,从而可能会影响团队的整个士气,所以我想我们应该从最简单(即容易遵循做到)、最重要的几个规范开始,即追求 【有效性/复杂性】 最大化。
联想到日程安排的十字表格,如法炮制了如下表格,以便分门别类:
A(简单&很重要) | C(复杂&很重要) |
B(简单&较重要) | D(复杂&较重要) |
当然,规范本没有重要与不重要之分,这里这样给其画上这样的标签,只是一个相对的概念,是给我们推进“代码审查”这项工作一个简单的指引,例如先实施A区的规则,一段时间后,当团队成员都习惯了这些规则,再实施B区的规则,基本上按照先易后难的顺序,依次推进。
由于笔者的学识水平,以及经验所致,对一些规则的认识肯定存在偏差与不妥,还请同学批评赐教。
《C++编程规范——101条规则、准则与最佳实践》(C++ Coding Standards——101 Rules, Guidelines and Best Practices)
组织和策略问题
第0条(D): 不要拘泥于小节(又名:了解哪些东西不应该标准化)
是的,有些东西不应该规定过死。但是我们认为,在一些个人风格和喜好方面,保持团队内部的一致性是有好处的。一致性的重要性似乎怎么强调都不过分。
第1条(B):在高警告级别下干净利落地进行编译
将编译器的警告级别调到最高,高度重视警告。编译器是我们的好朋友,如果它对某个构建发出警告,就说明这个构建可能存在潜在的问题。这个问题可能是良性的,也可能是恶性的。我们应该通过修改代码而不是降低警告级别来消除警告。
第2条(C):使用自动构建系统
“一键构建”,甚至使构建服务器根据代码的提交自动进行构建,持续集成,不断交付软件产品,在迭代中完善。尽管现在有了很多开源的自动化构建系统,但是搭建并维护这样一个自动化构建系统,需要团队中有一位经验丰富的高手。
第3条(B):使用版本控制系统(VCS)
VSS, SVN, GIT。
第4条(*):做代码审查
审查代码:更多的关注有助于提高质量。亮出自己的代码,阅读别人的代码。互相学习,彼此都会受益。代码审查无需太形式主义,但一定要做,团队可根据自己的实际情况尝试着去做,在做的过程中,慢慢改进,找到一个符合团队实际需要的方式。CppCheck
设计风格
第5条(C):一个实体应该只有一个紧凑的职责
一次只解决一个问题:只给一个实体(变量、类、函数、名字空间、模块和库)赋予一个定义良好的职责。随着实体变大,其职责范围自然也会扩大,但是职责不应该发散。
第6条(C):正确、简单和清晰第一
软件简单为美(KISS原则:Keep It Simple Software):质量优于速度;简单优于复杂;清晰优于机巧;安全第一。可读性:代码必须是为人编写的,其次才是计算机。
第7条(C):编程中应该知道何时和如何考虑可伸缩性
面对数据的爆炸性增长,应该集中精力改善算法的O(N)复杂度。在这种情况下,小型的优化(例如节约一个赋值、加法或乘法运算)通常无济于事。
第8条(A):不要进行不成熟的优化
第9条(A):不要进行不成熟的劣化
构造既清晰又有效的程序有两种方式:使用抽象(DIP)和库(STL)。
第10条(B):尽量减少全局和共享数据
第11条(C):隐藏信息
第12条(D):懂得何时和如何进行并发性编程
线程安全?并发编程?加锁解锁死锁?这些对我来说还只是属于概念......,鄙视一下自己!
第13条(B):确保资源为对象所拥有。使用显式的RAII和智能指针
利器在手,不要再徒手为之。当然,也要防止智能指针的过度使用。如果只对有限的代码可见(例如函数内部,类内部),原始指针就够用了。
编程风格
第14条(C):宁要编译时和连接时错误,也不要运行时错误
第15条(A):积极的使用const
const是我们的朋友,不变的值更易于理解,跟踪和分析。定义值的时候,应该将const作为默认选项。用mutable成员变量实现逻辑上的不变性,在给某些数据做缓存处理的时候经常使用这一特性。即在类的const成员函数中可以合法的修改类的mutable成员变量。
当然,对于那种通过值传递的函数参数声明为const 纯属多此一举,反而还会引起误解。
第16条(D):避免使用宏
这似乎是C++中一条人人皆知的编程规范。可真正严格遵守的团队并不多(纯属猜想,哈哈)。
第17条(D):避免使用“魔数”
同第16条。
应该用符号常量替换直接写死的字符串(或宏)。将字符串与代码分开(比如将字符串放入一个独立的CPP文件中),这样有利于审查和更新,而且有助于国家化。
第18条(A):尽可能局部地声明变量
避免作用域膨胀。变量的生存期越短越好。因此,尽可能只在首次使用变量之前声明之(通常这时你也有足够的数据对它初始化了)。
第19条(B):总是初始化变量。
这里有两段很好的示例代码:
// 虽然正确但不可取的方式:定义变量时没有初始化
int speedupFactor;
if (condition)
speedupFactor = 2;
else
speedupFactor = -1;
以下两种方式更好一些:
// 可取的方式一:定义变量时即初始化
int speedupFactor = -1;
if (condition)
speedupFactor = 2;
// 较好且简练的方式二:定义变量时即初始化
int speedupFactor = condition ? 2 : -1;
第20条(D):避免函数过长,避免嵌套过深
第21条(C):避免跨编译单元的初始化依赖
第22条(A):尽量减少定义性依赖。避免循环依赖。
尽可能的使用前置声明(forward declaration). Pimpl惯用法对遵循这一规范有实际性的帮助。DIP(依赖倒置原则)。
第23条(A):头文件应该自给自足
各司其责:应该确保每个头文件都能够单独编译。
第24条(A):总是编写内部的#include保护符,决不要编写外部的#include保护符
函数与操作符
第25条(B):正确地选择通过值、(智能)指针或者引用传递参数
第26条(C):保持重载操作符的自然语义
第27条(C):优先使用算术操作符和赋值操作符的标准形式
1)如果要定义 a+b,也应该定义 a+=b。一般利用后者实现前者,即赋值形式的操作符完成实际的工作,非赋值形式的操作符调用赋值形式的操作符。2)如果可能,优先选择将这些操作符函数定义为非成员函数,并将其和要类型T放入同一个名字空间中。3)非成员函数返回值或引用,成员函数返回引用。
帖几段示例代码:
// 成员函数 @=
T& operator@=(const T& rhs)
{
//.....具体的实现代码.....
return *this;
}
// 非成员函数 @
T operator@(const T& lhs, const T& rhs)
{
T temp = lhs;
return temp @= rhs;
}
// 非成员函数 @= 返回输入参数的引用
T& operator@=(T& lhs, const T& rhs)
{
// ......具体的实现代码......
return lhs; // 返回输入参数的引用
}
// 非成员函数 @
T operator@(T lhs, const T& rhs)
{
return lhs @= rhs;
}
第28条(A):优先使用++和--的标准形式。优先使用前缀形式
如果定义了++C,也应该定义 C++,而且应该用前缀形式实现后缀形式。
标准形式,示例代码:
// 前缀形式
T& operator++()
{
// ......执行递增的实现代码
return *this; // 返回递增后的新值
}
// 后缀形式
T operator++(int) // 返回值
{
T oldT(*this); // 先保存原值
++(*this); // 调用前缀形式执行递增
return oldT; // 返回原值
}
第29条(D):考虑重载以避免隐含类型转换
第30条(A):避免重载操作符&&, || 和,(逗号)
第31条(A):不要编写依赖于函数参数求值顺序的代码
调用函数时,参数的求值顺序是悬而未定的。