?? 第13章 函數(二).htm
字號:
<P>1)int max(int a, int b);</P>
<P>2)float max(float a, int b);</P>
<P>3)double max(double a, double b);</P>
<P> </P>
<P>現在我這樣調用:</P>
<P>int larger = max(1, 2);</P>
<P>被調用的將是第1)個函數。因為參數1,2是int類型。</P>
<P> </P>
<P>而:</P>
<P>double larger = max(1.0, 2);</P>
<P>被調用的將是第……注意了!是第3)個函數。為什么?</P>
<P>首先它不能是第1)個,因為雖然參數2是int類型,但1.0卻不是int類型,如果匹配第1)函數,編譯器認為會有丟失精度之危險。</P>
<P>然后,你可能忘了,一個帶小數的常數,例如1.0,在編譯器里,默認為比較保險的double類型(編譯器總是害怕丟失精度)。</P>
<P> </P>
<P>最后,關于這兩個規則,都是在同名的函數參數個數也相同的情況下需要考慮,如果參數個數不一樣:</P>
<P>int max(int a, int b);</P>
<P>int max(int a, int b ,int c);</P>
<P>當然就沒有什么好限制了,編譯器不會傻到連兩個和三個都區分不出,除非……</P>
<P> </P>
<P><B>實現函數重載的附加規則:</B>有時候你必須附加考慮參數的默認值對函數重載的影響。</P>
<P> </P>
<P>比如:</P>
<P>int max(int a, int b);</P>
<P>int max(int a, int b ,int c = 0);</P>
<P> </P>
<P>此例中,函數重載將失敗,因為你在第二個max函數中設置了一個有默認值的參數,這將造成編譯器對下面的代碼到底調用了哪一個max感到迷惑。不要罵編譯器笨,你自已說吧,該調用哪個?</P>
<P>int c = max(1, 2);</P>
<P> </P>
<P>沒法斷定。所以你應該理解、接受、牢記這條附加規則。</P>
<P> </P>
<P>事實上影響函數重載的還有其它規則,但我們學習這些就夠了。</P>
<H4><A name=13.3.3>13.3.3</A> 參數默認值與函數重載的實例</H4>
<P><B>例五:</B>參數默認值、函數重載的實例</P>
<P> </P>
<P>有關默認值和函數重載的例子,前面都已講得很多。這里的實例僅為了方便大家學習。請用CB打開下載的配套例子工程。所用的就是上面提到例子,希望大家自已動手分別寫一個默認值和重載的例子。</P>
<P> </P>
<H3><A name=13.4>13.4</A> inline 函數</H3>
<P>從某種角度上講,inline對程序影響幾乎可以當成是一種編譯選項(事實上它也可以由編譯選項實現)。</P>
<H4><A name=13.4.1>13.4.1</A> 什么叫inline函數?</H4>
<P>inline(小心,不是online),翻譯成“內聯”或“內嵌”。意指:當編譯器發現某段代碼在調用一個內聯函數時,它不是去調用該函數,而是將該函數的代碼,整段插入到當前位置。</P>
<P>這樣做的好處是省去了調用的過程,加快程序運行速度。(函數的調用過程,由于有前面所說的參數入棧等操作,所以總要多占用一些時間)。</P>
<P>這樣做的不好處:由于每當代碼調用到內聯函數,就需要在調用處直接插入一段該函數的代碼,所以程序的體積將增大。</P>
<P>拿生活現象比喻,就像電視壞了,通過電話找修理工來,你會嫌慢,于是干脆在家里養了一個修理工。這樣當然是快了,不過,修理工住在你家可就要占地兒了。</P>
<P>(某勤奮好學之大款看到這段教程,沉思片刻,轉頭對床上的“二奶”說:</P>
<P>“終于明白你和街上‘雞’的區別了”。</P>
<P>“什么區別?”</P>
<P>“你是內聯型。”)</P>
<P> </P>
<P>內聯函數并不是必須的,它只是為了提高速度而進行的一種修飾。要修飾一個函數為內聯型,使用如下格式:</P>
<P> </P>
<P>inline 函數的聲明或定義</P>
<P> </P>
<P>簡單一句話,在函數聲明或定義前加一個 inline 修飾符。</P>
<P> </P>
<P>inline int max(int a, int b)</P>
<P>{</P>
<P> return (a>b)? a : b;</P>
<P>}</P>
<P> </P>
<H4><A name=13.4.2>13.4.2</A> inline函數的規則</H4>
<P>規則一、一個函數可以自已調用自已,稱為遞歸調用(后面講到),含有遞歸調用的函數不能設置為inline;</P>
<P>規則二、使用了復雜流程控制語句:循環語句和switch語句,無法設置為inline;</P>
<P>規則三、由于inline增加體積的特性,所以建議inline函數內的代碼應很短小。最好不超過5行。</P>
<P>規則四、inline僅做為一種“請求”,特定的情況下,編譯器將不理會inline關鍵字,而強制讓函數成為普通函數。出現這種情況,編譯器會給出警告消息。</P>
<P>規則五、在你調用一個內聯函數之前,這個函數一定要在之前有聲明或已定義為inline,如果在前面聲明為普通函數,而在調用代碼后面才定義為一個inline函數,程序可以通過編譯,但該函數沒有實現inline。</P>
<P>比如下面代碼片段:</P>
<P> </P>
<P>//函數一開始沒有被聲明為inline:</P>
<P>void foo();</P>
<P> </P>
<P>//然后就有代碼調用它:</P>
<P>foo();</P>
<P> </P>
<P>//在調用后才有定義函數為inline:</P>
<P>inline void foo()</P>
<P>{</P>
<P> ......</P>
<P>}</P>
<P> </P>
<P>代碼是的foo()函數最終沒有實現inline;</P>
<P> </P>
<P>規則六、為了調試方便,在程序處于調試階段時,所有內聯函數都不被實現。</P>
<P> </P>
<P>最后是筆者的一點“建議”:如果你真的發覺你的程序跑得很慢了,99.9%的原因在于你不合理甚至是錯誤的設計,而和你用不用inline無關。所以,其實,inline根本不是本章的重點。</P>
<P> </P>
<P>所以,有關inline 還會帶來的一些其它困擾,我決定先不說了。</P>
<P> </P>
<H3><A name=13.5>13.5</A> 函數的遞歸調用(選修)</H3>
<P>第4次從洗手間里走出來。在一周前擬寫有關函數的章節時,我就將遞歸調用的內容放到了最后。</P>
<P> </P>
<P>函數遞歸調用很重要,但它確實不適于初學者在剛剛接觸函數的時候學習。</P>
<H4><A name=13.5.1>13.5.1</A> 遞歸和遞歸的危險</H4>
<P>遞歸調用是解決某類特殊問題的好方法。但在現實生活中很難找到類似的比照。有一個廣為流傳的故事,倒是可以看出點“遞歸”的樣子。</P>
<P>“從前有座山,山里有座廟,廟里有個老和尚,老和尚對小和尚說故事:從前有座山……”。</P>
<P>在講述故事的過程中,又嵌套講述了故事本身。這是上面那個故事的好玩之處。</P>
<P> </P>
<P>一個函數可以直接或間接地調用自已,這就叫做“遞歸調用”。</P>
<P>C,C++語言不允許在函數的內部定義一個子函數,即它無法從函數的結構上實現嵌套,而遞歸調用的實際上是一種嵌套調用的過程,所以C,C++并不是實現遞歸調用的最好語言。但只要我們合理運用,C,C++還是很容易實現遞歸調用這一語言特性。</P>
<P> </P>
<P>先看一個最直接的遞歸調用:</P>
<P> </P>
<P>有一函數F();</P>
<P> </P>
<P>void F()</P>
<P>{</P>
<P> F();</P>
<P>}</P>
<P> </P>
<P>這個函數和“老和尚講故事”是否很象?在函數F()內,又調用了函數F()。</P>
<P>這樣會造成什么結果?當然也和那個故事一樣,沒完沒了。所以上面的代碼是一段“必死”的程序。不信你把電腦上該存盤的存盤了,然后建個控制臺工程,填入那段代碼,在主函數main()里調用F()。看看結果會怎樣?WinNT,2k,XP可能好點,98,ME就不好說了……反正我不負責。出于“燃燒自己,照亮別人”的理念,我在自已的XP+CB6上試了一把,下面是先后出現的兩個報錯框:</P>
<P> </P>
<P><IMG height=117 src="第13章 函數(二).files/ls13.h23.gif" width=551
border=0></P>
<P> </P>
<P>這是CB6的調試器“偵察”到有重大錯誤將要發生,提前出來的一個警告。我點OK,然后無厭無悔地再按下一次F9,程序出現真正的報錯框:</P>
<P> </P>
<P><IMG height=114 src="第13章 函數(二).files/ls13.h15.gif" width=165
border=0></P>
<P> </P>
<P>這是程序拋出的一個異常,EStackOverflow這么看:E字母表示這是一個錯誤(Error),Stack正是我們前面講函數調用過程的“棧”,Overflow意為“溢出”。整個
StasckOverflow 意思就:棧溢出啦!</P>
<P>“棧溢出”是什么意思你不懂?拿個杯子往里倒水,一直倒,直到杯子滿了還倒,水就會從杯子里溢出了。棧是用來往里“壓入”函數的參數或返回值的,當你無限次地,一層嵌套一層地調用函數時,棧內存空間就會不夠用,于是發生“棧溢出”。</P>
<P>(必須解釋一下,本例中,void
F()函數既沒有返回值也沒有參數,為什么還會發生棧溢出?事實上,調用函數時,需要壓入棧中的,不僅僅是二者,還有某些寄存器的值,在術語稱為“現場保護”。正因為C,C++使用了在調用時將一些關鍵數值“壓入”棧,以后再“彈出”棧來實現函數調用,所以C,C++語言能夠實現遞歸。)</P>
<P> </P>
<P>這就是我們學習遞歸函數時,第一個要學會的知識:</P>
<P> </P>
<P><B>邏輯上無法自動停止的遞歸調用,將引起程序死循環,并且,很快造成棧溢出。</B></P>
<P> </P>
<P>怎樣才能讓程序在邏輯上實現遞歸的自動停止呢?這除了要使用到我們前面辛辛苦苦學習的流程控制語句以后,還要掌握遞歸調用所引起的流程變化。</P>
<P> </P>
<H4><A name=13.5.2>13.5.2</A> 遞歸調用背后隱藏的循環流程</H4>
<P> </P>
<P>遞歸引起什么流程變化?前面的黑體字已經給出答案:“循環”。自已調用自已,當然就是一個循環,并且如果不輔于我們前面所學的if...語句來控制什么時候可以繼續調用自身,什么時候必須結束,那么這個循環就一定是一個死循環。</P>
<P>如圖:</P>
<P> </P>
<P><IMG height=188 src="第13章 函數(二).files/ls13.h18.gif" width=187
border=0></P>
<P> </P>
<P>遞歸調用還可間接形成:比如 A() 調用 B(); B() 又調用 A(); 雖然復雜點,但實質上仍是一個循環流程:</P>
<P> </P>
<P><IMG height=300 src="第13章 函數(二).files/ls13.h19.gif" width=220
border=0></P>
<P> </P>
<P>在這個循環之里,函數之間的調用都是系統實現,因此要想“打斷”這個循環,我們只有一處“要害”可以下手:在調用會引起遞歸的函數之前,做一個條件分支判斷,如果條件不成立,則不調用該函數。圖中以紅點表示。</P>
<P> </P>
<P>現在你明白了嗎?一個合理的遞歸函數,一定是一個邏輯上類似于這樣的函數定義:</P>
<P> </P>
<P>void F()</P>
<P>{</P>
<P> ……</P>
<P> if(……) //先判斷某個條件是否成立</P>
<P> {</P>
<P> F(); //然后才調用自身</P>
<P> }</P>
<P> ……</P>
<P>}</P>
<P> </P>
<P>在武俠小說里,知道了敵人的“要害”,就幾乎掌握了必勝的機會;然而,“遞歸調用”并不是我們的敵人。我們不是要“除掉”它,相反我們利用它。所以盡管我們知道了它的要害,事情還要解決。更重要的是要知道:什么時候該打斷它的循環?什么時候讓它繼續循環?</P>
<P>這當然和具體要解決問題有關。所以這一項能力有賴于大家以后自已在解決問題不斷成長。就像我們前面的講的流程控制,就那么幾章,但大家今后卻要拿它們在程序里解決無數的問題。</P>
<P>(有些同學開始合上課本準備下課)程序的各種流程最終目的是要合適地處理數據,而中間數據的變化又將影響流程的走向。在函數的遞歸調用過程中,最最重要的數據變化,就是參數。因此,大多數遞歸函數,最終依靠參數的變化來決定是否繼續。(另外一個依靠是改變函數外的變量)。</P>
<P>所以我們必要徹底明了參數在遞歸調用的過程中如何變化。</P>
<H4><A name=13.5.3>13.5.3</A> 參數在遞歸調用過程中的變化</H4>
<P>我們將通過一個模擬過程來觀察參數的變化。</P>
<P> </P>
<P>這里是一個遞歸函數:</P>
<P> </P>
<P>void F(int a)</P>
<P>{</P>
<P> F(a+1);</P>
<P>}</P>
<P> </P>
<P>和前面例子有些重要區別,函數F()帶了一個參數,并且,在函數體內調用自身時,我們<FONT
color=#ff0000>傳給它當前參數加1的值,作為新的參數</FONT>。</P>
<P>紅色部分的話你不能簡單看過,要看懂。</P>
<P> </P>
<P>現在,假設我們在代碼中以1為初始參數,第一次調用F():</P>
<P> </P>
<P>F(1);</P>
<P> </P>
<P>現在,參數是1,依照我們前面“參數傳遞過程”的知識,我們知道1被“壓入”棧,如圖:</P>
<P><IMG height=187 src="第13章 函數(二).files/ls13.h20.gif" width=271
border=0></P>
<P>F()被第1次調用后,馬上它就調用了自身,但這時的參數是
a+1,a就是原參數值,為1,所以新參數值應為2。隨著F函數的第二次調用,新參數值也被入棧:</P>
<P><IMG height=187 src="第13章 函數(二).files/ls13.h21.gif" width=322
border=0></P>
<P>再往下模擬過程一致。第三次調用F()時,參數變成3,依然被壓入棧,然后是第四次……遞歸背后的循環在一次次地繼續,而參數a則在一遍遍的循環中不斷變化。</P>
<P>由于本函數仍然沒有做結束遞歸調用的判斷,所以最后的最后:棧溢出。</P>
<P> </P>
<P>要對這個函數加入結束遞歸調用的邏輯判斷是非常容易的。
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -