1. 程式人生 > >C語言中遞歸什麽時候能夠省略return引發的思考:通過內聯匯編解讀C語言函數return的本質

C語言中遞歸什麽時候能夠省略return引發的思考:通過內聯匯編解讀C語言函數return的本質

tle ext 多少 那不 語句 二次 () mar ado

  • 事情的經過是這種,博主在用C寫一個簡單的業務時使用遞歸,因為粗心而忘了寫return。結果發現返回的結果依舊是正確的。經過半小時的反匯編調試。證明了我的猜想,如今在博客裏分享。也是對C語言編譯原理的一次加深理解。
  • 引子:
  • 首先我想以一道題目引例,比較能體現出問題。
1:
#include <stdio.h>
/**
  函數功能:用遞歸實現位運算加法
 */
int Add_Recursion(int a,int b)
{
    int carry_num = 0, add_num = 0;
    if (b == 0)
    {
        return
a; } else { add_num = a^b; carry_num = (a&b)<<1; Add_Recursion(add_num, carry_num); } } int main() { int num = Add_Recursion(1, 1); printf("%d\n",num); getchar(); }
  • 問題是。運行如上的程序,打印出來的數值是多少?
  • 大家可能會覺得這個非常的弱智,即使作為小公司的筆試題來說都登不上大雅之堂。


    技術分享
    ——————————–圖1 例題1的運行結果———————

  • 答案是2,毫無疑問,僅僅是一個簡單的遞歸而已。


    可是假設我把題目改一下

2:
#include <stdio.h>
int changestack()
{
   return 3;
}
/**
  函數功能:用遞歸實現位運算加法
 */

int Add_Recursion(int a,int b)
{
    int carry_num = 0, add_num = 0;
    if (b == 0)
    {
        return a;
    }
    else
    {
        add_num = a^b;
        carry_num = (a&b)<<1
; Add_Recursion(add_num, carry_num); changestack(); } } int main() { int num = Add_Recursion(1, 1); printf("%d\n",num); getchar(); }
  • 大家看看上邊的程序。運行結果會是多少?
    可能有非常多朋友細心已經發現了貓膩。


    可能也有部分朋友會有些困惑,這個程序僅僅是在遞歸的實現函數後中加了一個無關緊要的函數調用,為什麽會影響函數返回的結果呢。
    其實printf打印出來的結果不對。運行結果是3
    技術分享
    —————————-圖2 例題2的運行結果————————-

  • 為什麽會出現這個問題呢。實際上正常情況下的遞歸。

    在else語句裏進行遞歸調用時。應當加上return。

    因為return的缺失,導致了函數返回值被changestack()函數篡改。從而在main函數中讀到了錯誤的返回值。

else
    {
        add_num = a^b;
        carry_num = (a&b)<<1;
        return Add_Recursion(add_num, carry_num);
         changestack();

    }
  • 假設將上文的代碼改正如上,那不會出現不論什麽問題。

    (當然不會出錯,此時有了return,return後邊的changestack根本就不會有不論什麽機會運行)
    如今來一步一步來分析發生錯誤的本質。
    技術分享

  • ——————–圖三 例二函數的遞歸分析—————————

  • 我們分析上邊代碼的運行過程。首先在main函數中調用Add_Recursion(1,1),本意就是計算1+1的值,而且將函數返回值傳遞給printf打印出來。


    在遞歸調用Add_Recursion函數(簡稱add)計算1+1時,前兩次遞歸調用因為不滿足遞歸出口條件(進位加數carry_num為0)。會跳入else分支進行遞歸調用。

    直到第三次遞歸調用時因為carry_num為0。這時返回了累加結果。

  • 問題是僅僅有第三次的add遞歸調用進行了return,第一次和第二次在函數返回時,都沒有return,而是在返回子層次遞歸後調用changestack()函數後返回調用自己的函數層級。

    在第一層遞歸調用返回給main的時候,add_recursion並沒有return,而是在運行完changestack直接返回main函數,而此時main函數的printf在解析返回值時,實際上錯誤的解析了changestack的返回值。

    因此才出現1+1=3的錯誤

  • 綜上分析發生這一切的原因,就是:
    函數運行結束返回時。會將返回值壓棧(理論上如此,實際上編譯器會優化,將返回值給eax寄存器過渡。VC就是使用的eax臨時保存)。VC編譯器解析函數返回值(整型)時,直接將eax的值讀出當做返回值。


    技術分享
    ———————-圖四 反匯編分析VC編譯器對return的處理———-

  • 依據反匯編分析能夠看到,VC編譯器對changestack()中的return 3匯編的結果,也就是 mov eax,3。實際上就是把返回值賦予eax,由eax寄存器過渡給此函數的調用函數使用。

  • 我們在下圖中能夠看到main函數中將changestack()的返回值給num賦值的詳細過程,也就是將eax的值返回給num的所在的內存地址。
    技術分享
    ——————————圖五 函數返回值的“彈棧”細則——————————-

  • 這樣一切就有了解釋。

  • 技術分享

——————-圖六 例題一為什麽會碰巧正確的遞歸分析—————

  • 盡管第一題的結果盡管正確,printf在讀取Add_Recursion返回值時。讀取的不是第一次遞歸調用的結果,而是第三次遞歸調用return b的結果(第三次遞歸返回時,暫存在eax寄存器中)。而在之後的遞歸返回中,湊巧eax都沒有被改變。

    因此這樣使用遞歸(盡管沒有在須要return的地方return)是能夠得到正確結果。
    實際上我們能夠用一條內聯匯編代碼驗證我們的猜想是否正確。

    我們在遞歸調用的後邊,使用內聯匯編加上一條匯編代碼改變eax的值。


    技術分享

——————————-圖七 用內聯匯編解讀C語言的return本質—————————–

  • 我們在遞歸函數Add_Recursion的後邊加了一條匯編代碼,讓函數結束時改變eax的值。能夠看到。主函數中,將函數返回值誤覺得了我們在匯編語言中設定的3.打印出了1+1=3這種謬論。

  • 實際上,我們在編譯例題中的程序在編譯時C編譯器會提出警告
    warning C4715: “Add_Recursion”: 不是全部的控件路徑都返回值
    有返回值的函數,不是全部的支路都會進行返回值,假設大家把博客中的程序在更加嚴格的C++編譯器上編譯會報錯。

  • 這僅僅是一個非常easy的案例。或許我們會運氣好實現函數的功能,可是在進行復雜情況的樹狀甚至圖狀遞歸中,假設不確定自己是否一定能得到終於結果,請務必將每一種情況都return返回值,這樣來避免程序意外出錯。

    C語言的靈活性應該給我們造福,而不應該給我們的程序提供不穩定的因素。

C語言中遞歸什麽時候能夠省略return引發的思考:通過內聯匯編解讀C語言函數return的本質