再谈C语言中的语句

作者: zenny_chen | 来源:发表于2020-09-01 23:16 被阅读0次

    在《C语言编程魔法书》中的14章第6节已经详细讨论了C语言的六种语句:标签语句(labeled statement)、复合语句(compound statement)、表达式语句(expression statement)、选择语句(selection statement)、迭代语句(iteration statement)以及跳转语句(jump statement)。这里,我将对其中的表达式语句中的一种特殊形式的语句再做讨论——即空语句null statement)。空语句在《C语言编程魔法书》中没怎么提到,这里正好做个补充。

    空语句属于一种特殊的表达式语句,其表达式完全为空,即就一个分号( ; )所构成的一条表达式语句。这里我们需要注意,别把void表达式语句、空语句、空复合语句搞混了,以下先展示这三种不同的语句概念:

    (void)0;    // 这是一条void表达式语句
    
    ;           // 这是一条空语句
    
    { }         // 这是一条空的复合语句
    

    空语句可以出现在所有语句所能出现的地方,一般来说它没有特殊语义。但是这里各位需要注意的是,C语言中分号可作为一条表达式语句、声明和定义的结束符,也能表示一条空语句,这是分号的两种不同的语义。此外,C语言标准中其实把声明/定义同语句是分开的,因此声明和定义不属于语句。如果我们把“声明”写作为“声明语句”就显得不够专业了~😄 另外,复合语句的 } 可作为结束符;而在声明和定义中,只有定义函数时,} 才能作为结束符,而其他场合则不行。
    下面我们举个例子:

    int a = 0;    // 这里的分号表示声明的结束
    a = 1; ;      // 这里有两个分号,左边的表示表达式语句的结束,右边的则表示一条空语句。因此这里有两条语句。
    

    C语言中除了表达式语句和跳转语句,其他类型的语句都需要跟一条其他语句来构成一条完整语句的。比如说:

    int a = 0;
    
    LABEL:    a = 1;        // 这是一条标签语句,在标签LABEL后面跟了一条表达式语句
    
    if(a > 0) a = -a;       // 这是一条选择语句,其后面伴随的是一条表达式语句作为一条完整的语句
    
    for(;;);                // 这是一条迭代语句,其后面伴随的是一条空语句构成一条完整的语句
    
    while(a > 0) { --a; }   // 这是一条迭代语句,其后面伴随一条复合语句构成一条完整的语句
    
    if(a < 0) a++;
    else { --a; }            // 这里从if开始到最后 } 结束是一条完整的选择语句
    

    以上这些概念请务必牢记,后面提到的容易触碰的“陷阱”其实就与语句的性质与条数有关。

    我们上面已经提到了,标签语句、选择语句和迭代语句需要伴随一条这6种语句中的任何一条作为完整语句,而且无论是哪种情况均可跟任何一种语句。为了方便起见,我们可以把标签语句的LABEL:,选择语句的if(condition)switch(selection),迭代语句的while(condition)for(sub-clauses)看作为“头”(header),然后后面再跟一条6种语句中的任何一种。因此,我们现在可以放开脑洞,看看以下代码,这些代码都是合法有效的,完全能通过编译的:

    static void ctest(void)
    {
        int a = 10;
        
        // 这里的选择语句头后面跟着一条标签语句
        if(a > 0)
    LABEL:
            ++a;
        
        // 这里的选择语句头后面跟着一条跳转语句
        if(a < 0)
            goto LABEL;
        
        // 这里的标签语句头后面跟着一条选择语句
    LABEL2:
        if(a == 0);
        
        // 这里的选择语句头后面跟着一条空语句
        switch(a);
        
        // 这里的选择语句头后面跟着一条标签语句
        switch(a)
    LABEL3:
            ++a;    // 这里的 ++a; 将永远执行不到,除非显式使用goto跳转语句跳到LABEL3
        
        // 这里的选择语句头后面跟着一条标签语句
        switch (a)
    LABEL4:
        {
        case 11:
            puts("case 11 reached!!");
            break;
            
        case 12:
            puts("case 12 reached!!");
            break;
            
        default:
            puts("default case reached!!");
            break;
        }
        
        if(a == 11)
        {
            ++a;
            goto LABEL4;
        }
        
        // 这里选择语句头后面跟着一条标签语句:case 12: --a;
        switch(a)
            case 12:
            --a;    // 这里的 --a; 将会被执行到。
        // 这里后面不允许添加 break; 跳转语句,因为到 --a; 为止已经是一条完整的选择语句了。
    }
    

    当你执行ctest函数之后,控制台将会打印出两行“ case 11 reached!! ”。看到这里,各位不要被C语言的灵活性吓到😅~正因如此,C语言在语法体系的设计方面是相当完备的。这里要详细讨论的是“LABEL4”这块。“LABEL4”所引出的标签语句与选择语句头switch(a)一起组成了一条完整的选择语句。这里与上面“LABEL3”处的选择语句不同,由于LABEL4标签语句是由一条带有case标签组的复合语句构成,因此这里完全可以跟switch配对上。因而,当执行到“LABEL4”上面的switch(a)之后,下面的case语句将会被选择执行。

    此外,由于“LABEL4”所引出的是一条标签语句,因此这里变量a的上下文将会被保持住,后续对a的修改不影响这里的case判断。也就是说,在LABEL4处的a的值已经被hold住不变了,我们可以认为这里的a被赋值给了一个隐藏的临时常量,而后面的case标签语句将会根据此隐藏的临时常量的值进行选择执行。因此,这个函数被执行后将会两次都输出“ case 11 reached!! ”。

    了解了上述语句的特性之后,我们以后在写选择语句、迭代语句的时候就要务必小心了,不要很悠哉地乱加分号,以免引起很难发现的bug。对于一般的表达式语句,后面跟多少分号都没问题,比如:

    int a = 0;
    a++;;;    // 这里其实有三条语句😄
    

    但以下代码将是一个bug:

    int a = 0;
    for(int i = 0; i < 10; i++);    // 这里来了个分号……😅
        ++a;
    

    大家猜猜,上述代码执行后,a的值是几?😏

    有了上述对C语言选择语句和迭代语句潜在的陷阱之后,我们下面就要聊聊一些更高级的主题了——当一个宏定义遇上选择语句或迭代语句之后,会发生神马化学反应~

    我们有时为了封装一些代码块,会使用宏函数。不过在使用宏函数的时候也很有可能会引发一些意向不到的问题,尤其是该宏函数与选择语句、循环语句连用的场合,比如以下代码:

    #define MY_MACRO(n)    if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); }
    

    这句代码初步看起来没啥问题吧?然而,我们再看看以下代码会发生神马:

        int n = 100;
        
        if(n >= 10)
            MY_MACRO(10);
        else    // 发生编译错误:Expected expression
        {
            puts("n below ")
        }
    

    我们看到,这个看似朴素的宏函数“MY_MACRO”上面加了if语句之后,这里就出问题了!问题原因在哪儿?上面已经详细讨论过了。由于MY_MACRO在预处理时进行宏展开后,实际上是一条完整的选择语句,然而这里MY_MACRO(10)后面又跟了一个分号,导致了该分号作为一条空语句的形式出现,从而与else无法配上。这里,MY_MACROz(10)宏展开后如以下代码所示:

        int n = 100;
        
        if(n >= 10)
            if((n) > 0)
                { printf("n = %d\n", n); puts("PASS!"); } ;
        else    // 发生编译错误:Expected expression
        {
            puts("n below 10!");
        }
    

    所以,我们如果写成以下形式则可避免宏展开后发生编译错误:

        int n = 100;
        
        if(n >= 10)
        {
            MY_MACRO(10);
        }
        else    // 发生编译错误:Expected expression
        {
            puts("n below 10!");
        }
    

    当然,如果我们强迫第三方开发者一定要在选择、迭代语句后要用复合语句,那么似乎也属于要求比较苛刻了,那么我们自己定义的宏函数用哪些技巧可以使得调用起来更加灵活,不加如此约束呢?我们在市面上看到比较常用的是使用do-while迭代语句。do-while有何好处呢?while()在最后并不是以 } 符号结尾的,因此后面所跟的分号将作为整条迭代语句的结束符使用,而不会变成空语句。比如:

    #define MY_MACRO(n)    do { \
            if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); }    \
        } while(0)    // 注意,这里可不能加分号
    
    
    int main(int argc, const char * argv[])
    {
        int x = 100;
        
        if(x >= 10)
            MY_MACRO(10);
        else
        {
            puts("x below 10!");
        }
    }
    

    这么一来,我们可以看到上述代码编译、运行都不会有任何问题。当然,除了do-while之外还可以用其他技巧,总之,我们的目标就是要把宏函数调用后面的分号作为语句的结束符,而不是空语句。比如,我这里“别出心裁”地使用if-else选择语句,当然不能说这种解决方案完全完美,但还是能解决问题😊

    #define MY_MACRO(n)     if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); } \
                            else
    
    
    int main(int argc, const char * argv[])
    {
        int x = 100;
        
        if(x >= 10)
            MY_MACRO(10);
        else
        {
            puts("x below 10!");
        }
    }
    

    这段代码编译运行也不会有任何问题。而且宏定义部分也比之前的do-while版本更简洁清爽。

    我这里也属于抛砖引玉,大家可以随之打开脑洞,设计出更有创造力的表达方式。

    相关文章

      网友评论

        本文标题:再谈C语言中的语句

        本文链接:https://www.haomeiwen.com/subject/whnbsktx.html