第五章 深入理解函式
1.return語句
有返回值的函式中,return語句的作用式提供整個函式的返回值,並結束當前函式返回到呼叫它的地方。在沒有返回值的函式中也可以使用return語句,例如當前檢查到乙個錯誤時提前結束當前函式的執行並返回:
這個函式首先檢查引數x是否大於0,如果x不大於0就列印錯誤提示,然後提前結束函式的執行返回到呼叫者,只有當x大於0時才能求對數,在列印了對數結果之後到達函式體的末尾,自然地結束執行並返回。注意,使用數學函式log需要包含標頭檔案math.h,由於x是浮點數,應該與同型別的數做比較,所以寫成0.0。
我們定義乙個檢查奇偶性的函式不是為了列印兩個字串就完事了,而是為了根據奇偶性的不同分別執行不同的後續動作。我們可以把它改成乙個返回布林值的函式:
返回布林值的函式是一類非常有用的函式,在程式中通常充當控制表示式,函式名通常帶有is或if等表示判斷的詞,這類函式也叫做謂詞(predicate)。is_even這個函式寫的有點囉嗦,x % 2這個表示式本來就有0值或非0值,直接把這個值當作布林值返回就可以了:
函式的返回值應該這樣理解:函式返回乙個值相當於定義乙個和返回值型別相同的臨時變數並用return後面的表示式來初始化。例如上面的函式呼叫相當於這樣的過程:
當if語句對函式的返回值做判斷時,函式已經退出,區域性變數x已經釋放,所以不可能在這時候才計算表示式 !(x % 2)的值,表示式的值必然是事先計算好了存在乙個臨時變數裡的,然後函式退出,區域性變數釋放,if語句對這個臨時變數的值做判斷。注意,雖然函式的返回值可以看作是乙個臨時變數,但我們只是讀一下它的值,讀完值就釋放它,而不能往它裡面存新的值,換句話說,函式的返回值不是左值,或者說函式呼叫表示式不能做左值,因此下面的賦值語句是非法的: is_even(20) = 1;
c語言的傳參規則是call by value,按值傳遞,現在我們知道返回值也是按值傳遞,即便返回語句寫成return x;,返回的也是變數x的值,而非變數x本身,因為變數x馬上就要被釋放了。
在寫帶有return語句的函式時要小心檢查所有的**路徑(code path)。有些**路徑在任何條件下都執行不到,這稱為dead code,例如把&&和||運算子記混了,寫出如下**:
最後一行printf永遠都沒機會被執行到,是一行dead code。有dead code就一定有bug,你寫的每一行**都是想讓程式在某種情況下去執行的,你不可能故意寫出一行永遠不會被執行的**,如果程式在任何情況下都不會去執行它,說明跟你預想的不一樣,要麼是你對所有可能的情況分析得不正確,也就是邏輯錯誤,要麼就是像上例這樣得筆誤,語義錯誤。還有一些時候,對程式中所有可能得情況分析得不夠全面將導致漏掉一些**路徑,例如:
這個函式被定義為返回int,就應該在任何情況下都返回int,但是上面這個程式在x==0時安靜地退出函式,什麼也不返回,c語言對於這種情況會返回什麼結果是未定義得,通常返回不確定得值。另外這個例子中把-號當負號用而不是當減號用,事實上+號也可以這麼用。正負號是單目運算子,而加減號是雙目運算子,正負號得優先順序和邏輯非運算子相同,比加減的優先順序要高。
以上兩段**都不會生產編譯錯誤,編譯器只做語法檢查和最簡單的語義檢查,而不檢查程式的邏輯。
2.增量式開發(incremental)
增量式開發非常適合初學者,每寫一行**都編譯執行,確保沒問題了再寫下一行,一方面在寫**時更有信心,另一方面也方便了除錯。總是有乙個先前的正確版本做參照,改動之後如果出了問題,幾乎可以肯定就是剛才改的那行**出的問題,這樣就避免了從很多行**中查詢分析到底是哪一行出的問題。在這個過程中printf功不可沒,你懷疑哪一行**有問題,就插入乙個printf進去看看其中的計算結果,任何錯誤都可以通過這個辦法找出來。
盡可能復用(reuse)以前寫的**避免寫重複的**。封裝就是為了復用,把解決各種小問題的**封裝成函式。
解決問題的過程是把大的問題分成小的問題,小的問題再分成更小的問題,這個過程在**中的體現就是函式的分層設計(stratify)。
3.遞迴
如果定義乙個概念需要用到這個概念本身,我們稱它的定義是遞迴的(recursive)。
n的階乘(factorial):
factorial這個函式自己呼叫自己。自己直接或間接呼叫自己的函式稱為遞迴函式。這裡的factorial是直接呼叫自己,有時候函式a呼叫函式b,函式b又呼叫函式a,也就是函式a間接呼叫自己,這也是遞迴函式。
分析儲存空間的變化過程,隨著函式呼叫的層層深入,儲存空間的一端逐漸增長,然後隨著函式的呼叫層層返回,儲存空間的這一端又逐漸縮短,並且每次訪問引數和區域性變數時只能訪問這一端的儲存單元,而不能訪問內部的儲存單元比如當factorial(2)的儲存空間位於末端時,只能訪問它的引數和區域性變數,而不能訪問factorial(3)和main()的引數和區域性變數。具有這種性質的資料結構稱為堆疊或棧(stack)。每個函式呼叫的引數和區域性變數的儲存空間稱為乙個棧幀(stack frame)。作業系統為程式的執行預留了一塊棧空間,函式呼叫時就砸這個棧空間裡分配棧幀,函式返回時就釋放棧幀。
用數學歸納法(mathematical induction)來證明只需要證明兩點:base case正確,遞推關係正確。寫遞迴函式時一定要記得寫base case,否則即使遞推關係正確,整個函式也不正確。如果factorial函式漏掉了base case:
那麼這個函式就會永遠呼叫下去,直到作業系統為程式預留的棧空間耗盡程式崩潰(段錯誤)為止,這稱為無窮遞迴(infinite recursion)。
有乙個重要的結論就是遞迴和迴圈是等價的,用迴圈能做到的事用遞迴都能做,反之亦然,事實上有的程式語言(比如某些lisp實現)只有遞迴而沒有迴圈。計算機指令能做的所有事情就是資料訪問、運算、測試和分支、迴圈(或遞迴),在計算機上執行高階語言寫的程式最終也要翻譯成指令,指令做不到的事情高階語言寫的程式肯定也做不到,雖然高階語言有豐富的語法特性,但也只是比指令寫起來更方便而已,能做的事情是一樣多的。
遞迴是計算機的精髓所在,也是程式語言的精髓所在,我們學習在c的語法時已經看到很多的遞迴定義了,例如:
函式呼叫的語法是用實參定義的,實參使用表示式定義的,而表示式又是用函式呼叫定義的,因為函式呼叫也是表示式的一種。
if/else是用兩個子語句定義的,子語句又是用if/else定義的,因為if/else也是語句的一種。
深入理解指標函式
1.指標函式的定義 顧名思義,指標函式即返回指標的函式。其一般定義形式如下 型別名 函式名 函式引數表列 其中,字尾運算子括號 表示這是乙個函式,其字首運算子星號 表示此函式為指標型函式,其函式值為指標,即它帶回來的值的型別為指標,當呼叫這個函式後,將得到乙個 指向返回值為 的指標 位址 型別名 表...
深入理解指標函式
顧名思義,指標函式 即返回指標的函式。其一般定義形式如下 型別名 函式名 函式引數表列 其中,字尾運算子括號 表示這是乙個函式,其字首運算子星號 表示此函式為指標型函式,其函式值為指標,即它帶回來的值的型別為指標,當呼叫這個函式後,將得到乙個 指向返回值為 的指標 位址 型別名 表示函式返回的指標指...
深入理解匿名函式
從簡單的字面理解就是乙個沒有名字的函式,但是如果說它只是這樣簡單,那我也就沒有必要來說這些。對匿名函式的理解1 function 報錯 不能直接使用。對匿名函式的理解2 var a function a 1 匿名函式可以依附於乙個變數,並且這個變數名就是這個匿名函式的名字。var a functio...