例1:
#include
/**
函數功能:用遞歸實現位運算加法
*/
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();
}
問題是,執行如上的程序,打印出來的數值是多少? 大家可能會覺得這個非常的弱智,即使作為小公司的筆試題來說都登不上大雅之堂。
例2:
#include
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();
}
大家看看上邊的程序,執行結果會是多少?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,這時返回了累加結果。
我們在下圖中可以看到main函數中將changestack()的返回值給num賦值的具體過程,也就是將eax的值返回給num的所在的內存地址。
——————————圖五 函數返回值的“彈棧”細則——————————-
這樣一切就有了解釋。
——————-圖六 例題一為什麼會碰巧正確的遞歸分析—————
雖然第一題的結果雖然正確,printf在讀取Add_Recursion返回值時,讀取的不是第一次遞歸調用的結果,而是第三次遞歸調用return b的結果(第三次遞歸返回時,暫存在eax寄存器中)。而在之後的遞歸返回中,湊巧eax都沒有被改變。因此這樣使用遞歸(盡管沒有在需要return的地方return)是可以得到正確結果。——————————-圖七 用內聯匯編解讀C語言的return本質—————————–
我們在遞歸函數Add_Recursion的後邊加了一條匯編代碼,讓函數結束時改變eax的值。可以看到,主函數中,將函數返回值誤認為了我們在匯編語言中設定的3.打印出了1+1=3這種謬論。
實際上,我們在編譯例題中的程序在編譯時C編譯器會提出警告
warning C4715: “Add_Recursion”: 不是所有的控件路徑都返回值
有返回值的函數,不是所有的支路都會進行返回值,如果大家把博客中的程序在更加嚴格的C++編譯器上編譯會報錯。
這只是一個很簡單的案例,也許我們會運氣好實現函數的功能,但是在進行復雜情況的樹狀甚至圖狀遞歸中,如果不確定自己是否一定能得到最終結果,請務必將每一種情況都return返回值,這樣來避免程序意外出錯。C語言的靈活性應該給我們造福,而不應該給我們的程序提供不穩定的因素。