华为嵌入式C语言代码简明规范
规范制订说明
前言
基于华为C语言编程规范 在线wiki文档
代码总体原则
清晰第一
代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化
- 优秀的代码可以自我解释(以我的水平可能做到比较难)
- 常写注释,并且注释写的清晰
简洁为美
写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。
如果重构/修改其他风格的代码时,比较明智的做法是根据现有代码的现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。
术语
原则:编程时必须坚持的指导思想。
规则:编程时强制必须遵守的约定。
建议:编程时必须加以考虑的约定。
说明:对此原则/规则/建议进行必要的解释。
示例:对此原则/规则/建议从正、反两个方面给出例子。
延伸阅读材料:建议进一步阅读的参考材料。
头文件
不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。
如果引入了新的依赖,则一旦被依赖的头文件修改,任何直接和间接依赖其头文件的代码都会被重新编译。
原则
原则1.1 头文件中适合放置接口的声明,不适合放置实现
头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
- 内部使用的函数(相当于类的私有方法)声明不应放在头文件中
- 内部使用的宏、枚举、结构定义不应放入头文件中。
- 变量定义不应放在头文件中,应放在.c文件中。
否则多次依赖会重复定义
- 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
原则1.2 头文件应当职责单一。
原则1.3 头文件应向稳定的方向包含。
说明:头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
规则
规则1.1 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。
说明:如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。
规则1.2 禁止头文件循环依赖
任何一个头文件的改变都会使得循环中的所有头文件重新编译
规则1.3 .c/.h文件禁止包含用不到的头文件。
规则1.4 头文件应当自包含。
“头文件应当自包含”是指头文件应该包含自身所需的所有内容,而不依赖于其他头文件。这样的头文件通常被称为”自包含头文件”。下面解释一下这个概念的意义:
- 独立性和可移植性:自包含头文件使得头文件本身更加独立,不依赖于其他头文件。这样做有助于提高代码的可移植性,因为当你在其他项目或环境中使用这个头文件时,不需要担心它依赖的其他头文件是否可用。
- 简化依赖关系:自包含头文件可以简化代码的依赖关系。如果一个头文件依赖于另一个头文件,而后者又依赖于其他头文件,这会形成复杂的依赖链。通过自包含头文件,可以减少这种依赖链,提高代码的可维护性。
- 避免重复包含:自包含头文件通常会包含预处理器指令来避免重复包含。这样可以确保在包含相同头文件多次时不会导致重复定义的问题。
- 提高效率:自包含头文件可以减少预处理器的工作量,因为它们不需要解析其他头文件的内容。这有助于提高编译效率。
规则1.5 总是编写内部#include保护符(#define 保护)。
所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H
,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H
。
规则1.6 禁止在头文件中定义变量。
说明:在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量。
规则1.8 禁止在extern “C”中包含头文件。
extern "C"
是用于在 C++ 中声明 C 函数时的一种语法。它告诉编译器这些函数按照 C 语言的约定进行链接。在 C++ 中,函数名的重载、名称修饰(name mangling)等特性会导致函数名在编译后被修改,这样的函数名在链接时可能无法与 C 代码中的函数名匹配。为了解决这个问题,C++ 提供了
extern "C"
,它告诉编译器不要对函数名进行 C++ 风格的名称修饰,而是按照 C 语言的规则进行链接。
函数
函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。
原则
原则2.1 一个函数仅完成一件功能。
说明:一个函数实现多个功能给开发、使用、维护都带来很大的困难。
将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。
原则2.2 重复代码应该尽可能提炼成函数
说明:重复代码提炼成函数可以带来维护成本的降低。
可以使用代码重复度检查工具
规则
规则2.1 避免函数过长,新增函数不超过50行(非空非注释行)。
规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层。
减少代码嵌套层数的方法
- 使用函数抽象:将嵌套的代码块提取成独立的函数,以便于重用和理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
plaintext >python复制代码def main_function(): if condition: process_items(items) else: handle_condition_not_met() >def process_items(items): for item in items: if item_valid(item): process_item(item) else: handle_invalid_item(item) >def item_valid(item): return item.condition >def process_item(item): # 处理item pass >def handle_invalid_item(item): # 处理无效item pass >def handle_condition_not_met(): # 处理条件未满足情况 pass
- 使用早期返回:在函数内部,尽早返回结果,而不是在多层嵌套中处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
python >def main_function(): if not condition: handle_condition_not_met() return for item in items: if not item_valid(item): handle_invalid_item(item) continue process_item(item) >def item_valid(item): return item.condition >def process_item(item): # 处理item pass >def handle_invalid_item(item): # 处理无效item pass >def handle_condition_not_met(): # 处理条件未满足情况 pass
- 使用异常处理:适用于处理特殊情况或错误的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
python >def main_function(): try: if condition: for item in items: process_item(item) else: raise ConditionNotMetError except ConditionNotMetError: handle_condition_not_met() >def process_item(item): if not item_valid(item): raise InvalidItemError # 处理item >def item_valid(item): return item.condition >def handle_condition_not_met(): # 处理条件未满足情况 pass >class ConditionNotMetError(Exception): pass >class InvalidItemError(Exception): pass
这些方法可以帮助将嵌套降低到合理的水平,使代码更易读、更易维护。
规则2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。
可能用不到。
说明:可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量。
规则2.4 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。
规则2.5 对函数的错误返回码要全面处理。
规则2.6 设计高扇入,合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
建议2.1 函数不变参数使用const。
建议2.2 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用。
建议2.4 函数的参数个数不超过5个。
建议2.5 除打印类函数外,不要使用可变长参函数。
标识符命名与定义
通用命名规则
unix like风格
单词用小写字母,每个单词直接用下划线‘_’分割,例如text_mutex,kernel_text_address。
Windows风格
大小写字母混用,单词连在一起,每个单词首字母大写
匈牙利命名法
匈牙利命名主要包括三个部分:基本类型、一个或更多的前缀、一个限定词。
原则
原则3.1标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。
原则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 产品/项目组内部应保持统一的命名风格。
示例:
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 copy/paste up/down
建议
建议3.2 尽量避免名字中出现数字编号,除非逻辑上的确需要编号。
建议3.3 标识符前不应添加模块、项目、产品、部门的名称作为前缀。
建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。
文件命名规则
建议
建议3.6 文件命名统一采用小写字符。
变量命名规则
规则
规则3.2 全局变量应增加“g_”前缀。
规则3.3 静态变量应增加“s_”前缀。
规则3.4 禁止使用单字节命名变量,但允许定义i、j、k作为局部循环变量。
建议
建议3.7 不建议使用匈牙利命名法。
建议3.8 使用名词或者形容词+名词方式命名变量。
函数命名规则
建议
建议3.9 函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构。
宏的命名规则
规则
规则3.5对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线‘_’的方式命名(枚举同样建议使用此方式定义)。
规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线‘_’开头和结尾。
变量
原则
原则4.1 一个变量只有一个功能,不能把一个变量用作多种用途。
我怎么感觉在某些情况下无妨啊,开多了变量会占用性能吧?
也就是说:重要性:代码的可读性、可维护性和减少错误>性能消耗
下列答案基于chatGPT
遵循“一个变量只有一个功能”的原则确实会增加一些变量的数量,但这通常不会显著增加系统开销,特别是在现代编程环境和硬件条件下。这是因为:
- 内存开销相对较小:
- 现代计算机的内存容量通常非常大,增加几个变量所占用的内存是非常小的。这种内存开销在绝大多数应用场景下可以忽略不计。
- 编译器优化:
- 现代编译器和解释器非常智能,它们可以优化代码,将不必要的变量删除或者合并,从而减少内存开销。例如,在优化编译过程中,编译器会尝试最小化变量的存储空间,并进行变量复用等优化操作。
- 代码可读性和维护性优先:
- 代码的可读性和可维护性在实际开发中比微小的性能优化更重要。可读性好的代码更容易被团队中的其他开发人员理解和维护,从而降低了开发和维护的成本。
- 开发效率和错误减少:
- 使用单一功能变量可以减少代码中的错误,使开发过程更加顺利。这种减少错误和提升开发效率的好处往往远远超过因为增加变量数量而带来的微小性能损失。
示例分析:
在这个示例中,尽管引入了多个变量,但它们使代码更容易理解和维护。变量名清晰地表达了它们的用途,减少了开发和调试中的潜在错误。
总结:
遵循“一个变量只有一个功能”的原则,不仅不会显著增加系统开销,反而可以通过提升代码质量和开发效率,降低长远的开发和维护成本。因此,在大多数情况下,这种做法是非常值得的。实际开发中,尤其是在团队协作和大型项目中,应优先考虑代码的可读性和可维护性。
原则4.2 结构功能单一;不要设计面面俱到的数据结构。
原则4.3 不用或者少用全局变量。
规则
规则4.1 防止局部变量与全局变量同名。
规则4.2 通讯过程中使用的结构,必须注意字节序。
字节序的解释(基于chatGPT)
字节序(Byte Order)是指在计算机内存中存储多字节数据时,字节的排列顺序。不同计算机系统可能采用不同的字节序,主要有两种类型:大端序(Big-Endian)和小端序(Little-Endian)。
大端序(Big-Endian)
定义:高位字节存储在低地址,低位字节存储在高地址。
示例
:假设有一个4字节(32位)的数值
。在大端序系统中,它的存储顺序如下:
小端序(Little-Endian)
定义:低位字节存储在低地址,高位字节存储在高地址。
示例
:同样的数值
在小端序系统中的存储顺序如下:
为什么字节序很重要
在多字节数据传输或跨平台交互时,字节序的不同可能导致数据解释错误。例如,一个大端序系统发送数据到一个小端序系统,如果不做任何处理,小端序系统可能会错误地解读数据内容。
字节序的处理
- 网络字节序:网络协议(如TCP/IP)通常规定使用大端序,这被称为网络字节序(Network Byte Order)。
- 字节序转换函数:大多数编程语言提供了用于字节序转换的函数。例如,在C语言中,
htons
(Host TO Network Short)、htonl
(Host TO Network Long)、ntohs
(Network TO Host Short)、ntohl
(Network TO Host Long)等函数用于在主机字节序和网络字节序之间进行转换。示例代码
C语言中的字节序处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
plaintext c复制代码#include <stdio.h> #include <stdint.h> #include <arpa/inet.h> void print_bytes(uint32_t value) { uint8_t *bytes = (uint8_t *)&value; for (int i = 0; i < 4; i++) { printf("%02x ", bytes[i]); } printf("\n"); } int main() { uint32_t host_value = 0x12345678; // 主机字节序 uint32_t network_value = htonl(host_value); // 转换为网络字节序(大端序) printf("Host byte order: "); print_bytes(host_value); // 打印主机字节序 printf("Network byte order: "); print_bytes(network_value); // 打印网络字节序 uint32_t converted_back = ntohl(network_value); // 转换回主机字节序 printf("Converted back to host byte order: "); print_bytes(converted_back); // 打印转换回的主机字节序 return 0; }
输出
总结
字节序在计算机内存中存储多字节数据时非常重要。理解和处理字节序问题,特别是在跨平台或网络通信中,确保数据正确传输和解释是至关重要的。通过明确协议中的字节序和使用合适的字节序转换函数,可以避免数据传输中的错误。
规则4.3 严禁使用未经初始化的变量作为右值。
建议
建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。
建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。
建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。
建议4.4 明确全局变量的初始化顺序,避免跨模块的初始化依赖。
建议4.5 尽量减少没有必要的数据类型默认转换与强制转换。
宏、常量
规则
规则5.1 用宏定义表达式时,要使用完备的括号。
规则5.2 将宏所定义的多条表达式放在大括号中。
规则5.3 使用宏时,不允许参数发生变化。
规则5.4 不允许直接使用魔鬼数字。
说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。
建议
建议5.1 除非必要,应尽可能使用函数代替宏。
建议5.2 常量建议使用const定义代替宏。
建议5.3 宏定义中尽量不使用return、goto、continue、break等改变程序流程的语句。
质量保证
原则
原则6.1 代码质量保证优先原则(性能并没有放在那么靠前的位置,有些意外!)
- 正确性,指程序要实现设计要求的功能。
- 简洁性,指程序易于理解并且易于实现。
- 可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
- 可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
- 代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
- 代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
- 可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。
- 个人表达方式/个人方便性,指个人编程习惯。
原则6.2 要时刻注意易混淆的操作符。
易混淆的操作符,如:赋值操作符“=” 逻辑操作符“==” 关系操作符“<” 位操作符"«" 关系操作符“>” 位操作符“»” 逻辑操作符“||” 位操作符"|" 逻辑操作符“&&” 位操作符"&" 逻辑操作符"!" 位操作符“~”。 易用错的操作符,如:除操作符"/"、求余操作符"%"、自加、自减操作符“++”、“–”。
原则6.3 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。
原则6.4 不仅关注接口,同样要关注实现。
规则
规则6.1 禁止内存操作越界。坚持下列措施可以避免内存越界:
- 数组的大小要考虑最大情况,避免数组分配空间不够。
- 避免使用危险函数sprintf/vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
- 使用memcpy/memset时一定要确保长度不要越界
- 字符串考虑最后的’\0’,确保所有字符串是以’\0’结束
- 指针加减操作时,考虑指针类型长度
- 数组下标进行检查
- 使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
规则6.2 禁止内存泄漏。坚持下列措施可以避免内存泄漏:
- 异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
- 删除结构指针时,必须从底层向上层顺序删除
- 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
- 避免重复分配内存
- 小心使用有return、break语句的宏,确保前面资源已经释放
- 检查队列中每个成员是否释放
规则6.3 禁止引用已经释放的内存空间。坚持下列措施可以避免引用已经释放的内存空间:
- 内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
- 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
- 避免操作已发送消息的内存。
- 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
规则6.4 编程时,要防止差1错误。
此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。
规则6.5 所有的if … else if结构应该由else子句结束;switch语句必须有default分支。
建议
建议6.1 函数中分配的内存,在函数退出之前要释放。
有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。
建议6.2 if语句尽量加上else分支,对没有else分支的语句要小心对待。
建议6.3 不要滥用goto语句。
goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。但好处是可以利用goto语句方面退出多重循环。
建议6.4 时刻注意表达式是否会上溢、下溢。
此种问题一般是出现在使用无符号变量时可能会出现边界i溢出情况。
程序效率
原则
原则7.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率。
让一个正确的程序更快速,比让一个足够快的程序正确,要容易得太多。大多数时候,不要把注意力集中在如何使代码更快上,应首先关注让代码尽可能地清晰易读和更可靠。
原则7.2 通过对数据结构、程序算法的优化来提高效率。
建议
建议7.1 将不变条件的计算移到循环体外。
建议7.2 对于多维大数组,避免来回跳跃式访问数组成员。
建议7.3 创建资源库,以减少分配对象的开销。
例如,使用线程池机制,避免线程频繁创建、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完毕再释放到内存池中,避免内存申请释放的频繁系统调用
建议7.4 将多次被调用的 “小函数”改为inline函数或者宏实现。
inline函数的优点:其一编译时不用展开,代码SIZE小。其二可以加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会做语法检查。
8 注释
原则
原则8.1 优秀的代码可以自我解释,不通过注释即可轻易读懂。
**优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,**需要很多注释来解释的代码往往存在坏味道,需要重构。
原则8.2 注释的内容要清楚、明了,含义准确,防止注释二义性。
有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。
原则8.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码。
注释不是为了名词解释(what),不是为了重复描述代码,而是说明用途(why)。
规则
规则8.1 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除。
这个要求本身不难,但是却是在开发过程中很难坚持做到的一点,也是现在我们公司代码里面存在较为广泛的现象。
规则8.2 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
通常头文件要对功能和用法作简单说明,源文件包含了更多的实现细节或算法讨论。
规则8.3 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。
重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。
规则8.4 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明。
规则8.5 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。
这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
规则8.6 对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
规则8.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写。
规则8.8 同一产品或项目组统一注释风格。
建议
建议8.1 避免在一行代码或表达式的中间插入注释。
建议8.2 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。对于有外籍员工的,由产品确定注释语言。
注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
建议8.3 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式。
以doxygen格式为例,文件头,函数和全部变量的注释的示例如下: 文件头注释:
|
|
函数头注释:
|
|
全局变量注释:
|
|
函数头注释建议写到声明处。并非所有函数都必须写注释,建议针对这样的函数写注释:重要的、复杂的函数,提供外部使用的接口函数。
9 排版与格式
规则
规则9.1 程序块采用缩进风格编写,每级缩进为4个空格。
宏定义、编译开关、条件预处理语句可以顶格(或使用自定义的排版方案,但产品/模块内必须保持一致)。
规则9.2 相对独立的程序块之间、变量说明之后必须加空行。
规则9.3 一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定。换行时有如下建议:
- 换行时要增加一级缩进,使代码可读性更好;
- 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
- 换行时建议一个完整的语句放在一行,不要根据字符数断行
规则9.4 多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句。
规则9.5 if、for、do、while、case、switch、default等语句独占一行。
规则9.6 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
1.在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。 2.逗号、分号只在后面加空格 3.比较操作符, 赋值操作符"="、 “+=",算术操作符”+"、"%",逻辑操作符"&&"、"&",位域操作符"«"、"^“等双目操作符的前后加空格。 4.”!"、"~"、"++"、"–"、"&"(地址操作符)等单目操作符前后不加空格。 5."->"、".“前后不加空格。
- if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
建议
建议9.1 注释符(包括‘/’‘//’‘/’)与注释内容之间要用一个空格进行分隔。
建议9.2 源程序中关系较为紧密的代码应尽可能相邻。
表达式
本小节内容虽少,但却是平时写代码过程中容易忽略并且会产生较大影响的问题,需要额外注意。
规则
规则10.1 表达式的值在标准所允许的任何运算次序下都应该是相同的。
说明:除了逗号(,),逻辑与(&&),逻辑或(||)之外,C标准没有规定同级操作符是从左还是从右开始计算,需要保证一个表达式有且只有一个计算结果,较好的方法就是将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。 1.自增或自减操作符
|
|
2.函数参数,函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。 示例:
|
|
应该修改代码明确先计算第一个参数:
|
|
3.函数指针 示例:
|
|
求函数地址p与计算p++无关,结果是任意值。必须单独计算p++:
|
|
4.函数调用 示例:
|
|
编译器可能先计算fun1(),也可能先计算fun2(),由于x的结果依赖于函数fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。 5.嵌套赋值语句 6.volatile访问 限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取。
建议
建议10.1 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利。
如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:
|
|
建议10.2 赋值语句不要写在if等语句中,或者作为函数的参数使用。
1.因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。 2.作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。
|
|
建议10.3 用括号明确表达式的操作顺序,避免过分依赖默认优先级。
1.一元操作符,不需要使用括号 2.二元以上操作符,如果涉及多种操作符,则应该使用括号 3.即使所有操作符都是相同的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序
|
|
建议10.4 赋值操作符不能使用在产生布尔值的表达式上。
11 代码编辑、编译
规则
规则11.1 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警。
规则11.2 在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略。
某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
规则11.3 本地构建工具(如PC-Lint)的配置应该和持续集成的一致。
规则11.4 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功。
及时签入代码降低集成难度。
建议
建议11.1 要小心地使用编辑器提供的块拷贝功能编程。
12 可测性
原则
原则12.1 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难。
单元测试实施依赖于:
- 模块间的接口定义清楚、完整、稳定;
- 模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
- 模块内部的关键状态和关键数据可以查询,可以修改;
- 模块原子功能的入口唯一;
- 模块原子功能的出口唯一;
- 依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。
规则
规则12.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。
本规则是针对项目组或产品组的。**代码至始至终只有一份代码,不存在开发版本和测试版本的说法。测试与最终发行的版本是通过编译开关的不同来实现的。**并且编译开关要规范统一。统一使用编译开关来实现测试版本与发行版本的区别,一般不允许再定义其它新的编译开关。
规则12.2 在同一项目组或产品组内,调测打印的日志要有统一的规定。
统一的调测日志记录便于集成测试,具体包括:
- 统一的日志分类以及日志级别;
- 通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
- 在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
- 调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
规则12.3 使用断言记录内部假设。
规则12.4 不能用断言来检查运行时错误。
断言的使用是有条件的。断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断,因为在网上实际运行时,是完全有可能出现外部输入非法数据的情况。
13 安全性
原则
原则13.1 对用户输入进行检查。
以下场景需要对用户输入进行检验,以确保安全:
- 用户输入作为循环条件
- 用户输入作为数组下标
- 用户输入作为内存分配的尺寸参数
- 用户输入作为格式化字符串
- 用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。 可采取以下措施对用户输入检查: * 用户输入作为数值的,做数值范围检查 * 用户输入是字符串的,检查字符串长度 * 用户输入作为格式化字符串的,检查关键字“%” * 用户输入作为业务数据,对关键字进行检查、转义
规则
规则13.1 确保所有字符串是以NULL结束。
C语言中’\0’作为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、strlen())依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为。 为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数。如:
- 用strncpy()代替strcpy()
- 用strncat()代替strcat()
- 用snprintf()代替sprintf()
- 用fgets()代替gets()
错误示例:
|
|
正确示例:
|
|
规则13.2 不要将边界不明确的字符串写到固定长度的数组中。
边界不明确的字符串(如来自gets()、getenv()、scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。 错误示例:
|
|
正确示例,使用malloc分配指定长度的内存:
|
|
规则13.3 避免整数溢出。
当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
规则13.4 避免符号错误。
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。 错误示例,符号错误绕过长度检查:
|
|
正确示例,将len声明为无符号整型:
|
|
规则13.5 避免截断错误。
将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失。 错误示例,符号错误绕过长度检查:
|
|
示例代码中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()的调用创造了缓冲区溢出的条件。 正确示例,将涉及到计算的变量声明为统一的类型,并检查计算结果:
|
|
规则13.6 确保格式字符和参数匹配。
使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。大部分格式化字符串出问题,都是由于 copy-paste省事导致的,需要格外注意!
规则13.7 避免将用户输入作为格式化字符串的一部分或者全部。
调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。 错误示例:
|
|
上述代码input直接来自用户输入,并作为格式化字符串直接传递给printf()。当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。格式字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符串,而没有提供参数,因此printf()读取栈中任意内存位置,指导格式字符耗尽或者遇到一个无效指针或未映射地址为止。 正确示例,给printf()传两个参数,第一个参数为”%s”,目的是将格式化字符串确定下来;第二个参数为用户输入input:
|
|
规则13.8 避免使用strlen()计算二进制数据的长度。
strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本。 错误示例:
|
|
上述代码试图从一个输入行中删除行尾的换行符(\n)。如果buf的第一个字符是NULL,strlen(buf)返回0,这时对buf进行数组下标为[-1]的访问操作将会越界。 正确示例,在不能确定从文件读取到的数据的类型时,不要使用依赖NULL结束符的字符串操作函数:
|
|
规则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的值)执行比较。 错误示例:
|
|
正确做法:使用int类型的变量接受getchar()的返回值。
|
|
规则13.10 防止命令注入。
如果system()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变system()调用的行为。 示例:
|
|
如果恶意用户输入参数:
|
|
最终shell会将字符串解释为两条独立的命令:“any_exe happy; useradd attacker”。
14 单元测试
规则
规则14.1 在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计/编码的正确。
建议
建议14.1 单元测试关注单元的行为而不是实现,避免针对函数的测试。
15 可移植性
规则
规则15.1 不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数。
建议
建议15.1 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。
使用标准的数据类型,有利于程序的移植。
建议15.2 除非为了满足特殊需求,避免使用嵌入式汇编。
16 业界编程规范
- 《Google C++编程指南》
- 《汽车业C语言使用规范(MISRA)》
仅供参考,可能有误