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

C語言中遞歸什么時候能夠省略return引發的思考:通過內聯匯編解讀C語言函數return的本質
  • 事情的經過是這種,博主在用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的運行結果
    ——————————–圖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的運行結果
    —————————-圖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語言的靈活性應該給我們造福,而不應該給我們的程序提供不穩定的因素。

posted on 2017-08-07 18:01 mthoutai 閱讀(...) 評論(...) 編輯 收藏

轉載于:https://www.cnblogs.com/mthoutai/p/7300489.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/282837.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/282837.shtml
英文地址,請注明出處:http://en.pswp.cn/news/282837.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C# 為什么說CM+Fody+HC是WPF開發的最強組合?

01—名詞解析CM&#xff1a;Caliburn.Micro(簡稱CM)一經推出便備受推崇&#xff0c;作為一款MVVM開發模式的經典框架&#xff0c;越來越多的受到wpf開發者的青睞.我們看一下官方的描述&#xff1a;Caliburn是一個為Xaml平臺設計的小型但功能強大的框架。Micro實現了各種UI模式&…

c語言邏輯運算符兩側運算對象,邏輯運算符兩側運算對象的數據類型是什么?...

邏輯運算符兩側運算對象的數據類型&#xff1a;可以是任何合法的類型數據&#xff1b;因為邏輯運算符兩邊的運算對象&#xff0c;最終都被轉換成bool值(邏輯值)操作。0、null轉換為false&#xff0c;而所有非零、非false、非null值轉換為true&#xff1b;然后進行運算。邏輯運算…

python-list:列表-元組-字符串

列表 “列表”是一個值&#xff0c;它包含多個字構成的序列。術語“列表值”指的是列表本身&#xff08;它作為一個值&#xff0c;可以保存在變量中、傳遞給函數&#xff09;--&#xff1a;按下標取值、切片、for循環、用于len()以及in not in等 list [aa,bb,cc,dd]是一個簡單的…

創建相似對象,就交給『工廠模式』吧

源碼&#xff1a; 源代碼C# 系列導航&#xff1a; 目錄 定義&#xff08;Factory Pattern&#xff09;&#xff1a; 用來創建目標對象的類&#xff0c;將相似對象的創建工作統一到一個類來完成。 一、簡單工廠模式&#xff1a; 代碼&#xff1a; /// <summary>/// 產品枚…

《ASP.NET Core 6框架揭秘》實例演示[26]:跟蹤應用接收的每一次請求

很多人可能對ASP.NET Core框架自身記錄的診斷日志并不關心&#xff0c;其實這些日志對糾錯排錯和性能監控提供了很有用的信息。如果需要創建一個APM&#xff08;Application Performance Management&#xff09;系統來監控ASP.NET Core應用處理請求的性能及出現的異常&#xff…

C語言循環為1404的循環,考試,求大神幫忙,C語言,小弟感激不盡

若有定義語句&#xff1a;int a10; double b3.14;&#xff0c;則表達式Aab值的類型是___________。  (1)A).char B)int C) double D)float(2)若有定義語句&#xff1a;int x12,y8,z;&#xff0c;在其后執行語句z0.9x/y;&#xff0c;則z的值為___________。A)1.9 B)1 C)2 D)2.…

js題集19

1.實現斐波那契數列。達到題目中的效果。不知道斐波那契數列是啥的請自行百度。 function fibonacci(){ } var ffibonacci(); for(var i0;i<10;i){ console.log(f()); } //output:按順序輸出斐波那契數列的數字。 eg&#xff1a; 1 2 3 5 8 13 21 34 55 89轉載于:https://ww…

阿里云Maven鏡像配置

2019獨角獸企業重金招聘Python工程師標準>>> <mirror><id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> …

c語言中有12個球,數學老師做不出來的一道邏輯推理題

同志們 那個球不一定輕啊正確的是平分三份 取兩分稱if(平)。。。。。。在未稱過的4球中取兩個放左邊 和標準的球稱(稱過的球一定標準)。。。。。。if(平)。。。。。。。。。。。。在兩次都未稱過的球中取一個 和標準的稱。。。。。。。。。。。。if(平)。。。。。。。。。。。。…

WPF 實現彈幕效果

WPF 實現彈幕效果控件名&#xff1a;BarrageExample作者&#xff1a;WPFDevelopersOrg原文鏈接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用大于等于.NET40&#xff1b;Visual Studio 2022;項目使用 MIT 開源許可協議&#xff1b;此篇代碼目的只…

js題集23

1.實現函數--defaultArguments 功能如下&#xff1a; function add(a,b) { return ab;}; var add_ defaultArguments(add,{b:9}); add_(10); // returns 19 add_(10,7); // returns 17 add_(); // returns NaN add_ defaultArguments(add_,{b:3, a:2}); add_(10); // returns…

iteritems()與items()

iteritems&#xff1a;以迭代器對象返回字典鍵值對 item:以列表形式返回字典鍵值對 >>> dic {a:3,c:1,b:2} >>> print dic.iteritems() <dictionary-itemiterator object at 0x7fa381599628> >>> print dic.items() [(a, 3), (c, 1), (b, 2)…

WPF效果第一百九十八篇之模塊對比

前面效果中分享了彩色馬蹄圖的效果和范圍內拖拽;這不大假期的時間反正沒啥事就在家擼代碼;今天又是LisBox實現的效果,看最終效果:1、剛開始一朋友說用DataGrid來實現.首先把行對象轉換成列對象,至于控制列的話,就后臺重新賦值對象來控制前臺.我是覺得太費勁直接放棄了;還是首選…

android 與后臺通信,Android后臺線程和UI線程通訊實例

本節向你展示如何在任務中發送數據給UI線程里的對象&#xff0c;這個特性允許你在后臺線程工作&#xff0c;完了在UI線程展示結果。在UI線程定義一個HandlerHandler是Android系統線程管理框架里的一部分。一個Handler對象接收消息&#xff0c;并且運行代碼來處理消息。正常情況…

saltstack的狀態文件

saltstack狀態文件設定&#xff1a;編輯/etc/salt/master&#xff0c;修改其中關于“設置文件的目錄”的設置&#xff1a;說明&#xff1a;注意語法格式&#xff0c;頂格/冒號/兩個空格state_top: top.sls # The state system uses a "top" file to tell the minions…

POJ 2798:二進制轉換十六進制

很郁悶&#xff0c;這道題一直WA&#xff0c;然而本地我測了好幾組數據都是通過的&#xff0c;上網找了網友陳宇龍加油加油加油的AC的代碼&#xff0c;http://blog.csdn.net/Since_natural_ran/article/details/51742149&#xff0c;發現沒有什么不同。。。很無語。。 #include…

【Shashlik.EventBus】.NET 事件總線,分布式事務最終一致性簡介

分布式事務、CAP定理、事件總線&#xff0c;在當前微服務、分布式、集群大行其道的架構前提下&#xff0c;是不可逃避的幾個關鍵字&#xff0c;在此不會過多闡述相關的理論知識。Shashlik.EventBus就是一個基于.NET6的開源事件總線解決方案&#xff0c;同時也是分布式事務最終一…

5個超實用的Visual Studio插件

工欲善其事&#xff0c;必先利其器,整理的一些我必裝的5款Visual Studio插件&#xff0c;希望你們能get到。01 CodeMaidCodeMaid快速整理代碼文件&#xff0c;規范你的代碼&#xff0c;提高代碼閱讀體驗。代碼自動對齊&#xff0c;格式化代碼&#xff08;ps&#xff1a;不用再按…

BZOJ1509: [NOI2003]逃學的小孩(樹的直徑)

Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1126 Solved: 567[Submit][Status][Discuss]Description Input 第一行是兩個整數N&#xff08;3 ? N ? 200000&#xff09;和M&#xff0c;分別表示居住點總數和街道總數。以下M行&#xff0c;每行給出一條街道的信息。第i1行…

Blazor University (52)依賴注入 —— 擁有多個依賴項:正確的方式

原文鏈接&#xff1a;https://blazor-university.com/dependency-injection/component-scoped-dependencies/owning-multiple-dependencies-the-right-way/擁有多個依賴項&#xff1a;正確的方式在上一節[1]中&#xff0c;我們看到了將多個擁有的依賴項注入組件的錯誤方法。本節…