华为c语言编程规范学习笔记(三)

五、变量

原则4.1 一个变量只有一个功能,不能把一个变量用作多种用途。
原则4.2 结构功能单一;不要设计面面俱到的数据结构。

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

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

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

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

规则4.1 防止局部变量与全局变量同名。
规则4.2 通讯过程中使用的结构,必须注意字节序。

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

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

规则4.3 严禁使用未经初始化的变量作为右值。
建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。
建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。

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

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

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

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

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

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

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

六、宏、常量

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

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

规则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 使用宏时,不允许参数发生变化。

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

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

魔鬼数字不会带来程序逻辑的错误,但它会使代码难以理解如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

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

解决途径:

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

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

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

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

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

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

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

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

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

建议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.1 代码质量保证优先原则

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

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

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

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

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

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

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

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

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

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

赋值操作符“=”       逻辑操作符“==”

关系操作符“”         位操作符“>>”

逻辑操作符“||”       位操作符"|"

逻辑操作符“&&”    位操作符"&"

逻辑操作符"!"        位操作符“~”

2、易用错的操作符

(1) 除操作符"/"

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

(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 禁止内存操作越界。

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

坚持下列措施可以避免内存越界:

数组的大小要考虑最大情况,避免数组分配空间不够。

避免使用危险函数sprintf/vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数 snprintf/strncpy/strncat/fgets代替。

使用memcpy/memset时一定要确保长度不要越界

字符串考虑最后的’\0’,确保所有字符串是以’\0’结束

指针加减操作时,考虑指针类型长度

数组下标进行检查

使用时sizeof或者strlen计算结构/字符串长度,避免手工计算

规则6.2 禁止内存泄漏。

内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各种资源)泄漏是常见的错误。

坚持下列措施可以避免内存泄漏:

异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放

删除结构指针时,必须从底层向上层顺序删除

使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了

避免重复分配内存

小心使用有return、break语句的宏,确保前面资源已经释放

检查队列中每个成员是否释放

规则6.3 禁止引用已经释放的内存空间。

在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块,而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。

坚持下列措施可以避免引用已经释放的内存空间:

内存释放后,把指针置为NULL;使用内存指针前进行非空判断。

耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。

避免操作已发送消息的内存。

自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更 大作用域的对象或者静态对象或者从一个函数返回的对象)

规则6.4 编程时,要防止差1错误。

此类错误一般是由于把“=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。 使用变量时要注意其边界值的情况。

规则6.5 所有的if ... else if结构应该由else子句结束 ;switch语句必须有default分支。
建议6.1 函数中分配的内存,在函数退出之前要释放。

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

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

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

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

建议6.4 时刻注意表达式是否会上溢、下溢。

八、程序效率

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

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

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

原则7.2 通过对数据结构、程序算法的优化来提高效率。
建议7.1 将不变条件的计算移到循环体外。
建议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[ ][ j ];

    }

}

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

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

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

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

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

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值