前言
l 本规范不观注于代码那些和正确性相关的方面,它只是统一规定了代码的风格。
l 本规范以可执行性为重,而不是追求大、全。太厚的编程规范,没人可以记清,自然也就无法良好执行。本规范的条目全部能打印在一页A4纸内。
l 本规范主要是推行正确的编程思想,要让大家在编码规范没有“明确规定”的情况下,能根据此思想自行推导合适的做法。
l 本规范的好多条款,是在宣贯“减少代码复杂度”的这样一个思想,代码复杂度的降低,能顺带减少大量编码错误。
l 本规范的实施,能让代码在“可维护性”、“可移植性”方面获得提高。这是代码的“内在质量”。它能在以后修改故障、增加功能上进行得更轻松,从而提高生产力的。
l 1.1~1.5是编程中的基本原则,应该尽量多采用。列为“原则”是因为它难以进行检查。必须依靠大家的自觉了。
l 对已有代码或者外购代码,尽量保持原风格,发生完整函数重写则建议遵守“规则2.4函数平均长度不超过50行”。
l 关于C语言正确性的话题,请自行参考《C FAQ / C 语言常见问题集》或者《C语言编程常见问题解答》
l 关于C++的更多话题,请自行参考《(More) Effective C++》、《C++编程规范:101条规则、准则与最佳实践》
原则1.1 首先考虑正确性、健壮性,其次考虑可维护性、可移植性,最后考虑效率和资源。
原则1.2 代码应该简单、清晰,显式表达意图,避免复杂的编程技巧。
原则1.3 在同一层次编写代码,不要用“如何做”取代“做什么”。
原则1.4 业务领域内有含义的类型,应该进行类型化,而不是直接使用INT16U等“可移植类型”。除非是为了和系统库函数对接,否则不要使用int等内建类型。
原则1.5 不要使用ANSI C保留的或者未定义的行为,不要使用编译器特定扩展功能。
规则2.1 遵循统一的布局顺序来书写实现文件、头文件和类。
规则2.2全局函数和全局变量,只在唯一一个头文件里存在前置声明,不得在实现文件内extern来访问它们。
规则2.3 缩进单位为4个空格。
规则2.4函数平均长度不超过50行。(增强:平均长度不超过20行,最长不超过50行)
规则2.5类内实现的成员函数,函数体有效语句不得超过3行。
规则3.1 宏、常量,全部使用大写字母来命名。
规则3.2在C语言代码中,有名结构和联合必须类型化。
规则3.3使用骆驼式命名。名字要体现其功能,核心信息放在名字的前部显眼位置。建议:使用g_、s_前缀来区分变量的活动范围,不再使用匈牙利命名法,类的私有数据成员以_结束。
规则4.1 一行只定义一个变量,一个变量只执行一个功能。
规则4.2 变量使用前必须先赋初值。简单局部变量在定义时赋初值;复杂变量在定义块结束处立即赋初值;动态申请的内存在申请完成后立即赋初值。
规则5.1一行只执行一条语句,不要使用“,运算符”来合并语句。
规则5.2每行字符不得多于132个。
规则5.3 换行在优先级较低的运算符后进行,新行和同级的操作数对齐。
规则5.4 同组的赋值运算,按=对齐。
规则5.5一元运算符及.、->、[]、()运算符和它们的操作数之间不加空格。
规则5.6 二元、三元运算符和它们的各操作数之间至少加一个空格,“,运算符”前跟后空。
规则6.1 if、for、while、do、switch语句独自占一行,不管有多少执行体语句,后面必须加{},并且{与}各占一行且对齐。Do{}while的while直接接在}后面不另起一行。
规则6.2 if语句内不能直接写赋值语句。
规则6.3 if内的条件语句不得超过3个条件表达式。
规则6.4 for循环语句,初始化语句只能对循环变量赋初值;循环体内不得修改循环变量。
规则6.5 switch语句,整体用{}包起来,case缩进一级;每个case分支用{}包起来,内部再缩进一级;最后一个分支必须是default分支;每个分支必须使用break或者return结尾。
规则6.6 if、for、do/while、switch累计不超过3重。
规则6.7 含++、--运算符和复合运算符(+=等)的语句内,不得再有二元运算符。
规则6.8用()明确运算符的求值顺序。
规则7.1 代码应该使用精心命名的变量名和函数名实现自注释,以减少注释。
规则7.2 在意愿层次上进行注释,表达代码本身表达不了的信息,解释“为什么”而不是“是什么”
规则7.3 接口函数前必须有注释,说明函数功能,解释各参数等。内部函数不强制加这样的注释。
规则8.1 指针变量移交了内存所有权而自己不立即结束生命周期的,必须明确清为NULL。
规则8.2 未经批准,不得使用goto语句和jmp族函数。
规则8.3 未经批准,不得编写函数型的宏和模板。(trace、assert等属于调试型宏)
章一:基本原则
原则1.1 首先考虑正确性、健壮性,其次考虑可维护性、可移植性,最后考虑效率和资源。 |
毫无疑问,正确和健壮是程序最重要的质量特性,我们正做着一切可能来实现或者提高它们。
可维护性、可移植性的地位提高,效率和资源占用的地位降低则是近20年来,软件业面临的需求变化导致的。CPU等硬件每18个月提高1倍性能;而用户对个性化、定制化的渴望,或者需求变动率,也一直在成倍地增长。
代码的维护期是编写期的10倍,我们经常在若干月后对代码改错或者增加功能;业界的实践证明,尽量地复用代码,可以减少1/3的总开发代价;统计表明,10%的代码消耗了90%的CPU等资源,而程序员对效率瓶颈的预测能力是极差的,编码期对效率或资源的过于看重基本都是在白白浪费精力。
所以,我们推出了这样的一个优先级。
原则1.2 代码应该简单、清晰,显式表达意图,避免复杂的编程技巧。 |
简单就是美,记住代码是给人看的。
20年前就存在的“C语言编程技巧大赛”如今已经改名为“混沌代码大赛”了。
原则1.3 在同一层次编写代码,不要用“如何做”取代“做什么”。 |
用“如何做”取代“做什么”是导致代码冗长、复杂的根源。
由于缺失了“做什么”,又导致了对代码注释强烈需求,需求的急速变化又导致了注释与代码不匹配。。。
简单地进行“做什么”陈述,代码就达到了自注释。
原则1.4业务领域内有含义的类型,应该进行类型化,而不是直接使用INT16U等“可移植类型”。除非是为了和系统库函数对接,否则不要使用int等内建类型。 |
Int等内建类型在各系统下字节长度不确定,代码没有可移植性。
INT16U等“可移植类型”是解决了长度或者说值域方面可移植问题。但是,它没有考虑可维护性方面的提高。
业务领域某个物理上或逻辑上存在的事物,它的类型都应该进行类型化,而不要直接使用INT16U等“可移植类型”。
用“INT16U UserId;”定义时,当系统现在需要支持更多用户而要改用INT32U时,代码的改动量是很大的。如果“typedef INT16U USER_ID; USER_ID UserId;”就好多了。
原则1.5 不要使用ANSI C保留的或者未定义的行为,不要使用编译器特定扩展功能。 |
这是可移植性问题,影响代码复用时的改动工作量。
C89规定外部标识符的前6个字符、内部标识符的前31个字符是有效的。C99扩展到外部标识符的前31个字符、内部标识符的前63个字符是有效的。我们必须在命名变量、函数时,确保前面的31个字符产生区别。
C、C++规定,_开头的标识符和含有__的标识符都是保留给编译器使用的。我们在命名变量、函数时,应该遵守这一点。
Gcc编译器很多自定义运算符,其中非常有用的就是“typeof”运算符,C++标准将在下一版中增加这个运算符。而现状是,使用了这个运算符的代码将无法在其它编译器下编译,甚至会根本没有可替代的解。
另外,作为特例,C89以来就是事实标准,C99正式支持的“柔性数组成员”则是应该大力推广的特性。
struct X
{
int i;
int j[];//编译器不同,或者需要写成int j[0];
};
sizeof(struct X)将等于sizeof(int),这与写成int j[1]比,更容易计算j的元素个数或者数据的总长度。
章二:布局
规则2.1 遵循统一的布局顺序来书写实现文件、头文件和类。 |
文件分解成多个布局区,各布局区间用空行或者注释行分隔。
在不影响正确性的情况下,相同性质的语句集中放置到一个布局区内。
下面是一种示例模板。各项目的最终模板可以根据开发环境的整体风格进行调整。
头文件:
[版权申明]
#ifndef 防重复编译宏名 (一般为“全大写文件名_H”或者“全大写文件名_H_”)
#define 防重复编译宏名
#include区 (系统头文件用<>,其它用””,)
常量定义区
数据结构定义区
函数声明区
全局变量声明区
#endif
实现文件:
[版权申明]
#include XXXX (系统头文件用<>,其它用””,)
常量定义区 (常量定义、数据结构定义量应该非常少,否则应该另提一个文件)
数据结构定义区
static函数声明区
全局变量定义区
函数实现区 (以每个对外接口函数为核心,它所需要的小函数放置在其附近)
类的定义:
public等关键字与{}对齐,其它内容缩进一级。
Struct/class 类名
{
常量定义区
内嵌类定义区
类型typedef区
公有数据成员区(有公有数据成员的类一般只再有构造函数,绝对不该有虚函数)
构造、析构函数区
公有函数区
保护函数区
私有函数区
保护数据成员区
私有数据成员区
};
类的实现:
把类的static数据成员等同全局变量,然后参照“实现文件”模板实现
规则2.2全局函数和全局变量,只在唯一一个头文件里存在前置声明,不得在实现文件内extern来访问它们。 |
只有实现函数或者全局变量的人才能提供其声明。只有他们才拥有维护声明和实现的一致性的全部信息。
规则2.3 缩进单位为4个空格。 |
注意:把编辑器的tab自动转空格功能打开和字体设为等宽字体是每个程序员的基本职业素养。
规则2.4函数平均长度不超过50行。(增强:平均长度不超过20行,最长不超过50行) |
函数行数上的限制,将迫使程序员在同一层次编写代码,把复杂、烦琐的实现细节封装到下一个层次里。
在编写代码时,更能关注主体流程上的正确性,而不是深陷细节中迷失方向;更简短的代码,具有更好的可读性,在数月后或者持续加班状态下仍然能轻松地理解代码;更简短的代码,迫使相似的操作都提取成小函数,有更多的机会进行复用;更简短的代码,更容易理清流程,减少流程上的动作重复。
简短到20行以内的代码,很难容纳下代码傻错误。
规则2.5类内实现的成员函数,函数体有效语句不得超过3行。 |
太长的实现体必然严重干扰对这个类的职责的读解。
章三:命名
规则3.1 宏、常量,全部使用大写字母来命名。 |
这是为了和变量、函数更容易区分。
规则3.2在C语言代码中,有名结构和联合必须类型化。 |
基于限制变量创建的目的,C语言提供了无名结构和联合,它们只能使用一次,对它们进行类型化是没有必要的。
规则3.3使用骆驼式命名。名字要体现其功能,核心信息放在名字的前部显眼位置。建议:使用g_、s_前缀来区分变量的活动范围,不再使用匈牙利命名法,类的私有数据成员以_结束。 |
骆驼式命名法:由前后缀加若干单词或缩写组成,每个单词或缩写的首字母大写,其它字母小写,单词间一般不使用_或其它分隔符。整个命名的首字母,对类型名,大写;对变量名,小写。
全局变量加g_前缀,static变量加s_前缀,指针可以加p前缀也可以不加。
有公有数据成员的类一般最多只再有构造函数,不再存在其它成员。
类的数据成员,不需要使用m_前缀,因为它前面一定有类对象加.或者->操作。
类的私有数据成员,以_后缀结束,这已经是C++界共同的约定了。
表示具体功能的信息放前面,表示模块等信息放后面;不要加入项目、版本等阻碍代码复用的信息。
匈牙利命名法同时耦合了功能和实现细节,不符合单一职责原则。
类型发生变化时,在许多情况下,程序员将简单地“忘记”在全局范围内修改变量的名字;同时,他们也放弃按照匈牙利命名法暗示的类型信息去检查其用途。
源代码是一种交流的方式,问题是,交流的目标是谁?编译器?不,源代码是一个程序员与其它程序员交流的一种媒介。它是一种对意图的表达,一种对期望结果的表达。我们必须努力使交流尽可能的简单和清楚。为了做到这一点,变量名应当反映变量所扮演的角色。而变量的确切类型相形之下居次要地位。一个象sz这样的变量名只能告诉你你所看到的是C风格的字符串。至于该字符串所扮演的角色,你无法通过它来了解。
《代码大全》第二版11.5,P279:目前,匈牙利命名法已经不再得到广泛使用。
《C++编程规范:101条规则、准则与最佳实践》第0条:。。。所以,任何C++编程规范都不应该要求使用匈牙利命名法,而且在规范中选择禁止该记法则是合理的。
章四:变量
规则4.1 一行只定义一个变量,一个变量只执行一个功能。 |
单一职责!
不要怕定义变量太多造成的函数变长。超过5个局部变量一般足以暗示代码没有在同一层次编码。
规则4.2 变量使用前必须先赋初值。简单局部变量在定义时赋初值;复杂变量在定义块结束处立即赋初值;动态申请的内存在申请完成后立即赋初值。 |
C99和C++允许在几乎任何地方定义变量,就是为了能有机会用更有意义的值来初始化变量。
推迟变量的定义,直到你能给它一个有意义的初值。
坚持尽可能晚地定义变量,让它更明确地表述出自己的生命期。
章五:表达式
规则5.1一行只执行一条语句,不要使用“,运算符”来合并语句。 |
单一职责!
规则5.2每行字符不得多于132个。 |
随着显示器的变大和对变量要求用有意义的命名造成变量名的增长,每行字符再要求80个已经不太合适了。
规则5.3 换行在优先级较低的运算符后进行,新行和同级的操作数对齐。 |
规则5.4 同组的赋值运算,按=对齐。 |
如果一起出现的赋值运算语句太多了,那么就该想一想这些赋值动作是不是在同一层次进行的。
规则5.5一元运算符及“.”、“->”、“[]”、“()”运算符和它们的操作数之间不加空格。 |
规则5.5 二元、三元运算符和它们的各操作数之间至少加一个空格,“,”前跟后空。 |
章六:语句
规则6.1 if、for、while、do、switch语句独自占一行,不管有多少执行体语句,后面必须加{},并且{与}各占一行且对齐。Do{}while的while直接接在}后面不另起一行。 |
关于}与谁对齐,一直有2种重要风格:和{对齐以及和关键字对齐。和{对齐,情况更单纯。
另外,强迫{}各占一行,就是为了让代码更快达到规则2.4的限制,让代码没有复杂的机会。
规则6.2 if语句内不能直接写赋值语句。 |
无论如何,出现了赋值语句,代码已经不满足“单一职责原则”。
我并不赞成常量放==语句左边的风格,它对大部分人来说,不利阅读。写和读代码时,99%的人都要为此打断一下思路。
它所谓的好处,靠全公司推行的pclint已经更可靠解决了。
当然,也不需要试图禁止这种风格。
规则6.3 if内的条件语句不得超过3个条件表达式。 |
抽取if里面的条件语句到一个小函数里,函数名具有很好的注释功能。
其实,2个条件表达式起,代码的自注释能力就开始变差,在阅读代码的时候,就已经在靠“怎么做”猜测“做什么”了。
如果标准提供的伪码就是多个条件表达式的,那么可以不改。不过,如果这样的条件表达组合需要多处出现的话,还是提取成公共函数更利于维护。很多标准也仍然在反复修改呢。
规则6.4 for循环语句,初始化语句只能对循环变量赋初值;循环体内不得修改循环变量。 |
凡是循环体内需要修改循环变量的,无疑都不满足for的使用条件,而该改为while族循环。
关于循环语句的选择问题,请看《代码大全》。
规则6.5 switch语句,整体用{}包起来,case缩进一级;每个case分支用{}包起来,内部再缩进一级;最后一个分支必须是default分支;每个分支必须使用break或者return结尾。 |
2个case的合并自然是允许的:
case 1:
case 2:
{
…
break;
}
而fall down是严格禁止的:
case 1:
{
foo();
}
case 2:
{
Bar();
break;
}
Switch/case是唯一可能造成函数写不到50行之内的地方,不过99%的场合都可以把它改成查表实现的,那样代码行数就很少了。
规则6.6 if、for、do/while、switch累计不超过3重。 |
函数复杂度的最大根源就是它们的嵌套层数太多。3重以上,代码可维护性急剧下降,5重以上的代码已经完全不可维护。
规则6.7 含++、--运算符和复合运算符(+=等)的语句内,不得再有二元运算符。 |
这几个运算符是造成某一行代码行混乱的大头。
规则6.8用()明确运算符的求值顺序。 |
没人可以记清全部的优先级表。
另外,记住,()只能明确运算符的运算顺序,运算符的多个操作数间的运算顺序是无法确保的,不要写出对操作数运算实现有要求的代码。
章七:注释
规则7.1 代码应该使用精心命名的变量名和函数名实现自注释,以减少注释。 |
规则7.2 在意愿层次上进行注释,表达代码本身表达不了的信息,解释“为什么”而不是“是什么”。 |
对代码含义的简单重复,好处有限,而在漫长的代码维护期,注释和代码不一致的情况比比皆是,造成了很大的混乱。
规则7.3 接口函数前必须有注释,说明函数功能,解释各参数等。内部函数不强制加这样的注释。 |
程序最容易出错,花了最多时间修改的,都是在接口函数上沟通不畅造成的。所以,接口函数要严格、慎重。
内部函数因为往往没有沟通代价,增加这种注释没有太多用处;强迫增加这种注释,只会使程序员不肯把“任何做”封装成小函数。
所以,我建议内部函数是能不加这样的注释就不加。函数名一定要承担起自注释的任务。
章八:杂项
规则8.1 指针变量移交了内存所有权而自己不立即结束生命周期的,必须明确清为NULL。 |
表达放弃内存所有权的唯一方式就是显式清NULL。
在C、C++语法里,拥有内存所有权的是指针变量,而不是函数或者模块。
注意:窗体类的析构函数里释放内存后还应该显式清NULL,因为受实现细节上的困扰,窗体类在析构后仍然可能响应消息处理函数。
规则8.2 未经批准,不得使用goto语句和jmp族函数。 |
也许,goto可以用于错误处理时统一处理,但是,在这种场合,封装小函数、C++里使用RAII惯用法的做法会更好。
如果在你的项目代码里,你发现了一个goto可以让代码更优雅的场合,请和我(陶东明103593)联系。
规则8.3 未经批准,不得编写函数型的宏和模板。(trace、assert等属于调试型宏) |
宏,基本上无法调试,调试器对它们一无所知。而实现正确、健壮的宏,很需要实力。
宏的“运行效率高”基本没有意义,C++有inline,gcc不但在C里扩展支持了inline,还会对小函数自动实行内联。宏节省了参数入栈时间,对系统效率毫无帮助可言,它从来都不是瓶颈所在。
调试型宏,基本上应该项目提供最基本最通用的,每个模块根据需要,对项目级调试型宏封装,每个人再对模块级的调试宏进行定制封装。
模板,需要更多的实力才能写出正确、可移植的模板。而大量出现模板,往往是过度设计的指示。