迭代控制结构全解析
1. 布尔表达式评估与非结构化代码
在研究汇编代码时,我们会发现某些程序会对原始布尔表达式的两个部分都进行评估,也就是完全布尔评估。不过,使用非结构化代码时需要格外小心。一方面,这样的代码结果更难阅读;另一方面,采用这种方式很难让编译器生成你期望的代码。而且,在一个编译器上能生成优质代码的代码序列,在其他编译器上可能无法产生类似的效果。
如果你的编程语言不支持
break
语句,你可以使用
goto
语句跳出循环来达到相同的效果。但往代码里注入
goto
语句并非良策。不过,要是你需要完全布尔评估语义,并且在你的语言里使用
goto
是实现这一目的的唯一办法,那也别无选择。
2. 在
while
循环中强制短路布尔评估
有时候,即便语言(如BASIC或Pascal)没有实现短路评估,你也需要确保
while
语句中布尔表达式的短路评估。就像
if
语句一样,你可以通过重新安排循环控制表达式的计算方式来在程序中实现这一点。与
if
语句不同的是,你不能使用嵌套
while
语句或在
while
循环前添加其他语句来实现,但在大多数编程语言中仍然是可行的。
例如下面的C代码片段:
while( ptr != NULL && ptr->data != 0 )
{
<< loop body >>
ptr = ptr->Next; // Step through a linked list.
}
如果C语言不保证布尔表达式的短路评估,这段代码可能会出错。
在像Pascal这样的语言中,强制完全布尔评估最简单的方法是编写一个函数,使用短路布尔评估来计算并返回布尔结果。但由于函数调用的开销较大,这种方案相对较慢。以下是一个用Borland Delphi编译器编译的Pascal示例:
program shortcircuit;
{$APPTYPE CONSOLE}
uses SysUtils;
var
ptr :Pchar;
function shortCir( thePtr:Pchar ):boolean;
begin
shortCir := false;
if( thePtr <> NIL ) then begin
shortCir := thePtr^ <> #0;
end; //if
end; // shortCircuit
begin
ptr := 'Hello world';
while( shortCir( ptr )) do begin
write( ptr^ );
inc( ptr );
end; // while
end.
对应的80x86汇编代码如下:
sub_408570 proc near
;EDX holds function return
; result (assume false).
;
; shortCir := false;
xor edx, edx
;if( thePtr <> NIL ) then begin
test eax, eax
jz short loc_40857C ;branch if NIL
; shortCir := thePtr^ <> #0;
cmp byte ptr [eax], 0
setnz dl ;DL = 1 if not #0
loc_40857C:
;Return result in EAX:
mov eax, edx
retn
sub_408570 endp
; Main Program (pertinent section):
;
; Load EBX with the address of the global "ptr" variable and
; then enter the "WHILE" loop (Delphi moves the test for the
; while loop to the physical end of the loop's body):
mov ebx, offset loc_408628
jmp short loc_408617
; --------------------------------------------------------
loc_408600:
; Print the current character whose address
; "ptr" contains:
mov eax, ds:off_4092EC ;ptr pointer
mov dl, [ebx] ;fetch char
call sub_404523 ;print char
call sub_404391
call sub_402600
inc ebx ;inc( ptr )
; while( shortCir( ptr )) do ...
loc_408617:
mov eax, ebx ;Pass ptr in EAX
call sub_408570 ;shortCir
test al, al ;Returns True/False
jnz short loc_408600 ;branch if true
sub_408570
过程包含一个函数,用于计算类似于前面C代码中表达式的短路布尔评估。可以看到,如果
thePtr
为NIL(零),解引用
thePtr
的代码永远不会执行。
如果不能使用函数调用,那么唯一合理的解决方案可能是采用非结构化方法。以下是前面C代码中
while
循环的Pascal版本,强制进行短路布尔评估:
while( true ) do begin
if( ptr = NIL ) then goto 2;
if( ptr^.data = 0 ) then goto 2;
<< loop body >>
ptr = ptr^.Next;
end;
2:
同样,生成像这个例子中的非结构化代码应该作为最后的手段。但如果所使用的语言(或编译器)不保证短路评估,而你又需要这些语义,那么非结构化代码或低效代码(使用函数调用)可能是唯一的解决方案。
3.
repeat..until
(
do..until
/
do..while
)循环
大多数现代编程语言中另一个常见的循环是
repeat..until
循环。
repeat..until
循环在循环底部测试终止条件,这意味着循环体至少会执行一次,即使布尔控制表达式在循环的第一次迭代中计算结果为False。虽然
repeat..until
循环的适用性比
while
循环稍窄,使用频率也远不如
while
循环,但在很多情况下,它是完成任务的最佳控制结构选择。经典的例子是从用户那里读取输入,直到用户输入某个特定值。以下是一个非常典型的Pascal代码片段:
repeat
write( 'Enter a value (negative quits): ');
readln( i );
// do something with i's value
until( i < 0 );
这个循环的循环体总是会执行一次,这是必要的,因为必须执行循环体才能从用户那里读取程序用于检查循环执行何时结束的值。
repeat..until
循环在其布尔控制表达式计算结果为True时终止(而
while
循环是在计算结果为False时终止)。这是合理的,因为“until”这个词表明循环在控制表达式计算结果为True时终止。不过,这只是一个小的语法问题,C/C++/Java等语言(以及许多继承了C语言传统的语言)提供了
do..while
循环,只要循环条件计算结果为True,就会重复执行循环体。从效率的角度来看,这两种循环没有任何区别,你可以通过使用语言的逻辑非运算符轻松地将一个循环的终止条件转换为另一个循环的终止条件。以下是Pascal、HLA和C/C++的
repeat..until
和
do..while
循环的语法示例:
Pascal的
repeat..until
循环示例:
repeat
(* Read a raw character from the "input" file, which in this case is
the keyboard *)
ch := rawInput( input );
(* Save the character away. *)
inputArray[ i ] := ch;
i := i + 1;
(* Repeat until the user hits the enter key *)
until( ch = chr( 13 ));
C/C++的
do..while
版本:
do
{
/* Read a raw character from the "input" file, which in this case is
the keyboard */
ch = getKbd();
/* Save the character away. */
inputArray[ i++ ] = ch;
/* Repeat until the user hits the enter key */
}
while( ch != '\r' );
HLA的
repeat..until
循环:
repeat
// Read a character from the standard input device.
stdin.getc();
// Save the character away.
mov( al, inputArray[ ebx ] );
inc( ebx );
// Repeat until the user hits the enter key.
until( al = stdin.cr );
将
repeat..until
(或
do..while
)循环转换为汇编语言相对简单直接。编译器只需要替换布尔循环控制表达式的代码,并在表达式计算结果为肯定(
repeat..until
为False,
do..while
为True)时跳回到循环体的开头。以下是前面HLA的
repeat..until
循环的纯汇编实现(C/C++和Pascal的编译器为其他示例生成的代码几乎相同):
rptLoop:
// Read a character from the standard input.
call stdin.getc;
// Store away the character.
mov( al, inputArray[ ebx ] );
inc( ebx );
// Repeat the loop if the user did not hit
// the enter key.
cmp( al, stdio.cr );
jne rptLoop;
可以看到,典型编译器为
repeat..until
(或
do..while
)循环生成的代码通常比普通
while
循环的代码效率略高。
由于编译器通常可以为
repeat..until
/
do..while
循环生成比
while
循环稍高效的代码,如果语义上可行,应该考虑使用
repeat..until
/
do..while
形式。在许多程序中,某些循环结构的布尔控制表达式在第一次迭代时总是计算结果为True。例如,在应用程序中经常会看到这样的循环:
i = 0;
while( i < 100 )
{
printf( "i: %d\n", i );
i = i * 2 + 1;
if( i < 50 )
{
i += j;
}
}
这个
while
循环可以很容易地转换为
do..while
循环:
i = 0;
do
{
printf( "i: %d\n", i );
i = i * 2 + 1;
if( i < 50 )
{
i += j;
}
} while( i < 100 );
这种转换是可行的,因为我们知道
i
的初始值(零)小于100,所以循环体至少会执行一次。
通过使用更合适的
repeat..until
/
do..while
循环而不是普通的
while
循环,可以帮助编译器生成更好的代码。不过要注意,效率提升很小,因此在这样做时要小心不要牺牲代码的可读性或可维护性。总之,始终要使用逻辑上最合适的循环结构。如果循环体至少会执行一次,即使
while
循环也能正常工作,也应该使用
repeat..until
/
do..while
循环。
下面用一个表格总结
while
循环和
repeat..until
(
do..while
)循环的特点:
| 循环类型 | 测试位置 | 循环体执行次数 | 终止条件 |
| ---- | ---- | ---- | ---- |
|
while
| 循环开始 | 可能为0次 | 条件为False |
|
repeat..until
/
do..while
| 循环结束 | 至少1次 | 条件为True |
下面是
repeat..until
循环的执行流程mermaid流程图:
graph TD;
A[开始] --> B[执行循环体];
B --> C{条件判断};
C -- 条件为False --> B;
C -- 条件为True --> D[结束];
4. 在
repeat..until
循环中强制完全布尔评估
由于
repeat..until
(或
do..while
)循环的终止测试在循环底部进行,因此在
repeat..until
循环中强制进行完全布尔评估的方式与在
if
语句中类似。考虑以下C/C++代码:
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
do
{
++a;
--b;
}while( a < f(x) && b > g(y));
return( 0 );
}
以下是GCC为PowerPC生成的代码(使用C标准的短路评估):
L2:
// ++a
// --b
lwz r9,0(r30) ; get a
lwz r11,0(r29) ; get b
addi r9,r9,-1 ; --a
lwz r3,0(r27) ; Set up x parm for f
stw r9,0(r30) ; store back into a
addi r11,r11,1 ; ++b
stw r11,0(r29) ; store back into b
; compute f(x)
bl L_f$stub ; call f, result to R3
; is a >= f(x)? If so, quit loop
lwz r0,0(r29) ; get a
cmpw cr0,r0,r3 ; Compare a with f's value
bge- cr0,L3
lwz r3,0(r28) ; Set up y parm for g
bl L_g$stub ; call g
lwz r0,0(r30) ; get b
cmpw cr0,r0,r3 ; Compare b with g's value
bgt+ cr0,L2 ; Repeat if b > g's value
L3:
从这个代码示例可以看出,如果表达式
a < f(x)
为False(即
a >= f(x)
),程序会跳过对
b > g(y)
的测试,直接跳转到标签
L3
。
为了在这种情况下强制进行完全布尔评估,C源代码需要在
while
子句之前计算布尔表达式的子组件(将子表达式的结果保存在临时变量中),然后在
while
子句中仅测试这些结果:
static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
int temp1;
int temp2;
do
{
++a;
--b;
temp1 = a < f(x);
temp2 = b > g(y);
}while( temp1 && temp2 );
return( 0 );
}
以下是GCC将其转换为PowerPC代码的结果:
L2:
lwz r9,0(r30) ;r9 = b
li r28,1 ;temp1 = True
lwz r11,0(r29) ;r11 = a
addi r9,r9,-1 ;--b
lwz r3,0(r26) ;r3 = x (set up f's parm)
stw r9,0(r30) ;Save b
addi r11,r11,1 ;++a
stw r11,0(r29) ;Save a
bl L_f$stub ;Call f
lwz r0,0(r29) ;Fetch a
cmpw cr0,r0,r3 ;Compute temp1 = a < f(x)
blt- cr0,L5 ;Leave temp1 true if a < f(x)
li r28,0 ;temp1 = false
L5:
lwz r3,0(r27) ;r3 = y, set up g's parm
bl L_g$stub ;Call g
li r9,1 ;temp2 = True
lwz r0,0(r30) ;Fetch b
cmpw cr0,r0,r3 ;Compute b > g(y)
bgt- cr0,L4 ;Leave temp2 true if b > g(y)
li r9,0 ;Else set temp2 false
L4:
;Here's the actual termination test in
;the while clause:
cmpwi cr0,r28,0
beq- cr0,L3
cmpwi cr0,r9,0
bne+ cr0,L2
L3:
可以注意到,实际的布尔表达式
(temp1 && temp2)
仍然使用短路评估。然而,这种短路评估仅涉及创建的临时变量。无论第一个子表达式的结果如何,循环都会计算两个原始子表达式。
5. 在
repeat..until
循环中强制短路布尔评估
如果编程语言提供了跳出
repeat..until
循环的功能,如C的
break
语句,那么强制进行短路评估相对容易。考虑上一节中强制进行完全布尔评估的C
do..while
循环:
do
{
++a;
--b;
temp1 = a < f(x);
temp2 = b > g(y);
}while( temp1 && temp2 );
以下是将此代码转换为使用短路布尔评估来计算终止表达式的一种方法:
static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
do
{
++a;
--b;
if( !( a < f(x) )) break;
}while( b > g(y) );
return( 0 );
}
以下是GCC为PowerPC生成的
do..while
循环代码:
L2:
lwz r9,0(r30) ;r9 = b
lwz r11,0(r29) ;r11 = a
addi r9,r9,-1 ;--b
lwz r3,0(r27) ;Set up f(x) parm
stw r9,0(r30) ;Save b
addi r11,r11,1 ;++a
stw r11,0(r29) ;Save a
bl L_f$stub ;Call f
; break if a < f(x):
lwz r0,0(r29)
cmpw cr0,r0,r3
bge- cr0,L3
; while( b > g(y) ):
lwz r3,0(r28) ;Set up y parm
bl L_g$stub ;Call g
lwz r0,0(r30) ;Compute b > g(y)
cmpw cr0,r0,r3
bgt+ cr0,L2 ;Branch if true
L3:
如果
a
小于
f(x)
返回的值,此代码会立即跳出循环(到标签
L3
),而不会测试
b
是否大于
g(y)
返回的值。因此,此代码模拟了表达式
a < f(x) && b > g(y)
的短路布尔评估。
如果使用的编译器不支持类似于C/C++的
break
语句,则需要使用更复杂的逻辑。以下是一种可能的方法:
static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
int temp;
do
{
++a;
--b;
temp = a < f(x);
if( temp )
{
temp = b > g(y);
};
}while( temp );
return( 0 );
}
以下是GCC为PowerPC生成的代码:
L2:
lwz r9,0(r30) ;r9 = b
lwz r11,0(r29) ;r11 = a
addi r9,r9,-1 ;--b
lwz r3,0(r27) ;Set up f(x) parm
stw r9,0(r30) ;Save b
addi r11,r11,1 ;++a
stw r11,0(r29) ;Save a
bl L_f$stub ;Call f
li r9,1 ;Assume temp is True
lwz r0,0(r29) ;Set temp false if
cmpw cr0,r0,r3 ;a < f(x)
blt- cr0,L5
li r9,0
L5:
cmpwi cr0,r9,0 ;If !(a < f(x)) then bail
beq- cr0,L10 ; on the do..while loop
lwz r3,0(r28) ;Compute temp = b > f(y)
bl L_g$stub ; using a code sequence
li r9,1 ; that is comparable to
lwz r0,0(r30) ; the above.
cmpw cr0,r0,r3
bgt- cr0,L9
li r9,0
L9:
; Test the while termination expression:
cmpwi cr0,r9,0
bne+ cr0,L2
L10:
即使这些示例使用的是逻辑与(
&&
)操作,使用逻辑或(
||
)操作也同样容易。以下是一个Pascal示例及其转换:
repeat
a := a + 1;
b := b - 1;
until( a < f(x) OR b > g(y) );
以下是强制进行完全布尔评估的转换:
repeat
a := a + 1;
b := b - 1;
temp := a < f(x);
if( not temp ) then begin
temp := b > g(y);
end;
until( temp );
以下是Borland的Delphi为这两个循环生成的代码(假设在编译器选项中选择了完全布尔评估):
; repeat
;
; a := a + 1;
; b := b - 1;
;
; until( (a < f(x)) or (b > g(y)));
loc_4085F8:
inc ebx ; a := a + 1;
dec esi ; b := b - 1;
mov eax, [edi] ;EDI points at x
call locret_408570
cmp ebx, eax ;Set AL to 1 if
setl al ; a < f(x)
push eax ;Save Boolean result.
mov eax, ds:dword_409288 ;y
call locret_408574 ;g(6)
cmp esi, eax ;Set AL to 1 if
setnle al ; b > g(y)
pop edx ;Retrieve last value.
or dl, al ;Compute their OR
jz short loc_4085F8 ;Repeat if false.
; repeat
;
; a := a + 1;
; b := b - 1;
; temp := a < f(x);
; if( not temp ) then begin
;
; temp := b > g(y);
;
; end;
;
; until( temp );
loc_40861B:
inc ebx ;a := a + 1;
dec esi ;b := b - 1;
mov eax, [edi] ;Fetch x
call locret_408570 ;call f
cmp ebx, eax ;is a < f(x)?
setl al ;Set AL to 1 if so.
; If the result of the above calculation is
; True, then don't bother with the second
; test (i.e., short-circuit evaluation)
test al, al
jnz short loc_40863C
;Now check to see if b > g(y)
mov eax, ds:dword_409288
call locret_408574
;Set AL = 1 if b > g(y):
cmp esi, eax
setnle al
; Repeat loop if both conditions were false:
loc_40863C:
test al, al
jz short loc_40861B
Delphi编译器为强制短路评估生成的代码远不如允许编译器自动处理(不选择完全布尔评估)时生成的代码好。以下是未选择完全布尔评估(即指示Delphi使用短路评估)的Delphi代码:
loc_4085F8:
inc ebx
dec esi
mov eax, [edi]
call nullsub_1 ;f
cmp ebx, eax
jl short loc_408613
mov eax, ds:dword_409288
call nullsub_2 ;g
cmp esi, eax
jle short loc_4085F8
虽然在编译器不支持短路评估时,这种技巧对于强制进行短路评估很有用,但后面的Delphi示例充分表明,如果可能的话,应该使用编译器的功能,这样通常会得到更好的机器代码。
下面用一个表格总结不同布尔评估方式的特点:
| 评估方式 | 实现方法 | 效率 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 完全布尔评估 | 提前计算子表达式并保存结果 | 相对较低 | 需要对所有子表达式求值时 |
| 短路布尔评估 | 使用
break
语句或临时变量控制 | 相对较高 | 部分子表达式结果可决定整体结果时 |
下面是在
repeat..until
循环中强制短路布尔评估的执行流程mermaid流程图:
graph TD;
A[开始] --> B[执行循环体];
B --> C{条件1判断};
C -- 条件1为False --> D[结束循环];
C -- 条件1为True --> E{条件2判断};
E -- 条件2为False --> D;
E -- 条件2为True --> B;
超级会员免费看

被折叠的 条评论
为什么被折叠?



