第 14 講:函數(shù)
What is 函數(shù)?
學(xué)過數(shù)學(xué)的人都知道,數(shù)學(xué)有一種函數(shù)表示方法,叫 ?的東西,其中等號右邊應(yīng)當(dāng)填入一個表達(dá)式(expression)。
C 語言和數(shù)學(xué)的函數(shù)差不太多,不過 C 語言是為編程服務(wù)的,所以 C 語言可能還會廣義化數(shù)學(xué)上的函數(shù)。C 語言里,函數(shù)其實是“一個過程”,這個“過程”下會執(zhí)行一系列的操作,比如輸入輸出、循環(huán)等等。不過和數(shù)學(xué)相同的地方是,C 語言函數(shù)在執(zhí)行完畢這一系列語句時,會有一個函數(shù)值(返回值),這個值就是這個“過程”最后的執(zhí)行結(jié)果。比如說 f(x) = x^2 + 2x + 3 的式子,我們可以這么去寫C語言。
int main(void)
前,還有一段代碼獨立于它而存在。它寫成 int f(int x)
,而里面只有一句話,是用類似于 main
里的 return 0;
差不太多的寫法,也用了 return
因為它和數(shù)學(xué)不同,定義域和值域不像數(shù)學(xué)那樣,實數(shù)范圍包含整數(shù)和小數(shù),人可以靈活處理,電腦不能,所以電腦的定義域內(nèi)整數(shù)和小數(shù)是區(qū)分開的,因此必定要為變量名和函數(shù)值先定下一個取值范圍,再來寫操作。定義定義域和值域時,即寫出操作過程之中,定義域的取值范圍(我們一般用整數(shù),所以 int
足夠了);而值域,即函數(shù)值,一定根據(jù)剛才的計算公式,得到的是也應(yīng)是 int
,所以還是 int
。所以,分別寫在對應(yīng)位置上。
然后你對照這個寫法和剛才 f(x) = x^2 + 2x + 3,你就會發(fā)現(xiàn),其實 return
語句下就寫的是這個表達(dá)式。這是因為,return
語句專門用于獲取函數(shù)的函數(shù)值(返回值)。因為這個數(shù)學(xué)函數(shù)在編程里面肯定只有這樣一個操作(得到數(shù)值結(jié)果),所以在 C 語言里,f
函數(shù)里只有 return
這一句話。
然后在 main
里,我們要使用這個函數(shù)功能的話,就可以這么寫。
f
函數(shù)了,但是因為那個函數(shù)最后是一個結(jié)果,所以我們可以直接當(dāng)成一個數(shù)來寫進(jìn)printf
之中,輸出這個數(shù)。我們想要把這個手動輸入的i當(dāng)成自變量放進(jìn) f
這樣要好理解一些,不過其實都一樣。
當(dāng)然,我們可以這么去想這個問題:輸入一個值,然后得到 ?f(x)
函數(shù)值,然后輸出結(jié)果。這樣的三個部分的操作,都可以同時丟進(jìn) C 語言的這個 f
函數(shù)之中。數(shù)學(xué)就不可以,C 語言就可以,而這就是因為,C 語言里的函數(shù)是執(zhí)行一系列的操作,不一定只求一個值就完了,不過,這樣執(zhí)行輸入輸出了,也就不必再去糾結(jié)它程序結(jié)果的函數(shù)值了(因為結(jié)果已經(jīng)輸出了,根本就不需要再單獨給這個程序一個函數(shù)值),這樣的“只執(zhí)行一系列語句”的函數(shù),我們可以為值域部分寫上 void
一詞(void
一詞就是空白、沒有的意思)。
看看這個程序和之前的程序,不同之處有三個:第一是 int f(int x)
改變?yōu)榱?void f()
;第二是最后末尾的 return
語句什么結(jié)果都沒有,只是一個單獨的 return
單詞;第三是 main
之中,使用 f
s函數(shù)時的寫法,括號是空的,啥都沒有,然后就直接分號結(jié)尾了。
至于第一點,是因為我們剛才說到,它執(zhí)行的是一系列的操作,而結(jié)果已經(jīng)在中間輸出過了,所以不需要對末尾再寫出函數(shù)值,所以為這種函數(shù)用 void
。至于括號里面為啥也沒有數(shù)值了呢?因為括號不需要數(shù)值了,因為輸入的數(shù)據(jù)值已經(jīng)在函數(shù)里面體現(xiàn)了,何必非要寫在上面括號里面去呢,這個函數(shù)功能根本就不需要用到這個 x
了,所以為了表示它是函數(shù),就只有一個空的括號而已;
第二點是因為,函數(shù)既然沒有函數(shù)值結(jié)果了,但因為要結(jié)束這個函數(shù),所以需要 return
作為結(jié)尾,表示函數(shù)執(zhí)行完畢。當(dāng)然了,這種末尾直接結(jié)束的函數(shù),是可以不寫這個 return;
語句的。但如果你想中途結(jié)束程序,就必須加上這句話,來表示在某個中間執(zhí)行結(jié)果下就結(jié)束這個函數(shù)的運行;
第三點是因為,剛才我們是當(dāng) f
函數(shù)是一個數(shù)值來處理的。但是現(xiàn)在 f
函數(shù)只是一個執(zhí)行輸入、求結(jié)果和輸出三個操作的過程,所以要使用它直接寫上去就可以了。
參數(shù)的定義
在 C 語言之中,我們稱函數(shù)使用的小括號內(nèi)的變量(可以有一個,也可以有很多個)叫參數(shù)。這一點來說,它和數(shù)學(xué)不太一樣,數(shù)學(xué)里的參數(shù)往往指的是函數(shù)表達(dá)式里不同于自變量的數(shù)值(如 ?之中的字母
a
、b
、c
)。
這里要提兩個定義,考試要考的:
形參(Parameter):函數(shù)的第一句話里,小括號內(nèi)的變量;
實參(Argument):函數(shù)內(nèi)使用形參的這些變量。
理解也很好理解。
小括號里寫 void
?
最后需要你注意的是,在小括號里也要寫 void
,這是為什么呢?
如果我們知道這個函數(shù)不應(yīng)該有參數(shù),那么就在小括號里寫上 void
關(guān)鍵字來告訴編譯器,這個函數(shù)不需要任何參數(shù)。如果你不寫它,往往程序也可以正常執(zhí)行,但少見有些情況會出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問題和 bug,導(dǎo)致很嚴(yán)重的錯誤。
所以為了安全起見,我們習(xí)慣在不需要參數(shù)的函數(shù)里,小括號里添加 void
關(guān)鍵字。
main
函數(shù)剖析
main
究竟是什么東西
我們經(jīng)常聽到老師提起“main
函數(shù)”這個說法。它其實也是一個函數(shù)。理解起來也很容易:因為它執(zhí)行的是一系列的操作。不過它更為特殊一些:所有 C 語言程序都需要從 main
的第一句開始執(zhí)行。注意哈,這里說的是“執(zhí)行”,后面會告訴大家,編譯(從 C 語言翻譯為機器識別的二進(jìn)制代碼串的過程)則是從最上面第一句開始的,跟 main
無關(guān)。
main
的返回值
那么,為什么 main
它也有函數(shù)值?main
有函數(shù)值的原因在于,為了大家方便查 bug 才搞的機制。讓程序完整無誤不出錯地走一遍,默認(rèn)為 0,表示沒錯;但如果中途出現(xiàn)某種錯誤了,比如沒有賦值就使用變量啊這種,就會報錯,這個時候程序會直接終止 main
,而不是調(diào)過錯誤的語句繼續(xù)往下走。這個時候,終止了 main
必然會直接得到返回值(函數(shù)值),這個時候為了方便查錯,我們就把這樣可以導(dǎo)致同一類出錯情況的返回值給它規(guī)定成同一個返回值(函數(shù)值),這樣開發(fā)人員就不會很頭痛,從頭開始找錯。
你需要記住一點,DevCpp 軟件在沒有為變量賦值時就使用變量時,
main
的函數(shù)值為 3221225477。
main
的參數(shù)
另外,main
函數(shù)里有時候有一坨東西,有時候又沒有,是什么情況。
int main();
int main(int argc, char *argv[]);
我們經(jīng)??吹竭@兩種寫法。第一種表示 main
沒有參數(shù);第二種則是有兩個參數(shù)。這兩個參數(shù)是什么呢?其實,這兩個參數(shù)指的是你在使用 cmd 命令行對程序操作時,產(chǎn)生的參數(shù)。
比如我們在使用 Linux 下的命令:rm
(remove)命令時,總會在網(wǎng)上看到資料說:
或者
有時候還可以省略這個星號。先不管是什么東西。我們來說明一下。
rm
是這個命令的名字(它實際上是一個程序名,功能是刪除文件),而我們習(xí)慣性寫上的 -rf
參數(shù),其實是刪除文件需要的參數(shù)。-rf
表示遞歸刪除文件,且不保留痕跡。所以這里的 -rf
其實就是在執(zhí)行這個 rm
程序在進(jìn)入 main
里的參數(shù),-rf
是第二個參數(shù)的數(shù)值。
為了達(dá)到程序執(zhí)行的功能,有些時候會在調(diào)用程序和進(jìn)入 main
前就要傳遞一系列的參數(shù)進(jìn)去。這個時候我們可以為 main
設(shè)定這兩個參數(shù),就可以達(dá)到效果;第一個參數(shù)表明在執(zhí)行命令行時,參數(shù)的個數(shù);第二個則是參數(shù)的具體寫法。目前你知道這個用途就可以了,因為參數(shù)的后者 char *argv[]
是一個字符串?dāng)?shù)組,這一點在這里闡述是超綱的,所以不在這里敘述。
函數(shù)的定義
聲明格式
當(dāng)函數(shù)放在 main
下的時候,在編譯時,因為 main
會從上到下執(zhí)行語句,但執(zhí)行過程之中,發(fā)現(xiàn)有些東西沒有聲明,自然就不能用。比如這樣:
printf
時,發(fā)現(xiàn) f
是前文里沒有看到過的函數(shù),這個時候就會給出警告。所以函數(shù)的聲明就需要放在執(zhí)行 main
前面(即 #include
命令和 int main(void)
甚至可以寫在執(zhí)行語句前一句處:
都可以。
聲明函數(shù)的原理
從原理上講,函數(shù)的聲明是什么東西呢?C 語言是一種面向過程的編程語言(Process Oriented Programming,簡稱 POP),這種編程語言就是按照流程一步一步執(zhí)行即可,無需考慮其它因素。但為了代碼書寫的方便,C 語言為我們提供了兩種可定義和聲明的元素:變量(含數(shù)組變量以及后面要說到的指針變量)和函數(shù)。變量和函數(shù)的相似之處就是都需要定義出來才能使用(即先定義后使用)。像是 C 語言這樣,如果我們嘗試把其它的函數(shù)寫在 main
之后,按照順次執(zhí)行,就會遇到無法解析的函數(shù)名稱,于是我們肯定需要聲明(也叫定義)這個函數(shù)了。
那么,聲明格式和函數(shù)名一致,是為了保證簡單易懂。把函數(shù)名寫在中間,左邊寫返回值,右邊寫參數(shù),并用小括號把所有參數(shù)括起來,這樣便是一個完整的函數(shù)變量的定義。所以實際上,從這個角度出發(fā),函數(shù)也可以被視作一種特殊的變量類型。不過它和普通變量不同的是,它往往寫在最外面(全局范圍),一般不寫在函數(shù)執(zhí)行邏輯里,是因為這樣書寫,就不好看到它們。
函數(shù)內(nèi)的變量
變量的生命周期和函數(shù)的銷毀
變量分為多種:外部變量、內(nèi)部變量(函數(shù)內(nèi)變量)等。它們各自有不同用途,所以會被放在不同的位置。不過,要知道,它們的生命周期也不同。
變量會在函數(shù)用完時被銷毀,main
也是一樣。如果程序執(zhí)行完畢時,即 main
也執(zhí)行完畢,里面的所有變量都會直接蒸發(fā),變得不存在。其他函數(shù)也是。所以我們來看這個函數(shù):
很顯然,這個寫法是交換兩個數(shù)據(jù)用的。但是函數(shù)在執(zhí)行完后,變量就會不復(fù)存在。所以交換完后,變量就會消失,使得交換之后的結(jié)果也不能繼續(xù)使用。所以,換句話說:
輸出語句其實是沒有任何變化的,即兩處 printf
函數(shù)的輸出結(jié)果都是 a = 2
、b = 3
。這一點要引起額外注意。這是因為,在執(zhí)行交換函數(shù) swap
后,它僅交換了在 swap
函數(shù)里的 a
和 b
變量,而并沒有對 main
函數(shù)里的 a
和 b
作出任何影響,因為函數(shù)傳遞數(shù)值的模式是值傳遞,即只會把數(shù)值內(nèi)容復(fù)制一份然后丟到 swap
里來用,所以實際上它交換的其實是復(fù)制后的副本,而真正的元素卻沒有發(fā)生改變。在交換后,函數(shù)被銷毀,交換的副本也被同時銷毀,所以原本的變量根本就沒有發(fā)生任何的變化。
另外,如果把變量寫在函數(shù)外部的話,這樣的變量在程序 main
執(zhí)行的過程之中,只要 main
沒有銷毀(程序沒有結(jié)束),變量就會一直存在。
最后,還有一個冷門的知識點。
大括號封閉變量
雖然是同一個變量名字,但變量不同,并且有一個變量是用大括號括起來的,雖然這個大括號看起來好像不知道到底是什么用途。這里的大括號只有一個用:封閉變量。這個大括號內(nèi)部的變量外部是無法使用的,而內(nèi)部的輸出語句也只是會找到最近的變量名作輸出。所以就是 2 了;而外部的輸出語句輸出的 a
,由于大括號封閉變量的關(guān)系,內(nèi)部的 2 完全不可能被外部的這個輸出語句的 a
看到,所以根本不可能是 2,所以是相對于這個內(nèi)部的 a
外面的這個 4。
這告訴了我們,之前用于分割語義的大括號,它的原理到底是如何的。
一些修飾函數(shù)內(nèi)成員和參數(shù)的修飾符
static
修飾符
在函數(shù)里對一些普通變量修飾 static
修飾符,可以讓程序在反復(fù)調(diào)用這個函數(shù)時,這個變量都只會初始化一次,后續(xù)都直接用這個變量,而且函數(shù)被銷毀的時候,這個變量也不會被同時銷毀。
如果調(diào)用這個函數(shù)的話:
那么,首次 z
會被初始化為 10,隨后執(zhí)行 z += 3
使之變?yōu)?13,然后返回 13 + i
。每一次 i
傳入的數(shù)值都不同,第一次是 0,第二次是 1,第三次是 2;而使用 f
函數(shù)時,z
第一次是 10(后自增 3,變?yōu)?13),第二次是 16,第三次則是 19。所以三次的輸出結(jié)果是 13 17 21
。
auto
修飾符
實際上,所有在函數(shù)里聲明的變量,都是 auto
的,也就是說,你不寫它們,都會被默認(rèn)添加這個修飾符。這種修飾符表示這個變量在棧內(nèi)存進(jìn)行分配。所謂的棧內(nèi)存分配,就是跟著函數(shù)走。函數(shù)死亡,它就死亡,你就無法再使用里面的資源了。
如果你不想讓它消失,請在函數(shù)里對這個變量使用 static
修飾符,表示它不跟著函數(shù)銷毀而銷毀。
register
修飾符
這個修飾符雖然現(xiàn)在說還為時尚早,但這個修飾符并不需要任何的語法依托。它表示這個變量將在執(zhí)行期間直接丟到寄存器里。這個操作就為了一個字:快。我們都知道,程序的分配的變量都是在內(nèi)存里的,而執(zhí)行這些程序,還是得被提取出來,丟進(jìn) CPU 里挨著執(zhí)行它們。如果我們最初就把它們丟進(jìn)了寄存器里,由于寄存器就在 CPU 里,所以這樣執(zhí)行起來,就會比其它變量算得更快。所以,我們?nèi)绻堑每紤]性能的話,我們會把常用的簡單類型變量直接丟進(jìn)寄存器里,方式就是添加 register
修飾符在定義語句之前就可以了。