1、前言
作为一个软件开发人员,应养成良好的编程习惯,随着编码越来越多,内容也会变得越来越多,规范化自己的编程有助于为了在程序代码量很大的时候,便于自己阅读,也便于别人阅读(团队合作),特别是作为一个合格的开发者,更需要规范自身写的程序代码,形成一种良好的习惯。
记得之前看过一本书,其中我感触最深的一句话就是“代码是写给人看的,不是写给机器看的,只是顺便计算机可以执行而已”,好的习惯在编码时能事半功倍。
在C语言中不遵守编译器的规定,编译器在编译时就会报错,这个规定叫作规则。但是有一种规定,它是一种人为的、约定成俗的,即使不按照那种规定也不会出错,这种规定就叫作规范。
1.1、目的
本文所写的是基于嵌入式软件开发之程序编程规范(四)的一些补充,也融合了自己的一些看法:
1、可读性和效率之间的取舍。
2、避免粗心导致程序运行异常。
1.2、参考文献
- 《MISRA C编码规范标准》
- 《华为C编程规范》
- 《计算机程序的构造和解释》
2、规范要求
2.1、头文件
对于C语言来说,头文件的设计体现了大部分的系统设计。 不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上反映了不合理的设计。
【规则 1】头文件中适合放置接口的声明,不适合放置实现
1、内部使用的函数(相当于类的私有方法)声明不应放在头文件中
2、内部使用的宏、枚举、结构定义不应放入头文件中
3、变量定义不应放在头文件中,应放在.c文件中
4、变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的
【规则 2】头文件应当职责单一,切忌依赖复杂
头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件,其根本原因是因为偷懒,想省事,所以往往会包含一大堆头文件,但是这种做法会导致编译时间拉长
【规则 3】头文件应向稳定的方向包含
头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块,而且能及时中止编译,缩短因错误导致的编译时间。
一般情况下为应用层头文件 > 模块层头文件 > 驱动层头文件 > 标准库头文件,根据代码后期可能修改的频率排序,如下代码,关于同一层的头文件排序方式,参考规则13。
include "app.h" // 应用层头文件
include "moudle.h" // 模块层头文件
include "device.h" // 驱动层头文件
include <string.h> // 标准库头文件
【规则 4】每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口
如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。
现有某些产品中,习惯一个 .c 文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制 .c 文件的代码行数,但是这种做法是不建议的。
.h 文件可以不需要有对应的 .c 文件,如定义配置选项的一些头文件、或者定义了寄存器地址的宏等头文件可以不需要对应的 .c 文件。
【规则 5】禁止头文件循环依赖
原因:头文件循环依赖,如 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 的源代码重新编译
【规则 6】.c/.h文件禁止包含用不到的头文件
很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦
【规则 7】头文件应当自包含
自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担
【规则 8】总是编写内部 #include 保护符( #define 保护)
在编写程序的头文件的时候,要注意每个头文件都应该用内部包含保护符来进行保护,以避免在多次包含时重新定义
#ifndef FOO_H_INCLUDED_
#define FOO_H_INCLUDED_
//....文件内容.....
#endif
定义包含保护符时,应该遵守如下规则:
保护符使用唯一名称;
不要在受保护部分的前后放置代码或者注释(特殊情况:头文件的版权声明部分以及头文件的整体注释部分可以放在保护符(#ifndef XX_H)前面)
【规则 9】禁止在头文件中定义变量
原因:在头文件中定义变量,将会由于头文件被其他 .c 文件包含而导致变量重复定义编译报错
只能在源文件中定义变量,在头文件中 extern 声明
【规则 10】只能通过包含头文件的方式使用其他 .c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量
原因:
1、若多处使用 extern 的方式声明使用,则改变变量类型或者函数的返回值等时需要改动的地方很多
2、影响模块的稳定性,因为头文件声明的都是API接口,源文件(.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。
【规则 11】禁止在 extern "C" 中包含头文件
原因:在extern "C"中包含头文件,会导致extern "C"嵌套
// 错误写法
// 错误写法
extern “C”
{
#include “xxx.h”
...
}
// 正确写法
#include “xxx.h”
extern “C”
{
...
}
【规则 12】一个模块通常包含多个 .c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名
需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。
如:产品普遍使用的VOS,作为一个大模块,其内部有很多子模块,他们之间的关系相对比较松散,就不适合提供一个vos.h。而VOS的子模块,如Memory(仅作举例说明,与实际情况可能有所出入),其内部实现高度内聚,虽然其内部实现可能有多个.c和.h,但是对外只需要提供一个Memory.h声明接口
【规则 13】同一产品统一包含头文件排列方式
常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。
1、以功能块方式排列头文件可以快速了解涉及的相关功能模块
2、以升序方式排列头文件可以避免头文件被重复包含
3、以稳定度排序,如 product.h修改的较为频繁,如果有错误,不必编译platform.h就可以发现product.h的错误,可以部分减少编译时间
2.2、函数
函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。
【规则 1】一个函数仅完成一件功能
一个函数实现多个功能给开发、使用、维护都带来很大的困难。
将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。
【规则 2】重复代码应该尽可能提炼成函数
原因:重复代码提炼成函数可以带来维护成本的降低。
1、当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复
2、当一段代码逻辑相同,但是内部的部分变量和输出不同,可以进行提炼成函数,内部的不同变量作为入参等方式(有些逻辑相同的可以使用表驱动方式)
【规则 3】避免函数过长,新增函数不超过 50 行 (非空非注释行)
原因:
1、过长的函数往往意味着函数功能不单一,过于复杂
2、延伸阅读材料: 业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读(根据实际情况可以超过 50 行,自行把握)
【规则 4】避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层
函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度
原因:每级嵌套都会增加阅读代码时的脑力消耗,需要记住前一级的状态,不利于代码阅读
做法:可以使用平级的代码控制块,如
// 错误示例
void serial(void)
{
if (...)
{
if (...)
{
if (...)
{
...;
}
}
}
}
// 正确示例
void serial(void)
{
if (!...)
{
return;
}
if (!...)
{
return;
}
...;
}
【规则 5】对参数的合法性检查,如果公司有相关规定,则按照规定走,如果没有,则由接口函数负责。
对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
【规则 6】检查函数所有非参数输入的有效性,如数据文件、公共变量等
函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查,防止调用者使用函数时传入不合理的入参,导致系统崩溃
【规则 7】函数的参数个数不超过5个
函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
函数的参数个数不要超过5个,如果超过了建议拆分为不同函数,或者使用结构体作为入参(结构体作为入参时,尽量使用使用指针传递,提高效率)
【规则 8】函数中的不变参数或者函数入参在函数内不会改变使用 const
1、不变的值更易于理解/跟踪和分析,把 const 作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全
2、对应调用者而言,函数的入参如果是const, 则一眼就能明白哪些是入参
void memcpy(void *pDst, const void *pSrc, int lenth)
{
...
}
【规则 9】函数入参的定义顺序应是输出参数比输入参数先定义
主要是统一规范,如函数 memcpy、sprintf、memset 等等,输出参数都是在函数的入参中先定义
【规则 10】函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用
实际开发过程中,难免会存在函数使用全局变量、静态局部变量和 I/O 操作,但是为了提高代码阅读性,建议集中使用
【规则 11】在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字
如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性;且在阅读上能迅速了解哪些是非公开函数。
2.3、变量
【规则 1】一个变量只有一个功能,不能把一个变量用作多种用途
一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同
如,value在同一个函数中表示结果又表示次数,在阅读时增加了难度,所以这种做法是错的,正确的做法应该定义两个变量,分别表示不同含义
【规则 2】结构功能单一,不要设计面面俱到的数据结构
相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中
【规则 3】不用或者少用全局变量
单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量
全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问
若该模块内需要定义多个全局变量,可以统一整合成一个结构体句柄,在使用或者调试时会十分方便
【规则 4】防止局部变量与全局变量同名
虽然同名没有语法错误,但是函数会优先使用局部变量,同时增加阅读难度
一般情况下,通过使用变量命名规范后,局部变量与全局变量同名基本不存在,最大前缀不同
【规则 5】变量在定义时就需要初始化
原因:若定义时没有初始化可能在一定条件下使用时出现错误,不利于后面排查问题
【规则 6】严禁使用未经初始化的变量作为右值
在首次使用前初始化变量,初始化的地方离使用的地方越近越好。
【规则 7】构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象
降低全局变量耦合度
【规则 8】使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放 ,应提供接口函数来设置、获取,同时注意全局数据的访问互斥
避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等。
因为对外提供变量很难把控该变量被其他模块随意修改,影响系统的稳定性
【规则 9】尽量减少没有必要的数据类型默认转换与强制转换
当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患
错误示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。
char ch;
unsigned short int exam;
ch = -1;
exam = ch; // 编译器不产生告警,此时exam为0xFFFF。
【规则 10】在定义变量时将其初始化
定义的同时初始化效率更高,具体原因可自行百度;且也能防止野指针的出现,没有进行初始化程序运行时出错很难发现问题
/* 先定义再赋值效率低 */
int flag;
flag = 1;
/* 定义的同时赋值效率高 */
int flag = 1;
或
int flag = GetFlag(); // 函数返回值赋值
2.4、宏、常量
【规则 1】用宏定义表达式时,要使用完备的括号
若因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。
// 错误示例
#define RECTANGLE_AREA(a, b) a * b // RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 1 + 2 * 3 + 6, 因为符号优先级此时和想要的结果完全不同
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b) // 10 / RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 10 / (1 + 2) * (3 + 6), 结果不同
// 正确示例
#define RECTANGLE_AREA(a, b) ((a) * (b)) // RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 ((1 + 2) * (3 + 6)), 结果一致
【规则 2】将宏所定义的多条表达式放在大括号中
若宏定义函数表达式,则记得加换行符 \,建议使用 do{...}while(0) 的方式
// 错误示例
#define RECTANGLE_AREA(a, b) a * b // RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 1 + 2 * 3 + 6, 因为符号优先级此时和想要的结果完全不同
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b) // 10 / RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 10 / (1 + 2) * (3 + 6), 结果不同
// 正确示例
#define RECTANGLE_AREA(a, b) ((a) * (b)) // RECTANGLE_AREA(1 + 2, 3 + 6) 替换时为 ((1 + 2) * (3 + 6)), 结果一致
【规则 3】不允许直接使用魔鬼数字
使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重
使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。
解决途径:对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义
【规则 3】除非必要,应尽可能使用函数代替宏
宏对比函数,有一些明显的缺点:
- 宏缺乏类型检查,不如函数调用检查严格;
- 宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用;
- 以宏形式写的代码难以调试难以打断点,不利于定位问题;
- 宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。
【规则 4】常量建议使用 const 定义代替宏
当编译报错时,只会显示常量,不会显示宏定义的名字,查找时很费劲(因为宏是替换,在编译过程中称为“预处理”)
一般情况下在意常量的类型,就使用const,如果是头文件对外提供的常量,使用宏定义
【规则 5】宏定义中尽量不使用 return 、 goto 、 continue 、 break等改变程序流程的语句
如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。
错误示例:在某头文件中定义宏CHECK_AND_RETURN:
2.5、表达式和基本语句
【规则 1】赋值语句不要写在 if 等语句中,或者作为函数的参数使用
因为 if 语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行
如:if (test == 15 || HandleLogicFun()) {...},此时若 test = 15,则函数 HandleLogicFun 就不会执行
【规则 2】用括号明确表达式的操作顺序,避免过分依赖默认优先级
使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;
同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。
一般代码行的运算符比较多就需要,如果很简单必要性不大,反而降低了美观性
2.5.1、复合表达式
【规则 1】不要编写太复杂的符合表达式
太复杂的符合表达式不利于代码阅读
如 i = a >= b && c < d && c + f <= g + h,过于复杂
【规则 2】不要有多用途的符合表达式
如 d = (a = b + c) + r 该表达式既求 a 值又求 d 值,应该拆分两个独立的语句:
a = b + c;
d = a + r;
2.5.2、if 语句
【规则 1】布尔变量与零值比较
不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较,虽然基本不会有什么大问题,但是会影响阅读性
根据布尔类型的语义,零值为“假” (记为 FALSE) ,任何非零值都是“真” (记为TRUE) 。TRUE 的值究竟是什么并没有统一的标准,例如 Visual C++ 将 TRUE 定义为 1,而 Visual Basic 则将 TRUE 定义为-1;因此对于布尔变量,它与零值比较的标准 if 语句如下:
if (flag) // 表示 flag 为真
{
}
if (!flag) // 表示 flag 为假
{
}
【规则 2】整型变量与零值比较
应当将整型变量用“==”或“!=”直接与 0比较。
对于整型变量,它与零值比较的标准 if 语句如下:
if (flag == 0)
{
}
if (flag != 0)
{
}
【规则 3】浮点变量与零值比较
不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为 x,应当将 if (x == 0.0) // 隐含错误的比较转化为
if ((x >= -EPSINON) && (x <= EPSINON))
{
...
}
其中 EPSINON是允许的误差(即精度)
【规则 4】指针变量与零值比较
应当将指针变量用“==”或“!=”与 NULL比较,虽然和0比较基本不会有什么大问题,但是会影响阅读性,误以为该变量是其它类型
指针变量的零值是“空” (记为 NULL) 。尽管 NULL 的值与 0 相同,但是两者意义不同。假设指针变量的名字为 p,它与零值比较的标准 if 语句如下:
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量
{
}
if (p != NULL)
{
}
【规则 5】若 if 有 else if 分支,则必须有 else 分支
虽然没有else分支,但是在以后的代码维护中能清楚表明自己考虑了这种情况,但是目前不需要做任何处理
if (...)
{
...
}
else if (...)
{
...
}
else
{
// TODO
}
其中表明注释 “TODO” 说明表明自己考虑了这种情况,但是目前不需要做任何处理
【规则 6】对于 if ("变量" == "常量") 通常建议写成 if ("常量" == "变量")
好处时能避免粗心大意写成 if ("变量" = "常量") ,而编译可能不会报错,最终代码运行时就会出现异常,而 if ("常量" == "变量") 这种写法若少了“=”,根据常量不能被赋值的规则,编译时就会报错。
当然这种写法可能不美观,如果强迫症,那建议养成习惯后可以再恢复 if ("变量" = "常量") 这种写法,因为写该语句时都会下意识想到该规则,从而避免少写 = ,也能避免粗心引起的该问题。
if (5 == test)
{
}
if (NULL == pTest)
{
}
2.5.3、for/while/do 循环语句
循环语句有 for 语句,while 语句,do语句,其中 for 语句是使用频率最高的,以下规则1、2介绍如何提高 for 循环语句的效率,其根本是降低循环体的复杂性。
【规则 1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数
其中第二种就比第一种的运行效率要高,光从C代码角度看,区别不大,这个需要从汇编的角度看才能明显看出,具体可以自行尝试看执行时间(循环次数足够)看或者网上百度两种方式的对比,这里不再描述,网上比较详细;不过我建议是直接看两种编译后的汇编语句,这样感触最深。
// 第一种
for (i = 0; i < 100; i++)
{
for (j = 0; j < 10; j++)
{
...
}
}
// 第二种
for (i = 0; i < 10; i++)
{
for (j = 0; j < 100; j++)
{
...
}
}
【规则 2】循环中存在判断语句等,根据实际情况选择
如以下代码中判断表达式在循环体中,第二种就效率来说比第一种高,但是就代码简洁性来看,第一种更好,那么如何取舍呢?
1、循环次数较少,可以采用第一种,原因是在循环次数较少的情况下,第二种的效率提高不明显
2、底层驱动开发时,采用第一种往往会极大地影响效率,所以普遍采用第二种(之前开发LCD驱动时,画点时第二种比第一种在速度上明显提高)
// 第一种
for (i = 0; i < 10; i++)
{
if (...)
{
...
}
else
{
...
}
}
// 第二种
if (...)
{
for (i = 0; i < 10; i++)
{
...
}
}
else
{
for (i = 0; i < 10; i++)
{
...
}
}
【规则 3】不能再 for 循环体内修改循环变量,防止 for 循环失去控制
下列代码容易失去控制
for (i = 0; i < 10; i++)
{
if (...)
{
i += 2;
}
else
{
...;
}
}
【规则 4】建议 for 循环控制变量的取值采用“半开半闭区间”原则
以下两种方式功能一样,但是第一种的写法更加直观
// 第一种
for (i = 0; i < N; i++)
{
...
}
// 第二种
for (i = 0; i <= N - 1; i++)
{
...
}
【规则 5】空循环也应该使用{} 或者 continue,而不是一个简单的分号
这样做的目的是直观地看出是一个空循环体
for (i = 0; i < N; i++)
{
}
while (flag)
{
condition;
}
2.5.4、switch 语句
【规则 1】每一个switch语句最后都要写 default 分支,即使什么也不做
这个和 else if 的 else 分支规则一样,目的是在以后的代码维护中能清楚表明自己考虑了这种情况,但是目前不需要做任何处理
【规则 2】每个 case 语句的结尾别忘记加 break,否则导致多个分支重叠,除非是有意使多个分支重叠
建议 case 和 break 成对编写,不要写了其他语句再写 break,防止遗忘,即使有意重叠,也应该这样做,写完后再删除 break
2.5.5、goto语句
【规则 1】尽量少用,但是我建议禁用更好,对于程序的阅读有很大的阻碍,如果必要可以采用 do{...}while(0) 的方式替代
一般情况 goto 能解决退出函数时需要做的处理,如打开文件中间出现错误都需要关闭文件,不需要再退出的地方都写
int ReadFileDate(char *pBuf, uint16_t lenth)
{
if (NULL == pBuf || 0 == lenth)
{
return;
}
if (open(file) == -1)
{
goto EOF;
}
if (read(file, pBuf, lenth) == -1)
{
goto EOF;
}
... // 其他处理,可能也存在错误需要退出处理(因为已经打开文件了)
EOF:
close(file);
}
这种方式如果禁用 goto,则基本每一个退出函数的地方都需要 close反而麻烦,此时可以通过do{...}while(0) 的方式替代
int ReadFileDate(char *pBuf, uint16_t lenth)
{
if (NULL == pBuf || 0 == lenth)
{
return;
}
do
{
if (open(file) == -1)
{
break;
}
if (read(file, pBuf, lenth) == -1)
{
break;
}
... // 其他处理,可能也存在错误需要退出处理(因为已经打开文件了)
} while (0);
close(file);
}
2.6、其他建议
【规则 1】变量的类型定义建议统一采用 uint8_t、uint16_t、int8_t 等定义
根据8位、16位和32位等机器的不同,int 等类型所占的字节不一样,如果采用 int 等方式定义,那么在不同位机器移植代码时就会出现很多问题
为了提高代码的移植性,一般采用 uint8_t、uint16_t、int8_t 等定义的方式,它是通过 typedef 定义的,而不是一种新的数据类型,typedef 定义的 uint8_t、uint16_t、int8_t 等可以更好的兼容各个平台,不用担心位不同机器造成的兼容性问题
虽然也有用 u8、u16、u32 等其他typedef 定义方式,但是毕竟是少数,而uint8_t、uint16_t、int8_t 等定义的方式是业界通用的,具有更高的兼容性
【规则 2】关于指针定义时的 * 的位置,建议是靠近变量或者函数
如下列代码,靠近变量的*更能直观地看出该变量是指针
uint8_t* src; // *靠近类型
uint8_t *src; // *靠近变量
uint8_t* GetInfo(void); // *靠近返回值类型
uint8_t *GetInfo(void); // *靠近函数
【规则 3】char 定义的字符串在申请内存或者定义大小时都需要留结尾标识'\0'空间
strncpy 等安全函数在拷贝字符串到达指定长度时,不会再目标字符串结尾添加'\0',如果不手动添加'\0',则会出现意向不到的问题
通常时在申请后立马清零或者定义时清零
【规则 4】文件的编码格式尽量统一,如有中文,则统一为 utf8 的编码格式
在单片机开发或者需要开发 LCD 驱动的情况,需要自己通过工具生成需要使用的中文字库(大多是 GB2312 格式),则需要留意该文件的编码格式,如果文件的编码格式是 utf8,则不能正常对应,如下是LCD 驱动的部分使用的字体库(一般为 GB2312 编码)
/**
* @brief 24 * 24 汉字字符集点阵索引
* @note 对应点阵数据 LCD_FONT_24X24_DATA
*/
const uint8_t LCD_FONT_24X24_IDX[] = {
"参数"
};
/**
* @brief 24 * 24 汉字字符集点阵数据
*/
const uint8_t LCD_FONT_24X24_DATA[]= {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x06,0x00,
0x00,0x81,0x01,0xC0,0x00,0x06,0xE0,0xFF,0x0D,0x00,0x04,0x08,
0x00,0x06,0x20,0xFC,0xFF,0x7F,0x00,0x41,0x00,0x80,0x91,0x00,
0xC0,0x18,0x03,0x20,0x0C,0x0E,0x10,0xC3,0x78,0x8C,0x60,0x00,
0x60,0x18,0x00,0x00,0x0C,0x06,0x00,0x83,0x03,0xE0,0xE0,0x00,
0x00,0x38,0x00,0x00,0x0E,0x00,0xF8,0x01,0x00,0x00,0x00,0x00,/*"参",0*/
0x00,0x00,0x00,0x00,0x80,0x00,0x80,0x81,0x01,0x88,0x98,0x00,
0x90,0xCC,0x00,0xB0,0x44,0x00,0x90,0x4A,0x20,0xFC,0xDF,0x7F,
0xC0,0x60,0x08,0xA0,0x67,0x08,0x90,0x5C,0x08,0x88,0x54,0x0C,
0x84,0x48,0x0C,0xC0,0x80,0x04,0x40,0x8C,0x04,0xFC,0x8F,0x04,
0x20,0x04,0x07,0x20,0x02,0x03,0xC0,0x03,0x03,0x80,0x8F,0x06,
0x40,0x48,0x1C,0x30,0x30,0x78,0x0E,0x0C,0x10,0x00,0x00,0x00,/*"数",1*/
}
而在调用 LCD 的显示中文函数的文件为 utf8 编码格式,则程序运行时函数 LCD_ShowChinese 无法和字体库匹配(编码格式不一样)
LCD_ShowChinese("参数");
解决方式有两种:
1、将调用 LCD 的显示中文函数的文件全部调整为 GB2312 编码格式,这样源码就存在 GB2312 和 utf8 的编码格式文件
2、在函数 LCD_ShowChinese 增加 UTF8 转 GB2312 格式功能,这样就不需要将调用 LCD 的显示中文函数的文件转成 GB2312 编码格式了