C语言编程规范
总体原则
- 清晰第一
- 简洁为美
- 选择合适的代码风格,与代码原有风格保持一致
头文件
原则
- 头文件中适合放置接口的声明,不是和放置实现
- 头文件应当职责单一
- 头文件应向稳定的方向包含
规则
- 每一个.c文件都应有一个同名.h文件,用于声明需要对外公开的接口
- 禁止头文件循环依赖
- .c/.h文件禁止包含用不到的头文件
- 头文件应当自包含
- 总是编写内部#include保护符(#define 保护)
- 禁止在头文件中定义变量
- 只能通过包含头文件的方式使用其他.c提供的接口,禁止在c中通过extern的方式是哟欧诺个外部函数接口、变量
- 禁止在extern “C”中包含头文件
建议
- 一个模块通常包含多个.c文件,建议放在统一目录下,目录名为模块名,方便外部使用,每一个模块提供一个.h,文件名为目录名
- 如果一个模块包含了多个子模块,建议每一个子模块提供一个对外的.h,文件名为子模块名,降低接口使用的编写难度
- 头文件不要使用非习惯法的扩展名
- 同一产品统一包含头文件排列方式:功能块排序、文件名排序、稳定度排序
函数
函数设计上:编写整洁函数,同时把代码有效组织起来
整洁函数要求:代码简单直接、不隐藏设计者的意图、干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织:逻辑层组织和物理层组织两个方面,逻辑层主要讲不同功能的函数通过某种联系组织,关注模块间的接口,物理层无论是什么目录或者名字空间,将函数用一种标准的方法组织起来,例入设计层级结构,名称等
原则
- 一个函数仅完成一个功能
- 重复代码应该尽可能提炼成函数
规则
- 避免函数过长,新增函数有效代码不超过五十行,除去某些实现算法的函数
- 避免函数的代码块嵌套过深,新增函数的代码块嵌套建议不超过四层
- 可重入函数应避免使用共享变量;若需要使用,则应该通过互斥手段对其加以保护
- 对参数的合法性检查,有调用者负责还是接口函数负责,应在项目组/模块内统一规定,缺省由调用者负责
- 对函数的错误返回码要全面处理
- 设计高扇入,合理扇出(小于7)的函数
- 废弃代码要及时清除
建议
- 函数不变参数使用const
- 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用
- 检查函数所有非参数输入的有效性,如数据文件、公共变量等。
- 函数的参数个数不超过5个
- 除了打印类函数外,不要使用可变长参函数
- 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字
标识符的命名与定义
通用命名规则
- Unix like 风格:单词全小写,用下划线分割
- Windows 风格:大小写混用,每个单词首字母大写
- 匈牙利命名法
原则
- 标识符的命名要清晰明了,同时使用完整单词或可以普遍理解的缩写,避免产生误会。
- 除了常见的通用缩写外,不实用单词缩写,不适用汉语拼音
规则
产品/项目组内保持统一命名风格
建议
- 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等
- 尽量避免名字中出现数字编号,除非逻辑上的确需要编号
- 标识符前不应添加模块、项目、产品、部门的名称作为前缀
- 平台/驱动等适配代码的标识符明明风格保持和平台/驱动一致
- 重构/修改部分代码时,应保持和原有代码的命名风格一致
文件命名规则
建议
文件命名统一采用小写字符
变量命名规则
规则
- 全局变量应增加"g_"前缀
- 静态变量应增加"s_"前缀
- 禁止使用单字节命名变量,但允许定义i,j,k作为局部循环变量
建议
- 变量命名不建议使用匈牙利命名法
- 使用名词或者形容词+名词的方式命名变量
函数命名规则
建议
- 函数命名应当以函数要执行的动作命名,一般采用动词或者动词+名词的结构
- 函数指针除了前缀,掐按照函数的命名规则命名
宏的命名规则
规则
- 对于数值或者字符串等常量的定义,建议采用全大写字母,单词之间加下划线的方式命名(枚举同样建议使用此方式)
- 出了头文件或编译开关等特殊标志定义,宏定义不能使用下划线开头和结尾
变量
原则
- 一个变量只有一个功能,不能讲一个变量用作多种用途
- 结构功能单一,不要设计面面俱到的数据结构
- 不用或者少用全局变量
规则
- 防止局部变量与全局变量同名
- 通讯过程中使用的结构,必须注意字节序
- 严禁使用未经初始化的变量作为右值
建议
- 构造仅有一个模块或函数可以修改、创建,而其余有关模块函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一个全局变量的现象发生。
- 使用面向接口编程思想,通过API访问数据,如果本模块的数据需要对外部模块开放,应提供接口函数来设置获取,同时注意全局数据的访问互斥
- 在首次使用前初始化变量,初始化的地方离使用的地方越近越好
- 明确全局变量的初始化顺序,避免跨模块的初始化依赖
- 尽量减少没有必要的数据类型默认转换和强制转换
宏、常量
规则
- 用宏定义表达式时,要使用完备的括号
- 将宏所定义的多条表达式放在大括号中,使用do...while(0)方式定义完全不用担心使用者如何使用宏
- 使用宏时,不允许参数发生变化
- 不允许直接使用魔鬼数字(没有具体含义的数字、字符串等)
建议
- 除非必要,尽量使用函数代替宏
- 常量建议使用const定义代替宏
- 宏定义中尽量不使用return, goto, continue, break等改编程序流程的语句
质量保证
原则
- 代码质量保证优先原则
- 正确性,指程序要实现设计要求的功能
- 简洁性,指程序易于理解并且易于实现
- 可维护性,程序被修改的能力,包括纠错、改进、新需求或者功能规格变化的适应能力
- 可靠性,指程序在给定时间间隔和环境条件下,an设计要求成功运行程序的概率
- 代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力
- 代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间
- 可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力
- 个人表达方式、个人方便性,个人的编程习惯
- 要时刻注意易混淆易用错的操作符
- 必须了解编译系统的内存分配方式,特别是编译系统对不听类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。
- 不仅关注接口,还要关注实现
规则
- 禁止内存越界操作
- 数组的大小要考虑最大情况,避免数组分配空间不够
- 便面使用危险函数 sprintf /vsprintf /strcpy /strcat /gets 操作字符串,使用相对安全的函数 snprintf /strncpy /strncat /fgets 代替
- 使用 memcpy / memset 时一定要确保长度不要越界
- 字符串考虑最后的"\0",确保所有字符串是以字符串结束符结尾
- 指针加减操作时,考虑指针类型长度
- 数组下表进行检查
- 使用时 sizeof 或者 strlen 计算结构/字符串长度,避免手工计算
- 禁止内存泄漏,内存和资源泄露是常见的错误
- 异常出口处检查内存、定时器、文件句柄、Socket、队列、信号量、GUI等资源是否全部释放
- 删除结构指针时,必须从底层向上层顺序删除
- 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
- 避免重复分配内存
- 小心使用有 return,break 语句的宏,确保前面资源已经释放
- 检查队列中每个成员是否释放
- 禁止引用已经释放的内存空间,防止已经释放得内存块在另一个模块中随后使用
- 内存释放后,把指针置为NULL,使用内存指针前进行非空判断
- 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用
- 避免操作已发送消息的内存
- 自动存储对象的地址不应赋值给其他的第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
- 编程时要防止差1错误,常见于大于小于,小于等于大于等于错误,注意边界值
- 所有的if ... else if结构应该由 else 子句结束,switch 语句必须有 default 分支
建议
- 函数中分配的内存,在函数退出之前要释放。申请内存要在申请处添加注释,说明在何处释放
- if 语句尽量加上 else 分支,对没有 else 分支的语句要小心对待
- 不要滥用 goto 语句,goto 语句会破坏程序结构性,尽量不要使用
- 时刻注意表达式是否会上溢、下溢
程序效率
原则
- 在保证软件系统的正确性、简介、可维护性、可靠性及可测性的前提下,提高代码效率。但是不能影响软件的正确、简介、可维护性、可靠性及可测性,将正确执行概率大的代码放在前面
- 通过对数据结构、程序算法的优化来提高效率
建议
- 将不变条件的计算移到循环外,不是每次循环都要做得操作,移出循环外部执行
- 对于多为大数组,避免来回跳跃式访问数组成员
- 创建资源库,以减少分配对象的开销,使用线程池机制,避免线程频繁创建、销毁的系统调用,使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块内存,当系统申请内存时,从内存池获取小块内存,使用完毕再是放到内存池,避免内存申请释放的频繁系统调用
- 讲多次被调用的“小函数”改为inline函数或者宏实现
注释
原则
- 优秀的代码可以自我解释,不经过注释即可轻易读懂
- 注释的内容要清楚、明了、含义准确,防止注释二义性
- 在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,而不是重复描述代码。
规则
-
修改代码时,维护代码周边的所有注释,保证代码注释的一致性,不再有用的注释要删除。
-
文件同步应进行注释,注释需列出:版权说明,版本号,文件名,作者,内容,功能说明,依赖关系,修改日志,头文件要包含简要函数功能说明。
-
函数声明处注释描述函数功能,性能及用法,包括输入和输出参数、函数返回值、可重入的要求等,定义出详细描述函数功能和实现要点,如实现的简要步骤,实现的理由,设计约束等。
-
全局变量要有较为详细的注释,包括对其功能、取值范围以及存取时注意事项等说明
2.png
BYTE g_GTTranErrorCode;
-
注释应放在其代码上方相邻位置或右方,不可放在下面。如果放在其上方则需与其上方代码用空行隔开,且与下放代码缩进相同
-
对于switch下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释,防止遗漏break
-
避免在注释中使用缩写,除非是标准化缩写
-
同一产品或项目组统一注释风格
建议
-
避免在一行代码或表达式中间插入注释
-
注释考虑易读性及外观排版因素,不能随便使用中英文混搭,建议使用中文
-
文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式,比如doxygen格式,方便导出生成帮助文档
3.png
排版与格式
规则
-
程序块采用缩进风格编写,每级缩进为4个空格,TAB缩进和空格缩进要记得转换,宏定义、变异开关、条件预处理语句可以顶格。
-
相对独立的程序块之间、变量说明之后必须加空行
-
一条语句不能过长,不能拆分则分行写。一般来说132比较合适。
- 换行时要增加一级缩进,使代码的可读性更好
- 低优先级操作符处划分新行,换行时操作符应该也放下来,放在新行首
- 换行时建议一个完整的语句放在一行,不要根据字符数断行
-
多个短语句(包括赋值语句)不允许写在同一行内,一行只写一条语句
-
if, for, do, while, case, switch, default 等语句独占一行,执行语句必须用缩进风格写。
如果 if/else 配套语句中有一个分支有括号,那么另一个分支即便一行代码也要建议增加括号;添加的括号可以再if之后也可以独占下一行,独占下一行时,可以和 if 在一个缩进级别,也可以在下一个缩进级别,如果if语句很长,建议独占一行。
-
在两个以上的关键字、变量、常量进行对等操作时,他们之间的操作符之前、之后或者前后都要加空格;进行非对等操作时,如果是关系密切的立即操作符(->),后不加空格,比较操作符和双目操作符前后加空格,单目操作符则不用。
建议
- 注释符与注释内容之间要用一个空格进行分割,使注释内容更清晰
- 源程序中关系较为紧密的代码应尽可能相邻
表达式
规则
表达式的值在标准所允许的任何运算次序下都应该是相同的,除了少数操作符之外,子表达式所依据的运算次序是未指定的
- 自增或自减操作符,使用时,将自增运算作为单独的语句,可以避免这个问题
- 函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同
- 函数指针和函数自身地址的计算次序未定义
- 函数调用应当明确函数的调用次序
- 嵌套赋值语句,不要再表达式中嵌套赋值。
- volatile访问,硬件可能会改变的变量
建议
- 函数调用不要作为另一个函数的参数使用,否则对于代码的调试阅读都不利
- 赋值语句不要写在if等语句中,或者作为函数的参数使用
- 用括号明确表达式的操作顺序,避免过分依赖默认优先级,使用括号强调优先级,避免默认优先级与设计思想不符导致程序出错
- 一元操作符不需要括号
- 二元以上操作符,涉及多种操作符,则使用括号
- 即使所有的操作符都是相通的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序
- 赋值操作符不能用在产生布尔值的表达式上
代码编辑、编译
规则
- 使用编译器的最高警告级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警
- 在产品软件中,要统一编译开关、静态检查选项以及相应告警清除策略。如果必须禁用某个告警,应尽可能单独局部禁用,并编写一个清晰的注释,说明为什么。
- 本地构建工具的配置应该和持续集成的一致
- 使用版本控制系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功,降低集成难度。
建议
要小心地使用编辑器提供的快拷贝功能编程
可测性
原则
模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难,单元测试依赖于
- 模块间的接口定义清晰完整稳定
- 模块功能有明确的验收条件(预置条件、输入和预期结果)
- 模块内部的关键状态一和关键数据可以查询,可以修改
- 模块原子功能的入口唯一
- 模块原子功能的出口唯一
- 依赖集中处理,和模块相关的全局变量应该尽可能的少,或者采用某种封装形式
规则
- 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调试开关及相应打印函数,并且有详细的说明
- 在同一项目组或产品组内,调测打印的日志要统一,便于集成测试
- 统一的日志分类及日志级别
- 通过命令行、网管等方式可以配置和改变日志输出的内容和格式
- 在关键分支要记录日志,日之间以不要记录在原子函数中,否则难以定位
- 调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生环境等
- 使用断言记录内部假设,断言是对某种内部模块的假设条件进行检查,如果假设不成立,说明存在编程设计错误。
- 不能用断言来检查运行时错误
建议
为单元测试和系统故障注入测试准备好方法和通道
安全性
代码的安全问题大都由代码缺陷导致,但并不是所有代码缺陷都有安全风险。
原则
对用户输入进行检查,不能假定所有用户输入都是合法的,特别是以下场景
- 用户输入作为循环条件
- 用户输入作为数组下标
- 用户输入作为内存分配的尺寸参数
- 用户输入作为格式化字符串
- 用户输入作为业务数据(作为命令执行参数,拼装sql语句,以特定格式持久化),这些情况下如果不对用户数据做合法性验证,很可能导致各种安全问题
采取以下措施对输入进行检查
- 用户输入作为数值的,做数值范围检查
- 用户输入是字符串的,检查字符串长度
- 用户输入作为格式化字符串的,检查关键字"%"
- 用户输入作为业务数据的,对关键字进行检查、转义
字符串操作安全
规则
-
确保所有的字符串是以NULL结束的,否则会导致缓冲区溢出和其他行为,使用相对安全的限制住数量的字符串操作函数代替一些危险的函数
- strncpy() ---- strcpy()
- strncat() ---- strcat()
- snprintf() ---- sprintf()
- fgets() ---- gets()
这些函数会截断超出指定限制的字符串,但是要注意他们并不能保证目标字符串以NULL结尾。截断使用sizeof() - 1 并向最后一个数组空间存入NULL 即可保证。
-
不要将边界不明确的字符串写入固定长度的数组中,否则可能导致缓冲区溢出。
整数安全
整型提升、整型转换级别、普通算术转换的整型操作都带来了整数溢出的问题
规则
- 避免整数溢出,当一个整数被增加超过其最大值是会发生上溢,被减小小于其最小值会发生下溢,有无符号都会发生
- 避免符号错误,有事从有符号到无符号会发生错误,最高位会丧失其符号位的功能。
- 避免截断错误,将较大整型转较小整型就会发生截断错误,低位保留高位丢弃引起数据丢失。
格式化输出安全
规则
- 确保格式字符和参数匹配,使用格式化字符串要注意匹配问题以及保留数量和数据类型,会导致程序异常终止。
- 避免将用户输入作为格式化字符串的一部分或者全部。调用而是花I/O函数的时候,不要直接或间接将用户输入作为格式化字符串的一部分或者全部,将存在很多风险。未经检查过滤的用户输入只能作为参数。
文件I/O 安全
规则
- 避免使用strlen()计算二进制数据的长度。
- 使用int类型变量来接受字符I/O函数的返回值。
其它
规则
防止命令注入,system()函数通过调用一个系统定义的命令解析器来执行一个指定的程序/命令,类似的还有POSIX的popen().一旦被恶意输入,将改变行为。正确的做法事使用POSIX函数execve()代替system()
单元测试
规则
在编写代码的同时或者在编写前,编写单元测试用例验证软件设计/编码的正确。
建议
单元测试关注单元的行为而不是实现,避免针对函数的测试,将被测单元看做一个被测的整体,根据实际资源进度等来进行测试,尽量避免针对函数的测试,关注函数的行为而不是具体的实现细节
可移植性
规则
不能定义、重定义或者取消定义标准库/平台中保留的标识符、宏和函数。
建议
- 不使用与硬件或操作系统关系很大的语句,使用建议的标准语句,提高软件的可移植及可重用性。
- 除非为了满足特殊需求,避免使用嵌入式汇编
网友评论