-- 簡書作者 謝恩銘 轉載請註明出處
第一部分第九課:函數
上一課是C語言探索之旅 | 第一部分第八課:第一個C語言小遊戲。
這一課我們將會用函數這個重中之重來結束《C語言探索之旅》的第一部分(基礎部分),而第二部分將要迎接我們的就是C語言的高級技術了。
第二部分會比較難哦,不過不用擔心,我們一點點學習。只要方向對,肯花時間,C語言一點也不可怕。
這一課裏我們也會給大家講C語言程序所基於的原則。
我們將要學習如何將程序分塊管理,有點像樂高積木。
其實所有C語言的大型程序都是小程序塊的集合,而這些小程序塊我們稱之爲函數。
在面向對象的語言(如Java,C++)裏面,函數又被稱爲方法,當然這裏我們只討論C語言(面向過程的語言),不討論面向對象的語言,sorry,我又廢話了... 好吧,我們只面向C語言這個對象。
函數的創建和調用
在之前的課程中我們已經學過:所有的C語言程序都是由main函數開始運行的。那時候我們也展示了一個概要圖,裏面有一些術語:
最上面的部分我們稱之爲“預處理指令”,很容易辨識,因爲以#號開頭,而且通常總是放在程序的最前面。
下面的部分就是我們要學習的函數了,這裏的示例是main函數。
前面說過,C語言的程度都是以main函數爲入口函數的。
一個C程序要運行,必須要有main函數。只不過,目前爲止我們寫的所有程序,包括上一課的小遊戲,也只是在main函數裏面搗鼓而已,我們還沒跳出過main函數過。
那你要問了:這樣不好嗎?
答案是:
並不是說這樣不好,但這並不是C程序員平時所做的。
幾乎沒有程序員會只在main函數的大括號內部寫代碼。
到目前爲止我們所寫的程序都還比較短小,但是想象一下如果程序變得很大,代碼幾千幾萬甚至上百萬行,難道我們還把這些代碼都塞在main函數裏面嗎?
所以我們現在來學習如何更好地規劃我們的程序。
我們要學習將程序分成很多小塊,就像樂高積木的每一個小塊一樣,這些小塊搭起來卻可以組成很多好玩的形狀。
這些程序小塊我們稱其爲函數。
一個函數會執行某些操作,並返回一個值。程序就是一個代碼序列,負責完成特定的任務。
我們說一個函數有輸入和輸出,如下圖所示:
可以把函數想象成一臺製作香腸的機器,在輸入那一頭你把豬裝進去,輸出那一頭就出來香腸了。這酸爽,不言而喻~
當我們在程序中調用一個函數的時候,會依次發生三個步驟:
-
輸入:給函數傳入一些信息(藉着給函數一些參數)
-
運算:因着輸入裏傳進去的信息,函數就可以完成特定任務了
-
輸出:做完運算後,函數會返回一個結果。我們稱其爲輸出或者返回值
舉個實際例子,比如我們有個函數叫做multipleTwo,作用是將輸入乘以二,如下所示:
函數的目的是爲了讓源代碼更加結構分明,也節省源代碼數目,因爲我們就不用每次都輸入重複的代碼片段而只需要調用函數就好了。
再設想一下:
之後我們可能會想要創建一個叫showWindow(顯示窗口)的函數,作用是在屏幕上顯示一個窗口。
一旦函數寫好之後(當然寫的過程是最難的),我們就只需要說:“那個誰,給我去打開一個窗口”,showWindow函數就會爲我們在屏幕上顯示一個窗口。
我們也可以寫一個displayPersonage的函數,作用是爲我們在屏幕上顯示一個遊戲人物。
函數的構成
我們在之前的課中已經接觸過函數了,就是非常重要的main函數。然而還是需要我們來介紹一下一個函數的構成到底是怎麼樣的。
下面是函數的語義學的結構,這是一個需要了解的模板:
類型 函數名(參數)
{
// 函數體,在這裏插入指令
}
關於這個模板我們需要掌握四點:
-
函數類型:對應輸出類型,也可以把其看做函數的類型。和變量類似,函數也有類型,這類型取決於函數返回值的類型。如果一個函數返回一個浮點數(帶小數點的),那麼自然我們會將函數類型定爲float或者double;如果返回整數,那麼我們一般會將類型定爲int或long。但是我們也可以創建不返回任何值的函數。
-
函數名:這是你的函數的名字。你可以給你的函數起任意名字,只要遵從給變量命名的相同的規則就好。
-
函數的參數(對應輸入):參數位於函數名之後的圓括號內。這些參數是函數要用來做操作(運算)的數據。你可以給函數傳入任意數量的參數,也可以不傳入任何參數。
-
函數體:大括號規定了函數的起始和結束範圍。在大括號中你可以寫入任意多的指令。對於上面的multipleTwo函數,需要寫入將輸入的數字乘以2的相關操作指令。
根據函數類型,函數可以分爲兩類:
-
返回一個值的函數,這樣的函數,我們將其類型定爲對應的值的類型(char,int,long,double等)
-
不返回任何值的函數,這樣的函數,我們將其類型定爲void(void在英語中是“空的,無效的”之意)
創建函數
我們不要再遲延了,馬上給出一個實例。用的還是我們上面提過的multipleTwo這個函數:
這個函數的輸入是一個整型int,輸出也是int類型的數。
int multipleTwo(int number)
{
int result = 0;
result = 2 * number; // 我們將提供的數乘以2
return result; // 我們將2倍的數返回
}
這就是你的第一個除了main以外的函數,自豪不?
return result;
這句話一般放在函數體的最後,用於返回一個值。這句話意味着:“函數你給我停下,然後返回這個值”。這裏的result必須是int類型的,因爲函數類型是int,所以返回值也必須是int類型。
result這個變量是在multipleTwo函數中聲明/創建的,所以它只能在這個函數裏面用,不能在另一個函數比如main中使用,所以是multipleTwo函數的私有變量。
但上面的代碼是不是最簡單的呢?
不是,還可以簡化,如下:
int multipleTwo(int number)
{
return 2 * number;
}
上面的代碼做的是一樣的事情,寫起來也更簡單,函數體內只有一句話。
通常來說,我們寫的函數都會有多個變量,以便做運算,multipleTwo這個函數算是相當簡單了。
多個參數,或沒有參數
多個參數
我們的multipleTwo函數只有一個參數,但是我們也可以創建有幾個參數的函數,比如下面這個加法函數addition:
int addition(int a, int b)
{
return a + b;
}
可以看到,只需要用一個逗號來分隔參數就好了。
沒有參數
有些函數,雖然不太常見,可能會沒有參數。例如一個用來顯示Hello(你好)的函數:
void hello()
{
printf("Hello");
}
如上所見,這個函數沒有任何參數,此外,可以看到我們還把函數類型定爲了void,就是空,所以也沒有return語句用於返回一個值,所以這個函數也沒有返回值。徹頭徹尾的黑五類…
調用函數
現在我們來看一個程序,複習一下我們之前學的內容。
我們要用到我們的multipleTwo函數,來計算一個數的兩倍的值。
我們暫時把multipleTwo函數寫在main函數之前,如果放在main函數之後會出錯,以後的課程我們會解釋爲什麼。
#include <stdio.h>
int multipleTwo(int number)
{
return 2 * number;
}
int main(int argc, char *argv[])
{
int initial = 0, twice = 0;
printf("請輸入一個整數... ");
scanf("%d", &initial);
twice = multipleTwo(initial);
printf("這個數的兩倍是 %d\n", twice);
return 0;
}
我們的程序是從main函數開始運行的,這個大家已經知道了。
我們首先請求用戶輸入一個整數,將其值傳遞給multipleTwo函數,並且把multipleTwo函數的返回值賦給twice這個變量。
仔細看下面這一行,這是我們最關心的一行代碼,因爲正是這一行調用了我們的multipleTwo函數。
twice = multipleTwo(initial);
在括號裏,我們將變量initial當做輸入傳遞給函數,也正是這個變量,函數將要用於其內部的處理。
這個函數返回一個值,這個值我們賦給twice這個變量。
其實這一行中,我們就是命令電腦:“讓multipleTwo函數給我計算initial的兩倍的值,並且將結果儲存到twice這個變量中”。
詳細的分步解釋
也許對於初學者,理解起來還是有些許困難。
不用擔心,我相信通過下面的分步解釋,大家會明白得更透徹。
這個特殊註釋的代碼向大家展示了程序的運行順序:
#include <stdio.h>
int multipleTwo(int number) // 6
{
return 2 * number; // 7
}
int main(int argc, char *argv[]) // 1
{
int initial = 0, twice = 0; // 2
printf("請輸入一個整數... "); // 3
scanf("%d", &initial); // 4
twice = multipleTwo(initial); // 5
printf("這個數的兩倍是 %d\n", twice); // 8
return 0; // 9
}
上面的編號表示了執行的順序:
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9
-
程序從main函數開始執行
-
在main函數中的命令一行一行地被執行
-
執行printf輸出
-
執行scanf讀入數據,賦值給變量initial
-
讀入指令... 啊,調用multipleTwo函數了,因此程序跳到上面的multipleTwo函數中去執行
-
我們運行multipleTwo函數,並接受一個數作爲輸入(number)
-
我們對number做運算,並且結束multipleTwo函數,return意味着函數的結束,並且返回一個值
-
我們重新回到main函數的下一條指令,用printf輸出
-
又一個return,這次是main函數的結束,於是整個程序運行完畢。
變量initial被傳值給multipleTwo的參數number(另一個變量),稱爲值傳遞。
當然其實原理是做了一份變量initial的拷貝,把拷貝賦值給了number,這個值傳遞的概念以後學習指針那一章會再詳述。
這裏如果我們把initial改名爲number也是可以的,並不會與函數multipleTwo的參數number衝突。因爲參數number是屬於multipleTwo這個函數的專屬變量。
測試程序
下面是程序運行起來的一個實例:
請輸入一個整數... 10
這個數的兩倍是 20
當然你不必將multipleTwo函數的返回值賦給一個變量,也可以直接將multipleTwo函數的返回值傳遞給另一個函數,就好像multipleTwo(intial)是一個變量一般。
仔細看下面這個程序,跟上面幾乎是一樣的,但是修改了最後一個printf的行爲,我們也沒有使用twice這個變量,因爲不必要:
#include <stdio.h>
int multipleTwo(int number)
{
return 2 * number;
}
int main(int argc, char *argv[])
{
int initial = 0, twice = 0;
printf("請輸入一個整數... ");
scanf("%d", &initial);
// 函數的結果(返回值)直接傳遞給printf函數,而沒有通過第三方變量
printf("這個數的兩倍是 %d\n", multipleTwo(initial));
return 0;
}
我們可以看到,這次的程序直接將multipleTwo函數的返回值傳遞給了printf函數。
當程序運行到這一行會發生什麼呢?
很簡單,電腦看到這一行是printf函數,所以調用標準輸入輸出庫的printf函數,向printf函數傳遞我們給的所有參數。
第一個參數是要顯示的語句,第二個參數是一個整數。
電腦又知道要把這個整數值傳遞給printf函數,必須先調用multipleTwo函數,所以它就乖乖地去調用multipleTwo函數,做兩倍乘法運算,並且直接把結果傳遞給printf函數。
這就是函數的層疊式調用,這樣做的好處是,一個函數可以按需調用另一個函數。
只要願意,我們的multipleTwo函數也可以再調用其他的函數,只要你肯寫,然後這個函數再調用其它函數,依次類推。
這就是C語言程序所基於的原則。所有的代碼都是有規劃地組合在一起的,類似樂高積木。
最後,最艱難的當然是編寫函數了,一旦完成,你就只需要調用它就好了,不需要太擔心函數內部所做的運算。
使用函數可以大大降低代碼的重複度,相信我,你會非常需要函數的。
一些函數的實例
如果一起學習過之前的課程,你應該會有這種印象:
我就是個“例子狂人”。
是的,因爲我很喜歡用實例來加深理解。
因爲我覺得理論雖好,但如果只有理論,那我們就不能很好地掌握知識,而且不知道怎麼應用,那就很可惜了。想起了“勁酒雖好,可不要貪杯哦”那句廣告詞…
所以下面我們會一起看幾個函數的實例,以便讀者對函數有更深入的瞭解。我們儘量展示不同情況,使大家看到可能出現的各種函數類型。
歐元/人民幣轉換
我們來寫一個函數,用於轉換歐元到人民幣。
查了一下最新的匯率:
1歐元=7.4694人民幣元
#include <stdio.h>
double conversion(double euros)
{
double rmb = 0;
rmb = 7.4694 * euros;
return rmb;
}
int main(int argc, char *argv[])
{
printf("10 歐元 = %f 人民幣\n", conversion(10));
printf("50 歐元 = %f 人民幣\n", conversion(50));
printf("100 歐元 = %f 人民幣\n", conversion(100));
printf("200 歐元 = %f 人民幣\n", conversion(200));
return 0;
}
你也可以寫一個人民幣轉換爲歐元的小程序。
懲罰
接下來看一個函數,這個函數不會返回任何值,所以類型是void。這個函數會根據傳入的參數在屏幕上顯示一定次數的信息。
這個函數只有一個參數,那就是顯示懲罰語句的次數:
#include <stdio.h>
void punish(int lineNumber)
{
int i;
for (i = 0 ; i < lineNumber ; i++)
{
printf("我不應該有錢任性\n");
}
}
int main(int argc, char *argv[])
{
punish(5);
return 0;
}
顯示結果如下:
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
矩形面積
矩形的面積很容易計算:長 x 寬。
我們來寫一個求矩形面積的函數,它有兩個參數:矩形的長和矩形的寬。返回值是矩形的面積:
#include <stdio.h>
double rectangleArea(double length, double width)
{
return length * width;
}
int main(int argc, char *argv[])
{
printf("長是10,寬是5的矩形面積是 %f\n", rectangleArea(10, 5));
printf("長是3.5,寬是2.5的矩形面積是 %f\n", rectangleArea(3.5, 2.5));
printf("長是9.7,寬是4.2的矩形面積是 %f\n", rectangleArea(9.7, 4.2));
return 0;
}
顯示結果:
長是10,寬是5的矩形面積是 50.000000
長是3.5,寬是2.5的矩形面積是 8.750000
長是9.7,寬是4.2的矩形面積是 40.740000
我們可以直接在函數裏顯示 長,寬和計算所得的面積嗎?
當然可以。這樣的情況下,函數就不必返回任何值了,函數計算出矩形面積,然後直接顯示在屏幕上:
#include <stdio.h>
void rectangleArea(double length, double width)
{
double area = 0;
area = length * width;
printf("長爲 %f 寬爲 %f 的矩形的面積是 %f\n", length, width, area);
}
int main(int argc, char *argv[])
{
rectangleArea(10, 5);
rectangleArea(3.5, 2.5);
rectangleArea(9.7, 4.2);
return 0;
}
我們可以看到,printf函數在函數體內被調用,顯示的結果和之前把printf放在main函數裏是一樣的。只不過我們用的方法不一樣罷了。
菜單
還記得之前的課程中菜單的那個例子嗎?(皇上,您還記得大明湖畔的夏雨荷麼?)
這次我們用自定義的函數來重寫一次,會更詳細和優化:
#include <stdio.h>
int menu()
{
int choice = 0;
while (choice < 1 || choice > 4)
{
printf("菜單 :\n");
printf("1 : 北京烤鴨\n");
printf("2 : 麻婆豆腐\n");
printf("3 : 魚香肉絲\n");
printf("4 : 剁椒魚頭\n");
printf("您的選擇是 ? ");
scanf("%d", &choice);
}
return choice;
}
int main(int argc, char *argv[])
{
switch (menu())
{
case 1:
printf("您點了北京烤鴨\n");
break;
case 2:
printf("您點了麻婆豆腐\n");
break;
case 3:
printf("您點了魚香肉絲\n");
break;
case 4:
printf("您點了剁椒魚頭\n");
break;
}
return 0;
}
這個程序還可以改進:
你可以在用戶輸入一個錯誤的數字時顯示一個錯誤信息,而不是直接繼續讓其點單。
總結
-
函數之間可以互相調用,因此main函數可以調用C語言系統定義好的函數例如scanf和printf等,也可以調用我們自己定義的函數
-
一個函數接受一些變量作爲輸入,我們將其稱爲函數的參數(也有空(void)參數的函數)
-
函數會用這些參數來做一系列的操作,之後會用return返回一個值(也有無返回值的函數)
第一部分第十課預告:練習題+習作
今天的課就到這裏,一起加油咯。
下一次我們學習第一部分第十課,來做一些幫助鞏固知識點的練習題吧!
有答案,但是希望大家先做完再對答案。