头文件
- 在一个设计良好的系统中, 修改一个文件,只需要重新编译数个,甚至是一个文件:
- 头文件中适合放置接口的声明,不适合放置实现
- 头文件是模块( Module)或单元( Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
- 内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
- 内部使用的宏、枚举、结构定义不应放入头文件中。
- 变量定义不应放在头文件中,应放在.c文件中。
- 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
- 头文件应当职责单一
- 不常用的头文件不应该被包含
- 头文件应向稳定的方向包含
- 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口
- 禁止头文件循环依赖
- c/.h文件禁止包含用不到的头文件
- 头文件应当自包含:
- 自包含就是任意一个头文件均可独立编译
- 总是编写内部#include保护符( #define 保护)
- 以阻止头文件内容被包含多于一次:通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容
- 所有头文件都应当使用#define 防止头文件被多重包含,命名格式为
FILENAME_H
,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H
- 没有在宏最前面加上
_
,即使用FILENAME_H
代替_FILENAME_H_
,是因为一般以”_”和”__”开头的标识符为系统保留或者标准库使用
- 禁止在头文件中定义变量
- 只能通过包含头文件的方式使用其他.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改变时可能导致声明和定义不一致。
- 禁止在extern “C”中包含头文件
- 一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名
- 如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名。说明:降低接口使用者的编写难度。
- 头文件不要使用非习惯用法的扩展名,如.inc
- 同一产品统一包含头文件排列方式:
- 常见的包含头文件排列方式: 功能块排序、文件名升序、稳定度排序
- 以升序方式排列头文件可以避免头文件被重复包含
- 以稳定度排序, 建议将不稳定的头文件放在前面,这样不必编译稳定文件就可知不稳定文件有没有问题,减少编译时间
函数
- 代码的有效组织包括:逻辑层组织和物理层组织两个方面。
- 逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。
- 物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找
原则
- 一个函数仅完成一件功能
- 重复代码应该尽可能提炼成函数
规则
- 避免函数过长,新增函数不超过50行(非空非注释行)
- 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层
- 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护
- 可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量
- 编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性
- P16 例子
- 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责
- 对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率
- 对函数的错误返回码要全面处理
- 一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示
- 设计高扇入,合理扇出(小于7)的函数
- 函数不变参数使用const
- 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用
- 带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。
- 在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类,则返回为错针。
- 检查函数所有非参数输入的有效性,如数据文件、公共变量等
- 函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。
- 函数的参数个数不超过5个
- 除打印类函数外,不要使用可变长参函数
- 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字
- 如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。
- 使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。
- 建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作
标识符命名与定义
- 文件命名统一采用小写字符
- 全局变量应增加“g_”前缀
- 静态变量应增加“s_”前缀
- 函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构
- 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线“_”的方式命名(枚举同样建议使用此方式定义)
变量
原则
- 一个变量只有一个功能,不能把一个变量用作多种用途
- struct 功能单一;不要设计面面俱到的数据结构
- 不用或者少用全局变量:
- 单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量
- 防止局部变量与全局变量同名
规则和建议
- 通讯过程中使用的结构,必须注意字节序
- 严禁使用未经初始化的变量作为右值
- 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象
- 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥
- 明确全局变量的初始化顺序,避免跨模块的初始化依赖
宏、常量
- 用宏定义表达式时,要使用完备的括号:
- e.g.: #define RECTANGLE_AREA(a, b) ((a) * (b))
将宏所定义的多条表达式放在大括号中, 多条语句时写成do while(0)的方式
1
2
3
4#define FOO(x) do { \
printf("arg is %s\n", x); \
do_something_useful(x); \
} while(0)
使用宏时,不允许参数发生变化
- e.g. 定义SQUARE(a) 为((a) * (a)), 并赋值 a++
- 不允许直接使用魔鬼数字:
- 对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释
- 对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的
- 常量建议使用const定义代替宏
- 宏定义中尽量不使用return、 goto、 continue、 break等改变程序流程的语句
质量保证
- 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等
- 禁止内存操作越界
- 数组的大小要考虑最大情况,避免数组分配空间不够
- 避免使用危险函数sprintf /vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数 snprintf/strncpy/strncat/fgets代替
- 使用memcpy/memset时一定要确保长度不要越界
- 字符串考虑最后的’ \0’, 确保所有字符串是以’ \0’结束
- 指针加减操作时,考虑指针类型长度
- 数组下标进行检查
- 使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
- 禁止内存泄漏
- 内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各种资源)泄漏是常见的错误
- 异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
- 删除结构指针时,必须从底层向上层顺序删除
- 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
- 避免重复分配内存
- 小心使用有return、 break语句的宏,确保前面资源已经释放
- 检查队列中每个成员是否释放
- 禁止引用已经释放的内存空间
- 内存释放后,把指针置为NULL;使用内存指针前进行非空判断
- 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用
- 避免操作已发送消息的内存
- 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
- 函数中分配的内存,在函数退出之前要释放
- 有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放
- if语句尽量加上else分支,对没有else分支的语句要小心对待
- 不要滥用goto语句
程序效率
- 将不变条件的计算移到循环体外
- 对于多维大数组,避免来回跳跃式访问数组成员
- 创建资源库,以减少分配对象的开销
- 将多次被调用的 “小函数”改为inline函数或者宏实现
注释
- 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
- 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等
- 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
安全性
- 对用户输入进行检查:
- 用户输入作为循环条件,数组下标,内存分配的尺寸参数,格式化字符串,业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)等时需尤为注意
- 用户输入作为数值的,做数值范围检查
- 用户输入是字符串的,检查字符串长度
- 用户输入作为格式化字符串的,检查关键字“ %”
- 用户输入作为业务数据,对关键字进行检查、转义
- 确保所有字符串是以NULL结束
- C语言中‟\0‟作为字符串的结束符,即NULL结束符。
- 标准字符串处理函数(如strcpy()、 strlen())依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为
- 不要将边界不明确的字符串写到固定长度的数组中
- 边界不明确的字符串(如来自gets()、 getenv()、 scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出
- 避免整数溢出
- 当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢
- 带符号和无符号的数都有可能发生溢出
- 避免符号错误
- 有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义
- 避免截断错误
- 将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失
- 避免使用strlen()计算二进制数据的长度
- strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本
文档引用资源
- google C++ Style Guide
- 《 敏捷软件开发:原则、模式与实践》 ( Robert C.Martin 著 ) 第二部分“敏捷设计”章节
- 《 敏捷软件开发:原则、模式与实践》 第八章,单一职责原则(SRP)
- 《代码整洁之道》