嵌入式C语言代码的基本编写规范要求

简介: 嵌入式C语言代码的基本编写规范要求
编码规范,没有最好,只有最合适,有但不执行不如没有。

1 编码原则

1.1 可读性原则

(1)清晰第一

清晰性是易于维护程序必须具备的特征。维护期变更代码的成本远远大于开发期,编写程序应该以人为本,计算机第二。一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

(2)简洁为美

简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。提倡通过简洁明了的代码来提升代码可靠性。废弃的代码要及时清除,重复代码应该尽可能提炼成函数。

(3)风格一致

所有人共同分享同一种风格,为后期维护,和代码交接带来便捷。

1.2 设计原则

(1)开放封闭原则

对于扩展是开放的,对于修改是封闭的。

(2)单一职责原则

每一个子函数或者类似的代码块应该只有一个职责,所以只有一个原因会使其改变。

(3)接口隔离原则

接口尽量细化,同时接口中的方法尽量少。

(4)最少知道原则

一个子模块应该与其它模块保持最少的了解。

(5)依赖倒置原则

高层模块,低层模块,细节(实现)都应该依赖抽象(即接口)。

微信公众号【嵌入式系统】建议,面向对象的思维在嵌入式C即使不能完全实现,但子功能模块封装、抽象化,也能实现高内聚、低耦合。多看多学,比如软考《软件设计师教材》,周立功《抽象接口技术和组件开发规范及其思想》,以及敏捷开发、设计模式的一些书籍,预知变化才能更好的设定架构。

2 编码规范

2.1 文件头申明

◎ 新增.c必须添加注释,标注公司名称、文件功能说明,创建日期、作者,后续修改说明 范例如下:

/*********************************************************************     
* Copyright (c)  Hehe,Ltd. All rights reserved.
* Description:用于详细说明此程序文件完成的主要功能,
*             与其他模块或函数的接口依赖等关系           
*
* History:修改历史记录列表,包括修改日期、修改者及修改内容简述                    
* Date               Author                Modification:                                                                     
* 2023-11-25        embedded-system        create v1.0
*********************************************************************/

可配置Source Insight 自动生成模板。

2.2 文件

◎ 所有.h头文件必须采取阻止内容被包含多于一次的机制

#ifndef __XXX__
#define __XXX__
#endif  /*__XXX__*/

◎ 头文件对外接口,应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

◎ 内部使用的函数声明不应放在头文件中。

◎ 内部使用的宏、枚举、结构定义不应放入头文件中。

◎ 变量定义禁止在头文件中,应放在.c文件中。

◎ 模块内使用的全局变量,不应通过在头文件中声明的方式直接暴露给外部。

◎ 头文件中只包含接口的声明,不含实现。

◎ 头文件应当职责单一,头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。

◎ 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。

◎ 禁止头文件循环依赖,禁止包含用不到的头文件。

◎ 每个.c源文件内容片段按如下顺序,文件注释-包含头文件-宏定义-数据结构定义-变量定义-引用外部变量-引用外部函数-本地函数-全局函数。

2.3 函数

◎ 一个函数仅完成一件功能

◎ 重复代码应该尽可能提炼成函数。

说明:重复代码提炼成函数可以带来维护成本的降低。重复代码是不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

◎ 避免递归函数的代码块嵌套过深。

◎ 对函数的错误返回码要全面处理。

说明:一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法,可以通过使用错误标记、特殊的返回数据或者其他手段,调用程序应该在函数返回时立刻检查错误指示。

◎ 废弃函数要及时清除

说明:程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

◎ 函数传入的不变参数使用const限制。

◎ 函数的参数个数不超过5个,检查输入参数的有效性。

说明:函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。函数的参数个数不要超过5个,如果超过了建议拆分为不同函数;函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

◎ 源文件范围内声明和定义的所有函数,除非外部可见,否则增加static关键字,针对单元测试的特殊情况,对这类函数尽量封装一层再使用。

◎ 传入参数表意有3种以上的禁止使用魔法数,必须使用枚举值且附带注释。

◎ 函数内部要对参数的合法性进行检查。

说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

◎ 除打印类函数外,不要使用可变长函数。

说明:可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。

◎ 每个函数都要返回错误码,调用程序必须在函数返回时检查错误码。

◎ 标识符的命名要清晰明了,有明确含义,使用完整的单词,尽量避免名字中出现数字编号或特殊符号。

◎ 函数名称需体现出函数具体功能,均由功能单词拼接组成,绝不允许出现中文拼音。

◎ 函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构。

2.4 变量

◎ 不用或者少用全局变量

说明:单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量。全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问。直接使用其他模块的私有数据,将使模块间的关系逐渐走向“剪不断理还乱”的耦合状态,这种情形是不允许的。

◎ 避免局部变量与全局变量同名。

说明:尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解。

◎ 严禁使用未经初始化的变量。

◎ 明确全局变量的初始化顺序,避免跨模块的初始化依赖。

说明:系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。

◎ 数据必须对外开放时,应封装接口函数来读写,同时注意全局数据的访问互斥。

说明:避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。

◎ 一个变量只有一个功能,不能把一个变量用作多种用途。

说明:一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。

◎ 数据结构功能单一,不要设计面面俱到的数据结构。

说明:相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构体中。

◎ 尽量减少没有必要的数据类型默认转换与强制转换。

说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

示例:如下赋值,多数编译器不产生告警,但值的含义有变化。

char ch; 
unsigned short int exam;  
ch = -1;
exam = ch; // 编译器不产生告警,此时exam为0xFFFF。

◎ 确认未使用的变量应当删除。

◎ 对于变量自增++和自减--,禁止在宏定义中使用,禁止和其他语句复合,因拆分单独执行。示例:if(++i>10) 错误写法,必须改为i++;if(i>10)

2.5 宏和常量

◎ 宏定义和常量使用大写字母或下划线。

◎ 用宏定义表达式时,要使用完备的括号,如下:

#define HEHE_AREA(a, b) ((a) * (b))

◎ 宏定义中尽量不要使用return、goto、continue、break等改变程序流程的语句。

◎ 常量建议使用const定义代替宏,如下

#define ASPECT_RATIO 1.653 替换成 const double ASPECT_RATIO = 1.653;

◎ 除非必要,应尽可能使用函数代替宏 。

◎ 将宏定义的多条表达式放在大括号中。

◎ 使用宏时,不允许参数发生变化。

◎ 尽量少用魔法数,或者必须加注释说明,或者修改方案,如内存长度操作禁止使用常数,非特殊情况必须使用sizeof自动处理。

2.6 命名

命名采用unix like风格,单词用小写字母,每个单词之间用下划线分割,引用的第三方的代码可保持原有风格,命名尽量使用通用英文单词或缩写。

2.6.1 文件

文件名命名可根据平台自有规则命名,一般采用小写字符,字段之间使用下划线分隔;相同功能的 .c和.h文件名相同。

2.6.2 枚举

枚举定义:宏定义和枚举值禁止使用小写字母,不能以下划线开头,字段之间使用下划线分隔,若逻辑中要标注多种状态,状态不允许用数字表示。

2.6.3 结构体

1、结构体定义,若同一功能所使用到的参数,尽量用结构体来定义表示,便于相关参数获取和设置。

2、纯业务逻辑代码,与平台无关的,必须使用小写字符和下划线分隔。

2.6.4 函数

函数名定义,函数名称需体现出函数具体功能,均由功能单词拼接组成,使用小写字母和下划线拼接,其中全局函数必须以xx_为前缀,在.h里面申明全局函数,补充完整注释;局部函数使用static限制。

2.6.5 变量

◎ 禁止使用全大写字母命名变量,全局变量至少5个字母,使用高频次的全局变量尽量简短。

◎ 全局变量命名表达其作用,且以小写字母g_开头,后面拼接功能英文,如地址:g_addr。

◎ 变量名的拼接,全部使用小写字母和下划线拼接,函数内局部变量允许使用单个字母。

◎ 多个同类的变量封装成结构体。

2.6.6 推荐命名

add/delete  begin/end  create/destroy  insert/delete
first/last  increment/decrement  get/set/release  up/down
lock/unlock  open/close  min/max  old/new
start/stop  next/previous  source/target  send/receive

2.7 注释

◎ 注释应放在其代码上方相邻位置或右方,不可放在下面。

◎ 注释的内容要清楚明了,防止注释二义性。

◎ 修改代码时同步更新注释,保证注释与代码的一致性。

◎ 函数声明处注释描述函数功能、性能及用法,提供参考范本如下:

/**
 * @brief    函数功能
 * @params 
 * @return
 * @par history
 *   xx create  @2023-xx-xx
 */

微信公众号【嵌入式系统】提示,注释格式可以参考Doxygen标准。

◎ 全局变量要有较详细的注释

◎ 函数内部注释:函数内部不是注释越多越好,而是变量命名和逻辑清晰,自注释最好,特殊情况或者需要特别注意的地方才加注释,并且注释要放在代码行的上方。

◎ 基于SDK开发,在基线工程上改动代码,不允许删除源代码,修改代码必须增加注释,必须使用关键字“XX_CODE”标注修改原因,方便后续打补丁,范例如下:

/**** XX_CODE begin ****/
/*修改原因,作者,时间*/
<代码块>
/**** XX_CODE end ****/

对于非c源码的文件,在这个注释格式的基础上,每行添加对应的注释符号。

◎ 修改与外设驱动、通信协议、系统底层等相关的代码,具有特殊隐含限制的代码,必须提交详细的修改原因,便于后续版本回溯查找原因。

◎ 复杂且相对独立的功能,单独使用markdown文档说明开发方案、实现技术、应用场景、使用限制等,随代码提交。

2.8 排版与格式

◎ 程序块釆用缩进风格编写,每级缩进为4个空格。

◎ 相对独立的程序块之间、变量说明之后必须加空行。

◎ 多个短语句不允许写在同一行内,长语句不能拆分需要分行写。

◎ if、for、do、while、case、switch、default等语句独占一行,{换行且独占一行。

◎ 赋值语句不要写在if等语句中,或者作为函数的参数使用。

◎ 逻辑表达式每个子项都使用()。

◎ if与else if/else必须以’{}’分隔,且 ‘{’与‘}’各占一行,if-else分3层以上必须以else子句结束,即使操作为空,并增加注释://do nothing

if(var==xx)
{
}
else if (var==yy)
{
}
else if (var==zz)
{
}
else
{
  //do nothing
}

◎ switch语句必须有default分支。

◎ 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时(如->),后面不应加空格。

◎ 文件编写完成后,统一使用Astyle自动格式化工具整理一遍再提交到版本库。

astyle.exe --style=allman -S -U -t -n -K -p -s4 -j -q -Y -xW -xV

微信公众号【嵌入式系统】提示,可以参考《代码的保养》;排版格式很多,一个团队最重要的是统一风格。

3 编码要求

3.1 安全性

◎ 对用户输入数值进行有效范围检查。

◎ 对输入参数进行边界判断,尤其对指针变量。

◎ 严禁使用未经初始化的变量作为右值,所有变量都要初始化。

◎ 每个普通变量(整型、字符型)的定义都要考虑范围,严防溢出。

◎ 每个数组的定义和使用都要严防越界的发生。

◎ 要尽量避免隐式或者显式的类型转换,防止截断的发生或者符号的丢失。

◎ 动态申请的内存尽量在本函数内释放,特殊情况下,必须补充注释提醒外界释放内存。

3.2 可移植性

◎ 不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数。

◎ 提取与平台有关的通用函数,封装后统一放在固定文件,方便后期替换升级。

◎ 源文件必须使用原有平台、SDK一致的文件编码(GB2312/UTF8),有效代码中不得出现中文字符。

◎ 通用功能使用功能宏控制,且代码集中。

◎ 使用系统接口,尤其是不同平台API不同的,使用函数名中带pal的接口封装并注释,便于后续移植到其他平台。

◎ 引用第三方开源代码,允许保留其原始风格,客制化修改必须补充完整注释。

4 规范实施

编码规范是软件开发团队合作的标准,但实际开发过程中存在各种不可控因素,尤其是项目进度压力和开发者水平与认知的差异,导致软件规则不能严格执行。随着软件工程规模的扩大,软件交期、代码同步、重构或交接,其风险也逐渐放大。因此,存在合适的编码规则并不能解决问题,只有强制代码格式化,才能真正落实编码规范统一。

软件质量是项目成败的关键点之一,在开发周期有限,人力资源不足的情况下,使用工具实现代码自动扫描,分析出潜在隐患点,从源头减少软件bug,是软件如期交付的重要保证。实现代码自动格式化和静态分析,可以有效规避软件风险。

目录
相关文章
|
29天前
|
存储 编译器 C语言
【数据结构】C语言实现链队列(附完整运行代码)
【数据结构】C语言实现链队列(附完整运行代码)
36 0
|
29天前
|
存储 算法 程序员
【数据结构】C语言实现顺序表万字详解(附完整运行代码)
【数据结构】C语言实现顺序表万字详解(附完整运行代码)
39 0
|
1月前
|
算法 安全 C语言
使用C语言实现DES算法代码
使用C语言实现DES算法代码
|
1月前
|
C语言
C语言栈的括号匹配的检验讲解及相关代码
C语言栈的括号匹配的检验讲解及相关代码
33 0
|
1月前
|
算法 C语言
【C语言】三子棋游戏实现代码
【C语言】三子棋游戏实现代码
【C语言】三子棋游戏实现代码
|
1月前
|
C语言
C语言-------扫雷游戏的代码实现
C语言-------扫雷游戏的代码实现
27 0
|
1月前
|
存储 编解码 编译器
嵌入式C语言(四)
嵌入式C语言(四)
27 0
|
1月前
|
存储 编译器 Linux
嵌入式C语言(三)
嵌入式C语言(三)
27 0
|
18天前
|
存储 编译器 C语言
嵌入式C语言(六)
嵌入式C语言(六)
20 0
|
3天前
|
存储 算法 C语言
C语言进阶:顺序表(数据结构基础) (以通讯录项目为代码练习)
C语言进阶:顺序表(数据结构基础) (以通讯录项目为代码练习)