- 清晰第一
- 简洁为美
- 相同项目,风格一致
头文件是模块(Module)或单元(Unit)的对外接口。
头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。
很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。
头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。
头文件循环依赖,指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的源代码重新编译。
很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。
简单的说,自包含就是任意一个头文件均可独立编译。
如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
多次包含一个头文件可以通过认真的设计来避免。
如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。
没有在宏最前面加上 _ ,即使用 FILENAME_H代替 FILENAME_H ,是因为一般以 _ 和 __ 开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以 _ 开头会给出告警。
定义包含保护符时,应该遵守如下规则:
- 保护符使用唯一名称;
- 不要在受保护部分的前后放置代码或者注释。
在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
若a.c使用了b.c定义的foo函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include
来使用foo。
禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。这一点我们因为图方便经常犯的。
在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名。
一个函数实现多个功能给开发、使用、维护都带来很大的困难。
重复代码提炼成函数可以带来维护成本的降低。
本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行。
本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次。
若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。
缺省由调用者负责。
对函数的错误返回码要全面处理;
设计高扇入,合理扇出(小于7)的函数;
扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。如下图:
A扇出为2,B扇出为1,C扇出为3。
不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用;
检查函数所有非参数输入的有效性,如数据文件、公共变量等;
函数的输入主要有两种:一种是参数输入;
另一种是全局变量、数据文件的输入,即非参数输入。
函数在使用输入参数之前,应进行有效性检查。
函数的参数个数不超过5个;
除打印类函数外,不要使用可变长参函数;
在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字。
目前比较常用的如下几种命名风格:
unix like风格:单词用小写字母,每个单词直接用下划线_分割,例如text_mutex,kernel_text_address。
Windows风格:大小写字母混用,单词连在一起,每个单词首字母大写。
不过Windows风格如果遇到大写专有用语时会有些别扭,例如命名一个读取RFC文本的函数,命令为ReadRFCText,看起来就没有unix like的read_rfc_text清晰了。
变量命名需要说明的是变量的含义,而不是变量的类型。
在变量命名前增加类型说明,反而降低了变量的可读性;
更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。
系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。
当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。
因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。
更好的方法是多条语句写成do while(0)的方式。
使用魔鬼数字的弊端:代码难以理解;
如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。
使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。
宏对比函数,有一些明显的缺点:宏缺乏类型检查,不如函数调用检查严格。
1.代码质量保证优先原则
(1) 正确性,指程序要实现设计要求的功能。
(2) 简洁性,指程序易于理解并且易于实现。
(3) 可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
(4) 可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
(5) 代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
(6) 代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
(7) 可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。
(8) 个人表达方式/个人方便性,指个人编程习惯。
比如说一些符号特性、计算优先级。
这个原则看似和“面向接口”编程思想相悖,但是实现往往会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,往往还受制于其他隐含约束,如:物理内存的限制,网络状况,具体看“抽象漏洞原则”。
坚持下列措施可以避免内存越界:
数组的大小要考虑最大情况,避免数组分配空间不够;
避免使用危险函数 sprintf /vsprintf/strcpy/strcat/gets 操作字符串,使用相对安全的函数 snprintf/strncpy/strncat/fgets 代替;
使用memcpy/memset时一定要确保长度不要越界;
字符串考虑最后的’\0’, 确保所有字符串是以’\0’结束;
指针加减操作时,考虑指针类型长度;
数组下标进行检查;
使用时sizeof或者strlen计算结构/字符串长度,避免手工计算。
坚持下列措施可以避免内存泄漏:
异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放;
删除结构指针时,必须从底层向上层顺序删除;
使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了。
小心使用有return、break语句的宏,确保前面资源已经释放;
检查队列中每个成员是否释放。
坚持下列措施可以避免引用已经释放的内存空间:
内存释放后,把指针置为NULL,使用内存指针前进行非空判断;
耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用;
避免操作已发送消息的内存;
自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)。
此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。
当编完程序后,应对这些操作符进行彻底检查。
使用变量时要注意其边界值的情况。
有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。
goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。
优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,
需要很多注释来解释的代码往往存在坏味道,需要重构。
不再有用的注释要删除。
定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。
全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明;
注释应放在其代码上方相邻位置或右方,不可放在下面;
如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。
对于有外籍员工的,由产品确定注释语言。
当前各种编辑器/IDE都支持TAB键自动转空格输入,需要打开相关功能并设置相关功能。
编辑器/IDE如果有显示TAB的功能也应该打开,方便及时纠正输入错误。
一行到底多少字符换行比较合适,产品可以自行确定。
换行时有如下建议:
换行时要增加一级缩进,使代码可读性更好;
低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
换行时建议一个完整的语句放在一行,不要根据字符数断行。
进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
单元测试实施依赖于:
模块间的接口定义清楚、完整、稳定;
模块功能的有明确的验收条件(包括:预置条件、输入和预期结果)。
模块内部的关键状态和关键数据可以查询,可以修改;
模块原子功能的入口唯一;
模块原子功能的出口唯一;
依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。
统一的调测日志记录便于集成测试,具体包括:
统一的日志分类以及日志级别;
通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
断言是用来处理内部编程或设计是否符合假设;
不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言。
如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
不能假定用户输入都是合法的,因为难以保证不存在恶意用户,即使是合法用户也可能由于误用误操作而产生非法输入。
用户输入通常需要经过检验以保证安全,特别是以下场景:
用户输入作为循环条件;
用户输入作为数组下标;
用户输入作为内存分配的尺寸参数;
用户输入作为格式化字符串;
用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)。
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
可采取以下措施对用户输入检查:
用户输入作为数值的,做数值范围检查;
用户输入是字符串的,检查字符串长度;
用户输入作为格式化字符串的,检查关键字“%”;
用户输入作为业务数据,对关键字进行检查、转义。
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函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。
因此用strlen处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本。
使用int类型变量来接受字符I/O函数的返回值;
防止命令注入。
C99函数system通过调用一个系统定义的命令解析器(如UNIX的shell,Windows的CMD.exe)来执行一个指定的程序/命令。
类似的还有POSIX的函数popen。
应该将被测单元看做一个被测的整体,根据实际资源、进度和质量风险,权衡代码覆盖、打桩工作量、补充测试用例的难度、被测对象的稳定程度等,
一般情况下建议关注模块/组件的测试,尽量避免针对函数的测试。
尽管有时候单个用例只能专注于对某个具体函数的测试,
但我们关注的应该是函数的行为而不是其具体实现细节。
程序中嵌入式汇编,一般都对可移植性有较大的影响。