外观
01.Code-Standards
约 17780 字大约 59 分钟
项目管理数据结构算法devops
2022-08-19
1、基本原则
① 软件的生命周期贯穿产品的开发、测试、生产、用户使用、版本升级和后期维护等长期过程,只有易读、易维护的软件代码才具有生命力。
② 所有的代码尽可能遵循ANSI标准,尽可能不使用ANSI C未定义的或编译器扩展的功能。
③ 编程首先考虑的是满足正确性、健壮性、可维护性、可移植性等质量因素,最后才考虑程序的效率和资源占用。
④ 过多地使用全局变量,会导致模块间的紧耦合,违反模块化的要求。
⑤ 尽量选择可借用的代码,对其修改优化以达到自身要求。
⑥ 定义指针类型的变量,*应放在变量前。float *pfBuffer;
2、布局
程序布局的目的是显示出程序良好的逻辑结构,提高程序的准确性、连续性、可读性、可维护性。更重要的是,统一的程序布局和编程风格,有助于提高整个项目的开发质量,提高开发效率,降低开发成本。同时,对于普通程序员来说,养成良好的编程习惯有助于提高自己的编程水平,提高编程效率。因此,统一的、良好的程序布局和编程风格不仅仅是个人主观美学上的或是形式上的问题,而且会涉及到产品质量,涉及到个人编程能力的提高,必须引起大家重视。
① 头文件布局《注释中英文模板-头文件》
② 实现文件布局《注释中英文模板-实现文件》
③ 源程序中关系较为紧密的代码应尽可能相邻。
④ 代码编译器设置4个空格代替TAB键。
⑤ 结构型的数组、多维的数组如果在定义时初始化,按照数组的矩阵结构分行书写。
⑥ if、for、while等关键字之后应留一个空格。
⑦ 长表达式(超过80列)要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐。
⑧ 函数声明时,类型与名称不允许分行书写。

⑨ 若函数或过程中的参数较长,则要进行适当的划分。
3、注释
① 一般情况下,源程序有效注释量必须在20%以上。
注释的原则是有助于对程序的阅读理解,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。有效的注释是指在代码的功能、意图层次上进行注释,提供有用、额外的信息。
② 包含在{ }中代码块的结束处应加注释,便于阅读。 /* end of while (…) */
③ 保证代码和注释的一致性。
④ 对分支语句(条件分支、循环语句等)必须编写注释。
对于维护人员来说,良好的注释有助于更好的理解程序
⑤ 注释格式尽量统一,建议使用“/* …… */”
因为不同编译器对 // 解释不同。
⑥ 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等。
说明: 重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。
⑦ 对于代码中未完成的编码工作,应建议标记性注释,其格式为:// TODO+工号+概述
4、命名规则
① 禁止使用连续的下划线,下划线也不能出现在标识符头或结尾(预编译开关除外)。
② 标识符的命名应当符合“min-length && max-information”原则。

③ 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
add/remove ; begin/end ; create/destroy ; 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 ; cut/paste ;
up/down
④ 宏、常量名都要使用大写字母, 用下划线 ‘_’ 分割单词。预编译开关的定义使用下划线 ‘_’ 开始。
⑤ 使用一致的前缀来区分变量的作用域。
g_ : 全局变量
s_ : 模块内静态变量
l_ : 文件内静态变量(Local)
空 : 局部变量不加范围前缀
⑥ 使用一致的小写类型指示符作为前缀来区分变量的类型。
w : unsigned short 或 WORD
dw : DWORD或 unsigned long
i : int
f : float
d : double
c : char
uc : unsigned char 或 BYTE
p : pointer
b : BOOL
h : HANDLE
a : 数组,array of TYPE
str : 字符串
st 、stru : 结构类型
u8 : unsigned char 或 BYTE
u16 : unsigned short
u32 : unsigned int
ul : unsigned long
s8 : signed char
s16 : signed short
s32 : signed int
sl : signed long
⑦ 完整的变量名应由前缀+变量名主体组成,变量名的主体应当使用“名词”或者“形容词+名词”,且首字母必须大写。

⑧ 函数名用大写字母开头的单词组合而成,且应当使用“动词”或者“动词+名词”(动宾词组)。
函数名中不同意义字段之间不要用下划线连接,而要把每个字段的首字母大写以示区分
⑨ 结构名、联合名、枚举名由前缀T_ 开头。
⑩ 类名采用大小写结合的方法。在构成类名的单词之间不用下划线,类名在开头加上C,类的成员变量统一在前面加m_ 前缀。
⑪ 命名规范与系统风格一致
命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,比如采用 UNIX 的全小写加下划线的风格或大小写混排的方式,不要使用大小写与下划线混排的方式,用作特殊标识如标识成员变量或全局变量的 m_和 g_,其后加上大小写混排的方式是允许的。
示例: Add_User 不允许, add_user、 AddUser、 m_AddUser 允许。
5、变量、常量与类型
变量、常量和数据类型是程序编写的基础,它们的正确使用直接关系到程序设计的成败,变量包括全局变量、局部变量和静态变量,常量包括数据常量和指针常量,类型包括系统的数据类型和自定义数据类型。
(1)变量和常量
① 定义全局变量时必须仔细分析,明确其含义、作用、取值范围及与其它全局变量间的关系。
全局变量关系到程序的结构框架,对于全局变量的理解关系到对整个程序能否正确理解,所以在对全局变量声明的同时,应对其含义、作用及取值范围进行详细地注释说明,若有必要还应说明与其它变量的关系。
② 明确全局变量与操作此全局变量的函数或过程的关系。
全局变量与函数的关系包括:创建、修改及访问。明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
③ 尽量构造仅有一个模块或函数可以修改、创建的全局变量,而其余有关模块或函数只能访问。防止多个不同模块或函数都可以修改、创建同一公共变量的现象。
减少全局变量操作引起的错误。

④ 对于全局变量通过统一的函数访问。
减少全局变量操作引起的错误,降低公共变量的耦合度。

⑤ 循环语句与判断语句中,不允许对其它变量进行计算与赋值。(否则容易出现 短路原则)
⑥ 尽量使用const说明常量数据,对于宏定义的常数,必须指出其类型。
const int MAX_COUNT = 1000;
#define MAX_COUNT (int)1000
⑦ 使用宏定义多行语句时, 必须使用 { } 把这些语句括起来。
⑧ 宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来。
⑨ 明确全局变量的初始化顺序,避免跨模块的初始化依赖。
⑩ 使用宏时,不允许参数发生变化。
SQUARE(a++);
⑪ 不允许直接使用魔鬼数字。
说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。
使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。
解决途径:
对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。
对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。
0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。
⑫ 除非必要,应尽可能使用函数代替宏。
说明:宏对比函数,有一些明显的缺点:
宏缺乏类型检查,不如函数调用检查严格。
宏展开可能会产生意想不到的副作用。
以宏形式写的代码难以调试难以打断点,不利于定位问题。
宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。
⑬ 常量建议使用const定义代替宏。
说明: “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。
⑭ 宏定义中尽量不使用return、 goto、 continue、 break等改变程序流程的语句。
说明:如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。
(2)类型
① 结构和联合必须被类型化。typedef struct {....} T_Student;
② 使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。
说明:使用统一的自定义数据类型,有利于程序的移植。

③ 结构是针对一种事务的抽象,功能要单一,不要设计面面俱到的数据结构。
设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
④ 不同结构间的关系要尽量简单,若两个结构间关系较复杂、密切,那么应合为一个结构。
两个结构关系复杂时,它们可能反映的是一个事物的不同属性。
⑤ 结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。
增加结构的可理解性、可操作性和可维护性。
⑥ 仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象,对于结构中未用的位明确地给予保留。
⑦ 结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。
⑧ 注意具体语言及编译器处理不同数据类型的原则及有关细节。
说明:如在C语言中,static局部变量将在内存“数据区”中生成,而非static局部变量将在“堆栈”中生成。注意这些细节对程序质量的保证非常重要。
⑨ 合理地设计数据并使用自定义数据类型,尽量减少没有必要的数据类型默认转换与强制转换。
⑩ 当声明用于分布式环境或不同 CPU 间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题 。
说明:在对齐方式下, CPU 的运行效率要快得多。
⑪ 结构定义时, 尽量做到 pack 1,2,4,8 无关。
全局紧缩对齐可能会导致代码效率下降。
⑫ 避免直接使用数字作为标识符
避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。
⑬ 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。
说明:涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格。
⑭ 重构/修改部分代码时,应保持和原有代码的命名风格一致。
说明:根据源代码现有的风格继续编写代码,有利于保持总体一致。
6、表达式与语句
表达式是语句的一部分,它们是不可分割的。正确使用表达式和if、for、while、goto、switch等基本语句的一些规则与建议。
运算符的优先级与结合律表

② 为了防止产生歧义并提高可读性,即使不加括号时运算顺序不会改变,也应当用括号确定表达式的操作顺序。
③ 不可将布尔变量和逻辑表达式直接与TRUE、FALSE或者1、0进行比较。
TURE和FALSE的定义值是和语言环境相关的,且可能会被重定义的。
④ 在条件判断语句中,当整型变量与0 比较时,不可模仿布尔变量的风格,应当将整型变量用“==”或“!=”直接与0比较。

⑤ 不可将浮点变量用“==”或“!=”与任何数字比较。
无论是float还是double类型的变量,都有精度限制。应该转化成“>=”或“<=”形式。
⑥ 循环嵌套次数不大于3次。
⑦ 当switch语句的分支比较多时,采用数据驱动方式。
当switch 语句中case 语句比较多时,会降低程序的效率。

⑧ 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
⑨ for语句的循环控制变量的取值采用“半开半闭区间”写法。 for (i = 0; i < NUM; i++)
这样做更能适应c语言数组的特点,c语言的下标属于一个“半开半闭区间”。
⑩ 在进行“”比较时,将常量或常数放在“”号的左边。
采用这种方式,让编译器去发现错误。
⑪ 高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。
⑫ 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利。
说明:当函数作为参数时,由于参数压栈次序不是代码可以控制的,可造成未知的输出:
⑬ 表达式的值在标准所允许的任何运算次序下都应该是相同的。
说明:除了少数操作符(函数调用操作符 ( )、 &&、 | |、 ? : 和 , (逗号)) 之外,子表达式所依据的运算次序是未指定的并会随时更改。注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。
将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。
(1)自增或自减操作符
(2)函数参数
说明:函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。
(3)函数指针
说明:函数参数和函数自身地址的计算次序未定义。
(4)函数调用
(5)volatile访问
说明:限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取
7、函数与过程
函数是C/C++程序的基本功能单元。如何编写出正确、高效、易维护的函数是软件编码质量控制的关键。*一个函数包括函数头,函数名,函数体,参数,返回值。
(1)参数
① 如果参数是指针,且仅作输入用,则应在类型前加const。
防止该指针在函数体内被意外修改。
② 当结构变量作为参数时,应传送结构的指针而不传送整个结构体,并且不得修改结构中的元素,用作输出时除外。
一个函数被调用的时候,形参会被一个个压入被调函数的堆栈中,在函数调用结束以后再弹出。一个结构所包含的变量往往比较多,直接以一个结构为参数,压栈出栈的内容就会太多,不但占用堆栈空间,而且影响代码执行效率,如果使用不当还可能导致堆栈的溢出。如果使用结构的指针作为参数,因为指针的长度是固定不变的,结构的大小就不会影响代码执行的效率,也不会过多地占用堆栈空间。
③ 避免函数有太多的参数,参数个数尽量控制在5个以内。
④ 参数的顺序要遵循程序员的习惯。如输入参数放在前面,输出参数放在后面等。
⑤ 尽量不要使用类型和数目不确定的参数。
对于参数个数可变的函数调用,编译器不作类型检查和参数检查。这种风格的函数在编译时丧失了严格的类型安全检查。
⑥ 避免使用BOOLEAN参数。
⑦ 局部变量
编写可重入函数(可以被中断的函数;多个进程调用)时,应注意局部变量的使用(如编写 C/C++语言的可重入函数时,应使用 auto 即缺省态局部变量或寄存器变量)。
说明:编写 C/C++语言的可重入函数时,不应使用 static 局部变量,否则必须经过特殊处理,才能使函数具有可重入性。
⑧ 全局变量
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即 P、 V 操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
⑨ 接口函数参数
在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。
说明:对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
⑩ 检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。
⑪ 在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。
说明:因为数据类型转换或多或少存在危险。
⑫ 非调度函数应减少或防止控制参数,尽量只使用数据参数。
说明:本建议目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命
令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
⑬ 系统接口
充分了解系统的接口之后,再使用系统提供的功能。
⑭ 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句。
以提高软件的可移植性和可重用性。
⑮ 精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块。
提高“内核”部分的可移植性和可重用性。
(2)返回值
① 如果返回值表示函数运行是否正常,规定0为正常退出,不同非0值标识不同异常退出。避免使用TRUE或FALSE作为返回值。
② 使用 return 语句
Unix 下,多线程的中的子线程退出必须采用主动退出方式,即子线程应 return 出口。
(3)内部实现
函数体的实现并不是随心所欲,而是有一定的规矩可循。不但要仔细检查入口参数的有效性和精心设计返回值,还要保证函数的功能单一,具有很高的功能内聚性,尽量减少函数之间的耦合,方便调试和维护。
① 将函数的参数作为工作变量。
将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之。

② 避免函数带有“记忆”功能。函数的输出应该具有可性,即相同的输入应当产生相同的输出。
带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。
③ 函数的功能要单一,不要设计多用途的函数。
多用途的函数往往通过在输入参数中有一个控制参数,根据不同的控制参数产生不同的功能。这种方式增加了函数之间的控制耦合性,而且在函数调用的时候,调用相同的一个函数却产生不同的效果,降低了代码的可读性,也不利于代码调试和维护。
④ 函数功能明确,防止把没有关联的语句放到一个函数中。
防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
⑤ 函数体的规模不能太大,尽量控制在200行代码之内。
冗长的函数不利于调试,可读性差。
⑥ 为简单功能编写函数。
虽然为仅用一两行就可完成的功能去编函数好象没有必要,但使用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
⑦ 减少函数本身或函数间的递归调用。
递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
对于前台软件为了系统的稳定性和可靠性,往往规定了进程的堆栈大小。如果采用了递归算法,收敛的条件又往往难以确定,很容易使得进程的堆栈溢出,破坏系统的正常运行;另外,由于无法确定递归的次数,降低了系统的稳定性和可靠性。
⑧ 设计高扇入、合理扇出(小于 7)的函数。
扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解成多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
⑨ 仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,据此来进行模块的函数划分与组织。
说明:函数的划分与组织是模块的实现过程中很关键的步骤,如何划分出合理的函数结构,关系到模块的最终效率和可维护性、可测性等。根据模块的功能图或/及数据流图映射出函数结构是常用方法之一。
⑩ 当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替。
说明:这样可以增加编程效率和程序的可读性。
⑪ 改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,
要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
(4)函数设计
函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织包括:逻辑层组织和物理层组织两个方面。
逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。
物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。
- 一个函数仅完成一件功能。
在标准C语言中, realloc是一个典型的不良设计。

- 除打印类函数外,不要使用可变长参函数*。
说明:可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。
- 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字。
说明:如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作。

8、头文件
对于C语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。
合理的头文件划分体现了系统设计的思想,但是从编程规范的角度看,仍然有一些通用的方法,用来合理规划头文件。
- 头文件中适合放置接口的声明,不适合放置实现。
说明: 头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
内部使用的宏、枚举、结构定义不应放入头文件中。
变量定义不应放在头文件中,应放在.c文件中。
变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。
变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。
即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
- 头文件应当职责单一
头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。
- 头文件应向稳定的方向包含。
头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
就我们的产品来说,依赖的方向应该是: 产品依赖于平台,平台依赖于标准库。
除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。
延伸阅读材料:编者推荐开发人员使用“依赖倒置”原则,即由使用者制定接口,服务提供者实现接口。
每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。
禁止头文件循环依赖。
.c/.h文件禁止包含用不到的头文件。
头文件应当自包含。
简单的说,自包含就是任意一个头文件均可独立编译。 如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
总是编写内部#include保护符(#ifndef #define #endif保护) 。
禁止在头文件中定义变量。
说明: 在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量。
禁止在extern "C"中包含头文件。
在extern "C"中包含头文件, 会导致extern "C"嵌套, Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏。
- 一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名。
说明:需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。
- 如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名。
说明:降低接口使用者的编写难度。
- 头文件不要使用非习惯用法的扩展名,如.inc。
目前很多产品中使用了.inc作为头文件扩展名,这不符合c语言的习惯用法。在使用.inc作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。
一些编译器也不会将其识别为头文件,导致一些功能无法使用。
- 同一产品统一包含头文件排列方式。
说明:常见的包含头文件排列方式: 功能块排序、文件名升序、稳定度排序。

9、可靠性
为保证代码的可靠性,编程时请遵循如下基本原则,优先级递减:
正确性,指程序要实现设计要求的功能。
稳定性、安全性,指程序稳定、可靠、安全。
可测试性,指程序要方便测试。
规范/可读性,指程序书写风格、命名规则等要符合规范。
全局效率,指软件系统的整体效率。
局部效率,指某个模块/子模块/函数的本身效率。
个人表达方式/个人方便性,指个人编程习惯。
(1)内存使用
① 在程序编制之前,必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。
② 防止内存操作越界。
内存操作主要是指对数组、指针、内存地址等的操作,内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细。
③ 必须对动态申请的内存做有效性检查,并进行初始化;动态内存的释放必须和分配成对以防止内存泄漏,释放后内存指针置为NULL。
对嵌入式系统,通常内存是有限的,内存的申请可能会失败,如果不检查就对该指针进行操作,可能出现异常,而且这种异常不是每次都出现,比较难定位。
指针释放后,该指针可能还是指向原有的内存块,可能不是,变成一个野指针,一般用户不会对它再操作,但用户失误情况下对它的操作可能导致程序崩溃。
④ 不使用realloc( )。
调用realloc对一个内存块进行扩展,导致原来的内容发生了存储位置的变化,realloc函数既要调用free,又要调用malloc。执行时究竟调用哪个函数,取决于是要缩小还是扩大相应内存块的大小。
⑤ 在通信程序中,为了保证高可靠性,一般不使用内存的动态分配。
⑥ 在往一个内存区连续赋值之前(memset,memcpy…),应确保内存区的大小能够容纳所赋的数据。
⑦ 尽量使用memmove( )代替memcpy( )。
在源、目的内存区域发生重叠的情况下,如果使用memcpy可能导致重叠区的数据被覆盖。
(2)指针使用
① 如果指针类型明确不会改变,应该强制为const类型的指针,以加强编译器的检查。
可以防止不必要的类型转换错误。
② 减少指针和数据类型的强制类型转化。
③ 移位操作一定要确定类型。

④ 对变量进行赋值时,必须对其值进行合法性检查,防止越界等现象发生。
尤其对全局变量赋值时,应进行合法性检查,以提高代码的可靠性、稳定性。
10、可测试性
在设计阶段就必须考虑所编写代码的可测试性,只有提供足够的测试手段才能全面、高效地发现和解决代码中的各类问题。编写的代码是否可测试,是衡量代码质量的最基本的、最重要的尺度之一。
程序设计过程中(或程序编码完毕后),必须编写软件模块测试文档,测试文档的编写规范参见后续规范,主要应包括:设计思路、程序输入、程序输出和数据结构等。测试是设计的一部分。
① 在同一项目组或产品组内,为准备集成测试和系统联调,要有一套统一的调测开关及相应信息输出函数,并且要有详细的说明。统一的调试接口和输出函数由模块设计和测试人员根据项目特性统一制订,由项目系统人员统一纳入系统设计中。
② 在同一个项目组或产品组内,调测打印出的信息串要有统一的格式。信息串中应当包含所在的模块名(或源文件名)及行号等信息。
统一的调测信息格式便于集成测试。
③ 在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码(如打印函数等)。
程序的调试与测试是软件生存周期中非常重要的一个阶段,如何对软件进行较全面、高效率的测试并尽可能地找出软件中的错误就成为非常关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列测试代码作为手段,为单元测试、集成测试及系统联调提供方便。
④ 设计人员在编程的同时要完成调试信息输出接口函数,但是测试点的选择可以由模块测试人员根据需要合理选择,测试点的选择可以根据测试用例而定,不同的测试用例选择不同的测试点。
为模块测试做准备。
⑤ 调测开关应分为不同级别和类型。
调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。
⑥ 在进行集成测试和系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。
好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
⑦ 程序的编译开关应该设置为最高优先级,并且编译选项不要选择优化。
将编译开关置为最高优先级,可以将程序的错误尽量暴露在编译阶段,便于修正程序;将编译选项设置为不优化,是为了避免编译器优化时出错,导致程序运行出错,也更容易在程序出错时对错误进行定位。
⑧ 在设计时考虑以下常见发现错误的方法。
以下发现错误的方法为可以为编写可测试性代码提供思路:
使用所有数据建立假设
求精发现错误的测试用例
通过不同的方法再生错误
产生更多的数据以生成更多的假设
使用否定测试结果
提出尽可能多的假设
缩小可疑代码区
检查最近作过修改的代码
扩展可疑代码区
逐步集成
怀疑以前出过错的子程序
耐心检查
为迅速的草率的调试设定最大时间
检查一般错误
使用交谈调试法
中断对问题的思考
⑨ 在设计时考虑以下常见改正错误的方法。
以下改正错误的方法可以为编写可测试性代码提供思路:
理解问题的实质
理解整个程序
确诊错误
放松情绪
保存初始源代码
修改错误而不是修改症状
仅为某种原因修改代码
一次作一个修改
检查你的工作,验证修改
寻找相似错误
⑩ 程序开发人员对自己模块内的函数必须通过有效的方法进行测试,保证所有代码都执行到。
⑪ 单元测试
编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例, 同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。
说明:为单元测试而准备。
⑫ 集成测试
在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。
说明:好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
⑬ 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难。
说明:单元测试实施依赖于:
● 模块间的接口定义清楚、完整、稳定;
● 模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
● 模块内部的关键状态和关键数据可以查询,可以修改;
● 模块原子功能的入口唯一;
● 模块原子功能的出口唯一;
● 依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。
⑭ 在同一项目组或产品组内,调测打印的日志要有统一的规定。
说明:统一的调测日志记录便于集成测试,具体包括:
● 统一的日志分类以及日志级别;
● 通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
● 在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
● 调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
⑮ 在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计/编码的正确。
⑯ 单元测试关注单元的行为而不是实现,避免针对函数的测试。
说明:应该将被测单元看做一个被测的整体,根据实际资源、进度和质量风险,权衡代码覆盖、打桩工作量、补充测试用例的难度、被测对象的稳定程度等,一般情况下建议关注模块/组件的测试,尽量避免针对函数的测试。尽管有时候单个用例只能专注于对某个具体函数的测试,但我们关注的应该是函数的行为而不是其具体实现细节。
11、断言与错误处理
断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告)。它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。在实际应用时,可根据具体情况灵活地设计断言。
① 整个软件系统应该采用统一的断言。如果系统不提供断言,则应该自己构造一个统一的断言供编程时使用。
整个软件系统提供一个统一的断言函数,如Assert(exp),同时可提供不同的宏进行定义(可根据具体情况灵活设计),如:
(1)#define ASSERT_EXIT_M 中断当前程序执行,打印中断发生的文件、行号,该宏一般在单调时使用。
(2)#define ASSERT_CONTINUE_M 打印程序发生错误或异常的文件,行号,继续进行后续的操作,该宏一般在联调时使用。
(3)#define ASSERT_OK_M 空操作,程序发生错误情况时,继续进行,可以通过适当的方式通知后台的监控或统计程序,该宏一般在RELEASE版本中使用。
② 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
③ 指向指针的指针及更多级的指针必须逐级检查。
④ 对较复杂的断言加上明确的注释。
⑤ 用断言保证没有定义的特性或功能不被使用。
假设某通信模块在设计时,在消息处理接口准备处理“同步消息”和“异步消息”。但当前的版本中的消息处理接口仅实现了处理“异步消息”,且在此版本的正式发行版中,用户层(上层模块)不应产生发送“同步消息”的请求,那么在测试时可用断言检查用户是否发送了“同步消息”。
⑥ 用调测开关来切换软件的DEBUG版和RELEASE版,而不要同时存在RELEASE版本和DEBUG版本的不同源文件,以减少维护的难度。
⑦ 正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。
加快软件运行速度。
⑧ 在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。
⑨ 用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。
对RELEASE版本不用的测试代码可以通过断言来检查测试代码中的非法情况。
⑩ 用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查。
程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。如用断言检查编译器的int型数据占用的内存空间是否为2个字节:Assert (sizeof(int) == 2);
⑪ 尽可能模拟出各种程序出错状态,测试软件对出错状态的处理。
⑫ 编写错误处理程序,然后在处理错误之后可用断言宣布发生错误。
假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告。

12、程序效率
① 编程时要经常注意代码的效率。
说明:代码效率分为全局效率、 局部效率、时间效率及空间效率。
全局效率是站在整个系统的角度上的系统效率;
局部效率是站在模块或函数角度上的效率;
时间效率是程序处理输入任务所需的时间长短;
空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。
② 提高代码效率
在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。
③ 全局效率高于局部效率
局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。
④ 提高代码空间效率
通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。
说明:这种方式是解决软件空间效率的根本办法
⑤ 循环体内工作量最小化
说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。
⑥ 仔细分析有关算法,并进行优化。
⑦ 仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。
⑧ 对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。
说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系, 仅在代码上下功夫一般不能解决根本问题
⑨ 要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。
说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。
⑩ 在多重循环中,应将最忙的循环放在最内层。尽量减少循环嵌套层次。
说明:减少 CPU 切入循环层的次数。
⑪ 避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。
⑫ 尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。
说明:浮点运算除法要占用较多 CPU 资源。
⑬ 对于多维大数组,避免来回跳跃式访问数组成员。

⑭ 将多次被调用的 “小函数”改为inline函数或者宏实现。
说明:如果编译器支持inline,可以采用inline函数。否则可以采用宏。
在做这种优化的时候一定要注意下面inline函数的优点:
其一编译时不用展开,代码SIZE小。
其二可以加断点,易于定位问题,例如对于引用计数加减的时候。
其三函数编译时,编译器会做语法检查。三思而后行。
13、质量保证
① 留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。
② 为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。
③ 系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。
④ 对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。
⑤ 第三方软件开发工具包
使用第三方提供的软件开发工具包或控件时,要注意以下几点:
(1)充分了解应用接口、使用环境及使用时注意事项。
(2)不能过分相信其正确性。
(3)除非必要,不要使用不熟悉的第三方工具包与控件。
说明:使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工具包与控件也有可能存在问题。
⑥ 资源文件
资源文件(多语言版本支持),如果资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、 DLL 文件或其它单独的描述文件(如数据库格式)。
⑦ 易用错的操作符
(1) 除操作符"/"
当除操作符“/”的运算量是整型量时,运算结果也是整型。
如:1/2=0
(2)求余操作符"%"
求余操作符"%"的运算量只能是整型。
如: 5%2=1,而5.0%2是错误的
⑧ 不仅关注接口,同样要关注实现。
说明:这个原则看似和“面向接口”编程思想相悖,但是实现往往会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,往往还受制于其他隐含约束,如:物理内存的限制,网络状况,具体看“抽象漏洞原则”。
⑨ 禁止内存操作越界。
坚持下列措施可以避免内存越界:
● 数组的大小要考虑最大情况,避免数组分配空间不够。
● 避免使用危险函数sprintf/vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
● 使用memcpy/memset时一定要确保长度不要越界
● 字符串考虑最后的’\0’,确保所有字符串是以’\0’结束
● 指针加减操作时,考虑指针类型长度
● 数组下标进行检查
● 使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
⑩ 禁止内存泄漏。
说明:内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各种资源)泄漏是常见的错误。
坚持下列措施可以避免内存泄漏:
● 异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
● 删除结构指针时,必须从底层向上层顺序删除
● 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
● 避免重复分配内存
● 小心使用有return、 break语句的宏,确保前面资源已经释放
● 检查队列中每个成员是否释放
⑪ 禁止引用已经释放的内存空间。
说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块,而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。
坚持下列措施可以避免引用已经释放的内存空间:
● 内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
● 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
● 避免操作已发送消息的内存。
● 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
14、安全性
代码的安全漏洞大都是由代码缺陷导致,但不是所有代码缺陷都有安全风险。理解安全漏洞产生的原理和如何进行安全编码是减少软件安全问题最直接有效的办法。
① 对用户输入进行检查。
说明:不能假定用户输入都是合法的,因为难以保证不存在恶意用户,即使是合法用户也可能由于误用误操作而产生非法输入。用户输入通常需要经过检验以保证安全,特别是以下场景:
● 用户输入作为循环条件
● 用户输入作为数组下标
● 用户输入作为内存分配的尺寸参数
● 用户输入作为格式化字符串
● 用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、 SQL注入、缓冲区溢出、数据破坏等问题。
可采取以下措施对用户输入检查:
● 用户输入作为数值的,做数值范围检查
● 用户输入是字符串的,检查字符串长度
● 用户输入作为格式化字符串的,检查关键字“%”
● 用户输入作为业务数据,对关键字进行检查、转义
② 确保所有字符串是以NULL结束。
说明: C语言中‟\0‟作为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、 strlen())依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为。
为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数。如:
● 用strncpy()代替strcpy()
● 用strncat()代替strcat()
● 用snprintf()代替sprintf()
● 用fgets()代替gets()
这些函数会截断超出指定限制的字符串,但是要注意它们并不能保证目标字符串总是以NULL结尾。如果源字符串的前n个字符中不存在NULL字符,目标字符串就不是以NULL结尾。
③ 不要将边界不明确的字符串写到固定长度的数组中。
说明:边界不明确的字符串(如来自gets()、 getenv()、 scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。

④ 避免整数溢出。
说明:当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
⑤ 避免符号错误。
说明:有时从有符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。
有符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
⑥ 避免截断错误。
说明:将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失。使用截断后的变量进行内存操作,很可能会引发问题。
⑦ 确保格式字符和参数匹配。
说明:使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。
⑧ 避免将用户输入作为格式化字符串的一部分或者全部。
说明:调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。
攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。

⑨ 避免使用strlen()计算二进制数据的长度。
说明: strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。
因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本。二进制字符串一般情况下是不能用strlen()来计算长度的。
⑩ 使用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的值)执行比较。
对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误。
⑪ 防止命令注入。
说明: C99函数system()通过调用一个系统定义的命令解析器(如UNIX的shell, Windows的CMD.exe)来执行一个指定的程序/命令。类似的还有POSIX的函数popen()。
如果system()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变system()调用的行为。

14、代码编辑、编译、审查
① 打开编译器的所有告警开关对程序进行编译
② 在产品软件(项目组)中,要统一编译开关选项
③ 通过代码走读及审查方式对代码进行检查。
说明:
代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或开发人员交叉的方式进行;
代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。
④ 测试部测试产品之前,应对代码进行抽查及评审
⑤ 编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。
⑥ 同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。
说明:同一项目组最好采用相同的智能语言编辑器,如 Muiti Editor, Visual Editor等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。
⑦ 要小心地使用编辑器提供的块拷贝功能编程。
说明:当某段代码与另一段代码的处理功能相似时,许多开发人员都用编辑器提供的块拷贝功能来完成这段代码的编写。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能都很相近,所以使用块拷贝时要注意,除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。不应指望编译器能查出所有这种错误,比如当使用的是全局变量时,就有可能使某种错误隐藏下来。
⑧ 合理地设计软件系统目录,方便开发人员使用。
说明:方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、 编译、链接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。
⑨ 某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
说明:在 Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。
⑩ 使用代码检查工具(如 C 语言用 PC-Lint)对源程序检查。
⑪ 使用软件工具(如 LogiSCOPE)进行代码审查。
⑫ 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警。
⑬ 不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数。
15、代码测试、维护
① 单元测试要求至少达到语句覆盖。
② 单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。
③ 清理、整理或优化后的代码要经过审查及测试。
④ 代码版本升级要经过严格测试。
⑤ 使用工具软件对代码版本进行维护。
⑥ 正式版本上软件的任何修改都应有详细的文档记录。
⑦ 发现错误立即修改,并且要记录下来。
⑧ 关键的代码在汇编级跟踪。
⑨ 仔细设计并分析测试用例,使测试用例覆盖尽可能多的情况,以提高测试用例的效率。
⑩ 尽可能模拟出程序的各种出错情况,对出错处理代码进行充分的测试。
⑪ 仔细测试代码处理数据、变量的边界情况。
⑫ 保留测试信息,以便分析、总结经验及进行更充分的测试。
⑬ 不应通过“试”来解决问题,应寻找问题的根本原因。
⑭ 对自动消失的错误进行分析,搞清楚错误是如何消失的。
⑮ 修改错误不仅要治标,更要治本。
⑯ 测试时应设法使很少发生的事件经常发生。
⑰ 明确模块或函数处理哪些事件,并使它们经常发生。
⑱ 坚持在编码阶段就对代码进行彻底的单元测试,不要等以后的测试工作来发现问题。
⑲ 去除代码运行的随机性(如去掉无用的数据、代码及尽可能防止并注意函数中的“内部寄存器”等),让函数运行的结果可预测,并使出现的错误可再现。
规范检查表










