DKBA
华为技术有限公司内部技术规范
DKBA 2826-2011.5
C语言编程规范
2011年5月9日发布 2011年5月9日实施
华为技术有限公司
Huawei Technologies Co., Ltd.
版权全部 侵权必究
All rights reserved
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第2页,共61页Page 2 , Total61
修订声明Revision declaration
本规范拟制与解释部门:
本规范的相关系列规范或文件:
相关国际规范或文件一致性:
替代或做废的其它规范或文件:
相关规范或文件的相互关系:
规范号
主要起草部门专家
主要评审部门专家
修订状况
DKBAxxxx.x-xxxx.xx
PSST质量部:
郭曙光00121837
网络:
张伟00118807
周灿00056781
王晶00041937
陈艺彪00036913
IP开发部:
薛治00038309
核心网:
张小林00058208
王德喜00040674
李明胜00042021
软件公司:
文 滔00119601
无线:
刘爱华00162172
中研:
谭洪00162654
PSST质量部:
李重霄00117374
郭永生00120218
核心网:
张进柏00120359
中研:
张建保00116237
无线:
苏光牛00118740
郑铭00118617
陶永祥00120482
软件公司:
周代兵00120359
刘心红00118478
朱文琦00172539
网络:
王玎00168059
黄维东49827
IP开发部:
饶远00152313
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第3页,共61页Page 3 , Total61
目 录Table of Contents
0 规范制订说明 ................................................................................................................................ 5
0.1 前言 ...................................................................................................................................... 5
0.2 代码整体原则 ....................................................................................................................... 5
0.3 规范实施、解释 .................................................................................................................... 6
0.4 术语定义 ............................................................................................................................... 6
1 头文件 ........................................................................................................................................... 6
2 函数 ............................................................................................................................................. 12
3 标识符命名与定义 ....................................................................................................................... 21
3.1 通用命名规则 ..................................................................................................................... 21
3.2 文件命名规则 ..................................................................................................................... 23
3.3 变量命名规则 ..................................................................................................................... 23
3.4 函数命名规则 ..................................................................................................................... 24
3.5 宏的命名规则 ..................................................................................................................... 24
4 变量 ............................................................................................................................................. 25
5 宏、常量...................................................................................................................................... 28
6 质量保证...................................................................................................................................... 32
7 程序效率...................................................................................................................................... 36
8 注释 ............................................................................................................................................. 39
9 排版与格式 .................................................................................................................................. 44
10 表达式 ..................................................................................................................................... 46
11 代码编辑、编译 ...................................................................................................................... 49
12 可测性 ..................................................................................................................................... 50
13 安全性 ..................................................................................................................................... 51
13.1 字符串操做安全 .................................................................................................................. 51
13.2 整数安全 ............................................................................................................................. 52
13.3 格式化输出安全 .................................................................................................................. 56
13.4 文件I/O安全 ........................................................................................................................ 57
13.5 其它 .................................................................................................................................... 59
14 单元测试 ................................................................................................................................. 59
15 可移植性 ................................................................................................................................. 60
16 业界编程规范 .......................................................................................................................... 60
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第4页,共61页Page 4 , Total61
C语言编程规范
范 围:
本规范适用于公司内使用C语言编码的全部软件。本规范自发布之日起生效,之后新编写的和修改的代码应遵照本规范。
简 介:
本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程做出了具体指导。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第5页,共61页Page 5 , Total61
0 规范制订说明
0.1 前言
为提升产品代码质量,指导广大软件开发人员编写出简洁、可维护、可靠、可测试、高效、可移植的代码,编程规范修订工做组分析、总结了我司的各类典型编码问题,并参考了业界编程规范近年来的成果,从新对我司1999年版编程规范进行了梳理、优化、刷新,编写了本规范。
本规范将分为完整版和精简版,完整版将包括更多的样例、规范的解释以及参考材料(what & why),而精简版将只包含规则部分(what)以便查阅。
在本规范的最后,列出了一些业界比较优秀的编程规范,做为延伸阅读参考材料。
0.2 代码整体原则
一、清晰第一
清晰性是易于维护、易于重构的程序必需具有的特征。代码首先是给人读的,好的代码应当能够像文章同样发声朗诵出来。
目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验,维护期变动代码的成本,小型系统是开发期的5倍,大型系统(100万行代码以上)能够达到100倍。业界的调查指出,开发组平均大约一半的人力用于弥补过去的错误,而不是添加新的功能来帮助公司提升竞争力。
“程序必须为阅读它的人而编写,只是顺便用于机器执行。”——Harold Abelson 和 Gerald Jay Sussman
“编写程序应该以人为本,计算机第二。”——Steve McConnell
本规范经过后文中的原则(如头优秀的代码能够自我解释,不经过注释便可轻易读懂/头文件中适合放置接口的声明,不适合放置实现/除了常见的通用缩写之外,不使用单词缩写,不得使用汉语拼音)、规则(如防止局部变量与全局变量同名)等说明清晰的重要性。
通常状况下,代码的可阅读性高于性能,只有肯定性能是瓶颈时,才应该主动优化。
二、简洁为美
简洁就是易于理解而且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。所以,咱们提倡你们经过编写简洁明了的代码来提高代码可靠性。
废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽量提炼成函数。
本规范经过后文中的原则(如文件应当职责单一/一个函数仅完成一件功能)、规则(重复代码应该尽量提炼成函数/避免函数过长,新增函数不超过50行)等说明简洁的重要性。
三、选择合适的风格,与代码原有风格保持一致
产品全部人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽量清晰,是一项很是重要的技能。若是重构/修改其余风格的代码时,比较明智的作法是根据现有代码的现有风格继续编写代码,或者使用格式转换工具进行转
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第6页,共61页Page 6 , Total61
换成公司内部风格。
0.3 规范实施、解释
本规范制定了编写C语言程序的基本原则、规则和建议。
本规范适用于公司内使用C语言编码的全部软件。本规范自发布之日起生效,对之后新编写的和修改的代码应遵照本规范。
本规范由质量体系发布和维护。实施中遇到问题,能够到论坛http://hi3ms.huawei.com/group/1735/threads.html上讨论。
在某些状况下(如BSP软件)须要违反本文档给出的规则时,相关团队必须经过一个正式的流程来评审、决策规则违反的部分,个体程序员不得违反本规范中的相关规则。
0.4 术语定义
原则:编程时必须坚持的指导思想。
规则:编程时强制必须遵照的约定。
建议:编程时必须加以考虑的约定。
说明:对此原则/规则/建议进行必要的解释。
示例:对此原则/规则/建议从正、反两个方面给出例子。
延伸阅读材料:建议进一步阅读的参考材料。
1 头文件
背景
对于C语言来讲,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。
术语定义:
依赖:本章节特指编译依赖。若x.h包含了y.h,则称做x依赖y。依赖关系会进行传导,如x.h包含y.h,而y.h又包含了z.h,则x经过y依赖了z。依赖将致使编译时间的上升。虽然依赖是不可避免的,也是必须的,可是不良的设计会致使整个系统的依赖关系无比复杂,使得任意一个文件的修改都要从新编译整个系统,致使编译时间巨幅上升。
在一个设计良好的系统中,修改一个文件,只须要从新编译数个,甚至是一个文件。
某产品曾经作过一个实验,把全部函数的实现经过工具注释掉,其编译时间只减小了不到10%,究其缘由,在于A包含B,B包含C,C包含D,最终几乎每个源文件都包含了项目组全部的头文件,从而致使绝大部分编译时间都花在解析头文件上。
某产品更有一个“优秀实践”,用于将.c文件经过工具合并成一个比较大的.c文件,从而大幅度提升编译效率。其根本缘由仍是在于经过合并.c文件减小了头文件解析次数。可是,这样的“优秀实践”是对合理划分.c文件的一种破坏。
大部分产品修改一处代码,都得须要编译整个工程,对于TDD之类的实践,要求对于模块级别的编译时间控制在秒级,即便使用分布式编译也难以实现,最终仍然须要合理的划分头文件、以及头文件之间的包含关系,从根本上下降编译时间。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第7页,共61页Page 7 , Total61
《google C++ Style Guide》1.2 头文件依赖 章节也给出了相似的阐述:
若包含了头文件aa.h,则就引入了新的依赖:一旦aa.h被修改,任何直接和间接包含aa.h代码都会被从新编译。若是aa.h又包含了其余头文件如bb.h,那么bb.h的任何改变都将致使全部包含了aa.h的代码被从新编译,在敏捷开发方式下,代码会被频繁构建,漫长的编译时间将极大的阻碍频繁构建。所以,咱们倾向于减小包含头文件,尤为是在头文件中包含头文件,以控制改动代码后的编译时间。
合理的头文件划分体现了系统设计的思想,可是从编程规范的角度看,仍然有一些通用的方法,用来合理规划头文件。本章节介绍的一些方法,对于合理规划头文件会有必定的帮助。
原则1.1 头文件中适合放置接口的声明,不适合放置实现。
说明:头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
内部使用的函数(至关于类的私有方法)声明不该放在头文件中。
内部使用的宏、枚举、结构定义不该放入头文件中。
变量定义不该放在头文件中,应放在.c文件中。
变量的声明尽可能不要放在头文件中,亦即尽可能不要使用全局变量做为接口。变量是模块或单元的内部实现细节,不该经过在头文件中声明的方式直接暴露给外部,应经过函数接口的方式进行对外暴露。 即便必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
延伸阅读材料:《C语言接口与实现》(David R. Hanson 著 傅蓉 周鹏 张昆琪 权威 译 机械工业出版社 2004年1月)(英文版: "C Interfaces and Implementations")
原则1.2 头文件应当职责单一。
说明:头文件过于复杂,依赖过于复杂是致使编译时间过长的主要缘由。不少现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能致使为了在.c中使用一个宏,而包含十几个头文件。
示例:以下是某平台定义WORD类型的头文件:
#include <VXWORKS.H>
#include <KERNELLIB.H>
#include <SEMLIB.H>
#include <INTLIB.H>
#include <TASKLIB.H>
#include <MSGQLIB.H>
#include <STDARG.H>
#include <FIOLIB.H>
#include <STDIO.H>
#include <STDLIB.H>
#include <CTYPE.H>
#include <STRING.H>
#include <ERRNOLIB.H>
#include <TIMERS.H>
#include <MEMLIB.H>
#include <TIME.H>
#include <WDLIB.H>
#include <SYSLIB.H>
#include <TASKHOOKLIB.H>
#include <REBOOTLIB.H>
…
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第8页,共61页Page 8 , Total61
typedef unsigned short WORD;
…
这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不经常使用的头文件。若是工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,因为上述头文件的职责过于庞大,而WORD又是每个文件必须包含的,从而致使stdio.h/syslib.h等可能被没必要要的展开了9900次,大大增长了工程的编译时间。
原则1.3 头文件应向稳定的方向包含。
说明:头文件的包含关系是一种依赖,通常来讲,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
就咱们的产品来讲,依赖的方向应该是:产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,致使平台没法单独编译、发布和测试,是一个很是糟糕的反例。
除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不须要从新编译另一个模块。在这里,咱们假设接口自己是最稳定的。
延伸阅读材料:编者推荐开发人员使用“依赖倒置”原则,即由使用者制定接口,服务提供者实现接口,更具体的描述能够参见《敏捷软件开发:原则、模式与实践》(Robert C.Martin 著 邓辉 译 清华大学出版社2003年9月) 的第二部分“敏捷设计”章节。
规则1.1 每个.c文件应有一个同名.h文件,用于声明须要对外公开的接口。
说明:若是一个.c文件不须要对外公布任何接口,则其就不该当存在,除非它是程序的入口,如main函数所在的文件。
现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部须要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就没法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。
本规则反过来并不必定成立。有些特别简单的头文件,如命令ID定义头文件,不须要有对应的.c存在。
示例:对于以下场景,如在一个.c中存在函数调用关系:
void foo()
{
bar();
}
void bar()
{
Do something;
}
必须在foo以前声明bar,不然会致使编译错误。
这一类的函数声明,应当在.c的头部声明,并声明为static的,以下:
static void bar();
void foo()
{
bar();
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第9页,共61页Page 9 , Total61
}
void bar()
{
Do something;
}
规则1.2 禁止头文件循环依赖。
说明:头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类致使任何一个头文件修改,都致使全部包含了a.h/b.h/c.h的代码所有从新编译一遍。而若是是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会致使包含了b.h/c.h的源代码从新编译。
规则1.3 .c/.h文件禁止包含用不到的头文件。
说明:不少系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了全部头文件,而后发布给各个项目组使用,这种只图一时省事的作法,致使整个系统的编译时间进一步恶化,并对后来人的维护形成了巨大的麻烦。
规则1.4 头文件应当自包含。
说明:简单的说,自包含就是任意一个头文件都可独立编译。若是一个文件包含某个头文件,还要包含另一个头文件才能工做的话,就会增长交流障碍,给这个头文件的用户增添没必要要的负担。
示例:
若是a.h不是自包含的,须要包含b.h才能编译,会带来的危害:
每一个使用a.h头文件的.c文件,为了让引入的a.h的内容编译经过,都要包含额外的头文件b.h。
额外的头文件b.h必须在a.h以前进行包含,这在包含顺序上产生了依赖。
注意:该规则须要与“.c/.h文件禁止包含用不到的头文件”规则一块儿使用,不能为了让a.h自包含,而在a.h中包含没必要要的头文件。a.h要刚刚能够自包含,不能在a.h中多包含任何知足自包含以外的其余头文件。
规则1.5 老是编写内部#include保护符(#define 保护)。
说明:屡次包含一个头文件能够经过认真的设计来避免。若是不能作到这一点,就须要采起阻止头文件内容被包含多于一次的机制。
一般的手段是为每一个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。
全部头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证惟一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。
注:没有在宏最前面加上“_",即便用FILENAME_H代替_FILENAME_H_,是由于通常以"_"和”__"开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。
定义包含保护符时,应该遵照以下规则:
1)保护符使用惟一名称;
2)不要在受保护部分的先后放置代码或者注释。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第10页,共61页Page 10 , Total61
示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按以下方式保护:
#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif
也能够使用以下简单方式保护:
#ifndef TIMER_H
#define TIMER_H
..
#endif
例外状况:头文件的版权声明部分以及头文件的总体注释部分(如阐述此头文件的开发背景、使用注意事项等)能够放在保护符(#ifndef XX_H)前面。
规则1.6 禁止在头文件中定义变量。
说明:在头文件中定义变量,将会因为头文件被其余.c文件包含而致使变量重复定义。
规则1.7 只能经过包含头文件的方式使用其余.c提供的接口,禁止在.c中经过extern的方式使用外部函数接口、变量。
说明:若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中经过#include <b.h>来使用foo。禁止经过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能致使声明和定义不一致。
规则1.8 禁止在extern "C"中包含头文件。
说明:在extern "C"中包含头文件,会致使extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
在extern "C"中包含头文件,可能会致使被包含头文件的原有意图遭到破坏。例如,存在a.h和b.h两个头文件:
#ifndef A_H__
#define A_H__
#ifdef __cplusplus
void foo(int);
#define a(value) foo(value)
#else
void a(int)
#endif
#endif /* A_H__ */
#ifndef B_H__
#define B_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void b();
#ifdef __cplusplus
}
#endif
#endif /* B_H__ */
使用C++预处理器展开b.h,将会获得
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第11页,共61页Page 11 , Total61
extern "C" {
void foo(int);
void b();
}
按照a.h做者的本意,函数foo是一个C++自由函数,其连接规范为"C++"。但在b.h中,因为#include "a.h"被放到了extern "C" { }的内部,函数foo的连接规范被不正确地更改了。
示例:错误的使用方式:
extern “C”
{
#include “xxx.h”
...
}
正确的使用方式:
#include “xxx.h”
extern “C”
{
...
}
建议1.1 一个模块一般包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每个模块提供一个.h,文件名为目录名。
说明:须要注意的是,这个.h并非简单的包含全部内部的.h,它是为了模块使用者的方便,对外总体提供的模块接口。
以Google test(简称GTest)为例,GTest做为一个总体对外提供C++单元测试框架,其1.5版本的gtest工程下有6个源文件和12个头文件。可是它对外只提供一个gtest.h,只要包含gtest.h便可使用GTest提供的全部对外提供的功能,使用者没必要关系GTest内部各个文件的关系,即便之后GTest的内部实现改变了,好比把一个源文件c拆成两个源文件,使用者也没必要关心,甚至若是对外功能不变,连从新编译都不须要。
对于有些模块,其内部功能相对松散,可能并不必定须要提供这个.h,而是直接提供各个子模块或者.c的头文件。
好比产品广泛使用的VOS,做为一个大模块,其内部有不少子模块,他们之间的关系相对比较松散,就不适合提供一个vos.h。而VOS的子模块,如Memory(仅做举例说明,与实际状况可能有所出入),其内部实现高度内聚,虽然其内部实现可能有多个.c和.h,可是对外只须要提供一个Memory.h声明接口。
建议1.2 若是一个模块包含多个子模块,则建议每个子模块提供一个对外的.h,文件名为子模块名。
说明:下降接口使用者的编写难度。
建议1.3 头文件不要使用非习惯用法的扩展名,如.inc。
说明:目前不少产品中使用了.inc做为头文件扩展名,这不符合c语言的习惯用法。在使用.inc做为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。可是从产品的实际代码来看,这一条并无被遵照,一个.inc文件被多个.c包含比比皆是。本规范不提倡将私有定义单独放在头文件中,具
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第12页,共61页Page 12 , Total61
体见 规则1.1。
除此以外,使用.inc还致使source insight、Visual stduio等IDE工具没法识别其为头文件,致使不少功能不可用,如“跳转到变量定义处”。虽然能够经过配置,强迫IDE识别.inc为头文件,可是有些软件没法配置,如Visual Assist只能识别.h而没法经过配置识别.inc。
建议1.4 同一产品统一包含头文件排列方式。
说明:常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。
示例1:
以升序方式排列头文件能够避免头文件被重复包含,如:
#include <a.h>
#include <b.h>
#include <c/d.h>
#include <c/e.h>
#include <f.h>
示例2:
以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面,以下:
#include <product.h>
#include <platform.h>
相对来讲,product.h修改的较为频繁,若是有错误,没必要编译platform.h就能够发现product.h的错误,能够部分减小编译时间。
2 函数
背景
函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不一样功能的函数经过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,不管使用什么样的目录或者名字空间等,须要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样能够方便查找。
原则2.1 一个函数仅完成一件功能。
说明:一个函数实现多个功能给开发、使用、维护都带来很大的困难。
将没有关联或者关联很弱的语句放到同一函数中,会致使函数职责不明确,难以理解,难以测试和改动。
案例:realloc。在标准C语言中,realloc是一个典型的不良设计。这个函数基本功能是从新分配内存,但它承担了太多的其余任务:若是传入的指针参数为NULL就分配内存,若是传入的大小参数为0就释放内存,若是可行则就地从新分配,若是不行则移到其余地方分配。若是没有足够可用的内存用来完成从新分配(扩大原来的内存块或者分配新的内存块),则返回NULL,而原来的内存块保持不变。这个函数不易扩展,容易致使问题。例以下面代码容易致使内存泄漏:
char *buffer = (char *)malloc(XXX_SIZE);
.....
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第13页,共61页Page 13 , Total61
buffer = (char *)realloc(buffer, NEW_SIZE);
若是没有足够可用的内存用来完成从新分配,函数返回为NULL,致使buffer原来指向的内存被丢失。
延伸阅读材料:《敏捷软件开发:原则、模式与实践》 第八章,单一职责原则(SRP)
原则2.2 重复代码应该尽量提炼成函数。
说明:重复代码提炼成函数能够带来维护成本的下降。
重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增长带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多相似或者重复的代码。
项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当马上着手消除重复。
通常状况下,能够经过提炼函数的形式消除重复代码。
示例:
UC ccb_aoc_process( )
{
... ...
struct AOC_E1_E7 aoc_e1_e7;
aoc_e1_e7.aoc = 0;
aoc_e1_e7.e[0] = 0;
... ... //aoc_e1_e7.e[i]从到赋值,下同
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
... ...
if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
... ...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
... ...
}
else if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
... ...
aoc_e1_e7.e[6] = 0;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第14页,共61页Page 14 , Total61
aoc_e1_e7.tariff_rate = 0;
}
ccb_caller_e1 = aoc_e1_e7.e[0];
... ...
ccb_caller_e7 = aoc_e1_e7.e[6];
ccb_caller_tariff_rate = aoc_e1_e7.tariff_rate;
... ...
}
... ...
if (xxx)
{
if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
... ...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
... ...
}
else if (xxx)
{
if (xxx)
{
aoc_e1_e7.e[0] = 0;
... ...
aoc_e1_e7.e[6] = 0;
aoc_e1_e7.tariff_rate = 0;
}
ccb_caller_e1 = aoc_e1_e7.e[0];
... ...
ccb_caller_e7 = aoc_e1_e7.e[6];
ccb_caller_tariff_rate = aoc_e1_e7.tariff_rate;
... ...
}
return 1;
}
else
{
return 0;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第15页,共61页Page 15 , Total61
}
}
刺鼻的代码坏味充斥着这个函数。红色字体的部分是简单的代码重复,粗体字部分是代码结构的重复,将重复部分提炼成一个函数便可消除重复。
规则2.1 避免函数过长,新增函数不超过50行(非空非注释行)。
说明:本规则仅对新增函数作要求,对已有函数修改时,建议不增长代码行。
过长的函数每每意味着函数功能不单一,过于复杂(参见原则2.1:一个函数只完成一个功能)。
函数的有效代码行数,即NBNC(非空非注释行)应当在[1,50]区间。
例外:某些实现算法的函数,因为算法的聚合性与功能的全面性,可能会超过50行。
延伸阅读材料:业界广泛认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;通常的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的全部行数(包括注释和空白行)不超过50行。
规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层。
说明:本规则仅对新增函数作要求,对已有的代码建议不增长嵌套层次。
函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增长阅读代码时的脑力消耗,由于须要在脑子里维护一个“栈”(好比,进入条件语句、进入循环„„)。应该作进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。优秀代码参考值:[1, 4]。
示例:以下代码嵌套深度为5。
void serial (void)
{
if (!Received)
{
TmoCount = 0;
switch (Buff)
{
case AISGFLG:
if ((TiBuff.Count > 3)
&& ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
{
Flg7E = false;
Received = true;
}
else
{
TiBuff.Count = 0;
Flg7D = false;
Flg7E = true;
}
break;
default:
break;
}
}
}
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第16页,共61页Page 16 , Total61
规则2.3 可重入函数应避免使用共享变量;若须要使用,则应经过互斥手段(关中断、信号量)对其加以保护。
说明:可重入函数是指可能被多个任务并发调用的函数。在多任务操做系统中,函数具备可重入性是多个任务能够共用此函数的必要条件。共享变量指的全局变量和static变量。
编写C语言的可重入函数时,不该使用static局部变量,不然必须通过特殊处理,才能使函数具备可重入性。
示例:函数square_exam返回g_exam平方值。那么以下函数不具备可重入性。
int g_exam;
unsigned int example( int para )
{
unsigned int temp;
g_exam = para; // (**)
temp = square_exam ( );
return temp;
}
此函数若被多个线程调用的话,其结果多是未知的,由于当(**)语句刚执行完后,另一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使g_exam赋于另外一个不一样的para值,因此当控制从新回到“temp =square_exam ( )”后,计算出的temp极可能不是预想中的结果。此函数应以下改进。
int g_exam;
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操做] // 若申请不到“信号量”,说明另外的进程正处于
g_exam = para; //给g_exam赋值并计算其平方过程当中(即正在使用此
temp = square_exam( ); // 信号),本进程必须等待其释放信号后,才可继
[释放信号量操做] // 续执行。其它线程必须等待本线程释放信号量后
// 才能再使用本信号。
return temp;
}
规则2.4 对参数的合法性检查,由调用者负责仍是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。
说明:对于模块间接口函数的参数的合法性检查这一问题,每每有两个极端现象,即:要么是调用者和被调用者对参数均不做合法性检查,结果就遗漏了合法性检查这一必要的处理过程,形成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种状况虽不会形成问题,但产生了冗余代码,下降了效率。
示例:下面红色部分的代码在每个函数中都写了一次,致使代码有较多的冗余。若是函数的参数比较多,并且判断的条件比较复杂(好比:一个整形数字须要判断范围等),那么冗余的代码会大面积充斥着业务代码。
void PidMsgProc(MsgBlock *Msg)
{
MsgProcItem *func = NULL;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第17页,共61页Page 17 , Total61
if (Msg == NULL)
{
return;
}
... ...
GetMsgProcFun(Msg, &func);
func(Msg);
return;
}
int GetMsgProcFun(MsgBlock *Msg, MsgProcItem **func)
{
if (Msg == NULL)
{
return 1;
}
... ...
*func = VOS_NULL_PTR;
for (Index = 0; Index < NELEM(g_MsgProcTable); Index++)
{
if ((g_MsgProcTable[Index].FlowType == Msg->FlowType)
&& (g_MsgProcTable[Index].Status == Msg->Status)
&& (g_MsgProcTable[Index].MsgType == Msg->MsgType))
{
*func = &(g_MsgProcTable[Index]);
return 0;
}
}
return 1;
}
int ServiceProcess(int CbNo, MsgBlock *Msg)
{
if ( Msg == NULL)
{
return 1;
}
... ...
// 业务处理代码
... ...
return 0;
}
规则2.5 对函数的错误返回码要全面处理。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第18页,共61页Page 18 , Total61
说明:一个函数(标准库中的函数/第三方库函数/用户定义的函数)可以提供一些指示错误发生的方法。这能够经过使用错误标记、特殊的返回数据或者其余手段,无论何时函数提供了这样的机制,调用程序应该在函数返回时马上检查错误指示。
示例:下面的代码致使宕机
FILE *fp = fopen( "./writeAlarmLastTime.log","r");
if(fp == NULL)
{
return;
}
char buff[128] = "";
fscanf(fp,“%s”, buff); /* 读取最新的告警时间;因为文件writeAlarmLastTime.log为空,致使buff为空 */
fclose(fp);
long fileTime = getAlarmTime(buff); /* 解析获取最新的告警时间;getAlarmTime函数未检查buff指针,致使宕机 */
正确写法:
FILE *fp = fopen( "./writeAlarmLastTime.log","r");
if(fp == NULL)
{
return;
}
char buff[128] = "";
if (fscanf(fp,“%s”,buff) == EOF) //检查函数fscanf的返回值,确保读到数据
{
fclose(fp);
return;
}
fclose(fp);
long fileTime = getAlarmTime(buff); //解析获取最新的告警时间;
规则2.6 设计高扇入,合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,代表函数过度复杂,须要控制和协调过多的下级函数;而扇出太小,例如:老是1,代表函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,而且程序运行时会对系统资源如堆栈空间等形成压力。一般函数比较合理的扇出(调度函数除外)一般是3~5。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第19页,共61页Page 19 , Total61
扇出太大,通常是因为缺少中间层次,可适当增长中间层次的函数。扇出过小,可把下级函数进一步分解多个函数,或合并到上级函数中。固然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,代表使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构一般是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
延伸阅读材料:扇入(Fan-in)和扇出(Fan-out)是Henry和Kafura在1981年引入,用来讲明模块间的耦合(coupling),后面人们扩展到函数/方法、模块/类、包等。
The Fan-in (Informational fan-in) metric measures the fan-in of a module. The fan-in of a module A is the number of modules that pass control into module A.
The Fan-out metric measures the number of the number of modules that are called by a given module.
规则2.7 废弃代码(没有被调用的函数和变量)要及时清除。
说明:程序中的废弃代码不只占用额外的空间,并且还经常影响程序的功能与性能,极可能给程序的测试、维护等形成没必要要的麻烦。
建议2.1 函数不变参数使用const。
说明:不变的值更易于理解/跟踪和分析,把const做为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
示例:C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为const。
int strncmp(const char *s1, const char *s2, register size_t n)
{
register unsigned char u1, u2;
while (n-- > 0)
{
u1 = (unsigned char) *s1++;
u2 = (unsigned char) *s2++;
if (u1 != u2)
{
return u1 - u2;
}
if (u1 == '\0')
{
return 0;
}
}
return 0;
}
延伸阅读:pc-lint 8.0的帮助材料(pc-lint.pdf)11.4 const Checking
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第20页,共61页Page 20 , Total61
建议2.2 函数应避免使用全局变量、静态局部变量和I/O操做,不可避免的地方应集中使用。
说明:带有内部“存储器”的函数的功能多是不可预测的,由于它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是static的局部变量的地址做为返回值,若为auto类,则返回为错针。
示例:以下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0;// 注意,是static类型的。
// 若改成auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
延伸阅读材料:erlang语言中关于dirty的概念,函数式语言的优点
建议2.3 检查函数全部非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另外一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数以前,应进行有效性检查。
示例:下面的代码致使宕机
hr = root_node->get_first_child(&log_item); // list.xml 为空,致使读出log_item为空
…..
hr = log_item->get_next_sibling(&media_next_node); // log_item为空,致使宕机
正确写法:确保读出的内容非空。
hr = root_node->get_first_child(&log_item);
…..
if (log_item == NULL) //确保读出的内容非空
{
return retValue;
}
hr = log_item->get_next_sibling(&media_next_node);
建议2.4 函数的参数个数不超过5个。
说明:函数的参数过多,会使得该函数易于受外部(其余部分的代码)变化的影响,从而影响维护工做。函数的参数过多同时也会增大测试的工做量。
函数的参数个数不要超过5个,若是超过了建议拆分为不一样函数。
建议2.5 除打印类函数外,不要使用可变长参函数。
说明:可变长参函数的处理过程比较复杂容易引入错误,并且性能也比较低,使用过多的可变长参函
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第21页,共61页Page 21 , Total61
数将致使函数的维护难度大大增长。
建议2.6 在源文件范围内声明和定义的全部函数,除非外部可见,不然应该增长static关键字。
说明:若是一个函数只是在同一文件中的其余地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,而且避免了和其余文件或库中的相同标识符发生混淆的可能性。
建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改成空,以便于后续的打热补丁等操做。
#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif
3 标识符命名与定义
3.1 通用命名规则
目前比较使用的以下几种命名风格:
unix like风格:单词用小写字母,每一个单词直接用下划线„_‟分割,例如text_mutex,kernel_text_address。
Windows风格:大小写字母混用,单词连在一块儿,每一个单词首字母大写。不过Windows风格若是遇到大写专有用语时会有些别扭,例如命名一个读取RFC文本的函数,命令为ReadRFCText,看起来就没有unix like的read_rfc_text清晰了。
匈牙利命名法是计算机程序设计中的一种命名规则,用这种方法命名的变量显示了其数据类型。匈牙利命名主要包括三个部分:基本类型、一个或更多的前缀、一个限定词。这种命令法最初在20世纪80年代的微软公司普遍使用,并在win32API和MFC库中普遍的使用,但匈牙利命名法存在较多的争议,例如:.NET Framework,微软新的软件开发平台,除了接口类型通常不适用匈牙利命名法。.NET Framework指导方针建议程序员不要用匈牙利命名法,可是没有指明不要用系统匈牙利命名法仍是匈牙利应用命名法,或者是二者都不要用。与此对比,Java的标准库中链接口类型也不加前缀。(来源http://zh.wikipedia.org/wiki/%E5%8C%88%E7%89%99%E5%88%A9%E5%91%BD%E5%90%8D%E6%B3%95)
匈牙利命名法更多的信息见http://en.wikipedia.org/wiki/Hungarian_notation。
标识符的命名规则从来是一个敏感话题,典型的命名风格如unix风格、windows风格等等,历来没法达成共识。实际上,各类风格都有其优点也有其劣势,并且每每和我的的审美观有关。咱们对标识符定义主要是为了让团队的代码看起来尽量统一,有利于代码的后续阅读和修改,产品能够根据本身的实际须要指定命名风格,规范中再也不作统一的规定。
原则3.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或你们基本能够理解的缩写,避免令人产生误解。
说明:尽量给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。
示例:好的命名:
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第22页,共61页Page 22 , Total61
int error_number;
int number_of_completed_connection;
很差的命名:使用模糊的缩写或随意的字符:
int n;
int nerr;
int n_comp_conns;
原则3.2 除了常见的通用缩写之外,不使用单词缩写,不得使用汉语拼音。
说明:较短的单词可经过去掉“元音”造成缩写,较长的单词可取单词的头几个字母造成缩写,一些单词有你们公认的缩写,经常使用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处作统一说明。
示例:一些常见能够缩写的例子:
argument 可缩写为 arg
buffer 可缩写为 buff
clock 可缩写为 clk
command 可缩写为 cmd
compare 可缩写为 cmp
configuration 可缩写为 cfg
device 可缩写为 dev
error 可缩写为 err
hexadecimal 可缩写为 hex
increment 可缩写为 inc、
initialize 可缩写为 init
maximum 可缩写为 max
message 可缩写为 msg
minimum 可缩写为 min
parameter 可缩写为 para
previous 可缩写为 prev
register 可缩写为 reg
semaphore 可缩写为 sem
statistic 可缩写为 stat
synchronize 可缩写为 sync
temp 可缩写为 tmp
规则3.1 产品/项目组内部应保持统一的命名风格。
说明:Unix like和windows like风格均有其拥趸,产品应根据本身的部署平台,选择其中一种,并在产品内部保持一致。
例外:即便产品以前使用匈牙利命名法,新代码也不该当使用。
建议3.1 用正确的反义词组命名具备互斥意义的变量或相反动做的函数等。
示例:
add/remove begin/end create/destroy
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第23页,共61页Page 23 , Total61
insert/delete first/last get/release
increment/decrement put/get add/delete
lock/unlock open/close min/max
old/new start/stop next/previous
source/target show/hide send/receive
source/destination copy/paste up/down
建议3.2 尽可能避免名字中出现数字编号,除非逻辑上的确须要编号。
示例:以下命名,令人产生疑惑。
#define EXAMPLE_0_TEST_
#define EXAMPLE_1_TEST_
应改成有意义的单词命名
#define EXAMPLE_UNIT_TEST_
#define EXAMPLE_ASSERT_TEST_
建议3.3 标识符前不该添加模块、项目、产品、部门的名称做为前缀。
说明:不少已有代码中已经习惯在文件名中增长模块名,这种写法相似匈牙利命名法,致使文件名不可读,而且带来带来以下问题:
第一眼看到的是模块名,而不是真正的文件功能,阻碍阅读;
文件名太长;
文件名和模块绑定,不利于维护和移植。若foo.c进行重构后,从a模块挪到b模块,若foo.c中有模块名,则须要将文件名从a_module_foo.c改成b_module_foo.c
建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。
说明:涉及到外购芯片以及配套的驱动,这部分的代码变更(包括为产品作适配的新增代码),应该保持原有的风格。
建议3.5 重构/修改部分代码时,应保持和原有代码的命名风格一致。
说明:根据源代码现有的风格继续编写代码,有利于保持整体一致。
3.2 文件命名规则
建议3.6 文件命名统一采用小写字符。
说明:由于不一样系统对文件名大小写处理会不一样(如MS的DOS、Windows系统不区分大小写,可是Linux系统则区分),因此代码文件命名建议统一采用全小写字母命名。
3.3 变量命名规则
规则3.2 全局变量应增长“g_”前缀。
规则3.3 静态变量应增长“s_”前缀。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第24页,共61页Page 24 , Total61
说明:增长g_前缀或者s_前缀,缘由以下:
首先,全局变量十分危险,经过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加当心。
其次,从根本上说,应当尽可能不使用全局变量,增长g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽可能少使用全局变量。
规则3.4 禁止使用单字节命名变量,但容许定义i、j、k做为局部循环变量。
建议3.7 不建议使用匈牙利命名法。
说明:变量命名须要说明的是变量的含义,而不是变量的类型。在变量命名前增长类型说明,反而下降了变量的可读性;更麻烦的问题是,若是修改了变量的类型定义,那么全部使用该变量的地方都须要修改。
匈牙利命名法源于微软,然而却被不少人以讹传讹的使用。而如今即便是微软也再也不推荐使用匈牙利命名法。从来对匈牙利命名法的一大诟病,就是致使了变量名难以阅读,这和本规范的指导思想也有冲突,因此本规范特地强调,变量命名不该采用匈牙利命名法,而应想法使变量名为一个有意义的词或词组,方便代码的阅读。
建议3.8 使用名词或者形容词+名词方式命名变量。
3.4 函数命名规则
建议3.9 函数命名应以函数要执行的动做命名,通常采用动词或者动词+名词的结构。
示例:找到当前进程的当前目录
DWORD GetCurrentDirectory( DWORD BufferLength, LPTSTR Buffer );
建议3.10 函数指针除了前缀,其余按照函数的命名规则命名。
3.5 宏的命名规则
规则3.5 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线„_‟的方式命名(枚举一样建议使用此方式定义)。
示例:
#define PI_ROUNDED 3.14
规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线„_‟开头和结尾。
说明:通常来讲,‟_‟开头、结尾的宏都是一些内部的定义,ISO/IEC 9899(俗称C99)中有以下的描述(6.10.8 Predefined macro names):
None of these macro names(这里上面是一些内部定义的宏的描述), nor the identifier defined, shall be the subject of a #define or a #undef preprocessing directive. Any other predefined macro names shall begin with a leading underscore followed by an uppercase letter or a second underscore.
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第25页,共61页Page 25 , Total61
延伸阅读材料:《代码大全第2版》(Steve McConnell 著 金戈/汤凌/陈硕/张菲 译 电子工业出版社 2006年3月)"第11章变量命的力量"。
4 变量
原则4.1 一个变量只有一个功能,不能把一个变量用做多种用途。
说明:一个变量只用来表示一个特定功能,不能把一个变量做多种用途,即同一变量取值不一样时,其表明的意义也不一样。
示例:具备两种功能的反例
WORD DelRelTimeQue(void)
{
WORD Locate;
Locate = 3;
Locate = DeleteFromQue(Locate); /* Locate具备两种功能:位置和函数DeleteFromQue的返回值 */
return Locate;
}
正确作法:使用两个变量
WORD DelRelTimeQue(void)
{
WORD Ret;
WORD Locate;
Locate = 3;
Ret = DeleteFromQue(Locate);
return Ret;
}
原则4.2 结构功能单一;不要设计面面俱到的数据结构。
说明:相关的一组信息才是构成一个结构体的基础,结构的定义应该能够明确的描述一个对象,而不是一组相关性不强的数据的集合。
设计结构时应力争使结构表明一种现实事务的抽象,而不是同时表明多种。结构中的各元素应表明同一事务的不一样侧面,而不该把描述没有关系或关系很弱的不一样事务的元素放到同一结构中。
示例:以下结构不太清晰、合理。
typedef struct STUDENT_STRU
{
unsigned char name[32]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第26页,共61页Page 26 , Total61
/* 0 - FEMALE; 1 - MALE */
unsigned char teacher_name[32]; /* the student teacher's name */
unsigned char teacher_sex; /* his teacher sex */
} STUDENT;
若改成以下,会更合理些。
typedef struct TEACHER_STRU
{
unsigned char name[32]; /* teacher name */
unsigned char sex; /* teacher sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* teacher index */
} TEACHER;
typedef struct STUDENT_STRU
{
unsigned char name[32]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* his teacher index */
} STUDENT;
原则4.3 不用或者少用全局变量。
说明:单个文件内部能够使用static的全局变量,能够将其理解为类的私有成员变量。
全局变量应该是模块的私有数据,不能做用对外的接口使用,使用static类型定义,能够有效防止外部文件的非正常访问,建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改成空,以便于后续的打补丁等操做。
#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif
直接使用其余模块的私有数据,将使模块间的关系逐渐走向“剪不断理还乱”的耦合状态,这种情形是不容许的。
规则4.1 防止局部变量与全局变量同名。
说明:尽管局部变量和全局变量的做用域不一样而不会发生语法错误,但容易令人误解。
规则4.2 通信过程当中使用的结构,必须注意字节序。
说明:通信报文中,字节序是一个重要的问题,我司设备使用的cpu类型复杂多样,大小端、32位/64位的处理器也都有,若是结构会在报文交互过程当中使用,必须考虑字节序问题。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第27页,共61页Page 27 , Total61
因为位域在不一样字节序下,表现看起来差异更大,因此更须要注意。
对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。
规则4.3 严禁使用未经初始化的变量做为右值。
说明:坚持建议4.3(在首次使用前初始化变量,初始化的地方离使用的地方越近越好。)能够有效避免未初始化错误。
建议4.1 构造仅有一个模块或函数能够修改、建立,而其他有关模块或函数只访问的全局变量,防止多个不一样模块或函数均可以修改、建立同一全局变量的现象。
说明:下降全局变量耦合度。
建议4.2 使用面向接口编程思想,经过API访问数据:若是本模块的数据须要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。
说明:避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。
定义的接口应该有比较明确的意义,好比一个风扇管理功能模块,有自动和手动工做模式,那么设置、查询工做模块就能够定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就能够定义为GetFanSpeed;风扇支持节能功能开关,能够定义EnabletFanSavePower等等。
建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。
说明:未初始化变量是C和C++程序中错误的常见来源。在变量首次使用前确保正确初始化。
在较好的方案中,变量的定义和初始化要作到亲密无间。
示例:
//不可取的初始化:无心义的初始化
int speedup_factor = 0;
if (condition)
{
speedup_factor = 2;
}
else
{
speedup_factor = -1;
}
//不可取的初始化:初始化和声明分离
int speedup_factor;
if (condition)
{
speedup_factor = 2;
}
else
{
speedup_factor = -1;
}
//较好的初始化:使用默认有意义的初始化
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第28页,共61页Page 28 , Total61
int speedup_factor = -1;
if (condition)
{
speedup_factor = 2;
}
//较好的初始化使用?:减小数据流和控制流的混合
int speedup_factor = condition?2:-1;
//较好的初始化:使用函数代替复杂的计算流
int speedup_factor = ComputeSpeedupFactor();
建议4.4 明确全局变量的初始化顺序,避免跨模块的初始化依赖。
说明:系统启动阶段,使用全局变量前,要考虑到该全局变量在何时初始化,使用全局变量和初始化全局变量,二者之间的时序关系,谁先谁后,必定要分析清楚,否则后果每每是低级而又灾难性的。
建议4.5 尽可能减小没有必要的数据类型默认转换与强制转换。
说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就颇有可能留下隐患。
示例:以下赋值,多数编译器不产生告警,但值的含义仍是稍有变化。
char ch;
unsigned short int exam;
ch = -1;
exam = ch; // 编译器不产生告警,此时exam为0xFFFF。
5 宏、常量
规则5.1 用宏定义表达式时,要使用完备的括号。
说明:由于宏只是简单的代码替换,不会像函数同样先将参数计算后,再传递。
示例:以下定义的宏都存在必定的风险
#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA(a, b) ((a) * (b))
这是由于:
若是定义#define RECTANGLE_AREA(a, b) a * b 或#define RECTANGLE_AREA(a, b) (a * b)
则c/RECTANGLE_AREA(a, b) 将扩展成c/a * b , c 与b 本应该是除法运算,结果变成了乘法运算,形成错误。
若是定义#define RECTANGLE_AREA(a, b) (a) * (b)
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第29页,共61页Page 29 , Total61
则RECTANGLE_AREA(c + d, e + f)将扩展成:(c + d * e + f), d与e 先运算,形成错误。
规则5.2 将宏所定义的多条表达式放在大括号中。
说明:更好的方法是多条语句写成do while(0)的方式。
示例:看下面的语句,只有宏的第一条表达式被执行。
#define FOO(x) \
printf("arg is %d\n", x); \
do_something_useful(x);
为了说明问题,下面for语句的书写稍不符规范
for (blah = 1; blah < 10; blah++)
FOO(blah)
用大括号定义的方式能够解决上面的问题:
#define FOO(x) { \
printf("arg is %s\n", x); \
do_something_useful(x); \
}
可是若是有人这样调用:
if (condition == 1)
FOO(10);
else
FOO(20);
那么这个宏仍是不能正常使用,因此必须这样定义才能避免各类问题:
#define FOO(x) do { \
printf("arg is %s\n", x); \
do_something_useful(x); \
} while(0)
用do-while(0)方式定义宏,彻底不用担忧使用者如何使用宏,也不用给使用者加什么约束。
规则5.3 使用宏时,不容许参数发生变化。
示例:以下用法可能致使错误。
#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果:a = 7,即执行了两次增。
正确的用法是:
b = SQUARE(a);
a++; // 结果:a = 6,即只执行了一次增。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第30页,共61页Page 30 , Total61
同时也建议即便函数调用,也不要在参数中作变量变化操做,由于可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所作的一个宏,结果可能不可预知。
规则5.4 不容许直接使用魔鬼数字。
说明:使用魔鬼数字的弊端:代码难以理解;若是一个有含义的数字多处使用,一旦须要修改这个数值,代价惨重。
使用明确的物理状态或物理意义的名称能增长信息,并能提供单一的维护点。
解决途径:
对于局部使用的惟一含义的魔鬼数字,能够在代码周围增长说明注释,也能够定义局部const变量,变量命名自注释。
对于普遍使用的数字,必须定义const全局变量/宏;一样变量/宏命名应是自注释的。
0做为一个特殊的数字,做为通常默认值使用没有歧义时,不用特别定义。
建议5.1 除非必要,应尽量使用函数代替宏。
说明:宏对比函数,有一些明显的缺点:
宏缺少类型检查,不如函数调用检查严格。
宏展开可能会产生意想不到的反作用,如#define SQUARE(a) (a) * (a)这样的定义,若是是SQUARE(i++),就会致使i被加两次;若是是函数调用double square(double a) {return a * a;}则不会有此反作用。
以宏形式写的代码难以调试难以打断点,不利于定位问题。
宏若是调用的不少,会形成代码空间的浪费,不如函数空间效率高。
示例:下面的代码没法获得想要的结果:
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
int MAX_FUNC(int a, int b) {
return ((a) > (b) ? (a) : (b));
}
int testFunc()
{
unsigned int a = 1;
int b = -1;
printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
return 0;
}
上面宏代码调用中,结果是(a < b),因此a只加了一次,因此最终的输出结果是:
MACRO: max of a and b is: -1
FUNC : max of a and b is: 2
建议5.2 常量建议使用const定义代替宏。
说明: “尽可能用编译器而不用预处理”,由于#define常常被认为好象不是语言自己的一部分。看下面的语句:
#define ASPECT_RATIO 1.653
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第31页,共61页Page 31 , Total61
编译器会永远也看不到ASPECT_RATIO这个符号名,由于在源码进入编译器以前,它会被预处理程序去掉,因而ASPECT_RATIO不会加入到符号列表中。若是涉及到这个常量的代码在编译时报错,就会很使人费解,由于报错信息指的是1.653,而不是ASPECT_RATIO。若是ASPECT_RATIO不是在你本身写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出如今符号调试器中,由于一样地,你所写的符号名不会出如今符号列表中。
解决这个问题的方案很简单:不用预处理宏,定义一个常量:
const double ASPECT_RATIO = 1.653;
这种方法颇有效,但有两个特殊状况要注意。首先,定义指针常量时会有点不一样。由于常量定义通常是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也常常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:
const char * const authorName = "Scott Meyers";
延伸阅读材料:关于const和指针的使用,这里摘录两段ISO/IEC 9899(俗称C99)的描述:
The following pair of declarations demonstrates the difference between a "variable pointer to a constant value" and a "constant pointer to a variable value".
const int *ptr_to_constant;
int *const constant_ptr;
The contents of any object pointed to by ptr_to_constant shall not be modified through that pointer,but ptr_to_constant itself may be changed to point to another object. Similarly, the contents of the intpointed to by constant_ptrmay be modified, but constant_ptritself shall always point to the same location.
The declaration of the constant pointer constant_ptr may be clarified by including a definition for the type "pointer to int".
typedef int *int_ptr;
const int_ptr constant_ptr;
declares constant_ptras an object that has type "const-qualified pointer to int".
建议5.3 宏定义中尽可能不使用return、goto、continue、break等改变程序流程的语句。
说明:若是在宏定义中使用这些改变流程的语句,很容易引发资源泄漏问题,使用者很难本身察觉。
示例:在某头文件中定义宏CHECK_AND_RETURN:
#define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
而后在某函数中使用(只说明问题,代码并不完整):
pMem1 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
pMem2 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时若是pMem2==NULL_PTR,则pMem1未释放函数就返回了,形成内存泄漏。*/
因此说,相似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,可是隐患很大,使用须谨慎。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第32页,共61页Page 32 , Total61
6 质量保证
原则6.1 代码质量保证优先原则
(1)正确性,指程序要实现设计要求的功能。
(2)简洁性,指程序易于理解而且易于实现。
(3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
(4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的几率。
(5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在必定的时间和成本前提下,进行测试设计、测试执行的能力。
(6)代码性能高效,指是尽量少地占用系统资源,包括内存和执行时间。
(7)可移植性,指为了在原来设计的特定环境以外运行,对系统进行修改的能力。
(8)我的表达方式/我的方便性,指我的编程习惯。
原则6.2 要时刻注意易混淆的操做符。
说明:包括易混淆和的易用错操做符
一、易混淆的操做符
C语言中有些操做符很容易混淆,编码时要很是当心。
赋值操做符“=” 逻辑操做符“==”
关系操做符“<” 位操做符"<<"
关系操做符“>” 位操做符“>>”
逻辑操做符“||” 位操做符"|"
逻辑操做符“&&” 位操做符"&"
逻辑操做符"!" 位操做符“~”
二、易用错的操做符
(1) 除操做符"/"
当除操做符“/”的运算量是整型量时,运算结果也是整型。
如:1/2=0
(2)求余操做符"%"
求余操做符"%"的运算量只能是整型。
如:5%2=1,而5.0%2是错误的。
(3)自加、自减操做符“++”、“--”
示例1
k = 5;
x = k++;
执行后,x = 5,k = 6
示例2
k = 5;
x = ++k;
执行后,x = 6,k = 6
示例3
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第33页,共61页Page 33 , Total61
k = 5;
x = k--;
执行后,x = 5,k = 4
示例4
k = 5;
x = --k;
执行后,x = 4,k = 4
原则6.3 必须了解编译系统的内存分配方式,特别是编译系统对不一样类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。
原则6.4 不只关注接口,一样要关注实现。
说明:这个原则看似和“面向接口”编程思想相悖,可是实现每每会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,每每还受制于其余隐含约束,如:物理内存的限制,网络情况,具体看“抽象漏洞原则”。
延伸阅读材料: http://local.joelonsoftware.com/mediawiki/index.php?title=Chinese_%28Simplified%29&oldid=9699
规则6.1 禁止内存操做越界。
说明:内存操做主要是指对数组、指针、内存地址等的操做。内存操做越界是软件系统主要错误之一,后果每每很是严重,因此当咱们进行这些操做时必定要仔细当心。
示例:使用itoa()将整型数转换为字符串时:
char TempShold[10] ;
itoa(ProcFrecy,TempShold, 10); /* 数据库刷新间隔设为值1073741823时,系统监控后台coredump,监控前台抛异常。*/
TempShold是以‘\0’结尾的字符数组,只能存储9个字符,而ProcFrecy的最大值可达到10位,致使符数组TempShold越界。
正确写法:一个int(32位)在-2147483647~2147483648之间,将数组TempShold设置成12位。
char TempShold[12] ;
itoa(ProcFrecy,TempShold,10);
坚持下列措施能够避免内存越界:
数组的大小要考虑最大状况,避免数组分配空间不够。
避免使用危险函数sprintf /vsprintf/strcpy/strcat/gets操做字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
使用memcpy/memset时必定要确保长度不要越界
字符串考虑最后的’\0’, 确保全部字符串是以’\0’结束
指针加减操做时,考虑指针类型长度
数组下标进行检查
使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
延伸阅读材料: 《公司常见软件编程低级错误:内存越界.ppt》
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第34页,共61页Page 34 , Total61
规则6.2 禁止内存泄漏。
说明:内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各类资源)泄漏是常见的错误。示例:异常出口处没有释放内存
MsgDBDEV = (PDBDevMsg)GetBuff( sizeof( DBDevMsg ), __LINE__);
if (MsgDBDEV == NULL)
{
return;
}
MsgDBAppToLogic = (LPDBSelfMsg)GetBuff( sizeof(DBSelfMsg), __LINE__ );
if ( MsgDBAppToLogic == NULL )
{
return; //MsgDB_DEV指向的内存丢失
}
坚持下列措施能够避免内存泄漏:
异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否所有释放
删除结构指针时,必须从底层向上层顺序删除
使用指针数组时,确保在释放数组时,数组中的每一个元素指针是否已经提早被释放了
避免重复分配内存
当心使用有return、break语句的宏,确保前面资源已经释放
检查队列中每一个成员是否释放
延伸阅读材料: 《公司常见软件编程低级错误:内存泄漏.ppt》
规则6.3 禁止引用已经释放的内存空间。
说明:在实际编程过程当中,稍不留心就会出如今一个模块中释放了某个内存块,而另外一模块在随后的某个时刻又使用了它。要防止这种状况发生。
示例:一个函数返回的局部自动存储对象的地址,致使引用已经释放的内存空间
int* foobar (void)
{
int local_auto = 100;
return &local_auto;
}
坚持下列措施能够避免引用已经释放的内存空间:
内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
耦合度较强的模块互相调用时,必定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
避免操做已发送消息的内存。
自动存储对象的地址不该赋值给其余的在第一个对象已经中止存在后仍然保持的对象(具备更大做用域的对象或者静态对象或者从一个函数返回的对象)
延伸阅读材料: 《公司常见软件编程低级错误:野指针.ppt》
规则6.4 编程时,要防止差1错误。
说明:此类错误通常是因为把“<=”误写成“<”或“>=”误写成“>”等形成的,由此引发的后果,不少状况下是很严重的,因此编程时,必定要在这些地方当心。当编完程序后,应对这些操做符进行完全检查。使用变量时要注意其边界值的状况。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第35页,共61页Page 35 , Total61
示例:如C语言中字符型变量,有效值范围为-128到127。故如下表达式的计算存在必定风险。
char ch = 127;
int sum = 200;
ch += 1; // 127为ch的边界值,再加将使ch上溢到-128,而不是128
sum += ch; // 故sum的结果不是328,而是72。
规则6.5 全部的if ... else if结构应该由else子句结束 ;switch语句必须有default分支。
建议6.1 函数中分配的内存,在函数退出以前要释放。
说明:有不少函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。
建议6.2 if语句尽可能加上else分支,对没有else分支的语句要当心对待。
建议6.3 不要滥用goto语句。
说明:goto语句会破坏程序的结构性,因此除非确实须要,最好不使用goto语句。
能够利用goto语句方面退出多重循环;同一个函数体内部存在大量相同的逻辑但又不方便封装成函数的状况下,譬如反复执行文件操做,对文件操做失败之后的处理部分代码(譬如关闭文件句柄,释放动态申请的内存等等),通常会放在该函数体的最后部分,再须要的地方就goto到那里,这样代码反而变得清晰简洁。实际也能够封装成函数或者封装成宏,可是这么作会让代码变得没那么直接明了。
示例:
int foo(void)
{
char* p1 = NULL;
char* p2 = NULL;
char* p3 = NULL;
int result = -1;
p1 = (char *)malloc(0x100);
if (p1 == NULL)
{
goto Exit0;
}
strcpy(p1, "this is p1");
p2 = (char *)malloc(0x100);
if (p2 == NULL)
{
goto Exit0;
}
strcpy(p2, "this is p2");
p3 = (char *)malloc(0x100);
if (p3 == NULL)
{
goto Exit0;
}
strcpy(p3, "this is p3");
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第36页,共61页Page 36 , Total61
result = 0;
Exit0:
free(p1); // C标准规定能够free空指针
free(p2);
free(p3);
return result;
}
建议6.4 时刻注意表达式是否会上溢、下溢。
示例:以下程序将形成变量下溢。
unsigned char size ;
…
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减不会小于0,而是0xFF,故程序是一个死循环。应以下修改。
char size; // 从unsigned char 改成char
…
while (size-- >= 0)
{
... // program code
}
7 程序效率
原则7.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提升代码效率。
本章节后面全部的规则和建议,都应在不影响前述可读性等质量属性的前提下实施。
说明:不能一味地追求代码效率,而对软件的正确、简洁、可维护性、可靠性及可测性形成影响。
产品代码中常常有以下代码:
int foo()
{
if (异常条件)
{
异常处理;
return ERR_CODE_1;
}
if (异常条件)
{
异常处理;
return ERR_CODE_2;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第37页,共61页Page 37 , Total61
}
正常处理;
return SUCCESS;
}
这样的代码看起来很清晰,并且也避免了大量的if else嵌套。可是从性能的角度来看,应该把执行几率较大的分支放在前面处理,因为正常状况下的执行几率更大,若首先考虑性能,应以下书写:
int foo()
{
if (知足条件)
{
正常处理;
return SUCCESS;
}
else if (几率比较大的异常条件)
{
异常处理;
return ERR_CODE_1;
}
else
{
异常处理;
return ERR_CODE_2;
}
}
除非证实foo函数是性能瓶颈,不然按照本规则,应优先选用前面一种写法。
以性能为名,使设计或代码更加复杂,从而致使可读性更差,可是并无通过验证的性能要求(好比实际的度量数据和目标的比较结果)做为正当理由,本质上对程序没有真正的好处。没法度量的优化行为其实根本不能使程序运行得更快。
记住:让一个正确的程序更快速,比让一个足够快的程序正确,要容易得太多。大多数时候,不要把注意力集中在如何使代码更快上,应首先关注让代码尽量地清晰易读和更可靠。
原则7.2 经过对数据结构、程序算法的优化来提升效率。
建议7.1 将不变条件的计算移到循环体外。
说明:将循环中与循环无关,不是每次循环都要作的操做,移到循环外部执行。
示例一:
for (int i = 0; i < 10; i++ )
{
sum += i;
back_sum = sum;
}
对于此for循环来讲语句“back_Sum = sum;” 不必每次都执行,只须要执行一次便可,所以能够改成:
for (int i = 0; i < 10; i++ )
{
sum += i;
}
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第38页,共61页Page 38 , Total61
back_sum = sum;
示例二:
for (_UL i = 0; i < func_calc_max(); i++)
{
//process;
}
函数func_calc_max()不必每次都执行,只须要执行一次便可,所以能够改成:
_UL max = func_calc_max();
for (_UL i = 0; i < max; i++)
{
//process;
}
建议7.2 对于多维大数组,避免来回跳跃式访问数组成员。
示例:多维数组在内存中是从最后一维开始逐维展开连续存储的。下面这个对二维数组访问是以SIZE_B为步长跳跃访问,到尾部后再从头(第二个成员)开始,依此类推。局部性比较差,当步长较大时,可能形成cache不命中,反复从内存加载数据到cache。应该把i和j交换。
...
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
sum += x[j][i];
}
}
...
上面这段代码,在 SIZE_B 数值较大时,效率可能会比下面的代码低:
...
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
sum += x[i][j];
}
}
...
建议7.3 建立资源库,以减小分配对象的开销。
说明:例如,使用线程池机制,避免线程频繁建立、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第39页,共61页Page 39 , Total61
毕再释放到内存池中,避免内存申请释放的频繁系统调用.
建议7.4 将屡次被调用的 “小函数”改成inline函数或者宏实现。
说明: 若是编译器支持inline,能够采用inline函数。不然能够采用宏。
在作这种优化的时候必定要注意下面inline函数的优势:其一编译时不用展开,代码SIZE小。其二能够加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会作语法检查。三思然后行。
8 注释
原则8.1 优秀的代码能够自我解释,不经过注释便可轻易读懂。
说明:优秀的代码不写注释也可轻易读懂,注释没法把糟糕的代码变好,须要不少注释来解释的代码每每存在坏味道,须要重构。
示例:注释不能消除代码的坏味道:
/* 判断m是否为素数*/
/* 返回值:: 是素数,: 不是素数*/
int p(int m)
{
int k = sqrt(m);
for (int i = 2; i <= k; i++)
if (m % i == 0)
break; /* 发现整除,表示m不为素数,结束遍历*/
/* 遍历中没有发现整除的状况,返回*/
if (i > k)
return 1;
/* 遍历中没有发现整除的状况,返回*/
else
return 0;
}
重构代码后,不须要注释:
int IsPrimeNumber(int num)
{
int sqrt_of_num = sqrt (num);
for (int i = 2; i <= sqrt_of_num; i++)
{
if (num % i == 0)
{
return FALSE;
}
}
return TRUE;
}
原则8.2 注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:有歧义的注释反而会致使维护者更难看懂代码,正如带两块表反而不知道准确时间。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第40页,共61页Page 40 , Total61
示例:注释与代码相矛盾,注释内容也不清楚,先后矛盾。
/* 上报网管时要求故障ID与恢复ID相一致*/
/* 所以在此由告警级别获知是否是恢复ID */
/* 如果恢复ID则设置为ClearId,不然设置为AlarmId */
if (CLEAR_ALARM_LEVEL != RcData.level)
{
SetAlarmID(RcData.AlarmId);
}
else
{
SetAlarmID(RcData.ClearId);
}
正确作法:修改注释描述以下:
/* 网管达成协议:上报故障ID与恢复ID由告警级别肯定,如果清除级别,ID设置为ClearId,不然设为AlarmId。*/
原则8.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码。
说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码之外的信息,帮助读者理解代码,防止不必的重复注释信息。
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
注释不是为了名词解释(what),而是说明用途(why)。
示例:以下注释纯属多余。
++i; /* increment i */
if (receive_flag) /* if receive_flag is TRUE */
以下这种无价值的注释不该出现(空洞的笑话,可有可无的注释)。
/* 时间有限,如今是:04,根原本不及想为何,也没人能帮我说清楚*/
而以下的注释则给出了有用的信息:
/* 因为xx编号网上问题,在xx状况下,芯片可能存在写错误,此芯片进行写操做后,必须进行回读校验,若是回读不正确,须要再重复写-回读操做,最多重复三次,这样能够解决绝大多数网上应用时的写错误问题*/
int time = 0;
do
{
write_reg(some_addr, value);
time++;
} while ((read_reg(some_addr) != value) && (time < 3));
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释,出彩的或复杂的代码块前要加注释,如:
/* 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;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第41页,共61页Page 41 , Total61
}
规则8.1 修改代码时,维护代码周边的全部注释,以保证注释与代码的一致性。再也不有用的注释要删除。
说明:不要将无用的代码留在注释中,随时能够从源代码配置库中找回代码;即便只是想暂时排除代码,也要留个标注,否则可能会忘记处理它。
规则8.2 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、做者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
说明:一般头文件要对功能和用法做简单说明,源文件包含了更多的实现细节或算法讨论。
版权声明格式:Copyright © Huawei Technologies Co., Ltd. 1998-2011. All rights reserved.
1998-2011根据实际须要能够修改, 1998是文件首次建立年份,而2011是最新文件修改年份。
示例:下面这段头文件的头注释比较标准,固然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Copyright © Huawei Technologies Co., Ltd. 1998-2011. All rights reserved.
File name: // 文件名
Author: ID: Version: Date: // 做者、工号、版本及完成日期
Description: // 用于详细说明此程序文件完成的主要功能,与其余模块
// 或函数的接口,输出值、取值范围、含义及参数间的控
// 制、顺序、独立或依赖等关系
Others: // 其它内容的说明
History: // 修改历史记录列表,每条修改记录应包括修改日期、修改
// 者及修改内容简述
1. Date:
Author: ID:
Modification:
2. ...
*************************************************/
规则8.3 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。
说明:重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。
规则8.4 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明。
示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ /* 变量做用、含义*/
/* 0 -SUCCESS 1 -GT Table error */
/* 2 -GT error Others -no use */ /* 变量取值范围*/
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第42页,共61页Page 42 , Total61
/* the function GetGTTransErrorCode() */ /* 使用方法*/
BYTE g_GTTranErrorCode;
规则8.5 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
可按以下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
规则8.6 对于switch语句下的case语句,若是由于特殊状况须要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
说明:这样比较清楚程序编写者的意图,有效防止无端遗漏break语句。
示例(注意斜体加粗部分):
case CMD_FWD:
ProcessFwd();
/* now jump into case CMD_A */
case CMD_A:
ProcessA();
break;
//对于中间无处理的连续case,已能较清晰说明意图,不强制注释。
switch (cmd_flag)
{
case CMD_A:
case CMD_B:
{
ProcessCMD();
break;
}
……
}
规则8.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写。
规则8.8 同一产品或项目组统一注释风格。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第43页,共61页Page 43 , Total61
建议8.1 避免在一行代码或表达式的中间插入注释。
说明:除非必要,不该在代码或表达中间插入注释,不然容易使代码可理解性变差。
建议8.2 注释应考虑程序易读及外观排版的因素,使用的语言如果中、英兼有的,建议多使用中文,除非能用很是流利准确的英文表达。对于有外籍员工的,由产品肯定注释语言。
说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
建议8.3 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式。
说明:采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释造成帮助文档。
以doxygen格式为例,文件头,函数和所有变量的注释的示例以下:
文件头注释: /** * @file (本文件的文件名eg:mib.h) * @brief (本文件实现的功能的简述) * @version 1.1 (版本声明) * @author (做者,eg:张三) * @date (文件建立日期,eg:2010年12月15日) */
函数头注释: /** *@ Description:向接收方发送SET请求 * @param req - 指向整个SNMP SET 请求报文. * @param ind - 须要处理的subrequest 索引. * @return 成功:SNMP_ERROR_SUCCESS,失败:SNMP_ERROR_COMITFAIL */
Int commit_set_request(Request *req, int ind);
全局变量注释: /** 模拟的Agent MIB */ agentpp_simulation_mib * g_agtSimMib;
函数头注释建议写到声明处。并不是全部函数都必须写注释,建议针对这样的函数写注释:重要的、复杂的函数,提供外部使用的接口函数。
延伸阅读材料:
一、《代码大全第2版》(Steve McConnell 著 金戈/汤凌/陈硕/张菲 译 电子工业出版社 2006年3月)"第32章自说明代码"。
二、《代码整洁之道》(Robert C.Martin 著 韩磊 译 人民邮电出版社2010年1月)第四章"注释"。
三、《敏捷软件开发:原则、模式与实践》(Robert C.Martin 著 邓辉 译 清华大学出版社2003年9月)"第5章重构"。
四、《Doxygen中文手册》(http://hi3ms.huawei.com/group/1735/files.html)。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第44页,共61页Page 44 , Total61
9 排版与格式
规则9.1 程序块采用缩进风格编写,每级缩进为4个空格。
说明:当前各类编辑器/IDE都支持TAB键自动转空格输入,须要打开相关功能并设置相关功能。
编辑器/IDE若是有显示TAB的功能也应该打开,方便及时纠正输入错误。
IDE向导生成的代码能够不用修改。
宏定义、编译开关、条件预处理语句能够顶格(或使用自定义的排版方案,但产品/模块内必须保持一致)。
规则9.2 相对独立的程序块之间、变量说明以后必须加空行。
示例:以下例子不符合规范。
if (!valid_ni(ni))
{
// program code
...
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
应以下书写
if (!valid_ni(ni))
{
// program code
...
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
规则9.3 一条语句不能过长,如不能拆分须要分行写。一行到底多少字符换行比较合适,产品能够自行肯定。
说明:对于目前大多数的PC来讲,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来讲,能够设置更大的值。
换行时有以下建议:
换行时要增长一级缩进,使代码可读性更好;
低优先级操做符处划分新行;换行时操做符应该也放下来,放在新行首;
换行时建议一个完整的语句放在一行,不要根据字符数断行
示例:
if ((temp_flag_var == TEST_FLAG)
&&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE) >= TEST_COUNT_THRESHOLD))
{
// process code
}
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第45页,共61页Page 45 , Total61
规则9.4 多个短语句(包括赋值语句)不容许写在同一行内,即一行只写一条语句。
示例:
int a = 5; int b= 10; //很差的排版
较好的排版
int a = 5;
int b= 10;
规则9.5 if、for、do、while、case、switch、default等语句独占一行。
说明:执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;
通常写if、for、do、while等语句都会有成对出现的„{}‟,对此有以下建议能够参考:
if、for、do、while等语句后的执行语句建议增长成对的„{}‟;
若是if/else配套语句中有一个分支有„{}‟,那么令一个分支即便一行代码也建议增长„{}‟;
添加„{‟的位置能够在if等语句后,也能够独立占下一行;独立占下一行时,能够和if在一个缩进级别,也能够在下一个缩进级别;可是若是if语句很长,或者已经有换行,建议„{‟使用独占一行的写法。
规则9.6 在两个以上的关键字、变量、常量进行对等操做时,它们之间的操做符以前、以后或者先后要加空格;进行非对等操做时,若是是关系密切的当即操做符(如->),后不该加空格。
说明:采用这种松散方式编写代码的目的是使代码更加清晰。
在已经很是清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不须要加空格,多重括号间没必要加空格,由于在C语言中括号已是最清晰的标志了。
在长语句中,若是须要加的空格很是多,那么应该保持总体清晰,而在局部不加空格。给操做符留空格时不要连续留两个以上空格。
示例:
(1) 逗号、分号只在后面加空格
int a, b, c;
(2) 比较操做符, 赋值操做符"="、 "+=",算术操做符"+"、"%",逻辑操做符"&&"、"&",位域操做符"<<"、"^"等双目操做符的先后加空格。
if (current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;
(3) "!"、"~"、"++"、"--"、"&"(地址操做符)等单目操做符先后不加空格。
*p = 'a'; // 内容操做"*"与内容之间
flag = !is_empty; // 非操做"!"与内容之间
p = &mem; // 地址操做"&" 与内容之间
i++; // "++","--"与内容之间
(4) "->"、"."先后不加空格。
p->id = pid; // "->"指针先后不加空格
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第46页,共61页Page 46 , Total61
(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)
建议9.1 注释符(包括„/*‟„//‟„*/‟)与注释内容之间要用一个空格进行分隔。
说明:这样能够使注释的内容部分更清晰。
如今不少工具均可以批量生成、删除'//'注释,这样有空格也比较方便统一处理。
建议9.2 源程序中关系较为紧密的代码应尽量相邻。
10 表达式
规则10.1 表达式的值在标准所容许的任何运算次序下都应该是相同的。
说明:除了少数操做符(函数调用操做符 ( )、&&、| |、? : 和 , (逗号)) 以外,子表达式所依据的运算次序是未指定的并会随时更改。注意,运算次序的问题不能使用括号来解决,由于这不是优先级的问题。
将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就能够有效消除非预期反作用。
一、自增或自减操做符
示例:
x = b[i] + i++;
b[i] 的运算是先于仍是后于 i ++ 的运算,表达式会产生不一样的结果,把自增运算作为单独的语句,能够避免这个问题。
x = b[i] + i;
i ++;
2﹑函数参数
说明:函数参数一般从右到左压栈,但函数参数的计算次序不必定与压栈次序相同。
示例:
x = func( i++, i);
应该修改代码明确先计算第一个参数:
i++;
x = func(i, i);
三、函数指针
说明:函数参数和函数自身地址的计算次序未定义。
示例:
p->task_start_fn(p++);
求函数地址p与计算p++无关,结果是任意值。必须单独计算p++:
p->task_start_fn(p);
p++;
4﹑函数调用
示例:
int g_var = 0;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第47页,共61页Page 47 , Total61
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int x = fun1() + fun2();
编译器可能先计算fun1(),也可能先计算fun2(),因为x的结果依赖于函数fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。
应该修改代码明确fun1/ fun2的计算次序:
int x = fun1();
x = x + fun2();
五、嵌套赋值语句
说明:表达式中嵌套的赋值能够产生附加的反作用。不给这种能致使对运算次序的依赖提供任何机会的最好作法是,不要在表达式中嵌套赋值语句。
示例:
x = y = y = z / 3;
x = y = y++;
六、volatile访问
说明:限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取。
示例:下面的写法可能没法实现做者预期的功能:
/* volume变量被定义为volatile类型*/
UINT16 x = ( volume << 3 ) | volume; /* 在计算了其中一个子表达式的时候,volume的值可能已经被其它程序或硬件改变,致使另一个子表达式的计算结果非预期,可能没法实现做者预期的功能*/
建议10.1 函数调用不要做为另外一个函数的参数使用,不然对于代码的调试、阅读都不利。
说明:以下代码不合理,仅用于说明当函数做为参数时,因为参数压栈次数不是代码能够控制的,可能形成未知的输出:
int g_var;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第48页,共61页Page 48 , Total61
g_var += 100;
return g_var;
}
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("func1: %d, func2: %d\n", fun1(), fun2());
g_var = 1;
printf("func2: %d, func1: %d\n", fun2(), fun1());
}
上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,因此不要为了节约代码行,而写这种代码。
建议10.2 赋值语句不要写在if等语句中,或者做为函数的参数使用。
说明:由于if语句中,会根据条件依次判断,若是前一个条件已经能够断定整个条件,则后续条件语句不会再运行,因此可能致使指望的部分赋值没有获得运行。
示例:
int main(int argc, char *argv[], char *envp[])
{
int a = 0;
int b;
if ((a == 0) || ((b = fun1()) > 10))
{
printf("a: %d\n", a);
}
printf("b: %d\n", b);
}
做用函数参数来使用,参数的压栈顺序不一样可能致使结果未知。
看以下代码,可否一眼看出输出结果会是什么吗?好理解吗?
int g_var;
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("set 1st: %d, add 2nd: %d\n", g_var = 10, g_var++);
g_var = 1;
printf("add 1st: %d, set 2nd: %d\n", g_var++, g_var = 10);
}
建议10.3 用括号明确表达式的操做顺序,避免过度依赖默认优先级。
说明:使用括号强调所使用的操做符,防止因默认的优先级与设计思想不符而致使程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其下降了可读性。下面是如何使用括号的建议。
1. 一元操做符,不须要使用括号
x = ~a; /* 一元操做符,不须要括号*/
x = -a; /* 一元操做符,不须要括号*/
2. 二元以上操做符,若是涉及多种操做符,则应该使用括号
x = a + b + c; /* 操做符相同,不须要括号*/
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第49页,共61页Page 49 , Total61
x = f ( a + b, c ) /* 操做符相同,不须要括号*/
if (a && b && c) /* 操做符相同,不须要括号*/
x = (a * 3) + c + d; /* 操做符不一样,须要括号*/
x = ( a == b ) ? a : ( a –b ); /* 操做符不一样,须要括号*/
3 .即便全部操做符都是相同的,若是涉及类型转换或者量级提高,也应该使用括号控制计算的次序
如下代码将3个浮点数相加:
/* 除了逗号(,),逻辑与(&&),逻辑或(||)以外,C标准没有规定同级操做符是从左仍是从右开始计算,以上表达式存在种计算次序:f4 = (f1 + f2) + f3 或f4 = f1 + (f2 + f3),浮点数计算过程当中可能四舍五入,量级提高,计算次序的不一样会致使f4的结果不一样,以上表达式在不一样编译器上的计算结果可能不同,建议增长括号明确计算顺序*/
f4 = f1 + f2 + f3;
.
建议10.4 赋值操做符不能使用在产生布尔值的表达式上。
说明:若是布尔值表达式须要赋值操做,那么赋值操做必须在操做数以外分别进行。这能够帮助避免=和= =的混淆,帮助咱们静态地检查错误。
示例:
x = y;
if (x != 0)
{
foo ();
}
不能写成:
if (( x = y ) != 0)
{
foo ();
}
或者更坏的
if (x = y)
{
foo ();
}
11 代码编辑、编译
规则11.1 使用编译器的最高告警级别,理解全部的告警,经过修改代码而不是下降告警级别来消除全部告警。
说明:编译器是你的朋友,若是它发出某个告警,这常常说明你的代码中存在潜在的问题。
规则11.2 在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略。
说明:若是必须禁用某个告警,应尽量单独局部禁用,而且编写一个清晰的注释,说明为何屏蔽。
某些语句经编译/静态检查产生告警,但若是你认为它是正确的,那么应经过某种手段去掉告警信息。
规则11.3 本地构建工具(如PC-Lint)的配置应该和持续集成的一致。
说明:二者一致,避免通过本地构建的代码在持续集成上构建失败。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第50页,共61页Page 50 , Total61
规则11.4 使用版本控制(配置管理)系统,及时签入经过本地构建的代码,确保签入的代码不会影响构建成功。
说明:及时签入代码下降集成难度。
建议11.1 要当心地使用编辑器提供的块拷贝功能编程。
12 可测性
原则12.1 模块划分清晰,接口明确,耦合性小,有明确输入和输出,不然单元测试实施困难。
说明:单元测试实施依赖于:
模块间的接口定义清楚、完整、稳定;
模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
模块内部的关键状态和关键数据能够查询,能够修改;
模块原子功能的入口惟一;
模块原子功能的出口惟一;
依赖集中处理:和模块相关的全局变量尽可能的少,或者采用某种封装形式。
规则12.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,而且要有详细的说明。
说明:本规则是针对项目组或产品组的。代码至始至终只有一份代码,不存在开发版本和测试版本的说法。测试与最终发行的版本是经过编译开关的不一样来实现的。而且编译开关要规范统一。统一使用编译开关来实现测试版本与发行版本的区别,通常不容许再定义其它新的编译开关。
规则12.2 在同一项目组或产品组内,调测打印的日志要有统一的规定。
说明:统一的调测日志记录便于集成测试,具体包括:
统一的日志分类以及日志级别;
经过命令行、网管等方式能够配置和改变日志输出的内容和格式;
在关键分支要记录日志,日志建议不要记录在原子函数中,不然难以定位;
调试日志记录的内容须要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
规则12.3 使用断言记录内部假设。
说明:断言是对某种内部模块的假设条件进行检查,若是假设不成立,说明存在编程、设计错误。断言能够对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提升系统的可测性。
规则12.4 不能用断言来检查运行时错误。
说明:断言是用来处理内部编程或设计是否符合假设;不能处理对于可能会发生的且必须处理的状况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第51页,共61页Page 51 , Total61
断言的使用是有条件的。断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断,由于在网上实际运行时,是彻底有可能出现外部输入非法数据的状况。
建议12.1 为单元测试和系统故障注入测试准备好方法和通道。
13 安全性
代码的安全漏洞大都是由代码缺陷致使,但不是全部代码缺陷都有安全风险。理解安全漏洞产生的原理和如何进行安全编码是减小软件安全问题最直接有效的办法。
原则13.1 对用户输入进行检查。
说明:不能假定用户输入都是合法的,由于难以保证不存在恶意用户,即便是合法用户也可能因为误用误操做而产生非法输入。用户输入一般须要通过检验以保证安全,特别是如下场景:
用户输入做为循环条件
用户输入做为数组下标
用户输入做为内存分配的尺寸参数
用户输入做为格式化字符串
用户输入做为业务数据(如做为命令执行参数、拼装sql语句、以特定格式持久化)
这些状况下若是不对用户数据作合法性验证,极可能致使DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
可采起如下措施对用户输入检查:
用户输入做为数值的,作数值范围检查
用户输入是字符串的,检查字符串长度
用户输入做为格式化字符串的,检查关键字“%”
用户输入做为业务数据,对关键字进行检查、转义
13.1 字符串操做安全
规则13.1 确保全部字符串是以NULL结束。
说明:C语言中‟\0‟做为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、strlen())依赖NULL结束符来肯定字符串的长度。没有正确使用NULL结束字符串会致使缓冲区溢出和其它未定义的行为。
为了不缓冲区溢出,经常会用相对安全的限制字符数量的字符串操做函数代替一些危险函数。如:
用strncpy()代替strcpy()
用strncat()代替strcat()
用snprintf()代替sprintf()
用fgets()代替gets()
这些函数会截断超出指定限制的字符串,可是要注意它们并不能保证目标字符串老是以NULL结尾。若是源字符串的前n个字符中不存在NULL字符,目标字符串就不是以NULL结尾。
示例:
char a[16];
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第52页,共61页Page 52 , Total61
strncpy(a, "0123456789abcdef", sizeof(a));
上述代码存在安全风险:在调用strncpy()后,字符数组a中的字符串是没有NULL结束符的,也没有空间存放NULL结束符。
正确写法:截断字符串,保证字符串以NULL结束。
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a) - 1 );
a[sizeof(a) - 1] = '\0';
规则13.2 不要将边界不明确的字符串写到固定长度的数组中。
说明:边界不明确的字符串(如来自gets()、getenv()、scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易致使缓冲区溢出。
示例:
char buff[256];
char *editor = getenv("EDITOR");
if (editor != NULL)
{
strcpy(buff, editor);
}
上述代码读取环境变量"EDITOR"的值,若是成功则拷贝到缓冲区buff中。而从环境变量获取到的字符串长度是不肯定的,把它们拷贝到固定长度的数组中极可能致使缓冲区溢出。
正确写法:计算字符串的实际长度,使用malloc分配指定长度的内存
char *buff;
char *editor = getenv("EDITOR");
if (editor != NULL)
{
buff = malloc(strlen(editor) + 1);
if (buff != NULL)
{
strcpy(buff, editor);
}
}
13.2 整数安全
C99标准定义了整型提高(integer promotions)、整型转换级别(integer conversion rank)以及普通算术转换(usual arithmetic conversions)的整型操做。不过这些操做实际上也带来了安全风险。
规则13.3 避免整数溢出。
说明:当一个整数被增长超过其最大值时会发生整数上溢,被减少小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
示例1:有符号和无符号整数的上溢和下溢
int i;
unsigned int j;
i = INT_MAX; // 2,147,483,647
i++;
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第53页,共61页Page 53 , Total61
printf("i = %d\n", i); // i=-2,147,483,648
j = UINT_MAX; // 4,294,967,295;
j++;
printf("j = %u\n", j); // j = 0
i = INT_MIN; // -2,147,483,648;
i--;
printf("i = %d\n", i); // i = 2,147,483,647
j = 0;
j--;
printf("j = %u\n", j); // j = 4,294,967,295
示例2:整数下溢致使报文长度异常
/* 报文长度减去FSM头的长度*/
unsigned int length;
length -= FSM_HDRLEN ;
处理太短报文时,length的长度可能小于FSM_HDRLEN,减法的结果小于。因为length是无符号数,结果返回了一个很大的数。
正确写法:增长长度检查
if (length < FSM_HDRLEN )
{
return VOS_ERROR;
}
length -= FSM_HDRLEN ;
规则13.4 避免符号错误。
说明:有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其做为符号位的功能。若是该带符号整数的值非负,那么转换后值不变;若是该带符号整数的值为负,那么转换后的结果一般是一个很是大的正数。
示例:符号错误绕过长度检查
#define BUF_SIZE 10
int main(int argc,char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]); //若是atoi返回的长度为负数
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第54页,共61页Page 54 , Total61
if (length < BUF_SIZE) // len为负数,长度检查无效
{
memcpy(buf, argv[2], length); /* 带符号的len被转换为size_t类型的无符号整数,负值被解释为一个极大的正整数。memcpy()调用时引起buf缓冲区溢出 */
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
}
正确写法1:将len声明为无符号整型
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
unsigned int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]);
if (length < BUF_SIZE)
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too much data\n");
}
return 0;
}
正确写法2:增长对len的更有效的范围校验
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第55页,共61页Page 55 , Total61
length = atoi(argv[1]);
if ((length > 0) && (length < BUF_SIZE))
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too much data\n");
}
return 0;
}
规则13.5:避免截断错误。
说明:将一个较大整型转换为较小整型,而且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引发数据丢失。
使用截断后的变量进行内存操做,极可能会引起问题。
示例:
int main(int argc, char* argv[])
{
unsigned short total = strlen(argv[1]) + strlen(argv[2]) + 1;
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
示例代码中total被定义为unsigned short,相对于strlen()的返回值类型size_t(一般为unsigned long)过小。若是攻击者提供的两个入参长度分别为65500和36,unsigned long的65500+36+1会被取模截断,total的最终值是(65500+36+1)%65536 = 1。malloc()只为buff分配了1字节空间,为strcpy()和strcat()的调用创造了缓冲区溢出的条件。
正确写法:将涉及到计算的变量声明为统一的类型,并检查计算结果。
int main(int argc, char* argv[])
{
size_t total = strlen(argv[1]) + strlen(argv[2]) + 1;
if ((total <= strlen(argv[1])) || (total <= strlen(argv[2])))
{
/* handle error */
return -1;
}
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第56页,共61页Page 56 , Total61
return 0;
}
13.3 格式化输出安全
规则13.6:确保格式字符和参数匹配。
说明:使用格式化字符串应该当心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会致使未定义的行为。大多数状况下,不正确的格式化字符串会致使程序异常终止。
示例:
char *error_msg = "Resource not available to user.";
int error_type = 3;
/* 格式字符和参数的类型不匹配*/
printf("Error (type %s): %d\n", error_type, error_msg);
/* 格式字符和参数的数量不匹配*/
printf("Error: %s\n");
格式化字符串在编码时会大量使用,容易copy-paste省事,这就容易出现不匹配的错误。
规则13.7 避免将用户输入做为格式化字符串的一部分或者所有。
说明:调用格式化I/O函数时,不要直接或者间接将用户输入做为格式化字符串的一部分或者所有。攻击者对一个格式化字符串拥有部分或彻底控制,存在如下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。
示例1:
char input[1000];
if (fgets(input, sizeof(input) - 1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(input);
上述代码input直接来自用户输入,并做为格式化字符串直接传递给printf()。当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。格式字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符串,而没有提供参数,所以printf()读取栈中任意内存位置,指导格式字符耗尽或者遇到一个无效指针或未映射地址为止。
正确作法:给printf()传两个参数,第一个参数为”%s”,目的是将格式化字符串肯定下来;第二个参数为用户输入input。
char input[1000];
if (fgets(input, sizeof(input)-1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(“%s”, input);
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第57页,共61页Page 57 , Total61
示例2:
void check_password(char *user, char *password)
{
if (strcmp(password(user), password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if (!msg)
{
/* handle error condition */
}
sprintf(msg, "%s login incorrect", user);
fprintf(STDERR, msg);
syslog(LOG_INFO, msg);
free(msg);
}
/*…*/
}
上述代码检查给定用户名及其口令是否匹配,当不匹配时显示一条错误信息,并将错误信息写入日志中。一样的,若是user为” %s%s%s%s%s%s%s%s%s%s%s%s”,通过格式化函数sprintf()的拼装后,msg指向的字符串为” %s%s%s%s%s%s%s%s%s%s%s%s login incorrect”,在fprintf()调用中,msg将做为fprintf()的格式化字符串,可能引起如同示例1同样的问题。并且,syslog()函数也同样存在格式化字符串的问题。
正确作法:格式化字符串由代码肯定,未经检查过滤的用户输入只能做为参数。
void check_password(char *user, char *password)
{
if (strcmp(password(user), password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if (!msg)
{
/* handle error condition */
}
sprintf(msg, "%s password incorrect", user);
fprintf(stderr, "%s", user);
syslog(LOG_INFO, "%s", msg);
free(msg);
}
}
13.4 文件I/O安全
规则13.8 避免使用strlen()计算二进制数据的长度。
说明:strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符以前的字符的数量。所以用strlen()处理文件I/O函数读取的内容时要当心,由于这些内容多是二进制也多是文本。
示例:
char buf[BUF_SIZE + 1];
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第58页,共61页Page 58 , Total61
if (fgets(buf, sizeof(buf), fp) == NULL)
{
/* handle error */
}
buf[strlen(buf) - 1] = '\0';
上述代码试图从一个输入行中删除行尾的换行符(\n)。若是buf的第一个字符是NULL,strlen(buf)返回0,这时对buf进行数组下标为[-1]的访问操做将会越界。
正确作法:在不能肯定从文件读取到的数据的类型时,不要使用依赖NULL结束符的字符串操做函数。
char buf[BUF_SIZE + 1];
char *p;
if (fgets(buf, sizeof(buf), fp))
{
p = strchr(buf, '\n');
if (p)
{
*p = '\0';
}
}
else
{
/* handle error condition */
}
规则13.9 使用int类型变量来接受字符I/O函数的返回值。
说明:字符I/O函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。若是这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF。
若是这些I/O函数的返回值须要与EOF进行比较,不要将返回值转换为char类型。由于char是有符号8位的值,int是32位的值。若是getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。由于这个值被有符号扩展为0xFFFFFFFF(EOF的值)执行比较。
示例:
char buf[BUF_SIZE];
char ch;
int i = 0;
while ( (ch = getchar()) != '\n' && ch != EOF )
{
if ( i < BUF_SIZE - 1 )
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
正确作法:使用int类型的变量接受getchar()的返回值。
char buf[BUF_SIZE];
int ch;
int i = 0;
while (((ch = getchar()) != '\n') && ch != EOF)
{
if (i < BUF_SIZE - 1)
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第59页,共61页Page 59 , Total61
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能没法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误。
13.5 其它
规则13.10 防止命令注入。
说明:C99函数system()经过调用一个系统定义的命令解析器(如UNIX的shell,Windows的CMD.exe)来执行一个指定的程序/命令。相似的还有POSIX的函数popen()。
若是system()的参数由用户的输入组成,恶意用户能够经过构造恶意输入,改变system()调用的行为。
示例:
system(sprintf("any_exe %s", input));
若是恶意用户输入参数:
happy; useradd attacker
最终shell将字符串“any_exe happy; useradd attacker”解释为两条独立的命令:
正确作法:使用POSIX函数execve()代替system().
void secuExec (char *input)
{
pid_t pid;
char *const args[] = {"", input, NULL};
char *const envs[] = {NULL};
pid = fork();
if (pid == -1)
{
puts("fork error");
}
else if (pid == 0)
{
if (execve("/usr/bin/any_exe", args, envs) == -1)
{
puts("Error executing any_exe");
}
}
return;
}
Windows环境可能对execve()的支持不是很完善,建议使用Win32 API CreateProcess()代替system()。
14 单元测试
规则14.1 在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计/编码的正确。
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第60页,共61页Page 60 , Total61
建议14.1 单元测试关注单元的行为而不是实现,避免针对函数的测试。
说明:应该将被测单元看作一个被测的总体,根据实际资源、进度和质量风险,权衡代码覆盖、打桩工做量、补充测试用例的难度、被测对象的稳定程度等,通常状况下建议关注模块/组件的测试,尽可能避免针对函数的测试。尽管有时候单个用例只能专一于对某个具体函数的测试,但咱们关注的应该是函数的行为而不是其具体实现细节。
15 可移植性
规则15.1 不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数。
建议15.1 不使用与硬件或操做系统关系很大的语句,而使用建议的标准语句,以提升软件的可移植性和可重用性。
说明:使用标准的数据类型,有利于程序的移植。
示例:以下例子(在DOS下BC3.1环境中),在移植时可能产生问题。
void main()
{
register int index; // 寄存器变量
_AX = 0x4000; // _AX是BC3.1提供的寄存器“伪变量”
... // program code
}
建议15.2 除非为了知足特殊需求,避免使用嵌入式汇编。
说明:程序中嵌入式汇编,通常都对可移植性有较大的影响。
16 业界编程规范
本次编程规范整理的原则是求精不求全,主要针对华为当前编码上的突出问题,因此在全面性上难免有所欠缺。业界一些公司、组织也发布了一些编程规范,对编程语言的缺陷、使用风险都有很好的描述,这里作一些简单的推介,有兴趣的同窗能够在平时学习中能够参考,提升本身的编程能力。
google C++编程指南
目标:
加强代码一致性,建立通用的、必需的习惯用语和模式能够使代码更加容易理解
C++是一门包含大量高级特性的巨型语言,某些状况下,咱们会限制甚至禁止使用某些特性使代码简化,避免可能致使的各类问题
包含的内容:头文件、命名规则、注释、语言特性的使用规则、编码格式
特色:强调理解基础上的遵循,一个规则一般明确说明其优势、缺点,并举不少例子,让读者在理解的基础上遵循,不像规章制度那样生硬和抽象,实际上读起来更像一个教程。好比:禁止使用C++异常,花了一页纸的篇幅来解释使用和不使用的优缺点,很是容易理解
推荐语:读起来很是舒服,抛开编程规范,拿来做为理解学习C++也是不错的
推荐度:★★★★★
密级:confidentiality level
DKBA 2826-2011.5
2011-06-02 华为机密,未经许可不得扩散 Huawei Confidential 第61页,共61页Page 61 , Total61
汽车业C语言使用规范(MISRA)
目标:由于编译器、编程人员理解、C语言本等缘由,彻底放开使用C语言存在一些风险,所以制定这个规范的目标为了促进C语言的最为安全的使用而定义一些规则。
特色:规则都是针对的是C语言自己缺陷或容易被误解的点,如:自动变量若是不初始化就使用会出现随机值、不一样类型数据赋值容易出现的隐式转换;没有包含诸如注释、变量名、编码格式等统一编程风格的内容。
推荐语:对C的缺点了如指掌,能够帮助更好的掌握C语言,避免编程陷阱,提升程序可靠性。
推荐度:★★★★php