C语言编程规范总结

1    编写目的

一致且良好的代码规范,是软件项目开发高效和高质量的一个基本条件,它能够提升代码的可读性和健壮性,更进一步就是提升软件开发的效率和质量。

–     能够节省大量的阅读代码的时间,据统计,软件开发过程中,40%-70%的工作量是在首次编码完成后投入的,这些工作中就需要反复地阅读代码。

–     能够节省团队各成员间的沟通成本,沟通成本中包含了因为代码风格差而需要额外增加说明文档所多付出的资源。

–     能够在编码环节中避免一些没必要的软件bug,否则这些bug等待集成甚至发布时才发现,解决这些bug的成本将大大增加。

所以,我们需要在软件团队中践行一致且良好的代码规范。但是代码规范有很多,并不是所有规范细则都很重要,都适合,所以就从中挑选出一些基本的代码规范细则汇编。

 

2    术语和缩写词

缩写、术语    解释

语法:语法指用字符组合成合法语句的规则集,语法定义语言的各种要素间的形式关系。语法给出了语言中各种不同的合法的语句的结构描述,语法只关注句法结构,而不管其含义。

语义:语义指定一条合法语句的含义,语义描述了计算机执行一个程序时所表现的行为。

      

3    代码规范

3.1  命名规范

3.1.1命名的要求

选择有意义的名称:所有的变量、类型、文件和函数的名称都应该有意义,不要误导。名称应该如实地描述它所代表的实体。命名方案应该前后一致,以避免出现令人不快的惊奇。应确保变量的使用方法与它的名称相符。

好的名称也许是我们避免多余注释的最好办法。它们是我们在代码中获得接近自然语言的表达所拥有的最唾手可得的工具。

 

选择描述性的类型:

1.     如果你要定义一个永远都不改变的值,那么就强制将其定义为一个常量类型。Const

2.     如果一个变量不应当包含负值,那么就使用一种无符号类型。

3.     使用枚举来描述一组相关的值。

 

避免使用magic数字,应该使用命名得当的常量,或者宏常量。

 

3.1.2命名一般规范

1.     文件名一律小写。

2.     采用小写,除另有规定的之外(比如宏定义全部采用大写的方式)。

3.     允许中间使用 _。

4.     采用动名词结构  动词  行为  对象。

5.     合理使用缩写:全局的、经常用的,一定精简;局部的,不经常用的,一定要能说明其意义。

6.     宏定义全部采用大写的方式  宏函数一定要有括号。

7.     尽量不要在工程的设置中声明宏,以免影响程序的可读性。

8.     标准数据类型,使用bool/int8/uint8/int16/uint16/int32/uint32等,原有char/uchar/word/BYTE等全部改过来。

9.     全局变量一律以g_*描述。

10.   指针变量,特别是指向一个结构体对象的指针,需要添加 p的前缀或后缀(历史原因,有些代码采用后缀标识),比如g_p_app_info 。

3.1.3结构体

1.     结构体的作用:某一模块、某一范畴内的不同数据,组织在一起,便于集中分类,便于查找理解;因此需将模块数据结构,尽量抽象成结构体,而不是独立、分散的变量;

2.     结构体名称加后缀:_t;使用typedef封装;

3.     结构体内成员,与结构体名的关系:成员名应以简短为佳,不再以结构体名开头;

 

3.1.4枚举定义

1.     枚举的作用:将某一数据可能范围内的值都以成员名称的方式罗列出来,即:使用名称代替数字;因此某一类的数值,需抽象成枚举;而不是使用宏定义;如:同一类的cmd数值;

2.     枚举名称加后缀:_e;使用typedef封装;

3.     枚举内成员,与枚举类型名的关系:成员名以枚举类型名开头;

4.     枚举成员增加最小、最大值:NULL、MIN、MAX;

 

3.2  API接口规范

这里的API就是指我们的API机制的API,而非普通的外部函数声明。

 

//该接口用于显示某个LED标识,包括数字LED管

//addr    : 0 ~ 25(视具体LED段码屏而定),各标识 addr 请参见led_driver.h 中的定义

//content : 仅对数字LED管有效,是方案自己实现的编码,对应于LED显示驱动中的LED码表

//          非数字LED管,content必须填写为0xff,表示非数字LED管

//type    : 显示与否,1表示显示,0表示不显示

#defineled_display(uint8_addr, uint8_content, uint8_type) \

lcd_op_entry((void*)(uint32)(uint8_addr), (void *)(uint32)(uint8_content), (void*)(uint32)(uint8_type), LED_DISPLAY)

 

解释:首先要说明API的目的,接下来说明参数,如果有返回值必须说明清楚各个返回值的意义;对于声明的形式,必须指明参数类型,要对应于真正API接口定义,如果是小于4字节的类型,必须用 uint32 或 int32 进行强制转换,以减轻调用API时的编码负担。

 

3.3  代码风格format

每个人的代码风格都会有或多或少的差异。代码风格没有说哪一种风格就是最好的,但是如果团队中不同成员的代码风格都是自创一套,就难免会增加各成员之间的学习成本,并且会让标案整体阅读起来不够和谐,让阅读者产生过多的心理压力。

 

所以,我们将借用Eclipse 工具中的代码格式化功能,来规范团队中所有成员的代码风格。我们要求每个成员在check in代码到SVN等版本管理系统之前,必须用 Eclipse 的Formatter 工具进行代码格式化。

 

NOTE:Formatter工具不能让数组初始化列表的大括号从0列对齐,所以在代码格式化后,需要手动将数组初始化列表前面的4个空格去掉。

 

3.4  编译器 0 warning

编译器是我们开发过程中使用最频繁的工具,是我们编写完代码后很快就会执行的代码自动“评审”活动。所以,我们应该让编译器的“评审”规则做到最严格,也就是将所有warning 都打开,然后我们还将 warning 当作error 来看待,这样就会保证源代码是 “0 warning” 的。

 

3.5  函数设计要求

1.     一个函数,一个操作:在一个函数中,只进行一种操作。选择一个毫无歧义的说明这种操作的名称,一个好的名称就意味着不需要额外的文档了。

2.     减少任何出人意料的副作用,不管它们看上去有多无害。

3.     函数应该执行的错误检查:

a)    检查所有的函数参数。确保你得到了正确和前后一致的输入。使用断言?

b)    检查执行过程中关键点处的不变条件是否都满足。

c)     在使用任何来自外部的值之前都要检查其有效性。文件的内容和交互性的输入必须是合理的,没有漏掉任何部分。

d)    检查所有系统调用和其他下级函数的返回状态。

 

3.6  错误处理机制

绝不要忽视任何一种错误情况。如果你不知道如何处理这个错误,就向调用代码发送一个故障信号,不要把错误扫到毯子下面并心存侥幸。

 

3.6.1错误报告机制

1.     返回值

从函数返回一个表示成功或失败的值。布尔型返回值提供了一个简单的yes或no的答案。一种更高级的方法是列举出所有可能的退出状态,并返回一个响应的原因代码。一个值表示成功,其余的值分别代码许多不同的异常终止情况。这些列举出的状态可以在整个代码库中共享,在这种情况下你的函数将返回一个可用值的子集。

这种机制对于不返回数据的程序来说非常管用,但是如果返回的错误代码中带有数据就会变得很麻烦。这种情况可以预留一些返回值来表示各种故障,比如负数或者NULL指针。(有时候比较困难,有时候会减少成功值的可用范围的副作用。)

 

2.     错误状态变量:

设置一个共享的全局错误变量,在调用函数之后,你必须查看这个状态变量,以确定函数是否已经成功完成。这个共享的变量减少了函数签名中的混乱,而且也完全不会限制返回值的数据范围。但是,通过单独的渠道报告的错误更容易丢失或被有意忽略。共享的全局变量也具有令人讨厌的线程安全问题。——缺陷太多,不足以使用。

 

3.6.2错误处理机制

一旦了解如何正确的处理错误,就马上在最恰当的上下文中对各个错误进行处理。

尽可能早处理:一发现错误就对其进行处理。由于错误是在其来源附近处理的,所以你可以留住重要的上下文信息,从而使处理错误的代码更加清晰。在其来源附近管理每个错误,意味着控制流只会穿过较少的处于无效状态的代码。

 

1.     日志系统:日志使你可以收集重要的跟踪信息,并且是调查那些严重问题的切入点。

日志的作用是记录程序的生命周期中值得注意的事件,以使你可以深入研究程序的内部工作方式和执行的重构路径。由于这个原因,在程序日志中应该详细记录你所遇到的所有错误;它们是所有事件中最令人感兴趣和最生动的事件。

2.     报告:程序应该只在已没有什么可做的情况下才向用户报告错误。用户不应该遭受成千上万无用信息的轰炸,也不应该被大量无意义的问题所困扰。在遇到可以恢复的状况时,不要报告。有一些问题只有用户才可以解决,对于这些问题,好的做法是立即报告问题,以便使用户可以利用最佳的时机来解决问题,或决定程序如何继续。

3.     恢复:有时你唯一的行动方案是立即停止程序,但如果可以恢复,或者继续很好运行,那么可以采用后者。如果你的代码遇到了一个错误并且不知道该怎么办,那么就把错误向上传。很可能你的调用方有能力解决这个问题。

4.     忽略:如果你选择不去处理错误,而只是袖手旁观的看着代码继续,那么这将成为软件中大部分bug的发源地。忽略可能会造成系统行为失常的错误,将不可避免的导致大量的调试工作。

然而,你可以编写让你在错误突然发生时什么都不做的代码,这是可能的,但是这种代码常常会比较复杂。如果你采用了这种方法,你就必须在代码中明显表示出来。不要把这种方法错误的看作无知或错误。

5.     传播:当一个下级函数的调用失败时,你可能会无法继续,但是你也许并不知道该怎么办,唯一的选择是进行清理并将错误报告向上传播。

a)    输出你所收到的错误信息(返回相同的原因代码或传播异常)

b)    重新解释错误信息,向上一级发送一个更加有意义的消息

 

当错误发生时,首先要进行恰当清理,可靠的代码即使在错误发生的情况下也不会泄露资源,或者让世界处于不稳定的状态中,除非这真是不可避免。

不要在你的错误报告中向外界泄露不恰当的信息。只返回调用方能够理解并可以根据其进行操作的有用信息。

 

错误处理机制最基本的实施,就是要在函数中进行一些错误检测,这部分请见“函数设计要求”中的函数错误检测列表。

 

3.7  文件头注释和文件组织

3.7.1文件头注释

文件头注释中应该包含的信息是文件的目的和描述所有权及版权信息的版权声明。头注释中不应该包含容易过时的信息,比如作者、修改者或者文件最后一次修改的时间。这些信息可能得不到及时更新而误导读者,并且版本控制就可以提供这些信息。头注释中也不需要包含描述每次都做了哪些修改的源文件历史记录。这些信息会就在源文件控制系统中。

 

/********************************************************************************

 *       Copyright(c) 2014-2015 xxx (adder) Technology Co., Limited,

 *                            All RightsReserved.

 *

* 描述:协议相关的宏、枚举类型、结构体等定义

 * 作者:xxx

 ********************************************************************************/

描述包括:本文件的内容、功能、内部各部分之间的关系及本文件与其它文件关系等。

 

如果觉得文件头注释很难写,那么就说明该文件的定位不够清晰,可能包含了太多模块的内容,又可能与其他文件一起组成一个独立性很强的模块,导致模块之间关系混乱。这就表示我们需要对该文件进行分拆或组合了。

3.7.2文件组织

源代码及其头文件的组织,同样要遵循“高内聚,低耦合”的基本设计原则,要保持逻辑和思路清晰条理。

只有将各个文件都划分得很合理,其复用难度才会变得很小,模块之间的逻辑关系也将变得恰到好处。

 

源代码文件:

1.     强调重要的代码:公共信息(外部函数)应当被放在首位,因为“用户”需要看到这些信息,而内部信息(私有的实现细节,内部函数)放在最后,因为这对大多数读者来说并不重要。

2.     Static函数,以 “_”作为前缀;可以去掉模块名前缀,以精简仅局部调用的函数名;本文件内部使用的函数用static定义,并需要在文件开头位置进行声明。

3.     内部全局数据使用 static 定义,并放在源代码文件的头部。

4.     头文件包含:先包含整个方案的公共头文件,再包含模块自己的头文件。

5.     源代码文件不应该包含结构体定义等,而应该放到相对应的头文件中。

 

头文件:

1.     头文件中的外部函数声明应该和源代码文件中的外部函数的定义是一一对应的。

2.     包含宏定义、结构体定义、枚举类型定义、全局变量及外部函数声明等,头文件是模块对外的重要窗口,所以必须精心编写,让模块的使用者通过阅读头文件即可“安全”使用该模块。

 

3.8  注释原则

3.8.1黄金原则

1.     注释不会比它们所标注的代码更重要——你无法使用注释把糟糕的代码变好。你的目标应该是,编写根本无需注释的自文档代码。(编写得好的代码实际上并不需要注释,因为每行代码都可以自我解释。)

2.     选择一种维护成本较低的注释风格。事实证明:我们必须让注释足够简单以便于更新,否则它们会很容易过时。

3.     不要描述代码,我们要做到:一个事实—,一个源头。不要在注释中重复代码。

4.     当你发现自己在编写密密麻麻的注释来解释你的代码时,赶快停下来,是不是有一个更大的问题需要解决。

5.     注释应该记录意想不到的内容:如果代码的任何一部分是不常见、意想不到或者令人吃惊的,或者有特定的问题需要规避,用注释将其记录下来,因为这个会很容易忘记。

3.8.2注释杂谈

注释的目标读者是人,而非计算机,从这个意义上来讲,在编程的墙体中,注释是最以人为本的砖块。

代码注释不是你应当放在代码中的唯一文档。注释不是规范,不是设计文档,不是API参考。但是注释是一种总是会物理的附在代码上的宝贵的文档形式。它们与代码如此贴近,意味着它们更有可能被更新,并且更有可能在上下文中被人读到。可见,注释是一种内部的文档化机制。

注释要解释为什么,而不是怎么样:注释不应该描述程序是怎样运行的,这完全可以通过阅读代码来了解。毕竟对于代码的运行方式,代码自己就是最权威的描述,而且代码应该已经编写得非常清晰和完整了。

别让注释让我们分心:

A.过去的事情:我们不需要记录过去某件事我们是怎样做的。这应该由修订控制系统来完成。我们不需要在注释中看到复制的旧代码,也不需要看到对旧算法的描述。

B.你不想要的代码:不要把需要剔除的代码包含在注释中。这会让人迷惑。在进行突击式的调试时,可以使用 #if 0   #endif 来代替注释,在代码提交时,可以查找 #if 0 ,将这些代码去掉。如果要将代码注释起来,那么必须留下一个标注,解释一下为什么将这段代码放在注释中。

C.ASCII艺术:避免使用ASCII艺术图形,或者任何试图巧妙地吐出显示代码的排版方式。这样的注释会加重代码维护的负担。

D.代码块的结尾:// end if (a < 1)  ,这是一种多余的注释形式,你需要先过滤掉这个注释,才能真正理解这个语句。代码块的结尾应该与开头一起显示在同一页中,而且代码的版面应当使块的开头和结尾都一目了然。所有的冗词赘语都应该避免。

 

3.9  调试信息打印规范

 

/******************************************

应用打印

*******************************************/

//err信息;理论上永远打开,除非空间不够用

PRINT_ERR(str)

PRINT_ERR_INT(str,data)

 

//警告信息(用户级的AP用到的较少,主要是系统使用),建议打开

PRINT_WARNING(str)

PRINT_WARNING_INT(str,data)

 

//关键路径、算法等重要信息

PRINT_INFO(str)

PRINT_INFO_INT(str,data)

 

//普通函数的进入、退出,参数等调试信息;跟踪bug时打开

PRINT_DBG(str)

PRINT_DBG_INT(str,data)

 

//打印 裸数据

PRINT_DATA(buf,len)

 

/******************************************

DRV驱动打印

*******************************************/

//err信息;理论上永远打开,除非空间不够用

PRINTD_ERR(str)

PRINTD_ERR_INT(str,data)

 

//警告信息(用户级的AP用到的较少,主要是系统使用),建议打开

PRINTD_WARNING(str)

PRINTD_WARNING_INT(str,data)

 

//关键路径、算法等重要信息

PRINTD_INFO(str)

PRINTD_INFO_INT(str,data)

 

//普通函数的进入、退出,参数等调试信息;跟踪bug时打开

PRINTD_DBG(str)

PRINTD_DBG_INT(str,data)

 

//打印 裸数据

PRINTD_DATA(buf,len)

 

/******************************************

中断打印

*******************************************/

DEBUG_PRINT_IRQ(str,data,mode)

 

3.10       防御性编程

顾名思义,防御性编程是一种细致、谨慎的编程方法。为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能地“保护”自己。我们通过明确的在代码中对设想进行检查,击碎了未记录下来的设想。防止(或至少是观察)我们的代码以将会展现错误行为的方式被调用。

 

防御性编程使我们可以尽早发现较小的问题,而不是等到它们发展成大的灾难的时候才发现。

 

我们常常可以看到“职业”的开发人员,不断地受到那些从未有时间验证的错误的打击。

 

防御性编程帮助我们从一开始就编写正确的软件,而不再需要经历“编写-尝试-编写-尝试”的循环过程。在采用了防御性编程之后,开发软件的过程就编程了:编写代码-测试-成功!!

 

当然,防御性编程并不能排除所有的程序错误。但是问题所带来的麻烦将会减少,并易于修改。防御性编程员只是抓住飘落的雪花,而不是被埋葬在错误的雪崩中。

 

防御性编程是一种防卫方式,而不是一种补救形式(调试)。

 

支持防御性编程的意见:

1.     防御性编程可以节省大量的调试时间,使你可以去做更有意义的事情。(墨菲:凡是可能会被错误使用的代码,一定会被错误地使用。)

2.     编写可以正确运行、只是速度有些慢的代码,要远远好过大多数时间都正常运行、但是有时候会崩溃的代码。

3.     我们可以设计一些在版本构建中物理移除的防御性代码,以解决性能问题。

 

防御性编程规则:

1.     使用好的编码风格和合理的设计。

2.     不要仓促地编写代码,在写每一行代码时都三思而后行。

3.     不要相信任何人。任何人(包括你自己)都可能把缺陷引入你的程序逻辑当中,用怀疑的眼光审视所有的输入和所有的结果,直到你能证明它们是正确的时为止。防御性编程,就是要保持对现实世界的一种健康的怀疑。

a)    真正的用户:意外的提供了假的输入,或者错误的操作了程序

b)    恶意的用户:故意造成不好的程序行为

c)     客户端代码:使用错误的参数调用了你的函数,或者提供不一致的输入

d)    运行环境:没有为程序提供足够的服务

e)    外部程序库:运行失误,不遵从你所依赖的接口协议

4.     编码的目标是清晰,而不是简洁(我的理解是代码量少)。(将复杂的代数运算拆分为一系列单独的语句,使逻辑更清晰)。

5.     不要让任何人做他们不该做的修补工作。(比如,将所有变量保持在尽可能小的范围内,这样别人就修改不到了。)

6.     编译时打开所有警告开关。

7.     使用静态分析工具。

8.     使用安全的数据结构。

9.     检查所有的返回值。

10.   审慎地处理内存(和其他宝贵的资源)。

11.   在声明位置初始化所有变量。

12.   尽可能推迟一些声明变量。

13.   使用标准语言工具(编译器版本号问题)。

14.   使用好的诊断信息日志工具。

15.   审慎地进行强制转换。

 

3.11       Goto语句

Goto语句的滥用,会产生混乱不堪的代码,这些代码的控制流程难以跟踪和理解。

 

所以我们要限制Goto语句的使用,只能在以下场合中使用:

1.     只能在不影响程序结构的少数特定环境中使用goto语句。

2.     goto语句还经常用来在执行了一些活动(例如打印一条错误信息或者释放分配的资源)后退出函数或程序。

3.     公共错误处理器:函数的正常退出路径位于错误处理器之前,从而保证在没有错误发生时该处理器不会被调用。

4.     goto语句还经常用来在某些变量发生改变,或是执行完某些处理后,重新执行某一部分代码。(这里,使用goto语句有时候可以更好的实现编码者的意图;标签可以命名为again或retry)

5.     goto语句可以代替break和continue,在嵌套循环和switch语句中改变程序的控制流程。在大型复杂循环中采用这种做法可以阐明控制流程的走向,同时避免嵌套循环中添加特定break或continue语句引起错误的可能性。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值