如何使用Switch语句和语句表达式被解雇(2016)

2020-10-16 00:36:11

更新2016年10月27日:根据建议修复了协程示例中的示例代码注释,编辑了文本以注意D';的类似行为。

不管你是试图获得工作保障,通过向别人展示你有多聪明来给他们留下深刻印象,还是被动地积极地断言你对代码库的支配地位,编写不可维护的代码都有很多实际应用。对于C编程来说,一种极难维护且充满错误的技术涉及同时使用Switch语句和语句表达式。

在本文中,我们将讨论如何利用Switch语句和语句表达式来生成非常难以理解的C代码,因此您需要查看程序集来弄清楚它的作用。本文中的许多语法示例不符合标准,或者即使是最简单的静态分析测试也无法通过。不过,这应该没问题,因为用你公司的代码库编写许多这样的例子,最终可能会让你被解雇。

Int i=...;开关(I){案例0:{...中断;}案例1:{...中断;}案例2:{...中断;}默认值:{...}}。

这是大多数人习惯于在C中提到';switch语句时会想到的问题。粗略的想法是,在检查某些不相交的属性时,switch语句是一种比使用大量';Else IF';语句更吸引人的选择。你们中的一些人可能会惊讶地发现,下面的语句也是有效的SWITCH语句:

Int i=...;switch(I){i++;默认值:{}i++;案例0:{案例3:i;}if(i<;10){案例1:{Break;}for(i=0;i<;10;i++){案例2:;}。

值得一提的是,几乎没有其他语言像在C中那样支持switch语句(尽管D语言就是一个例子)。但大多数其他语言都有一个SWITCH语句,其工作原理类似于许多If;Else检查的更吸引人的替代方案。

C中的switch语句应该更恰当地称为goto字段。*这意味着交换机(...)。Part只是决定分支到哪个标签。在分支到该标签之后,没有什么特别的事情发生,这与这样一个事实有关:您在switch语句中,代码将继续执行接下来出现的任何机器指令。当然,有一个例外是Break语句,它将跳到switch语句体之后。以下是上面仅使用if和goto编写的switch语句的等效版本。

Int i=...;If(i==0)转到Label_0;If(i==1)转到Label_1;If(i==2)转到Label_2;If(i==3)转到Label_3;/*否则,转到Default Label*/转到Label_Default;{i++;Label_Default:{}i++;Label_0:{Label_3:i;}If(i<;10){Label_1:{Goto Break_From_Switch;}for(i=0;i<;10;i++){LABEL_2:;}Break_From_Switch:

如果你已经熟悉了著名的达夫的设备,以上这些对你来说可能不是什么新鲜事:

/*DUFF';的DEVICE*/INT TOTAL_BYTES=...;INTAL_BYTES=...;INT n=(TOTAL_BYTES+3)/4;SWITCH(TOTAL_BYTES%4){案例0:do{*to=*from++;case 3:*to=*from++;case 2:*to=*from++;;case 1:*to=*from++;}while(--n>;0);}。

借鉴Duff的设备作为灵感,您可以使用C语言中Switch语句的独特行为来实现协程:

#include<;stdio.h>;#default coroutine_egin()static int state=0;switch(State){case 0:#定义coroutine_return(X){state=__line__;return x;case__line__:;}}定义coroutine_Finish()}int get_next(Void){static int i=0;coroutine_egin();而(1){coroutine_return(++i);coroutine_return(100);}coroutine_Finish();}int main(Void){printf(";i is%d\n";,get_next());/*printf';i is 1';*/printf(";i is%d\n";,get_next());/*print';i is 100';*/printf(";i is%d\n";,get_next();/*prints';i is 2';*/printf(";i is%d\n";,get_next());/*prints';i is 100';*/return 0;}。

本节中的示例使用Simon Tatham在C中的Coroutines中找到的一个示例。最初的来源描述了一些基于交换机的协程的注意事项,我不会在本文中讨论这些内容。

如果我们解析宏,并更好地缩进代码,然后删除一些多余的方括号和分号,则';get_next';函数变为:

#include<;stdio.h>;/*假设__line_的值为1234,4567*/int get_next(Void){static int i=0;static int state=0;switch(State){case 0:;while(1){state=1234;return++i;case 1234:;state=4567;return 100;case 4567:;}

静态变量在这里很关键,因为它们允许我们在调用函数GET_NEXT之间传递信息。Switch语句有效地为我们提供了一种实现goto语句的便捷方法,这些语句需要跳转到协程的开头(state=0),或者跳到协程返回之后的点(state=1234,state=4567)。*如果要创建更多退货/恢复点,只需将更多调用添加到';COROUTINE_RETURN';宏即可。*您还必须确保这些呼叫出现在';协程_开始';和';协程_结束';之间。

第一个表示没有表达式的';语句,第二个表示没有声明或表达式的复合语句。*这两种形式都没有Break语句,所以执行总是会继续做Case标签之后的任何事情,就像GoTo的工作方式一样。

现在我们已经了解了switch语句是如何变得非常奇怪的,让我们来探索几个您可能从未见过的有效switch语句语法的有趣示例(这取决于您是一个1337黑客的程度):

/*不需要大括号,因为只有一条语句(从不执行)。*/switch(0)i++;

开关(0)案例0:for(i=0;i<;10;i++)案例1:for(j=0;j<;10;j++)案例2:for(k=0;k<;10;k++);

/*与上一个示例的想法相同,但花括号更多。*/switch(0){案例0:{for(i=0;i<;10;i++){案例1:{While(J){案例2:{for(k=0;k<;10;k++){}}j++;}案例3:{/*...*/}默认值:{/*...*/}}。

/*编译器期望';语句';出现在';case:';之后,但case语句本身是';标记为';的语句,它只是另一个常规ole语句。*/switch(I)默认值:案例0:案例1:案例2:案例3:;

/*当您有一些需要相同行为的开关案例子集,而其余案例具有自己独特的行为时,我发现此示例很有用:*/Switch(0){case Unique_Case_A:{Break;}Case Unique_Case_B:{Break;}Case SIMILAR_CASE 1:CASE SIMILAR_CASE2:CASE SIMILAR_CASE3:{Break;}}

语句表达式是C标准不支持的GNU扩展,但在GCC和Clang中默认支持。它们允许您在表达式中嵌入复合语句。*最后一个表达式返回的值是整个语句表达式返回的值:

/*正则ole表达式*/int i=0;/*幻想新语句表达式*/int j=({int k;k=i+1;i;});

您可能会问,为什么要做这样的事情?有许多不同的答案,其中许多都与便利性有关:一个用例涉及确保在可能导致表达式在函数宏体中多次出现的情况下,表达式语句副作用只评估一次。

#include<;stdio.h>;int get_ero(Void){return 0;}int main(Void){/*打印0-9*/for(int i=({get_zero();});i<;10;i++)printf(";%d\n";,i);return 0;}。

您几乎可以在语句表达式中执行常规复合语句中可以执行的任何操作:

#include<;stdio.h>;int main(Void){/*将i设置为从0到99*/int i=({int j=0;for(int i=0;i<;100;i++)j+=i;j;});printf(";Sum is%d\n";,i);return 0;}。

不幸的是,由于GCC不允许分支到语句表达式中的标签(这可能是最好的),因此这些示例中的大多数都只能用clang编译。

是的,这是用GCC编译的,然后啪!这段代码没有做任何有趣的事情,但它说明了一种编写难以阅读的C代码的方法。

现在,让我们尝试构建一个更有用的示例,它结合了开关表达式和语句表达式。下面是一个示例,您希望有条件地更改循环的边界。您可以使用变量或函数,但也可以使用带有嵌入案例标签的语句表达式!

#include<;stdio.h>;void print_Stuff(Int Type){int i=0;int r=0;switch(Type){for(i=0;i<;({if(0){case 1:r+=2;case 0:r+=3;}r;});i++){printf(";i is%d\n";i);}int main(Void){printf(";First Run\n";);Print_Stuff(0);printf(";第二次运行\n";);Print_Stuff(1);返回0;}。

第一个Runi是0i是1i是2,第二个Runi是0i是1i是2i是3i是4。

但是,如果您通过valgrind运行此程序,您会发现它正在执行未初始化的读取:

==16228==条件跳转或移动取决于未初始化值==16228=在0x4005AB:PRINT_STUSH(Main.c:7)==16228=By 0x400619:Main(Main.c:15)...==16228==条件跳转或移动取决于未初始化值==16228=在0x4005AB:PRINT_STUSH(Main.c:7)==16228=By 0x400637:Main(Main.c:17)。

这相当令人印象深刻,因为我们能够在所有变量都已初始化的程序中创建未初始化的读取,并且没有指针或数组魔术!让我们来看一个有相同问题的较小示例:

#include<;stdio.h>;int main(Void){int i=0;switch(I){for(i=0;i<;({case 0:;10;});i++){(Void)i;}}返回0;}。

Pusq%RBP.Ltmp0:.cfi_def_cfa_Offset 16.Ltmp1:.cfi_Offset%rbp,-16 movq%rsp,%rbp.Ltmp2:.cfi_def_cfa_register%rbp movl$0,-4(%rbp).loc 1 4 13 prologue_end#main.c:4:13.Ltmp3:movl$0,-8(%rbp).loc 1 5 2#main.c:5:2 movb$1,%al testb%al,%al jne.LBB0_2 JMP.LBB0_6.LBB0_1:#In Loop:Header=BB0_2 Depth=1.loc 1 6 14鉴别器1#main.c:6:14.Ltmp4:movl-8(%rbp),%eax movl%eax,-16(%rbp)#4字节溢出.LBB0_2:#=>;此内循环标头:Depth=1.loc 1 6 19 is_stmt 0鉴别器2#main.c:6:19 movl$10,-12(%rbp).loc 1 6 16鉴别器2#main.c:6:16 movl-16(%rbp),%eax#4字节重新加载cmpl-12(%rbp),%eax.loc 1 6 3鉴别器2#main.c:6:3 jge.LBB0_5#bb#3:#in Loop:Header=BB0_2 Depth=1#BB#4:#In Loop:Header=BB0_2 Depth=1.loc 1 6 37鉴别器3#main.c:6:37 movl-8(%rbp),%eax Addl$1,%eax movl%eax,-8(%RBP).loc 1 6 3鉴别器3#main.c:6:3 jmp.LBB0_1.Ltmp5:.LBB0_5:.loc 1 9 2 is_stmt 1#main.c:9:2 jmp.LBB0_6.Ltmp6:.LBB0_6:xorl%eax,%eax.loc 1 10 9#main.c:10:9 popq%RBP retq.Ltmp7:.Lfunc_end0::

这些指令复制';i&39;变量,并将其存储在-16(%rbp)处,以用于比较';i<;({case 0:;10;})';。在第一次迭代中,使用来自switch语句的JUMP跳过这些指令,尽管它们是在后面的循环迭代中执行的。编译器会这样做似乎是很合理的,毕竟你是在告诉它转移到for循环比较的中间。

下面是另一个几乎实际的示例,在该示例中,我们可以使用case标签跳到函数参数求值的中间:

#include<;stdio.h>;void f(Int Type){}int main(Void){int i=0;switch(I){f(i+({case 0:;1;})+i);}返回0;}。

...clang:错误:无法执行命令:分段故障(核心转储)clang:错误:clang前端命令因信号而失败(使用-v查看调用)clang版本3.8.0-2ubuntu4(Tags/Release_380/Final)目标:x86_64-pc-linux-gnuThread model:position xInstalledDir:/usr/binclang:注意:诊断消息:请向http://llvm.org/bugs/提交错误报告,并包括崩溃回溯、预处理源、。和关联的运行脚本。clang:注意:诊断消息:*请将以下文件附加到错误报告:预处理的源和关联的运行脚本位于:clang:注意:诊断消息:/tmp/main-df5301.cclang:注意:诊断消息:/tmp/main-df5301.sh...。

下面是另一个类似的案例,它将把我的clang版本送入(似乎是)无限循环:

Int main(Void){int i=0;switch(I){i=i+({case 0:;0;});}返回0;}。

当你在做它的时候,为什么不在位域宽度计算中偷偷添加一个Case语句标签。*此示例将在clang中编译,但它不会输出任何内容(大小写0似乎被完全忽略,如果添加大小写0将被默认大小写捕获):

#include<;stdio.h>;/*这在clang 3.8.0-2ubuntu4中编译,但不输出任何内容。*/int main(Void){int i=0;switch(I){int j=sizeof(struct{int i:({case 0:;1;});});printf(";Fin%d..。\n";,j);}返回0;}

#include<;stdio.h>;int main(Void){int i=1;switch(I){case({case 1:;0;}):printf(";here\n";);}返回0;}。

正如您在本文中看到的,您可以使用Switch语句来编写许多非常难以理解的有效C程序。您甚至可以通过在语句表达式中嵌入case标签来进一步实现这一点,这会产生真正难以理解的下一级代码,这些代码可能会导致各种微妙的问题。正如我们在上面看到的,这包括编译器崩溃、编译器挂起和细微损坏的可执行代码。*如果您将足够多的这些提交到您的代码库,肯定会让您被解雇!