開篇
幾乎每種程序設計語言的語法中都會有語句的循環,跳轉。像最為熟知的C語言便有 for 、 while 、 do---while 等等。這些循環一般都很容易理解和使用,對於程序中邏輯的實現也很有幫助。
只是很多人不曾知道,這些循環、跳轉在計算機內部、在底層是如何實現的,於是在出現問題時還是沒有好的解決辦法,或者是雖然寫出來程序,對於內部的邏輯,卻還是隔了一層迷霧。
比如有人對這樣一個問題:
for( i=0 ; i< 10 ; i++)
{
printf(”%i“,i);
}
for語句里面的 i++ 是什么時候執行的呢? 當循環開始時,是先執行括號里的 i++ 還是printf(”%i“,i)? 也就是說 ,第一個打印的數字是0 還是 1?
我相信這個問題很多人都遇到過,就是當for 循環結束后 i 的值到底是多少不是很確定,在這個問題上犯難,是在不值得。如果你也曾有過這種疑問,那么這篇文章就很值得繼續看下去。
這篇文章將撥開迷霧,讓你看到循環的本質。
這篇文章主要談一下C中的這些循環, 跳轉語句的內部實現機制。通過深入了解他們的內在,將讓你在編程的時候邏輯更加清晰,出現問題的時候也更容易排查。
注:這篇文章中會涉及到一些基本的匯編知識,我們將通過分析 for 、 while 、 do---while 等的匯編表示形式,來弄清楚他們的實現機理。
if--else
if--else是基本的條件轉移控制指令,也可以說是程序循環跳轉實現的基礎。
先看一個具體的例子,函數absdiff()比較兩個參數x,y 的大小,返回他們差的絕對值。
我們創建了對於的C語言版本(a)、goto形式的版本(b)、 以及其對應的匯編形式(c):
創建goto版本是為了能更好的理解他的匯編形式。因為里面的goto語句很類似於匯編中的跳轉語句。
(a)中 的C代碼應該不用多做解釋了吧,大家應該都能看懂。
(b)中的got0語句形式:第4 行是一個跳轉語句,跳轉到執行第8 行。也就是說當第3 行的條件滿足的時候,跳轉到第8 行執行。如果第3 行的條件不滿足,則執行第 5 行的語句。 然后無條件跳轉到代碼的結尾:“ goto done;” 使用goto 語句通常被認為是一種不好的 編程風格,因為這樣的語句通常難於調試和閱讀,這里使用goto語句是為了構造出一種類似匯編實現的C語句。
(c)就是我們要討論的重點了。其實條件跳轉指令的匯編形式和他的goto形式有一定的相似之處。有語句跳轉的時候都是這樣的過程:先判斷是否滿足某個條件,如果滿足,則跳到一個指定的的代碼段繼續執行程序,如果不滿足,則執行另一條語句。
為了弄清楚它內部是如何實現的,現在我們來詳細看一下他的匯編指令形式(c)
程序的 1 、2 行取得x y 的值。放在寄存器 edx eax 中,第三行比較x y 的大小(也就是比較edx eak 的大小)如果x > y 則跳到 .L2的地方執行(8 、9 行)執行x - y ,並將結果放在返回值中,然后程序順序執行到程序結束處 .L3,反之,如果第三行判斷結果是x<y ,則語句不會跳轉,順序執行y-x,並跳轉到程序結束處 .L3.
現在,我們基本了解if --else 的執行機理了。主要是先判斷條件值,從這個例子出發就是第 3 條語句: cmpl %eax,%edx。學過匯編的同學應該知道,cmpl是一條比較指令,他的執行會影響標志位。做個簡單的假設: cmpl A,B 是比較A,B兩個數的大小,比較結果存放在標志位 F 中,當A>B時,F為0,A<BF為1。 后面的jge .L是跳轉語句,他的執行是和上一步比價的結果相關的,這里也就反映在標志位F 當中。當F 為1時(A<B),則跳轉到 .L2執行 B-A。當比較結果為A>B 查看后標志位為0,則jge不跳轉, 程序繼續往下執行。
C中的if --else 的通用模板是這樣的:
這里的 test-expr 是一個整數表達式,它的值為真(1、>0)則會執行第一個分支語句 , 否則執行第二個。但不管怎么樣,都只會執行其中的一個。
對於這種模板,匯編的實現通常會用下面的形式:(這里用C語言描述)
匯編器會為 then-statement 和else-statement產生各自的代碼快 ,通過條件跳轉來執行相應的代碼。
注意:程序並不是智能的,執行跳轉的時候只會有兩種判斷(真和假),而且內部的跳轉形式都是基於 go to 模式。所以說雖然goto 在編程中要少用,但在理解程序跳轉的時候還是很重要的。我想因為goto 的實現方式和程序最終的實現方式如此的相似,才在C中引入goto 吧。
do---while
C中提供了多中循環結構,do-while while 和 for ,然而匯編中沒有相應的指令存在。通過上面對 if -else 的分析, 已經知道了 C 中條件跳轉在匯編中的基本實現方式(goto),現在要討論的這些循環,在循環中都有一個條件判斷的環節,當條件滿足時,繼續循環體, 如果條件不滿足,則跳出循環體。和if-else 不同的在於這里的條件表達式可能會有改變,也就是每次判斷的條件和上次不同。
只要理解了上面的if-else 的內容 ,其他的循環都能通過對if -else 的改進來實現。先給出這些循環的goto 形式,就能很快推測出其內部的實現方式。
通用形式:
do-while的通用形式如下:
do
body-staement
while(test-expr)
這個循環的效果就是反復執行 body-staement,對test-expr進行求值,如果求值結果不是0 則繼續執行。可以看到,body-statement至少會被執行一次。
do-while 的通用形式可翻譯為 如下所示的goto 語句:
loop:
body-statement
t=test-expr;
if(t)
goto loop;
對於上面的goto形式, 可以先想想匯編會是如何實現的:把loop里的代碼放在一個單獨的代碼段里面(包含if 的判斷跳轉部分)程序順序執行下來,第一次執行不用判斷條件。執行玩body-statement后判斷條件,如果滿足,則繼續這個循環。
好了,下面看一個具體的例子:
這是一個計算階乘的函數實現,
(a)是c代碼,
(c)是對應的匯編形式
(b)是匯編中寄存器和變量的映射關系
(a)中的代碼比較簡單就不在解釋了。現在看(c)中的代碼:
第 1 行是獲得函數參數n的值,放在寄存器 edx 中,第 2 行設置 result =1 第 3 行的 L2 表明這是一個循環體。 (c) 中的第 4、 5 行實現了 (a)中 5、 6 行的功能。
(c)中的第6 行判斷循環的條件,如果條件滿足,則執行第 7 行 的跳轉語句。
和do- while的通用形式所描述的一樣,body-staement至少會被執行一次,這從匯編代碼中也有體現,觀察循環體 L2,在L2 沒有關於L2 的跳轉代碼,也就是說在程序順序往下執行的情況下,L2 至少會被執行一次,然后在 L2 內部判斷是否繼續執行循環體 L2。
while
whil循環和do-while類似,他的通用形式如下:
while(test-expr)
body-statement
和do-while不同的是,他先對test-expr求值,所以body-statement可能一次都不被執行。將while翻譯為機器碼有多種方法,其中一種常見的方式i就是把他翻譯成do-while的形式。在第一次執行循環體之前加一個判斷條件,如果條件滿足,則進入do-while的循環體,如果不滿足,直接跳過循環體。也就是上面說的:body-statement可能一次都不被執行。
下面先把while轉換為do-while循環:
if(!test-expr)
goto done;
do
body-statement
while(test-expr);
done;
接下來可以吧這個do-while循環轉換為goto語句:
t=test-expr;
if(!t)
goto done;
loop:
body-statement
t=test-expr;
if(t)
goto loop;
done
這個goto的版本和do-while的版本只是在循環體loop的前面加上了一個判斷條件,如果條件滿足, 就執行上面中的do-while循環體,如果不滿足,直接跳過循環。
通過上面的goto版本,現在應該可以想一下while 的 匯編是如何實現了吧:
現在就可以猜測,while 的匯編只是在do-while匯編的基礎上做出一些改動:在循環體loop之前加一個判斷條件,如果條件成立,則執行loop,不成立則跳過loop。
下面還通過一個具體例子來說:
還是計算n 的階乘,不過這次是用while實現
下面是C代碼:
這很簡單,就不做解釋了,下面看他的goto形式:
和上面描述的一樣,在3、4 行中加入了條件判斷,如果條件不滿足 第5行直接跳攻loop循環體,只有當條件滿足時才進入循環體loop。再看loop中,7、8 行執行計算階乘的必要操作,9、10行通過判斷test-expr 決定是否繼續循環。
好,下面來看while的匯編形式:
現在看匯編代碼應該比較有經驗了吧,這里的3、4 行執行條件判斷,決定是否進入循環體.L10,相當於goto版本中的4、5 行,如果不滿足,直接跳轉到.L7的位置。
再看一下.L10 循環體:6、7 行執行階乘的必要操作,相當於goto中的 7、8 行,這里的8、9 行判斷循環是否繼續,相當於goto中的9、10行。
for循環
for循環的通用形式如下;
for(init-expr ; test-expr ; update-expr)
body-statement
大概在學C的時候會提到for 循環可以用while 來表示:
init-expr
while(test-expr){
body-statement
update-expr
}
程序首先初始化表達式init-expr的值,然后進入循環,再循環中先對測試值test-expr求值,如果為假,則退出循環,否則執行循環體body-statement,並更新表達式update-expr的值。
基於前面講過的do-while到while的轉換,先給出do-whle形式:
然后將它轉為goto代碼:
和上面一樣,我們將用一個實例來說明問題:
考慮用for 循環寫的階乘函數
用for循環寫的計算階乘的函數是從2 開始,這和前面的從1開始不同,不過這並不影響其邏輯實現的結構,這段代碼中,for循環的組成如下:
通過上面的分析,可以得到其goto形式:
到現在為止 ,已經在實例中給出了他的C 形式和goto形式,現在請看他的匯編形式:
上面都解釋了很多,這里就不在解釋這個匯編指令了,通過上面應該都能看懂。
開始的問題
到現在,應該弄清楚文章開頭所說的問題了吧
for( i=0 ; i< 10 ; i++)
{
printf(”%i“,i);
}
現在應該了解什么時候執行 i++ 設么時候執行printf(”%i“,i); 了吧。
按照我們的分析,執行順序應該是這樣的:
1、 初始化 i=0
2、判斷條件是否滿足 i<10 是否成立
3、如果成立則執行循環體printf(”%i“,i); 並執行自加運算 i++
可以這段代碼在自己的編譯器中運行一下,看首先打印出來的是0 還是1 (按照我們的分析,應該從0 開始答應哦)
小結
其實這篇文章闡述的道理很簡單,就是C 語言中循環的實現問題。如果你在linux環境下操作的話,要理解這一部分就更容易了,在linux下可以直觀的看到一個程序編譯后的匯編形式,在自己電腦上分析自己的程序,這樣學起來才更有興趣,更深刻。
簡單介紹:
linux下編譯器:GCC、
可執行文件查看器:objdump、還有一個好像是 elfread,不確定了。
如果調試上面的for語句的話,可以用gdb調試器,這樣能讓程序一步步執行,並且跟蹤變量的值。關於linux下的這些命令這里不是本文重點,不再介紹。
全文完。
一條魚、yanlingyin@ 博客園
尹雁鈴 2012-3-27
E-mail:yanlingyin@yeah.net