质量保障

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

  • (1)正确性,指程序要实现设计要求的功能。
  • (2)简洁性,指程序易于理解并且易于实现。
  • (3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
  • (4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
  • (5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
  • (6)代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
  • (7)可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。
  • (8)个人表达方式/个人方便性,指个人编程习惯。

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

  1. 易混淆的操作符

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

赋值操作符“=” 逻辑操作符“==” 关系操作符“<” 位操作符"<<" 位操作符“>>” 关系操作符“>” 位操作符"|" 逻辑操作符“&&” 逻辑操作符"!" 位操作符“~” 2. 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 禁止内存操作越界。

内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。 示例:使用 itoa()将整型数转换为字符串时:

char TempShold[10] ;
itoa(ProcFrecy,TempShold, 10); /* 数据库刷新间隔设为值1073741823时,系统监控后台coredump,
监控前台抛异常。*/

TempShold 是以‘\0’结尾的字符数组,只能存储 9 个字符,而 ProcFrecy 的最大值可达到 10 位,导致符数组 TempShold 越界。

正确写法:一个 int(32 位)在-2147483647 ~ 2147483648 之间,将数组 TempShold 设置成 12 位。

char TempShold[12] ;
itoa(ProcFrecy,TempShold,10);

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

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

规则 6.2 禁止内存泄漏。

示例:异常出口处没有释放内存

MsgDBDEV = (PDBDevMsg)GetBuff( sizeof( DBDevMsg ), **LINE**);
if (MsgDBDEV == NULL)
{
return;
}
MsgDBAppToLogic = (LPDBSelfMsg)GetBuff( sizeof(DBSelfMsg), **LINE** );
if ( MsgDBAppToLogic == NULL )
{
return; //MsgDB_DEV 指向的内存丢失
}

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

  • 异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI 等资源是否全部释放
  • 删除结构指针时,必须从底层向上层顺序删除
  • 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了  避免重复分配内存
  • 小心使用有 return、break 语句的宏,确保前面资源已经释放
  • 检查队列中每个成员是否释放

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

示例:一个函数返回的局部自动存储对象的地址,导致引用已经释放的内存空间

int* foobar (void)
{
int local_auto = 100;
return &local_auto;
}

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

  • 内存释放后,把指针置为 NULL;使用内存指针前进行非空判断。
  • 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
  • 避免操作已发送消息的内存。
  • 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)

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

使用变量时要注意其边界值的情况。

示例:如 C 语言中字符型变量,有效值范围为-128 到 127。故以下表达式的计算存在一定风险。

char ch = 127;
int sum = 200;
ch += 1; // 127为ch的边界值,再加将使ch上溢到-128,而不是128
sum += ch; // 故sum的结果不是328,而是72。

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

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

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

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

建议 6.3 不要滥用 goto 语句。

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

int foo(void)
{
 char* p1 = NULL;
 char* p2 = NULL;
 char* p3 = NULL;
 int result = -1;
 p1 = (char *)malloc(0x100);
 if (p1 == NULL)
 {
 goto Exit0;
 }
 strcpy(p1, "this is p1");
 p2 = (char *)malloc(0x100);
 if (p2 == NULL)
 {
 goto Exit0;
 }
 strcpy(p2, "this is p2");
 p3 = (char *)malloc(0x100);
 if (p3 == NULL)
 {
 goto Exit0;
 }
 strcpy(p3, "this is p3");
 result = 0;
Exit0:
 free(p1); // C标准规定可以free空指针
 free(p2);
 free(p3);
 return result;
}

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

示例:如下程序将造成变量下溢。

unsigned char size ;
…
while (size-- >= 0) // 将出现下溢
{
 ... // program code
}

当 size 等于 0 时,再减不会小于 0,而是 0xFF,故程序是一个死循环。应如下修改。

char size; // 从unsigned char 改为char
…
while (size-- >= 0)
{
 ... // program code
}