vlambda博客
学习文章列表

嵌入式软件高质量实现(C语言)

建议1:if语句应该尽量保持简洁,减少嵌套的层数。 
C89指明,编译程序必须最少支持15层嵌套,C99把限度提升到127层。实际上,多数编译器程序支持远大于15层嵌套的if。虽然如此,但我们为了使if语句保持简洁与可读性,应该尽量减少嵌套的层数。
1.先处理正常情况,再处理异常情况。
我们在编写代码时,首要原则就是要使正常情况的执行代码清晰,确认那些不常发生的异常情况处理代码不会遮掩正常的执行路径。也就是说,我们应该把正常情况的处理放在if后面,而不要放在else后面。这样,不仅符合我们平时的逻辑思维习惯,同时这对代码的可读性和性能也很重要。例如,下面的代码是对学生的成绩及格与不及格进行判断:
if(grade>=60)
{
    /*处理成绩及格的学生*/
}
else if(grade>=30&&grade<60)
{
    /*处理成绩大于等于30,并且小于60的学生*/
}
else
{
    /*处理成绩30以下的学生*/
}
这样的代码,不仅看起来很符合我们平时的逻辑思维习惯,而且if语句在做判断时,正常情况一般比异常情况发生的概率更大(否则就应该把异常和正常调过来了),即及格的学生多于不及格的学生。如果把执行概率更大的代码放到后面,也就意味着if语句将进行多次无谓的比较,如下面的代码所示:
if(grade<30)
{
    /*处理成绩30以下的学生*/
}
else if(grade>=30&&grade<60)
{
    /*处理成绩大于等于30,并且小于60的学生*/
}
else
{
    /*处理成绩及格的学生*/
}
因为及格的学生总是多于不及格的学生,所以在上面的代码中,if语句将进行多次无谓的比较,同时也难以理解。
2.避免“悬挂”的else。
对于“悬挂”的else这个问题,或许大家都已经了解甚至熟知了。尽管如此,但在平时编码中还是会有许多菜鸟和老手在此为程序埋下Bug的种子,如下面的示例代码所示:
#include <stdio.h>
int main (void)
{
    int a=30;
    int b=31;
    int c=7;
    int x=0;
    if(a>b)
            if(b<c)
                    x=1;
            else
                    x=-1;
    printf("%d\n",x);
    return 0;
}
上面的代码虽然看起来比较简洁,但却给程序带来致命性错误,同时这种写作方式也是许多老手所崇拜的(if语句后省略掉花括号{})。
在上面的这段代码中,我们的本意应该有两种主要情况:a>b与a<=b。如果满足条件a>b,我们将继续判断条件b<c,如果满足条件b<c,则将1赋给变量x;如果满足条件a<=b,则直接将-1赋给变量x。
然而实际情况并非如此,这段代码所做的与我们的意图相去甚远。其根本原因就在于C语言中有这样一条规则:else始终与同一对括号内最近的未匹配的if相结合。根据这条规则,代码中的else应该与if(b<c)进行配对,而并非与if(a>b)进行配对,所以变量x的原值没有变,依然还是0。
为了使大家能够更加清楚地看到“悬挂”的else所产生的原因以及带来的问题,我们可以通过给if/else语句加上“{}”,写成如下等价形式:
#include <stdio.h>
int main (void)
{
    int a=30;
    int b=31;
    int c=7;
    int x=0;
    if(a>b)
    {
            if(b<c)
            {
                    x=1;
            }
            else
            {
                    x=-1;
            }
    }
    printf("%d\n",x);
    return 0;
}
现在,大家就能够很清楚地看到这个else到底与谁匹配了。由此可以看出,不论是菜鸟还是老手,在什么时候都不能够偷懒,还是老老实实地把“{}”加上,这样就不会让我们迷糊了。正确的写法如下面的代码所示:
#include <stdio.h>
int main (void)
{
    int a=30;
    int b=31;
    int c=7;
    int x=0;
    if(a>b)
    {
            if(b<c)
            {
                    x=1;
            }
    }
    else
    {
            x=-1;
    }
    printf("%d\n",x);
    return 0;
}
现在,代码中的else可以正确地与第一个if(a>b)相结合了,即使它离第二个if(b<c)更近也是如此。因此,变量x的结果正是我们所期望的-1。
除此之外,有些C程序员也通过使用宏定义的方式来能达到类似的效果,如下面的示例代码所示:
#include <stdio.h>
#define IF {if(
#define THEN ){
#define ELSE else {
#define END }
int main (void)
{
    int a=30;
    int b=31;
    int c=7;
    int x=0;
    IF a>b
             THEN IF b<c
                   THEN x=1;
                        END
             END
     END
    ELSE
            x=-1;
   END
    END
    printf("%d\n",x);
    return 0;
}
虽然这样也可以避免“悬挂”的else这个问题,但是这样的代码却很难读懂,尤其会给后来的代码维护人员带来麻烦,所以我们不建议大家这样做。
3.避免在if/else语句后面添加分号“;”。
在C语言中,只有分号“;”组成的语句称为空语句。空语句是什么也不执行的语句,常常被用来作为空循环体。如果你不小心在if/else语句后面添加了分号“;”,那么程序将很容易违背你的意愿,导致意外的运算结果,如下面的示例代码所示:
int main (void)
{
    int x=1;
    if(x<0);
    x++;
    printf("%d\n",x);
    return 0;
}
在上面的代码中,语句x++并不是在“if(x<0)”为真的时候才被调用,而是任何时候都会被调用,所以最后变量x的值为2。这究竟是怎么回事呢?
其实,问题就出在if语句后面的分号“;”上。我们知道,在C语言中,分号预示着一条语句的结尾。但值得注意的是,并不是每条C语言语句都需要分号作为结束标志。比如,if语句的后面就并不需要分号,但如果你不小心添加了分号,编译器并不会提示出错。因为编译器会把这个分号解析成一条空语句,即上面的代码等价于下面的代码:
int main (void)
{
    int x=1;
  if(x<0)
    {
            ;
    }
    x++;
    printf("%d\n",x);
    return 0;
}
其实,这种手误性错误是我们很容易犯的,往往一不小心多写了一个分号,就会导致结果与预想的相差很远。因此,建议大家使用NULL来替代空语句,这样做可以明显地区分真正必需的空语句和不小心多写的分号造成的误解,如下面的示例代码所示:
int main (void)
{
    int x=1;
    if(x<0)
            NULL;
    x++;
    printf("%d\n",x);
    return 0;
}
4.对深层嵌套的if语句进行重构。
在代码中,如果某个函数的if语句嵌套太深,为了程序的可读性、可维护性与效率,我们应该尽量想办法进行重构,以减少if语句的嵌套层数,如下面的示例代码所示:
int main (void)
{
    int x=0;
    int a=1;
    int b=3;
    int c=5;
    int d=7;
    if(a<b)
    {
            x+=b;
            if(b<c)
            {
                    x+=c;
                    if(c<d)
                    {
                            x++;
                            if(a>d)
                            {
                                    x=d;
                            }
                    }
            }
    }
    printf("%d",x);
    return 0;
}
在上面这个简单的示例代码中,我们使用了4层if嵌套。为了减少程序中if语句的嵌套层数,我们首先能够想到的办法就是可以通过重复检测条件中的某一部分来简化嵌套的if语句,如下面的示例代码所示:
int main (void)
{
    int x=0;
    int a=1;
    int b=3;
    int c=5;
    int d=7;
    if(a<b)
    {
            x+=b;
            if(b<c)
            {
                    x+=c;
            }
    }
    if(a<b&&b<c&&c<d)
    {
            x++;
            if(a>d)
            {
                    x=d;
            }
    }
    printf("%d",x);
    return 0;
}
通过上面的代码,我们可以清楚地看到通过重复检测条件中的某一部分来简化嵌套的if语句的办法并不能无偿地减少嵌套层数。同时,作为减少嵌套层数的代价,必须容忍使用一个更复杂的判断。也就是说,虽然这样减少了if语句的嵌套层数,但增加了一些复杂的判断,让我们有点得不偿失的感觉。
既然我们对通过重复检测条件中的某一部分来简化嵌套的if语句的办法不是很满意,那么我们还可以使用if/break块来简化if语句的嵌套层数。上面的示例代码采用if/break块重构后如下所示:
int main (void)
{
    int x=0;
    int a=1;
    int b=3;
    int c=5;
    int d=7;
    do{
            if(a>=b)
                    break;
            x+=b;
            if(b>=c)
                    break;
            x+=c;
            if(c>=d)
                    break;
            x++;
            if(a<=d)
                    break;
            x=d;
    } while(false);
    printf("%d",x);
    return 0;
}
从上面的代码可以看出,相对于通过重复检测条件中的某一部分来简化嵌套的if语句的办法,if/break块真正地实现了逻辑的扁平化,减少了if语句的嵌套层数,从而使代码看起来比较清晰,增加了程序的可读性。与此同时,该方法还不会增加复杂的条件判断,从而可以避免因为重构而导致的if条件出错。当然,你还可以使用它的另外两个相似的if/return和if/goto块来达到同样的效果,但这种方法唯一的缺陷就是破坏了程序的内聚性。
除了上面的两种方法之外,我们还可以通过把代码分割开来,把深层嵌套的if语句抽取出来放进单独的函数中。这样,不仅减少了if语句的嵌套层数,同时,一个好的函数名对代码也具有自我注解的作用,在一定程度上也可以提高程序的可读性。但这种方法与前面的方法一样,程序逻辑的复杂度依然存在,甚至更复杂。
因此,如有可能,我们应该选择完全重新设计深层嵌套的代码。在通常情况下,如果程序中存在着比较复杂的逻辑代码,就说明你还没有充分地理解程序,从而无法简化它。所以对程序员来说,深层嵌套的if语句也是一个警告,它说明你要么想办法进行重构,要么重新设计该程序。