【问题标题】:Non-traditional uses of switch statementswitch 语句的非传统用法
【发布时间】:2016-01-22 14:52:08
【问题描述】:

最近我发现 switch 的主体可以是任何语句(C99 6.8.4)。这个想法首先是由这个向我提出的: https://stackoverflow.com/a/9220598/515212

所以可以有类似的 switch 语句

void f(int n)
{
    switch (n)
    case 0:
        printf("zero\n");
}

甚至可以输入ifs、whiles等

void f(int n)
{
    switch (n) 
    if (1)
    {
        case 0:
            printf("zero\n");
    }
    else
        while (--n)
        {
            default:
                printf("non-zero\n");
        }
}

出于兴趣,我想知道这个语法是否有一些用途,或者只是标准中如何定义 switch 语句的产物?

【问题讨论】:

  • @iharob 可以跳过大括号这一事实正是问题的重点。
  • 我的意思是 ifelse while 看起来很让我不安。
  • @iharob 我也是这么想的。但显然这是标准允许的。
  • @MartinJames 我知道这是个坏主意。我不建议使用。如果您阅读了我的问题,您会看到我在问为什么允许这样做以及是否可以使用。
  • @MartinJames “你认为我没有机会?”我不明白您为什么还要提到代码样式并解释这是不好的(我显然知道,因为我问为什么会允许这样做)。我的问题的重点是是否有某种原因允许这样做。

标签: c switch-statement


【解决方案1】:

您可以将switch 语句视为带有标签的代码块(case(s) 确实是标签),其中通过 goto 语句传递控件。

类似

void f(int n)
{
    if ( n == 0 ) goto Label_case_0;
    else goto Label_default;

    {
        if ( 1 )
        {
            Label_case_0:
            printf("zero\n");
        }
        else 
            while (--n)
            {
                Label_default:
                printf("non-zero\n");
            }
    }
}

在我看来,将 case 标签放在其他一些控制结构中并不是一个好主意,因为这会使代码难以阅读并可能导致错误。

【讨论】:

  • case(s) are indeed labels 它们的工作方式类似于标签,但它们并不是真正的标签。例如,不能在 switch 中使用 goto default;
【解决方案2】:

这是有效的 C 代码。 它起源于每个条件语句都使用 if、goto 和 label 跳转的程序集。

使用此功能实现数组复制称为Duff's Device

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    switch(count % 8) {
        case 0: do { *to++ = *from++;
        case 7:      *to++ = *from++;
        case 6:      *to++ = *from++;
        case 5:      *to++ = *from++;
        case 4:      *to++ = *from++;
        case 3:      *to++ = *from++;
        case 2:      *to++ = *from++;
        case 1:      *to++ = *from++;
                   } while(--n > 0);
    }
}

当您将while 替换为ifgoto

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    switch(count % 8) {
        case 0: 
        loop:        *to++ = *from++;
        case 7:      *to++ = *from++;
        case 6:      *to++ = *from++;
        case 5:      *to++ = *from++;
        case 4:      *to++ = *from++;
        case 3:      *to++ = *from++;
        case 2:      *to++ = *from++;
        case 1:      *to++ = *from++;
        if(--n > 0) goto loop;
    }
}

然后将switch替换为ifgoto

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    if(count%8==7)goto case7;
    if(count%8==6)goto case6;
    if(count%8==5)goto case5;
    if(count%8==4)goto case4;
    if(count%8==3)goto case3;
    if(count%8==2)goto case2;
    if(count%8==1)goto case1;
    if(count%8==0)goto case0; // this can be omitted

    case0:                    // this can be omitted
    loop:        *to++ = *from++;
    case7:       *to++ = *from++;
    case6:       *to++ = *from++;
    case5:       *to++ = *from++;
    case4:       *to++ = *from++;
    case3:       *to++ = *from++;
    case2:       *to++ = *from++;
    case1:       *to++ = *from++;

    if(--n > 0) goto loop;
}

在功能上(几乎)等同于

void copy(char *from, char *to, int count)
{
    while(--n > 0) { 
        *to++ = *from++;
    }
}

这几乎是等效的,因为在最后一个实现中,循环检查的执行频率要高出 8 次,这会对性能产生影响。

【讨论】:

    【解决方案3】:

    这里有一些 switch()case 的不寻常用法,我今晚正在玩这些用法,只是想看看当我遇到这个问题时它会延伸多远。其中大部分来自各种 SO 帖子和答案。

    最著名的不寻常的例子是 Duff 的设备,该问题的其他答案已经涵盖了它。

    switch()的基础知识

    switch() 语句的基本形式为:

    switch (<expression>) <statement>;
    

    在哪里

    • &lt;expression&gt; 是一个计算结果为整数值的表达式
    • &lt;statement&gt; 是带有标签的语句或复合语句

    &lt;statement&gt; 要么是一个以分号结尾的语句,要么是用花括号括起来的一系列语句,即复合语句。如果常量值等于在switch() 中指定的&lt;expression&gt; 的值,则可以使用指定常量值的可选case 标签作为跳转目标。

    case 标签与goto 标签相似,语法相似,switch() 语句可以被认为是一个计算的goto 语句,其中&lt;expression&gt; 的值决定了哪个标签在switch() 的执行范围会跳转。

    case 标签必须指定一个整数常量值或一个表达式,编译器可以从中创建一个整数常量值。在case 标签中指定的每个值在switch() 的范围内必须是唯一的,或者default 的特殊标签可用于指定除指定case 标签值之外的任何值。 case 标签的顺序无关紧要。

    不需要指定casedefault 标签,但是switch(i) return 0; 不会执行return,因为没有标签可以跳转。另一方面,switch(i) default: return 0; 将始终返回,无论变量 i 的值是什么。

    所以以下是很好的switch() 语句:

    switch (i) case 1: case 2: return i;
    
    switch (i) {
    case 3:
       i++;
        break;
    
    case 1:
        // FALL THROUGH
    case 2:
        return i;
    
    default:
        i -= ((i < 10) ? 2 : 5);
        break;
    }
    
    // an enum is an integral type so its possible to use them with switch()
    typedef enum { TYPE_1 = 0, TYPE_2, TYPE_3 } MyTypes;
    
    MyTypes jjkk = TYPE_1;
    switch (jjkk) {
    case TYPE_1: doType1(22); break;
    case TYPE_2: doType2(33); break;
    }
    
    // you can use #define constants so long as they evaluate to an integral valued constant
    // the Preprocessor will do the text substitution for you.
    #define JKJK1   1
    #define JKJK2   2
    
    switch (i) case JKJK1: case JKJK2: printf("JKJK1 or JKJK2\n");
    
    
    // following use of logical express should transform logical true/false result
    // into an integral value of 1 (true) or 0 (false). Expect a warning about
    // this usage though.
    switch (i < 4) case 1: i *= 2;   // logical true evaluated as integral value of 1
    

    switch() 使用示例

    第一个示例使用do {} while(0),因此多个案例使用相同的printf() 语句,但案例准备了要在输出中使用的案例特定数据。

    假设我们在销售点有一个包含外币投标的数据结构,例如客户在巴黎的一家机场商店,欧元是标准货币,但商店也接受美元,销售点将美元转换为欧元。在收据打印中,如果客户使用欧元,我们只想打印欧元,如果客户使用美元,那么我们想要打印美元金额、兑换率和欧元金额。

    我们有一个包含招标数据的结构,看起来像:

    typedef struct {
        unsigned char   uchMajorCode;
        unsigned char   uchMinorCode;       // indicates which tender
        long    lTenderAmt;         // amount of tender in Euros
        long    lRate;              // conversion rate
        long    lForeignCurrency;   // amount of the foreign currency
        // .... other data
    } ItemTender;
    

    在打印逻辑中,我们必须确定如何格式化此投标的收据行。是的,我知道还有其他更易读的方法来做到这一点。

    switch (pItem->uchMinorCode) {
        char *mnemonic;
    case FOREIGN_1: do {
            mnemonic = getMnemonic(TRN_FOREIGN_1);
            break;  // break from do {} while()
    case FOREIGN_2:
            mnemonic = getMnemonic(TRN_FOREIGN_2);
            break;  // break from do {} while()
    case FOREIGN_3:
            mnemonic = getMnemonic(TRN_FOREIGN_3);
            break;  // break from do {} while()
        } while(0);
        printf ("%s\t%ld  %ld @ %ld\n", mnemonic, pItem->lTenderAmt, pItem->lForeignCurrency, pItem->lRate);
        break;    // break from switch()
    case LOCAL:
        // FALL THROUGH
    default:
        printf ("%s\t%ld\n", getMnemonic(TRN_LOCAL), pItem->lTenderAmt);
        break;    // break from switch()
    }
    

    注意:请注意,使用switch() 跳转到循环将绕过任何带有一些循环结构的循环初始化表达式,例如for()。跳转到循环体时也会绕过循环条件检查表达式。如果case 标签位于if 的正文中,则类似的注释适用于if 语句。

    switch (i) {
        default: if (iVal > 3) {  // when i is not equal to 5 then iVal is checked
            case 5: iVal++;       // when i equals 5 then iVal is not checked
        }
    }
    

    或使用for() 循环,如果case 标签位于循环内,则绕过初始化。

    switch (i) {
        default: for (iVal= 0; iVal< 4; iVal++) {  // i not equal to 5 so iVal is initialized
        case 5:      doSomething (iVal);   // i equals 5 so iVal is not initialized
                 }
            break;
    }
    

    下一个不寻常的用法是测试一系列值并为这些值执行一组步骤。这使用了上面相同的数据结构。如果我们使用花括号,我们也可以有不止一行。如果没有大括号,第一个终止分号也会终止 switch()

    // in the case of a foreign tender perform the currency conversion
    switch (pItem->uchMinorCode)
        case FOREIGN_1: case FOREIGN_2: case FOREIGN_3:
          pItem->lTenderAmt = pItem->lForeignCurrency * pItem->lRate;
    

    第三种不寻常的用法是在while() 循环中生成一组值,并根据值执行某些操作。这个例子在某种程度上是为了展示语法的可能性。我还没有在野外看到过这样的事情,但是在调用函数并根据错误代码重试之后进行错误检查是可能的。

    while ((iVal = f(iVal))) {  // get next value in a sequence and do something with it.
        switch (iVal) {
            int j;        // define temporary variable used below.
        case 1:
            continue;     // continue the while() to get next value
        case 2:
            break;        // break from switch() to do printf() below
        case 0: do {
                j = 17;
                break;    // break from do()
        default:
                j = 12;
                break;    // break from do()
            } while (0);  // bottom of the do(). print follows
            printf("   do while - iVal = %d, j = %d\n", iVal, j);
            break;   // break from switch() to do printf() below
        }
        printf(" while - iVal = %d\n", iVal);
        break;       // break from while() loop
    }
    

    作为第四个示例,我们使用switch() 语句来测试各种位。由于 case 必须是在编译时计算的整数值,如果编译器(大多数现代编译器都会这样做)将在编译时执行计算,我们可以使用带有位运算符的常量。

    #define VAL_1  0x0001
    #define VAL_2  0x0002
    #define VAL_3  0x0004
    #define VAL_4  0x0008
    
    switch (iBitMask & 0x000f) {
    case VAL_1:            // only VAL_1 is set
        printf("  only VAL_1 found\n");
        break;
    case VAL_1 | VAL_2:    // both and only both VAL_1 and VAL_2 are set
        printf("  both VAL_1 and VAL_2 found\n");
        break;
    case VAL_1 | VAL_3:    // both and only both VAL_1 and VAL_3 are set
        printf("  both VAL_1 and VAL_3 found\n");
        break;
    case 0x000f & ~VAL_1:
        printf("  everything except for VAL_1 found\n");
        break;
    }
    

    参考文献

    How does switch statement work?

    Does the C standard explicitly indicate truth value as 0 or 1?

    Using continue in a switch statement

    【讨论】:

      【解决方案4】:

      您可以查看 here 的 switch 语句的异常用法示例。但是不要在真实的代码中这样做。 链接示例:

      int duffs_device(char *from, char *to, int count)
      {
          {
              int n = (count + 7) / 8;
      
              switch(count % 8) {
                  case 0: do { *to++ = *from++;
                              case 7: *to++ = *from++;
                              case 6: *to++ = *from++;
                              case 5: *to++ = *from++;
                              case 4: *to++ = *from++;
                              case 3: *to++ = *from++;
                              case 2: *to++ = *from++;
                              case 1: *to++ = *from++;
                          } while(--n > 0);
              }
          }
      
          return count;
      }
      

      它叫做Duff's device。此函数复制 char 数组。它使用称为“循环展开”的技巧。

      长循环可能会很慢,因为每次迭代都需要做额外的工作,比如比较和变量递增。所以加速它们的一种方法是复制重复的代码。就像它在示例中所做的那样。

      但现代编译器可以更好地做到这一点,不推荐使用这样的代码,因为它只会让阅读它的人感到困惑。

      【讨论】:

        猜你喜欢
        • 2011-12-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多