switch 语句的非传统用法

Non-traditional uses of switch statement

最近发现switch的主体可以是任意语句(C99 6.8.4)。这个想法首先是通过以下方式向我提出的:

所以可以有像

这样的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 语句的产物?

您可以将 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 标签放在其他一些控制结构中并不是一个好主意,因为这会使代码难以阅读并可能导致错误。

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

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 数组。它使用称为 "loop unrolling".

的技巧

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

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

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

使用此功能实现数组复制称为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 倍,这对性能有影响。

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

最著名的不寻常示例是 Duff 的设备,该问题的其他答案中已经介绍了它。

switch()

的基础知识

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

switch (<expression>) <statement>;

哪里

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

<statement> 要么是以分号结尾的单个语句,要么是用花括号括起来的一系列语句,即复合语句。如果常量值等于 switch().

中指定的 <expression> 的值,则指定常量值的可选 case 标签可用作跳转目标。

case 标签类似于具有相似语法的 goto 标签,switch() 语句可以被认为是一个计算的 goto 语句,其中的值of <expression> 决定跳转到 switch() 执行范围内的哪个标签。

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

不需要指定 casedefault 标签,但是 switch(i) return 0; 不会执行 return 因为没有要跳转到的标签。另一方面,无论变量 i.

的值是什么,switch(i) default: return 0; 总是 return

所以下面是很好的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?

Using continue in a switch statement