代码编写规范

2023-05-16

目录

1.头文件

2 函数

3 标识符命名与定义

3.1 通用命名规则

3.2 文件命名规则

3.3 变量命名规则

3.4 函数命名规则

3.5 宏的命名规则

4 变量

5 宏、常量

6 质量保证

7 程序效率

8 注释

9 排版与格式

10 表达式

11 代码编辑、编译


1.头文件

原则1.1 头文件中适合放置接口的声明,不适合放置实现。

说明:头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

内部使用的函数(相当于类的私有方法)声明不应放在头文件中。

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

变量定义不应放在头文件中,应放在.c文件中。

变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

原则1.2 头文件应当职责单一。

说明:头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

#include <VXWORKS.H>
#include <KERNELLIB.H>
#include <SEMLIB.H>
#include <INTLIB.H>
#include <TASKLIB.H>
#include <MSGQLIB.H>
#include <STDARG.H>
#include <FIOLIB.H>
#include <STDIO.H>
#include <STDLIB.H>
#include <CTYPE.H>
#include <STRING.H>
#include <ERRNOLIB.H>
#include <TIMERS.H>
#include <MEMLIB.H>
#include <TIME.H>
#include <WDLIB.H>
#include <SYSLIB.H>
#include <TASKHOOKLIB.H>
#include <REBOOTLIB.H>
...
typedef unsigned short WORD;
...

 这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不常用的头文件。如果工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,由于上述头文件的职责过于庞大,而WORD又是每一个文件必须包含的,从而导致stdio.h/syslib.h等可能被不必要的展开了9900次,大大增加了工程的编译时间。

原则1.3 头文件应向稳定的方向包含。

说明:头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

就我们的产品来说,依赖的方向应该是:产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。

除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

编者推荐开发人员使用“依赖倒置”原则,即由使用者制定接口,服务提供者实现接口。

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

说明:如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就无法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。

本规则反过来并不一定成立。有些特别简单的头文件,如命令ID定义头文件,不需要有对应的.c存在。

规则1.2 禁止头文件循环依赖。

说明:头文件循环依赖,指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的源代码重新编译。

规则1.3 .c/.h文件禁止包含用不到的头文件。

说明:很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

规则1.4 头文件应当自包含。

说明:简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。

示例:

如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:

每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。

额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。

注意:该规则需要与“.c/.h文件禁止包含用不到的头文件”规则一起使用,不能为了让a.h自包含,而在a.h中包含不必要的头文件。a.h要刚刚可以自包含,不能在a.h中多包含任何满足自包含之外的其他头文件。

规则1.5 总是编写内部#include保护符(#define 保护)。

说明:多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。

通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。

所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

注:没有在宏最前面加上“_",即使用FILENAME_H代替_FILENAME_H_,是因为一般以"_"和”__"开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。

定义包含保护符时,应该遵守如下规则:

1)保护符使用唯一名称;

2)不要在受保护部分的前后放置代码或者注释。

示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按如下方式保护:

#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif

也可以使用如下简单方式保护:

#ifndef TIMER_H
#define TIMER_H
..
#endif

例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。

规则1.6 禁止在头文件中定义变量。

说明:在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.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,后面这种写法容易在foo改变时可能导致声明和定义不一致。

2 函数

原则2.1 一个函数仅完成一件功能。

说明:一个函数实现多个功能给开发、使用、维护都带来很大的困难。

将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

原则2.2 重复代码应该尽可能提炼成函数。

说明:重复代码提炼成函数可以带来维护成本的降低。

重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。

一般情况下,可以通过提炼函数的形式消除重复代码。

规则2.1 避免函数过长,新增函数不超过50行(非空非注释行)。

说明:本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行。

过长的函数往往意味着函数功能不单一,过于复杂(参见原则2.1:一个函数只完成一个功能)。

函数的有效代码行数,即NBNC(非空非注释行)应当在[1,50]区间。

例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。

延伸阅读材料:业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QAC建议一个函数中的所有行数(包括注释和空白行)不超过50行。

规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层。

说明:本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次。

函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环„„)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。

规则2.3 废弃代码(没有被调用的函数和变量)要及时清除。

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

3 标识符命名与定义

3.1 通用命名规则

目前比较使用的如下几种命名风格:

unix like风格:单词用小写字母,每个单词直接用下划线„_‟分割,例如:text_mutex, kernel_text_address。

Windows风格:大小写字母混用,单词连在一起,每个单词首字母大写。不过Windows风格如果遇到大写专有用语时会有些别扭,例如命名一个读取RFC文本的函数,命令为ReadRFCText,看起来就没有unix like的read_rfc_text清晰了。

匈牙利命名法是计算机程序设计中的一种命名规则,用这种方法命名的变量显示了其数据类型。匈牙利命名主要包括三个部分:基本类型、一个或更多的前缀、一个限定词。这种命令法最初在20世纪80年代的微软公司广泛使用,并在win32API和MFC库中广泛的使用,但匈牙利命名法存在较多的争议,例如:.NET Framework,微软新的软件开发平台,除了接口类型一般不适用匈牙利命名法。.NET Framework 指导方针建议程序员不要用匈牙利命名法,但是没有指明不要用系统匈牙利命名法还是匈牙利应用命名法,或者是两者都不要用。与此对比,Java的标准库中连接口类型也不加前缀。

标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定命名风格,规范中不再做统一的规定。

原则3.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。

说明:尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。

示例:好的命名:

int error_number;
int number_of_completed_connection;
不好的命名:使用模糊的缩写或随意的字符:
int n;
int nerr;
int n_comp_conns; 

原则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 产品/项目组内部应保持统一的命名风格。

说明:Unix like和windows like风格均有其拥趸,产品应根据自己的部署平台,选择其中一种,并在产品内部保持一致。

例外:即使产品之前使用匈牙利命名法,新代码也不应当使用。

建议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 尽量避免名字中出现数字编号,除非逻辑上的确需要编号。

示例:如下命名,使人产生疑惑。

#define EXAMPLE_0_TEST_
#define EXAMPLE_1_TEST_
应改为有意义的单词命名
#define EXAMPLE_UNIT_TEST_
#define EXAMPLE_ASSERT_TEST_

建议3.3 标识符前不应添加模块、项目、产品、部门的名称作为前缀。

说明:很多已有代码中已经习惯在文件名中增加模块名,这种写法类似匈牙利命名法,导致文件名不可读,并且带来带来如下问题:

第一眼看到的是模块名,而不是真正的文件功能,阻碍阅读;

文件名太长;

文件名和模块绑定,不利于维护和移植。若foo.c进行重构后,从a模块挪到b模块,若foo.c 中有模块名,则需要将文件名从a_module_foo.c改为b_module_foo.c

建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。

说明:涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格。

建议3.5 重构/修改部分代码时,应保持和原有代码的命名风格一致。

说明:根据源代码现有的风格继续编写代码,有利于保持总体一致。

3.2 文件命名规则

建议3.6 文件命名统一采用小写字符。

说明:因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。

3.3 变量命名规则

规则3.2 全局变量应增加“g_”前缀。

规则3.3 静态变量应增加“s_”前缀。

说明:增加g_前缀或者s_前缀,原因如下:

首先,全局变量十分危险,通过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加小心。

其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。

规则3.4 禁止使用单字节命名变量,但运行定义i、j、k作为局部循环变量。

建议3.7 不建议使用匈牙利命名法。

说明:变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。

匈牙利命名法源于微软,然而却被很多人以讹传讹的使用。而现在即使是微软也不再推荐使用匈牙利命名法。历来对匈牙利命名法的一大诟病,就是导致了变量名难以阅读,这和本规范的指导思想也有冲突,所以本规范特意强调,变量命名不应采用匈牙利命名法,而应想法使变量名为一个有意义的词或词组,方便代码的阅读。

建议3.8 使用名词或者形容词+名词方式命名变量。

3.4 函数命名规则

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

示例:找到当前进程的当前目录

DWORD GetCurrentDirectory( DWORD BufferLength, LPTSTR Buffer );

建议3.10 函数指针除了前缀,其他按照函数的命名规则命名。

3.5 宏的命名规则

规则3.5 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线„_‟的方式命名(枚举同样建议使用此方式定义)。

示例:

#define PI_ROUNDED 3.14 

规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线„_‟开头和结尾。

4 变量

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

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

原则4.2 结构功能单一;不要设计面面俱到的数据结构。

说明:相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。

设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

原则4.3 不用或者少用全局变量。

说明:单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量。

全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问,建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打补丁等操作。

#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif

 直接使用其他模块的私有数据,将使模块间的关系逐渐走向“剪不断理还乱”的耦合状态,这种情形是不允许的。

规则4.1 防止局部变量与全局变量同名

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

规则4.2 通讯过程中使用的结构,必须注意字节序。

说明:通讯报文中,字节序是一个重要的问题,我司设备使用的cpu类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。

由于位域在不同字节序下,表现看起来差别更大,所以更需要注意。

对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。

规则4.3 严禁使用未经初始化的变量作为右值。

说明:坚持建议4.3(在首次使用前初始化变量,初始化的地方离使用的地方越近越好。)可以有效避免未初始化错误。

建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。

说明:降低全局变量耦合度。

建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。

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

定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为 GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等等。

建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。

说明:未初始化变量是C和C++程序中错误的常见来源。在变量首次使用前确保正确初始化。在较好的方案中,变量的定义和初始化要做到亲密无间。

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

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

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

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

5 宏、常量

规则5.1 用宏定义表达式时,要使用完备的括号。

说明:因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。

示例:如下定义的宏都存在一定的风险

#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA(a, b) ((a) * (b))

这是因为: 如果定义#define RECTANGLE_AREA(a, b) a * b 或#define RECTANGLE_AREA(a, b) (a * b)

则c/RECTANGLE_AREA(a, b) 将扩展成c/a * b , c 与b 本应该是除法运算,结果变成了乘法运算,造成错误。

如果定义#define RECTANGLE_AREA(a, b) (a) * (b)

则RECTANGLE_AREA(c + d, e + f)将扩展成:(c + d * e + f), d与e 先运算,造成错误。

规则5.2 将宏所定义的多条表达式放在大括号中。

说明:更好的方法是多条语句写成do while(0)的方式。

示例:看下面的语句,只有宏的第一条表达式被执行。

#define FOO(x) \
printf("arg is %d\n", x); \
do_something_useful(x);

为了说明问题,下面for语句的书写稍不符规范

for (blah = 1; blah < 10; blah++)
FOO(blah)

用大括号定义的方式可以解决上面的问题:

#define FOO(x) { \
printf("arg is %s\n", x); \
do_something_useful(x); \
}

但是如果有人这样调用:

if (condition == 1)
FOO(10);
else
FOO(20);

那么这个宏还是不能正常使用,所以必须这样定义才能避免各种问题:

#define FOO(x) do { \
printf("arg is %s\n", x); \
do_something_useful(x); \
} while(0)

用do-while(0)方式定义宏,完全不用担心使用者如何使用宏,也不用给使用者加什么约束。

规则5.3 使用宏时,不允许参数发生变化。

示例:如下用法可能导致错误。

#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果:a = 7,即执行了两次增。
正确的用法是:
b = SQUARE(a);
a++; // 结果:a = 6,即只执行了一次增。

同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。

规则5.4 不允许直接使用魔鬼数字。

说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。

解决途径: 对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。

对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。

0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。

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

说明:宏对比函数,有一些明显的缺点:

宏缺乏类型检查,不如函数调用检查严格。

宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是 SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用。

以宏形式写的代码难以调试难以打断点,不利于定位问题。

宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。

示例:下面的代码无法得到想要的结果:

#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
int MAX_FUNC(int a, int b) {
 return ((a) > (b) ? (a) : (b));
}
int testFunc()
{
 unsigned int a = 1;
 int b = -1;
 printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
 printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
 return 0;
}

上面宏代码调用中,结果是(a < b),所以a只加了一次,所以最终的输出结果是:

MACRO: max of a and b is: -1
FUNC : max of a and b is: 2

建议5.2 常量建议使用const定义代替宏。

说明: “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:

#define ASPECT_RATIO 1.653

编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。

解决这个问题的方案很简单:不用预处理宏,定义一个常量:

const double ASPECT_RATIO = 1.653;

这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:

const char * const authorName = "Scott Meyers";

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

说明:如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。

示例:在某头文件中定义宏CHECK_AND_RETURN:

#define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}

然后在某函数中使用(只说明问题,代码并不完整):

pMem1 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
pMem2 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返
回了,造成内存泄漏。*/

所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。

6 质量保证

原则6.1 代码质量保证优先原则

(1)正确性,指程序要实现设计要求的功能。

(2)简洁性,指程序易于理解并且易于实现。

(3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。

(4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。

(5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。

(6)代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。

(7)可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。

(8)个人表达方式/个人方便性,指个人编程习惯。

原则6.2 要时刻注意易混淆的操作符。

说明:包括易混淆和的易用错操作符

1、易混淆的操作符

C语言中有些操作符很容易混淆,编码时要非常小心。

赋值操作符“=”逻辑操作符“==”
关系操作符“<”位操作符"<<"
位操作符“>>”关系操作符“>”
位操作符"|"逻辑操作符“&&”
逻辑操作符"!"位操作符“~”

2、易用错的操作符

(1) 除操作符"/"

当除操作符“/”的运算量是整型量时,运算结果也是整型。

如:1/2=0

(2)求余操作符"%"

求余操作符"%"的运算量只能是整型。

如:5%2=1,而5.0%2是错误的。

(3)自加、自减操作符“++”、“--”

示例1

k = 5; 
x = k++; 
执行后,x = 5,k = 6

示例2

k = 5;
x = ++k;
执行后,x = 6,k = 6

示例3

k = 5;
x = k--;
执行后,x = 5,k = 4

示例4

k = 5;
x = --k;
执行后,x = 4,k = 4

原则6.3 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。

原则6.4 不仅关注接口,同样要关注实现。

说明:这个原则看似和“面向接口”编程思想相悖,但是实现往往会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,往往还受制于其他隐含约束,如:物理内存的限制,网络状况,具体看“抽象漏洞原则”。

规则6.1 禁止内存操作越界。

说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。

  1. 坚持下列措施可以避免内存越界:
  2. 数组的大小要考虑最大情况,避免数组分配空间不够。
  3. 避免使用危险函数sprintf /vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
  4. 使用memcpy/memset时一定要确保长度不要越界
  5. 字符串考虑最后的’\0’, 确保所有字符串是以’\0’结束
  6. 指针加减操作时,考虑指针类型长度 l 数组下标进行检查
  7. 使用时sizeof或者strlen计算结构/字符串长度,避免手工计算

规则6.2 所有的if ... else if结构应该由else子句结束 ;switch语句必须有default分支。

建议6.1 函数中分配的内存,在函数退出之前要释放。

说明:有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。

建议6.2 if语句尽量加上else分支,对没有else分支的语句要小心对待。

建议6.3 不要滥用goto语句。

说明:goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。

可以利用goto语句方面退出多重循环;同一个函数体内部存在大量相同的逻辑但又不方便封装成函数的情况下,譬如反复执行文件操作,对文件操作失败以后的处理部分代码(譬如关闭文件句柄,释放动态申请的内存等等),一般会放在该函数体的最后部分,再需要的地方就goto到那里,这样代码反而变得清晰简洁。实际也可以封装成函数或者封装成宏,但是这么做会让代码变得没那么直接明了。

7 程序效率

原则7.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率。

本章节后面所有的规则和建议,都应在不影响前述可读性等质量属性的前提下实施。

说明:不能一味地追求代码效率,而对软件的正确、简洁、可维护性、可靠性及可测性造成影响。 产品代码中经常有如下代码:

int foo()
{
 if (异常条件)
 {
 异常处理;
 return ERR_CODE_1;
 }
 if (异常条件)
 {
 异常处理;
 return ERR_CODE_2;
 }
 正常处理;
 return SUCCESS;
}

这样的代码看起来很清晰,而且也避免了大量的if else嵌套。但是从性能的角度来看,应该把执行概率较大的分支放在前面处理,由于正常情况下的执行概率更大,若首先考虑性能,应如下书写:

int foo()
{
 if (满足条件) 
{
 正常处理;
 return SUCCESS;
 }
 else if (概率比较大的异常条件)
 {
 异常处理;
 return ERR_CODE_1;
 }
 else
 {
 异常处理;
 return ERR_CODE_2;
 }
}

除非证明foo函数是性能瓶颈,否则按照本规则,应优先选用前面一种写法。

以性能为名,使设计或代码更加复杂,从而导致可读性更差,但是并没有经过验证的性能要求(比如实际的度量数据和目标的比较结果)作为正当理由,本质上对程序没有真正的好处。无法度量的优化行为其实根本不能使程序运行得更快。

记住:让一个正确的程序更快速,比让一个足够快的程序正确,要容易得太多。大多数时候,不要把注意力集中在如何使代码更快上,应首先关注让代码尽可能地清晰易读和更可靠。

原则7.2 通过对数据结构、程序算法的优化来提高效率。

建议7.1 将不变条件的计算移到循环体外。

说明:将循环中与循环无关,不是每次循环都要做的操作,移到循环外部执行。

示例一:

for (int i = 0; i < 10; i++ )
{
 sum += i;
 back_sum = sum;
}

对于此for循环来说语句“back_Sum = sum;” 没必要每次都执行,只需要执行一次即可,因此可以改为:

for (int i = 0; i < 10; i++ )
{
 sum += i;
}
back_sum = sum;

示例二:

for (_UL i = 0; i < func_calc_max(); i++)
{
//process;
}

函数func_calc_max()没必要每次都执行,只需要执行一次即可,因此可以改为:

_UL max = func_calc_max();
for (_UL i = 0; i < max; i++)
{
//process;
}

建议7.2 对于多维大数组,避免来回跳跃式访问数组成员。

示例:多维数组在内存中是从最后一维开始逐维展开连续存储的。下面这个对二维数组访问是以SIZE_B 为步长跳跃访问,到尾部后再从头(第二个成员)开始,依此类推。局部性比较差,当步长较大时,可能造成cache不命中,反复从内存加载数据到cache。应该把i和j交换。

...
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
 sum += x[j][i];
}
}
...

上面这段代码,在 SIZE_B 数值较大时,效率可能会比下面的代码低:

...
for (int i = 0; i < SIZE_B; i++)
{
for (int j = 0; j < SIZE_A; j++)
{
 sum += x[i][j];
}
}
...

建议7.3 创建资源库,以减少分配对象的开销。

说明:例如,使用线程池机制,避免线程频繁创建、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完毕再释放到内存池中,避免内存申请释放的频繁系统调用.

建议7.4 将多次被调用的 “小函数”改为inline函数或者宏实现。

说明: 如果编译器支持inline,可以采用inline函数。否则可以采用宏。

在做这种优化的时候一定要注意下面inline函数的优点:其一编译时不用展开,代码SIZE小。其二可以加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会做语法检查。三思而后行。

8 注释

原则8.1 优秀的代码可以自我解释,不通过注释即可轻易读懂。

说明:优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码 往往存在坏味道,需要重构。

示例:注释不能消除代码的坏味道:

/* 判断m是否为素数*/
/* 返回值:: 是素数,: 不是素数*/
int p(int m)
{
 int k = sqrt(m);
 for (int i = 2; i <= k; i++)
 if (m % i == 0)
 break; /* 发现整除,表示m不为素数,结束遍历*/
 /* 遍历中没有发现整除的情况,返回*/
 if (i > k)
 return 1;
 /* 遍历中没有发现整除的情况,返回*/
 else
 return 0;
}

重构代码后,不需要注释:

int IsPrimeNumber(int num)
{
 int sqrt_of_num = sqrt (num);
 for (int i = 2; i <= sqrt_of_num; i++)
 {
 if (num % i == 0)
 {
 return FALSE;
 }
 }
 return TRUE;
}

原则8.2 注释的内容要清楚、明了,含义准确,防止注释二义性。

说明:有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。

示例:注释与代码相矛盾,注释内容也不清楚,前后矛盾。

/* 上报网管时要求故障ID与恢复ID相一致*/
/* 因此在此由告警级别获知是不是恢复ID */
/* 若是恢复ID则设置为ClearId,否则设置为AlarmId */
if (CLEAR_ALARM_LEVEL != RcData.level)
{
 SetAlarmID(RcData.AlarmId);
}
else
{
 SetAlarmID(RcData.ClearId);
}

正确做法:修改注释描述如下:

/* 网管达成协议:上报故障ID与恢复ID由告警级别确定,若是清除级别,ID设置为ClearId,否
则设为AlarmId。*/

原则8.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描 述代码。

说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。

对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。

注释不是为了名词解释(what),而是说明用途(why)。

示例:如下注释纯属多余。

++i; /* increment i */
if (receive_flag) /* if receive_flag is TRUE */

如下这种无价值的注释不应出现(空洞的笑话,无关紧要的注释)。

/* 时间有限,现在是:04,根本来不及想为什么,也没人能帮我说清楚*/

而如下的注释则给出了有用的信息:

/* 由于xx编号网上问题,在xx情况下,芯片可能存在写错误,此芯片进行写操作后,必须进行回读校
验,如果回读不正确,需要再重复写-回读操作,最多重复三次,这样可以解决绝大多数网上应用时的
写错误问题*/
int time = 0;
do
{
 write_reg(some_addr, value);
 time++;
} while ((read_reg(some_addr) != value) && (time < 3));

对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释,出彩的或复杂的代码块前要加注释,如:

/* Divide result by two, taking into account that x contains the carry from the add. */
for (int i = 0; i < result->size(); i++)
{
 x = (x << 8) + (*result)[i];
 (*result)[i] = x >> 1;
 x &= 1;
}

规则8.1 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除。

说明:不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它。

规则8.2 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。

说明:通常头文件要对功能和用法作简单说明,源文件包含了更多的实现细节或算法讨论。

示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

/*************************************************
 Copyright © Huawei Technologies Co., Ltd. 1998-2011. All rights reserved.
 File name: // 文件名
 Author: ID: Version: Date: // 作者、工号、版本及完成日期
 Description: // 用于详细说明此程序文件完成的主要功能,与其他模块
 // 或函数的接口,输出值、取值范围、含义及参数间的控
 // 制、顺序、独立或依赖等关系
 Others: // 其它内容的说明
 History: // 修改历史记录列表,每条修改记录应包括修改日期、修改
 // 者及修改内容简述
 1. Date:
 Author: ID:
 Modification:
 2. ...
*************************************************/

规则8.3 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。

说明:重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。

规则8.4 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明。

示例:

/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ /* 变量作用、含义*/
/* 0 -SUCCESS 1 -GT Table error */
/* 2 -GT error Others -no use */ /* 变量取值范围*/
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */ /* 使用方法*/
BYTE g_GTTranErrorCode;

规则8.5 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。

示例:

/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
 N_UNITDATA_IND, /* sccp notify sccp user unit data come */
 N_NOTICE_IND, /* sccp notify user the No.7 network can not transmission this message */
 N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};

规则8.6 对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。

说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。

示例(注意斜体加粗部分):

case CMD_FWD:
 ProcessFwd();
 /* now jump into case CMD_A */
case CMD_A:
 ProcessA();
 break;
 //对于中间无处理的连续case,已能较清晰说明意图,不强制注释。
switch (cmd_flag)
{
case CMD_A:
case CMD_B:
 {
 ProcessCMD();
 break;
 }
 ……
}

规则8.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写。

规则8.8 同一产品或项目组统一注释风格。

建议8.1 避免在一行代码或表达式的中间插入注释。

说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。

建议8.2 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。对于有外籍员工的,由产品确定注释语言。

说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

建议8.3 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式。

说明:采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。 以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:

文件头注释:

/**
* @file (本文件的文件名eg:mib.h)
* @brief (本文件实现的功能的简述)
* @version 1.1 (版本声明)
* @author (作者,eg:张三)
* @date (文件创建日期,eg:2010年12月15日)
*/

函数头注释:

/**
*@ Description:向接收方发送SET请求
* @param req - 指向整个SNMP SET 请求报文.
* @param ind - 需要处理的subrequest 索引.
* @return 成功:SNMP_ERROR_SUCCESS,失败:SNMP_ERROR_COMITFAIL
*/
Int commit_set_request(Request *req, int ind);

全局变量注释:

/** 模拟的Agent MIB */
agentpp_simulation_mib * g_agtSimMib;

函数头注释建议写到声明处。并非所有函数都必须写注释,建议针对这样的函数写注释:重要的、复杂的函数,提供外部使用的接口函数。

9 排版与格式

规则9.1 程序块采用缩进风格编写,每级缩进为4个空格。

说明:当前各种编辑器/IDE都支持TAB键自动转空格输入,需要打开相关功能并设置相关功能。

编辑器/IDE如果有显示TAB的功能也应该打开,方便及时纠正输入错误。

IDE向导生成的代码可以不用修改。

宏定义、编译开关、条件预处理语句可以顶格(或使用自定义的排版方案,但产品/模块内必须保持一致)。

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

示例:如下例子不符合规范。

if (!valid_ni(ni))
{
 // program code
 ...
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

应如下书写

if (!valid_ni(ni))
{
 // program code
 ...
}

repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

规则9.3 一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定。

说明:对于目前大多数的PC来说,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来说,可以设置更大的值。

换行时有如下建议:

  • 换行时要增加一级缩进,使代码可读性更好;
  • 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
  • 换行时建议一个完整的语句放在一行,不要根据字符数断行

示例:

if ((temp_flag_var == TEST_FLAG)
 &&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE) >= TEST_COUNT_THRESHOLD))
{
 // process code
}

规则9.4 多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句。

示例:

int a = 5; int b= 10; //不好的排版
//较好的排版:
int a = 5;
int b = 10;

规则9.5 if、for、do、while、case、switch、default等语句独占一行。

说明:执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;

一般写if、for、do、while等语句都会有成对出现的„{}‟,对此有如下建议可以参考:

if、for、do、while等语句后的执行语句建议增加成对的„{}‟;

如果if/else配套语句中有一个分支有„{}‟,那么令一个分支即使一行代码也建议增加„{}‟;

添加„{‟的位置可以在if等语句后,也可以独立占下一行;独立占下一行时,可以和if在一个缩进级别,也可以在下一个缩进级别;但是如果if语句很长,或者已经有换行,建议„{‟使用独占一行的写法。

规则9.6 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。

说明:采用这种松散方式编写代码的目的是使代码更加清晰。

在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。

在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

示例: (1) 逗号、分号只在后面加空格

int a, b, c;

(2) 比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符 "<<"、"^"等双目操作符的前后加空格。

if (current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;

(3) "!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。

*p = 'a'; // 内容操作"*"与内容之间
flag = !is_empty; // 非操作"!"与内容之间
p = &mem; // 地址操作"&" 与内容之间
i++; // "++","--"与内容之间

(4) "->"、"."前后不加空格。

p->id = pid; // "->"指针前后不加空格

(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。

 if (a >= b && c > d) 

建议9.1 注释符(包括„/*‟„//‟„*/‟)与注释内容之间要用一个空格进行分隔。

说明:这样可以使注释的内容部分更清晰。

现在很多工具都可以批量生成、删除'//'注释,这样有空格也比较方便统一处理。

建议9.2 源程序中关系较为紧密的代码应尽可能相邻。

10 表达式

规则10.1 表达式的值在标准所允许的任何运算次序下都应该是相同的。

说明:除了少数操作符(函数调用操作符 ( )、&&、| |、? : 和 , (逗号)) 之外,子表达式所依据的运算次序是未指定的并会随时更改。注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。

将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。

1、自增或自减操作符

示例:

x = b[i] + i++;

b[i] 的运算是先于还是后于 i ++ 的运算,表达式会产生不同的结果,把自增运算做为单独的语句,可以避免这个问题。

x = b[i] + i;
i ++;

2﹑函数参数

说明:函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。

示例:

x = func( i++, i);

应该修改代码明确先计算第一个参数:

i++;
x = func(i, i);

3、函数指针

说明:函数参数和函数自身地址的计算次序未定义。

示例:

p->task_start_fn(p++);

求函数地址p与计算p++无关,结果是任意值。必须单独计算p++:

p->task_start_fn(p);
p++;

4﹑函数调用

示例:

int g_var = 0;
int fun1()
{
 g_var += 10;
 return g_var;
}
int fun2()
{
 g_var += 100;
 return g_var;
}
int x = fun1() + fun2();

编译器可能先计算fun1(),也可能先计算fun2(),由于x的结果依赖于函数fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。

应该修改代码明确fun1/ fun2的计算次序:

int x = fun1();
x = x + fun2();

5、嵌套赋值语句

说明:表达式中嵌套的赋值可以产生附加的副作用。不给这种能导致对运算次序的依赖提供任何机会的最好做法是,不要在表达式中嵌套赋值语句。

示例:

x = y = y = z / 3;
x = y = y++;

6、volatile访问

说明:限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取。

示例:下面的写法可能无法实现作者预期的功能:

/* volume变量被定义为volatile类型*/
UINT16 x = ( volume << 3 ) | volume; /* 在计算了其中一个子表达式的时候,volume的值可能已
经被其它程序或硬件改变,导致另外一个子表达式的计算结果非预期,可能无法实现作者预期的功能
*/

建议10.1 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利。

说明:如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:

int g_var;
int fun1()
{
 g_var += 10;
 return g_var;
}
int fun2()
{
 g_var += 100;
 return g_var;
}
int main(int argc, char *argv[], char *envp[])
{
 g_var = 1;
 printf("func1: %d, func2: %d\n", fun1(), fun2());
 g_var = 1;
 printf("func2: %d, func1: %d\n", fun2(), fun1()); 
}

上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码。

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

说明:因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。

示例:

int main(int argc, char *argv[], char *envp[])
{
 int a = 0;
 int b;
 if ((a == 0) || ((b = fun1()) > 10))
 {
 printf("a: %d\n", a);
 }
 printf("b: %d\n", b);
}

作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。

看如下代码,能否一眼看出输出结果会是什么吗?好理解吗?

int g_var;
int main(int argc, char *argv[], char *envp[])
{
 g_var = 1;
 printf("set 1st: %d, add 2nd: %d\n", g_var = 10, g_var++);
 g_var = 1;
 printf("add 1st: %d, set 2nd: %d\n", g_var++, g_var = 10);
}

建议10.3 用括号明确表达式的操作顺序,避免过分依赖默认优先级。

说明:使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。

1. 一元操作符,不需要使用括号

x = ~a; /* 一元操作符,不需要括号*/
x = -a; /* 一元操作符,不需要括号*/

2. 二元以上操作符,如果涉及多种操作符,则应该使用括号

x = a + b + c; /* 操作符相同,不需要括号*/
x = f ( a + b, c ) /* 操作符相同,不需要括号*/
if (a && b && c) /* 操作符相同,不需要括号*/
x = (a * 3) + c + d; /* 操作符不同,需要括号*/
x = ( a == b ) ? a : ( a –b ); /* 操作符不同,需要括号*/

3 .即使所有操作符都是相同的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序以下代码将3个浮点数相加:

 /* 除了逗号(,),逻辑与(&&),逻辑或(||)之外,C标准没有规定同级操作符是从左还是从右开始计
算,以上表达式存在种计算次序:f4 = (f1 + f2) + f3 或f4 = f1 + (f2 + f3),浮点数计算过
程中可能四舍五入,量级提升,计算次序的不同会导致f4的结果不同,以上表达式在不同编译器上
的计算结果可能不一样,建议增加括号明确计算顺序*/
f4 = f1 + f2 + f3; 

建议10.4 赋值操作符不能使用在产生布尔值的表达式上。

说明:如果布尔值表达式需要赋值操作,那么赋值操作必须在操作数之外分别进行。这可以帮助避免=和= =的混淆,帮助我们静态地检查错误。

示例:

x = y;
if (x != 0)
{
 foo ();
} 

不能写成:

if (( x = y ) != 0)
{
 foo ();
} 

或者更坏的

if (x = y)
{
 foo ();
}

11 代码编辑、编译

规则11.1 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警。

说明:编译器是你的朋友,如果它发出某个告警,这经常说明你的代码中存在潜在的问题。

规则11.2 在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略。

说明:如果必须禁用某个告警,应尽可能单独局部禁用,并且编写一个清晰的注释,说明为什么屏蔽。

某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

代码编写规范 的相关文章

  • 【linux网络编程学习笔记】第二节:创建TCP通信(双向)(socket、bind、listen、accept、connect、recv、send、shutdown、server\client)

    Work won 39 t kill but worry will 劳动无害 xff0c 忧愁伤身 上一篇章中创建了TCP的客户端的服务器 xff0c 但是只能单向发送 xff0c 本章节主要讲解如何进行双向互发消息 xff0c 实现的过程
  • 航模电池及稳压降压模块—毕设简记

    航模电池及稳压降压模块简介 简述 准备给设计的控制系统选一块航模电池 xff0c 需要关注什么参数 xff1f 控制系统的传感器需要5V供电 直流减速电机需要12V供电 单片机需要7 12V供电 xff0c 这么多供电该怎么处理 xff1f
  • Laplance算子(二阶导数)

    理论 xff1a 在二阶导数的时候 xff0c 最大变化处的值为0 即边缘是零 xff0c 通过二阶导数计算 xff0c 依据此理论我们可以计算图像的二阶导数 xff0c 提取边缘 Laplance算子 二阶导数我不会 xff0c 别担心
  • yolo3_pytorch 训练voc数据集和训练自己的数据集并进行预测(github代码调试)

    训练voc数据集的步骤 xff1a xff1a 首先下载voc数据集 xff0c 将数据集放在从github中下载的项目中VOCdevkit目录中 xff08 直接将数据集拉入到项目中 xff0c 替代目标文件即可 xff09 源码下载 x
  • ros的通信机构

    ros的通信是在os层之上 xff0c 基于TCP IP协议实现 os层 xff08 操作系统层 xff09 对于开发者来讲 xff0c 是不需要关系的 中间层 xff1a TCPROS UDPROS 这是基于TCP IP协议进行重新封装的
  • 视频追踪(meanshift和camshift算法)

    import numpy as np import cv2 as cv opencv实现meanshift的api cv meanShift probImage window criteria 参数一 xff1a roi区域 xff0c 目
  • 国产的Arduino Mega 2560 R3改进版串口1丝印标注错误

    Mega 2560有四个串口 xff1a 分别是串口0 xff0c 串口1 xff0c 串口2 xff0c 串口3 而串口1的丝印标注反了 在板子中烧录如下代码 xff0c 则串口1的TX应该不断的有输出 xff0c RX没有 void s
  • Visual Studio实现光流法(opencv and Eigen)

    环境问题 xff1a 首先是在vs中安装opencv和eigen两个库 安装eigen库所推荐的链接 xff1a VS2019正确的安装Eigen库 xff0c 解决所有报错 xff08 全网最详细 xff01 xff01 xff09 Ma
  • Deformable DETR环境配置和应用

    准备工作 xff1a Deformable DETR代码路径如下 xff1a GitHub fundamentalvision Deformable DETR Deformable DETR Deformable Transformers
  • A review of visual SLAM methods for autonomous driving vehicles

    自主驾驶车辆的视觉SLAM方法回顾 原论文在文章末尾 摘要 xff1a 自主驾驶车辆在不同的驾驶环境中都需要精确的定位和测绘解决方案 在这种情况下 xff0c 同步定位和测绘 xff08 SLAM xff09 技术是一个很好的研究解决方案
  • slam原理介绍和经典算法

    1 传统slam局限性 slam算法假设的环境中的物体都是处于静态或者低运动状态的 xff0c 然而 xff0c 现实世界是复杂多变的 xff0c 因此这种假设对于应用 环境有着严格的限制 xff0c 同时影响视觉slam系统在实际场景中的
  • Git教程(李立超git和GitHub使用)

    Git教程 配置 配置name和email git config global user name 34 xxxx 34 git config global user email 34 xxx 64 xxx xxx 34 git statu
  • 需求:节目上传至MINIO后,使用mqtt进行上报

    需求 xff1a 节目上传至MINIO后 xff0c 使用mqtt进行上报 环境准备 文件管理平台 xff1a 首先需要使用minio搭建属于自己的对象存储 xff08 此步骤跳过 xff09 通信方式 xff1a MQTT方式 xff0c
  • Vue.js自定义事件的使用(实现父子之间的通信)

    vue v model修饰符 xff1a lazy number trim attrs数据的透传 xff0c 在组件 xff08 这个是写在App vue中 xff09 数据就透传到student组件中 xff0c 在template中可以
  • 简单算法——二分搜索的递归版本和非递归版本

    二分搜索 这是大家比较熟悉的算法了 xff0c 我们今天来复习一下 xff1a 前提 xff1a 二分查找要求所查找的顺序表必须是有序的 算法思路 定义left为顺序表最左端元素位置 xff0c right为顺序表右端元素位置 定义mid
  • Mysql(14)——事务

    概念 一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元 只有当事务中的所有操作都正常执行完了 xff0c 整个事务才会被提交给数据库 xff1b 如果有部分事务处理失败 xff0c 那么事务就要回退到最初的状态 x
  • Mysql(15)——锁机制 + MVCC(全)

    前言 事务的隔离级别在之前我们已经学习过 xff0c 那么事务隔离级别的实现原理是什么呢 xff1f 锁 43 MVCC 下面我们就来分开讲解 xff1a 表级锁 amp 行级锁 注意 xff1a 表锁和行锁说的是锁的粒度 xff0c 不要
  • DIY无人机组装与飞控参数调试记录(DJI NAZA-LITE)

    早就想玩一玩无人机 xff0c 奈何各种原因一直没有机会 xff0c 工作之后资金富足 xff0c 加上本身工作和这个相关性比较大 xff0c 于是就自己DIY了一台无人机 一 材料准备 xff1a F450机架 GPS支架 好盈乐天 20
  • Mysql(16)——日志

    前言 我们之前了解过redo log和undo log xff0c 他们是作用在InnoDb存储引擎层的 xff0c 今天我们来讲讲服务层的其他日志类型 一 错误日志 错误日志是 MySQL 中最重要的日志之一 xff0c 它记录了当 my

随机推荐

  • Mysql(17)——优化

    前言 一 SQL和索引优化 二 应用优化 除了优化SQL和索引 xff0c 很多时候 xff0c 在实际生产环境中 xff0c 由于数据库服务器本身的性能局限 xff0c 就必须要对上层的应用来进行一些优化 xff0c 使得上层应用访问数据
  • 项目——C++实现数据库连接池

    前言 在学习Mysql的时候 xff0c 我们都有这个常识 xff1a 对于DB的操作 xff0c 其实本质上是对于磁盘的操作 xff0c 如果对于DB的访问次数过多 xff0c 其实就是涉及了大量的磁盘IO xff0c 这就会导致MYsq
  • Redis入门——发展历程及NoSQL

    前言 随着社会的发展 xff0c 数据存储经历了诸多的过程 xff0c 这篇文章就是介绍Redis的发展由来 xff1a 1 单机Mysql时代 这种模式存在以下的瓶颈 xff1a 数据量太大 xff0c 一个机器存放不下数据的索引太大 x
  • Redis(1)——基本命令及数据类型(5+3)

    Redis的基本概念 Remote Dictionary Server xff1a 远程字典服务Redis 是一个开源 xff08 BSD许可 xff09 的 xff0c 内存中的数据结构存储系统 xff0c 它可以用作数据库 缓存和消息中
  • Redis(2)——事务机制

    Redis的事务机制 Redis的事务本质 xff1a 一组命令的集合一个事务中的所有命令都会都被序列化 xff0c 在事务执行的过程中 xff0c 会按照顺序执行 xff01 一次性 顺序性 排他性 执行一系列的命令Redis没有事务隔离
  • Redis(3)—— 持久化、发布订阅

    持久化 Redis是内存数据库 xff0c 如果不将内存中的数据库状态保存到磁盘中 xff0c 那么一旦服务器进程退出 xff0c 服务器中的数据库状态也会消失 所以Redis提供了持久化的功能 1 RDB xff08 Redis Data
  • Redis(4)——主从复制

    Redis主从复制 主从复制 xff1a 指的是将一个Redis服务器的数据 xff0c 复制到其他的Redis服务器 前者称为主节点 xff08 master leader xff0c 后者称为从节点 xff08 slave follow
  • Redis(5)——缓存穿透和雪崩

    概要 Redis缓存的使用 xff0c 极大的提高了应用程序的性能和效率 xff0c 特别是数据查询等 但同时 xff0c 它也带来了一些问题 其中 xff0c 最主要的问题就是数据一致性 xff0c 从严格意义上来讲 xff0c 这个问题
  • 复习:结构体大小的内存对齐问题

    内存对齐 内存对齐是指 xff1a 任意单个类型的数据都需要存放在能被它本身大小所能整除的地址上 基本类型的大小 char 1 short 2 int 4 long 4 long long 8 float 4 double 8 指针 4 8
  • 0.一些自己初学Solidworks的疑惑

    1 为什么要选择学习SolidWorks 首先 作为初学者 我们对一个东西并不是很了解 那么就需要别人来教我们 对吧 这些人可以是老师 可以是同学 可以是师傅 可以是网络上热心肠的大神 可以是一些培训机构 等等 首先呢 学习三维设计软件 看
  • LInux——五种IO模型

    Linux中的IO简述 IO主要分为以下的三种 xff1a 内存IO网络IO磁盘IO 通常我们所说的IO是后两者 xff0c Linux中无法直接操作IO设备 xff0c 必须通过系统调用请求kernal来协助完成IO的动作 xff0c 内
  • 复习:Linux中的软连接和硬连接

    前言 首先我们先来复习以下Linux的文件系统 Linux的文件系统是EXT4 以EXT4文件系统格式化磁盘时 xff0c 将磁盘分成了三个区 xff0c 分别是 xff1a 1 superblock xff1a 记录文件系统的整体信息 x
  • 复习:字节对齐的原则

    为什么需要字节对齐 xff1f 现代计算机中内存空间都是按照byte划分的 xff0c 从理论上讲似乎对任何类型的变量的访问可以从任何地址开始 xff0c 但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问 xff0c 这就需要各
  • Reactor模型

    前言 首先让我们来回顾一下select poll和epoll是如何获取网络事件的 xff1a 在获取事件时 xff0c 先把我们要关心的连接传给内核 xff0c 再由内核检测 xff1a 若没有事件发生 xff0c 线程只需阻塞在这个系统调
  • Proactor模型

    前言 上一篇讲解的Reaactor是非阻塞的同步网络模式 xff0c 而Proactor是异步网络模式 至于异步IO怎么理解 xff1a 可以参考我的这一篇博客 xff1a Linux的五种IO模型 理解之后 xff1a 你就会感受到 xf
  • STL空间配置器(一级配置器及二级配置器)

    前言 在我们日常使用STL中的容器时 xff0c 我们是几乎感受不到空间配置器的存在 xff0c 因为他一直在默默工作 xff0c 我们在之前的这一篇博客中也大概介绍过 xff1a C 43 43 xff08 21 xff09 vector
  • HTTP各个版本的区别

    HTTP 1 0 短连接版本 HTTP 1 0规定浏览器与服务器只保持短暂的连接 xff0c 即每一次请求都需要与服务器建立一次TCP连接 xff0c 服务器完成请求处理后立即断开TCP连接 服务器不会跟踪每个客户也不记录过去的请求 xff
  • 实时时钟芯片DS1307的使用及驱动代码

    DS1307实时时钟芯片的介绍及驱动代码 目录 一 DS1307是什么 xff1f 二 DS1307的功能 三 DS1307的寄存器 四 代码 1 读出数据 2 写入数据 3 时间初始化设置 4 获取当前时间 五 注意事项 总结 一 DS1
  • 单片机测量NTC热敏电阻温度的方法(含程序代码)

    1 NTC介绍 NTC是负温度系数热敏电阻 xff0c 随着温度的升高 xff0c NTC的阻值会呈非线性的下降 2 硬件连接 这里采用100k 3950的热敏电阻 xff0c 100k代表的是在25 下的标准阻值 xff0c 3950是热
  • 代码编写规范

    目录 1 头文件 2 函数 3 标识符命名与定义 3 1 通用命名规则 3 2 文件命名规则 3 3 变量命名规则 3 4 函数命名规则 3 5 宏的命名规则 4 变量 5 宏 常量 6 质量保证 7 程序效率 8 注释 9 排版与格式 1