3.2 标号、goto,以及switch的实现
goto语句及标号(label)是最古老的C语言特性,也是最早被人们抛弃的语言特性之一。像汇编语言中的jmp指令一样,goto语句可以跳转到同一函数体中任何标号位置:
void f()
{int i=0;
Loop: //A label
++i;
if(i<10)goto Loop; //Jump to the label
}
在原始而和谐的早期Fortran和Basic时代,我们没有if then else,没有for和while,甚至没有函数的概念,一切控制结构都靠goto(带条件的或无条件的)构件。软件工程师将这样的代码称作“意大利面条”代码。实践证明这样的代码极容易造成混乱。
自从证明了结构化的程序可以做意大利面条做到的任何事情,人们就开始不遗余力地推广结构化设计思想,将goto像猛兽一般囚禁在牢笼,标号也因此消失。
标号唯一散发余热的地方,是在switch中控制分支流程。
很多人不甚了解switch存在的意义,认为它只是大型嵌套if then else结构的缩略形式,并且比if语句多了很多“不合理”的限制。如果你了解到switch在编译器内部的实现机制,就不难理解强加在switch之上的诸多限制,比如case后只能跟一个编译期整型常量,比如用break结束每一个case。首先看一个switch实例:
switch (shape.getAngle())
{
case 3: cout<<”Triangle”;break;
case 4: cout<<”Square”;break;
case 0:case1: cout<<”Not a sharp!”;break;
default: cout<<”Polygon”;
}
任何程序员都可以写出与之对应的if结构:
int i= getAngle(shape);
if (i==3) cout<<”Triangle”;
else if(i==4) cout<<”Square”;
else if(i==0||i==1) cout<<”Not a sharp!”;
else cout<<”Polygon”;
看起来这两段代码在语义上是完全一样的,不是么?
不!或许代码的执行结果完全一样,但是就执行效率而言,switch版本的更快!
要了解为什么switch的更快,我们需要知道编译器是怎样生成switch的实现代码的:
首先,保留switch之后由{}括起来的语具体,仅将其中case、default和break替换为真正的标号:
switch (getAngle(shape))
{
_case_3: cout<<”Triangle”;goto _break;
_case_4: cout<<”Square”; goto _break;
_case_0:_case_1: cout<<”Not a sharp!”; goto _break;
_default: cout<<”Polygon”;
_break:
}
随后,对于所有出现在case之后的常量,列出一张只有goto的跳转表,其顺序按case后的常量排列:
goto _case_0;
goto _case_1;
goto _case_3;
goto _case_4;
然后,计算case之后的常量与跳转表地址之间的关系,如有需要,在跳转表中插入空缺的项目:
100105: goto _case_0;
100110: goto _case_1;
100115: goto _default; //因为没有case 2,所以插入此项以条转到default
100120: goto _case_3;
100125: goto _case_4;
假设一个goto语句占用5个字节,那么在本例中,goto的地址=case后的常量*5+100105
之后,生成跳转代码,在其余条件下跳转至default,在已知范围内按照公式跳转,全部的实现如下:
{
int i= getAngle(shape);
if (i<0||i>=5)goto _default;
i=i*5+100105; //按照得出的公式算出跳转地址
goto i; //伪代码,C中不允许跳转到整数,但是汇编允许
100105: goto _case_0;
100110: goto _case_1;
100115: goto _default;
100120: goto _case_3;
100125: goto _case_4;
_case_3: cout<<”Triangle”;goto _break;
_case_4: cout<<”Square”; goto _break;
_case_0:_case_1: cout<<”Not a sharp!”; goto _break;
_default: cout<<”Polygon”;
_break:
}
经过这样处理整个switch结构,使得无论switch后的变量为何值,都可以通过最多两次跳转到达目标代码。相比之下if版本的代码则采用线性的比较和跳转,在case语句很多的情况下效率极低。
由此,我们也可以知道,为什么case后跟的一定是编译期整型常数,因为编译器需要根据这个值制作跳转表。我们可以明白为什么case与case之间应该用break分隔,因为编译器不改变switch语句体的结构,case其本身只是一个具有语义的标号而已,要想跳出switch,就必须用break语句。
这篇文章是 "CPP_Tricks" 系列文章的第 12 篇:
- C++ Tricks
- C++ Tricks 1.1 条件运算符(?:)
- C++ Tricks 1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱
- C++ Tricks 2.1 X86概述
- C++ Tricks 2.2 I386平台的内存布局
- C++ Tricks 2.3 I386平台C函数内部的栈分配
- C++ Tricks 2.4 I386平台C函数调用边界的栈分配
- C++ Tricks 2.5 I386平台的边界对齐(Align)
- C++ Tricks 2.6 I386平台C函数的可变参数表(Variable Arguments)
- C++ Tricks 2.7 I386平台的其它函数调用模型
- C++ Tricks 3.1 左值右值与常量性(lvalue,rvalue & constant)
- C++ Tricks 3.2 标号、goto,以及switch的实现
Github Issue 留言
Disqus 留言