进入计算机开发行业,开发水平其实是很难量化的一种东西。但是写出来的代码是否通俗易懂,是否便于后续的开发人员维护与开发,却是很直观的。一定程度上,写出来的代码可以达到上述要求,就可以反映出一定的开发水平,而达成上述观点很重要的一部分是可以量化的,那便是——代码规范。由于本人的工作偏向于嵌入式的开发,所以对于代码规范方面的学习也是参考了华为的C语言编程规范和正点原子的嵌入式Linux C代码规范化,前者是业界比较推崇的C开发规范了,后者可能是很多人买开发板的学习过程中会接触到的。下面会对编程规范进行一些总结整理,算是对规范从不知其然到知其然的一段记录吧。当然,代码规范算是一种比较主观的事,下面的介绍也仅仅是作为一种参考。
排版与格式
首先是注释的格式,包括文件信息的注释和必要的函数注释:
/********************************************************* Copyright © name Co., Ltd. 1998-2018. All rights reserved. File name: // 文件名 Author: // 作者 Version: // 版本号 Description: // 用于详细说明此程序文件完成的主要功能,与其他模块 // 或函数的接口,输出值、取值范围、含义及参数间的控 // 制、顺序、独立或依赖等关系 Others: // 其它内容的说明 Log: // 修改日志,包括修改内容,日期,修改人等 **********************************************************/
上方是文件信息的注释,一般会放在文件开始处,起到一个对文件的概括作用。除此外还有对函数的注释,放置在函数的起始处,标注函数功能及参数、返回值:
/* * @Description: 函数描述,描述本函数的基本功能 * @param 1 – 参数 1.必要时对参数作出说明 * @param 2 – 参数 2.必要时对参数作出说明 * @return – 返回值,必要时对返回值作出说明 * @others – 必要时对函数的特殊(复杂)实现做出说明 */
除开对函数和文件信息的注释,当然在适当时还需要对变量、结构体、代码块等作注释,不然可能等过一段时间后自己的代码自己都看不懂,在这里也对其注释格式做出说明,很多人喜欢用"//........."进行注释,当然这是在单行注释的情况下,多行注释情况下一般使用"/*...........*/",但要注意的是如果使用"/*...........*/"注释了大段的代码块时,在外层再次进行了大段的注释,即如果出现下面的情况是会出现问题的:
/* int a; /*
funcA();
funcB(); */ a = 1;//此段代码生效,想要的注释符未起到作用,有时产生的现象会很奇怪,还比较难以察觉
*/
除了注释相关的部分,一般会直接被看到的排版格式也是需要注意的,没人会愿意看和调试一团乱麻似的代码,总结为以下几点:
1.TAB缩进代替空格缩进,且占四个字符(LINUX内核代码也大量使用四字符),八字符在多层嵌套时缩进太多,不便观看。
2.相对独立的代码块和变量说明之间使用空行隔开,即不要所有代码紧缩成一团,尽量张弛有度。
3.当一行代码过长(超过80列)时分成两行,并在第二行前加上一级TAB缩进。
4.一行只放一句语句,包括赋值语句。避免使用"int a = 10; int b = 10;"这样的语句。
5.if、for、do、while、case、switch、default 等语句单独占用一行,因为有时后面只有一条语句时,为了看起来简洁,会直接放在同一行,但有时会因此产生问题。
6.在两个变量、常量、关键字之间进行对等操作时,在它们的操作符之间需要加上空格,双目、三目操作符前后都加空格(如
非对等操作中关系亲密的操作符(如->、.)之间不需要加空格,单目操作符"& 、++、--"等不加空格,逗号、分号只在后面加空格。
7.注释符和注释内容之间加一个空格进行分隔。
8.还有比较重要的一点那就是"{ }"的使用,在非函数代码块中出现时,将起始大括号放置在行尾,结尾大括号放在行首,形如:
if (condition) { do... }
除if外,对于switch、for、do、while等也同样适用,需要注意的是,不要将大括号省略,这样代码块范围不清晰,很容易写出不符合本意的代码。另外若是在函数体中起始的大括号,一定要另起一行放在行首,结尾大括号则同样放置在末尾行首位置。
头文件
头文件存在一种依赖现象,即x.h中包含y.h。这种依赖关系有时无可避免且必要,但一旦变成一个头文件依赖或间接依赖(x.h->y.h->z.h,x.h间接依赖z.h)几十个头文件时,或是工程中头文件全都互相依赖时,那么任意的头文件修改都会使得工程中的其他文件重新编译,大大增加编译时间,那么此时头文件的设计无疑是存在问题的。此时可以遵循一定的规范来尽量避免或减少此种现象。
规则一:头文件中只放置声明,不放置具体实现。
只放置提供给外部的声明,如函数接口,宏定义,类型定义等。
.C文件只在内部使用的函数不在头文件中声明。
.C文件只在内部使用的宏、枚举、结构体定义也不应在头文件中声明。
变量定义不放在头文件中,在.C文件中定义。尽量减少在头文件中声明变量,即减少全局变量的使用,最好使用函数接口来传递变量,必须使用时在头文件声明为全局变量,定义放在.C文件中。
规则二:头文件职责单一
头文件应该尽量简洁,负责单一功能,否则可能导致过多间接依赖、循环依赖,如:为了使用一个宏而包含几十个头文件。
除上述规则,还有一些注意事项:
1.每个.c文件有一个同名.h头文件,在其中定义外部接口,若无外部接口则该.c文件无存在必要。
2.头文件应该自包含,即可以独立编译。
3.头文件使用#define保护符防止头文件重复包含:
#ifndef FILENAME_H #define FILENAME_H #endif
4.只使用头文件包含方式使用外部接口,禁止使用extern来引用其他文件中定义的外部接口。
5.禁止头文件循环依赖,即a.h依赖b.h,b.h依赖c.h,c.h依赖a.h形成闭环。
6. .c/.h文件禁止包含未使用的头文件,即要注意将无用头文件删除。
7.禁止在头文件中定义变量,会引起变量的重复定义问题。
8.禁止在extern "C"中包含头文件。
函数
个人认为函数设计理想实现便是:简洁,负责功能清晰单一。函数作为代码的载体和工程主要组成部分,将各种功能封装在自己的限定范围,是工程模块化的基石。将函数写好还是有一定标准和思想可以借鉴指导的,除却文章开头说明的做好注释外,还有下面的几点可以作为参考。
原则一:一个函数只实现一个功能。
当一个函数同时负责多种功能实现时,无论在开发、使用和维护都是会带来很大难度。比如将关联很弱或是无关联的函数语句放在同一函数中时,不仅会造成函数职责不明、逻辑混乱,对于后续维护者的理解也会造成困难,测试者难以设计测试,逻辑上的混乱也会使修改难以进行。
原则二:重复代码尽可能提炼为函数。
一般在我们将代码写好并且可以跑起来后,想让我们主动修改几乎是不会出现的,毕竟修改就意味着问题出现的可能。但是一般此时的函数中都还存在着大块的重复代码在四处纵横,这个时候一定要将其提炼成函数,否则后续工程逐渐变大时,各种功能模块的复制粘贴,重复代码的重复会变得愈发严重。一般出现两次的代码块可以考虑封为函数,出现三次则一定要提炼为函数。
以上原则是函数设计的指导原则,还有一些需要遵守的规范:
1.避免函数过长,新增函数不超过50行。在已修改已存在函数时,建议不增加代码行。
2.函数嵌套不宜太深,新增函数嵌套不超过四层。
3.若非提供给外部的接口函数,都需要用static进行修饰。
4.对函数的参数进行合法性检查,尤其是指针类型的参数,极易造成内存泄露问题。需要规定好由调用方还是被调用方进行检查,其余的如参数大小范围等也需要注意检验。
5.对于函数的错误返回码要全面处理,对对应的错误码进行提示,这对调试和程序稳定运行都非常有效。
6.函数体中无效或注释的代码及时清除。
7.对于非参数输入的有效性进行检查,如数据文件、全局变量等。这点比较少被人注意,可以多加留意一下。
标识符
标识符主要是需要注意其命名,包括文件、变量、函数和宏的命名。有一部分是通用的规则,还有一些是针对文件、变量、函数对应的规则,但是很重要的一点是,前后命名风格要一致,不要出现混用的现象。
下面对通用性的命名规则先进行介绍。
有几种比较常见的命名风格:
unix like风格:单词用小写字母,每个单词直接用下划线“_”进行分割,如handsome_boy、beautiful_girl。
Windows风格:大小写字母混用,单词连在一起,每个单词首字母大写。如HandsomeBoy、BeautifulGirl。这种用法遇到大写专用名词会稍显违和,如GetNFCID。也有称其为大驼峰命名法的,相应的还有小驼峰命名法(首字母小写,其余单词首字母大写),即大写字母突起的样子像骆驼的驼峰神似。
匈牙利命名法,包括三个部分:基本类型、一个或更多的前缀、一个限定词。如char cMyName, c即char。
除了命名风格,这个因人而异,用喜欢的风格就好,还有一些一定要遵循的规则:
1.标识符的命名需要清晰、含义明确。同时使用完整的单词或通用易理解的缩写,而不是自己想当然的缩写以为别人可以懂,避免歧义产生。当然也可以在协议中规定好统一的缩写并进行说明。
2.汉语拼音的使用在标识符中也应该禁止。
下面还有一些建议,在代码中尽量遵循增加其可读性和易于维护。
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
2.在移植代码时,如驱动代码,命名风格需与原风格一致。重构修改代码时也以原风格为准。
3.不使用单字节如i,j,k命名变量,但可使用i,j,k在循环体内作为局部循环变量。
4.尽量不在变量中添加编号,尽量使用贴近原意的单词表示,除非逻辑上确实需要编号。
文件命名规则
文件统一为小写命名。
变量命名规则
非必要尽量不使用全局变量,可以用外部接口来传递。
变量命名要有意义且含义准确,单词使用小写并用“_”连接。格式可以是名词或形容词+名词形式:如int number_of_book。
函数命名规则
函数命名以函数要执行动作命名,一般采用动词或者动词加名词形式。风格方面与变量命名相同。
宏的命名规则
对于数值和字符串等常量的定义,使用全大写字母,并在单词之间用下划线“_”分隔的方式命名,枚举类型定义也建议如此。
#define PI_ROUNDED 3.14
此外,头文件或编译开关等特殊标识外的宏定义不使用下划线"_"起始。
变量
变量的使用在实际中是很需要注意谨慎的,如果命名不明确或注释没做好,基本上一段时间后代码就没人看得懂了,哪怕原作者也会茫然。要避免上述情况,有下面的原则和建议可以参考:
1.一个变量只有一个功能,不将其用作多种用途。
/* 反面例子 */ int time; time = 200; //表示时间 time = getvalue(); //rtime此时有两种作用,时间值和函数返回值 /* 正确示例 */ int time; int ret; time = 200; //表示时间 ret = getvalue();
2.结构体设计时应功能单一,而不是面面俱到。
将相关的信息组合构成一个结构体,用来明确的描述一个结构体,而不是相关性不强的数据集合。
3.不用或少用全局变量。
单个文件中的全局变量可以使用static修饰。
上述三点是必要的参考,还有一些余下更为细节的点可以注意:
1.防止局部变量与全局变量同名,在代码编译方面没有问题,但很容易引起代码阅读者误解。
2.禁止使用未经初始化的变量作为右值,避免未初始化错误。
3.明确全局变量的初始化顺序,避免跨模块的初始化依赖,即明确全局变量初始化和使用的先后顺序。
4.减少没有必要的数据类型默认转化和强制转换,如下是会产生问题的情况:
char ch; unsigned short int exam; ch = -1; exam = ch; //编译器无警告,但exam值为0xFFFF。
宏、常量
宏和常量在代码中也比较常见,也很容易忽略对于宏、常量的规范化使用。但一不注意的话也很容易出现问题,所以提出以下规范。
1.在使用宏定义表达式时,一定要加上完备的括号,否则在实际代码展开时很容易产生违背本意的代码。
2.将宏定义中的多条表达式放在大括号中,当然更好的办法是将语句放入do{...}whiile(0)的形式。
3.使用宏时,禁止参数发生变化。
#define SQUARE(a) ((a) * (a)) int a = 5; int b; b = SQUARE(a++); //结果:a = 7,执行了两次自增操作。 //正确做法 b = SQUARE(a); a++;
4.除非必要,尽量使用函数代替宏。宏的缺点:缺乏类型检查,不如函数调用检查严格;难以调试难打断电;宏在多次调用时造成代码空间浪费,不如函数空间效率高;最重要的是宏展开一不留神就会出问题,对开发人员要求较高。
5.常量最好使用const定义代替宏。
6.在宏中尽量不要使用returngotocontinuereak等改变程序流程的关键字,在宏中使用这些关键字很容易引起资源泄露问题,而使用者自身却很难察觉。
关于代码规范基本上就介绍这些,这里只作为个人的一种总结或是想要初步了解开发规范时的入门博客。更详细的更多的细节还是建议去读《华为C语言编程规范》,里面的介绍已经不止于规范了,有很多产品或者大工程协同开发中可能会出现问题的归纳,很多问题会是之前没考虑过的,是对自身软件开发思维与眼界一种很好的拓展。