1、第一部分第七課:函數效應,分而治之
2、第一部分第八課預告:傳值引用,文件源頭
上一課《【C++探索之旅】第一部分第六課:控制流程,隨心所至》中,我們學習了條件語句和循環語句。
這兩種語句也算是算法的核心了。在更早的課程中,我們學習了變量。這些都是所有編程語言的必備元素。
這一課我們又要學習一個幾乎所有編程語言都有的極重要元素:
函數。
C++的所有程序都或多或少用到函數,到目前為止,你其實也已經用了好多次了,不過你可能只緣身在此山中,還雲深不知處。
函數的功用是:
將程序切分成更小的可重用的單元,有點像磚塊。一旦磚塊制作完畢,程序員所要做的就是用磚塊來造東西。
我們砌一堵牆,蓋一座小屋,或者摩天大樓,都需要不少磚塊。用函數構建我們的程序就似用磚塊搭建,而且如果拆除了,之後這些磚塊還可以重復使用。
慢慢你就能體會了。編程之美是需要時間去印證的,如果只遠觀而不"亵玩",沒有動手實驗,是不能進步的。
首先我們就挽起袖子,和好水泥,來學習如何制作"磚塊"(函數)吧。
函數的創建和使用
從這個課程的最初,我們已經使用了函數了。而且到目前為止都是那個函數:main函數(是我,是我,還是我...)。
這是C++程序的入口,程序是從main函數開始執行的。如下所示:
#includeusing namespace std; int main() // main函數開始,程序的開始 { cout << "Hello World !" << endl; return 0; } // main函數的結束,程序也隨之結束
以上的程序實際是從第四行開始,在第八行結束。也就是說,此程序的所有動作都是在一個函數裡完成的。我們並沒有跳轉到其他地方,沒有出這個main函數,而是按順序執行main函數中的各句指令。
既然我這樣說,聰明如你應該料想到了:我們可以寫其他的函數,把一個復雜的程序分割成相對獨立的區塊。
是的。但是為什麼要這麼做呢?
雖然說,把所有代碼都放在一個main函數裡是完全可以的,但這並不是一個好習慣。
假設我們要開發一個大型3D游戲。
大型3D游戲可是很復雜的,代碼數動辄上萬行。如果我們把這麼多代碼都一股腦兒放到main函數裡,那麼是很容易迷失在茫茫碼海中的。
比較理想的方式是在某個地方存放一小塊代碼,比如用於移動人物;另一個地方存放另一塊代碼,用於加載關卡,等等。
把代碼分成函數可以方便管理,使我們的代碼更容易被別人理解,我們自己回看時也不至於迷失了方向。
而且,假如你們是好幾個程序員一起協同開發,那麼函數可以讓你們更好地分配工作,比如某個人負責哪些功能開發,就需要寫哪些函數。
但這並不是函數的全部益處。
舉個例子:我們要計算平方根。上一課我們已經學習了怎麼做,我們使用了數學庫中的sqrt這個函數。
類似開平方根這樣常用的代碼塊,如果包裝成一個函數,那麼在不同地方使用時就不需要把相同的代碼重新拷貝一次了,只需要寫一下函數名就可以了。
所以函數使我們可以重復使用已有的代碼塊,極大地提高效率。
函數簡介
函數包含完成特定任務的一個代碼塊。它接收需要處理的數據,處理之,然後返回一個值。
可以把函數想象成一台制作香腸的機器,在輸入那一頭你把豬裝進去,輸出那一頭就出來香腸了。這酸爽。
輸入函數的數據稱為參數,而函數輸出的數據稱為返回值。如下圖所示:
你是否還記得我們上一課列出的多個數學函數中的pow函數,是用於計算次方值的。例如2的3次方,可以用pow(2, 3)來計算。
如果我們使用剛才所說的專業術語(參數,返回值),那麼pow函數的基本介紹可以如下所示:
接收兩個參數x和y
進行數學計算(x的y次方)
返回值就是計算結果
如下圖所示:
定義函數
好了,該放手一搏了。上面看了一些函數的例子,我們自己來定義一個函數吧。
首先,C++中所有的函數基本都遵循以下模板:
type functionName(arguments)
{
// Body : Instructions
}
上面的是英語的表述法,如果翻成中文就是:
類型 函數名(參數)
{
// 函數體:指令
}
關於這個模板我們需要掌握四點:
函數類型:對應輸出類型,也可以把其看做函數的類型。和變量類似,函數也有類型,這類型取決於函數返回值的類型。如果一個函數返回一個浮點數(帶小數點的),那麼自然我們會將函數類型定為float或者double;如果返回整數,那麼我們一般會將類型定為int或long。但是我們也可以創建不返回任何值的函數。
函數名:這是你的函數的名字。我們已經見過不少函數名了,比如main,sqrt,pow。你可以給你的函數起任意名字,只要遵從給變量命名的相同的規則就好。
函數的參數(對應輸入):參數位於函數名之後的圓括號內。這些參數是函數要用來做操作(運算)的數據。你可以給函數傳入任意數量的參數,也可以不傳入任何參數(例如main函數)。
函數體:大括號規定了函數的起始和結束范圍。在大括號中你可以寫入任意多的指令。
注意:
在C語言中,同一個程序裡不允許同名的函數。但是C++中允許有同名函數,稱為函數重載。只需要它們的參數不同(注意是參數不同,如果返回值不同而參數一樣不是函數重載,也不能通過編譯)。例如在C++中,int multiplication(int a, int b) 和 double multiplication(double c, double d) 就是不同的函數,盡管它們的函數名一樣,但是參數列表不一樣。但是如果定義兩個函數如下:int multiplication(int a, int b) 和 double multiplication(int c, int d),編譯是會出錯的,因為它們的參數一樣,只是返回值不一樣,是不能算不同函數的。
我們自己來定義一個函數:
int addTwo(int number) { int value(number + 2); // 在內存中申請一個int類型的"抽屜",起名叫value // 將參數中接收到的number進行加2操作 // 將加法的結果存入value裡面 return value; // 指定value為函數的返回值 }
注意:在參數的那個括號和函數體的大括號後面都沒有分號哦!
分析此函數
根據我們之前的解釋,你應該明白第一行是干什麼了吧。就是定義一個函數,名叫addTwo(英語"加2"的意思),接收一個int型參數作為輸入,操作結束之後會返回一個int型值。如下圖:
return value; 那一行是什麼意思呢?
return是英語"返回"的意思,所以這一行指令的作用就是將value作為此函數的返回值返回。addTwo的參數number就類似之前那個圖中放入香腸制造機的豬,value就類似輸出的香腸。
value的類型必須是int,因為在函數定義的第一行的返回值類型是int。
函數調用
我們的函數定義完畢,接下來該是使用它的時候了。函數的使用也有一個術語,叫做:函數調用。
#includeusing namespace std; int addTwo(int number) { int value(number + 2); return value; } int main() { int a(2), b(3); cout << "a的值是 " << a << endl; cout << "b的值是 " << b << endl; b = addTwo(a); // 函數調用 cout << "a的值是 " << a << endl; cout << "b的值是 " << b << endl; return 0; }
運行以上程序,顯示:
a的值是 2
b的值是 3
a的值是 2
b的值是 4
在調用函數addTwo之後,a的值沒有改變;b的值改變了,等於a的值加上2。
詳細解釋
現在我們來看一個程序,包含一個multipleTwo函數,用於計算一個數的兩倍的值。
我們暫時把multipleTwo函數寫在main函數之前,如果放在main函數之後會出錯,以後的課程我們會解釋為什麼。
#includeusing namespace std; int multipleTwo(int number) { return 2 * number; } int main() { int initial = 0, twice = 0; cout << "請輸入一個整數... "; cin >> initial; twice = multipleTwo(initial); cout << "這個數的兩倍是 " << twice << endl; return 0; }
我們的程序是從main函數開始運行的,這個大家已經知道了。
我們首先請求用戶輸入一個整數,將其值傳遞給multipleTwo函數,並且把multipleTwo函數的返回值賦給twice這個變量。
仔細看下面這一行,這是我們最關心的一行代碼,因為正是這一行調用了我們的multipleTwo函數。
twice = multipleTwo(initial);
在括號裡,我們將變量initial當做輸入傳遞給函數,也正是這個變量,函數將其用於內部的處理。
這個函數返回一個值,就是twice這個變量。
其實這一行中,我們就是命令電腦:“讓multipleTwo函數給我計算initial的兩倍的值,並且將結果儲存到twice這個變量中”。
詳細的分步解釋
也許對於初學者,理解起來還是有些許困難。
不用擔心,我相信通過下面的分步解釋,大家會明白得更透徹。
這個特殊注釋的代碼向大家展示了程序的運行順序:
#includeusing namespace std; int multipleTwo(int number) // 6 { return 2 * number; // 7 } int main() // 1 { int initial = 0, twice = 0; // 2 cout << "請輸入一個整數... "; // 3 cin >> initial; // 4 twice = multipleTwo(initial); // 5 cout << "這個數的兩倍是 " << twice << endl; // 8 return 0; // 9 }
上面的編號表示了執行的順序:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9
程序從main函數開始執行
在main函數中的命令一行一行地被執行
執行cout輸出
執行cin讀入數據,賦值給變量initial
讀入指令... 調用multipleTwo函數了,因此程序跳到上面的multipleTwo函數中去執行
我們運行multipleTwo函數,並接受一個數作為輸入(number)
我們對number做運算,並且結束multipleTwo函數,return意味著函數的結束,並且返回一個值。將返回值賦給twice變量。
我們重新回到main函數的下一條指令,用cout輸出
又一個return,這次是main函數的結束,於是整個程序運行完畢。
變量initial被傳值給multipleTwo的參數number(另一個變量),稱為值傳遞。當然其實原理是做了一份變量initial的拷貝,把拷貝賦值給了number。
這裡如果我們把initial改名為number也是可以的,並不會與函數multipleTwo的參數number沖突。因為參數number是屬於multipleTwo這個函數的專屬變量。
多個參數
上面的addTwo函數只有一個參數,我們也可以定義有多個參數的函數。其實之前見過的pow函數和getline函數就有兩個參數。
int addition(int a, int b) { return a+b; }
double multiplication(double a, double b, double c) { return a * b * c; }
以上所定義的兩個函數中,我們合理地偷懶了:將參數運算的結果直接作為返回值,而沒有定義例如之前在addTwo函數中用到的value這樣的變量。這樣我們的代碼也達到了最簡化,我們是鼓勵這樣做的。程序員要會"偷懶"。
上面的第一個函數addition(英語"加法,相加"的意思)接收兩個int型參數,返回它們的相加值。
第二個函數multiplication(英語"乘法,相乘"的意思)接收三個double型參數,返回它們的相乘值。
我們也可以定義參數類型不一樣的函數。例如:double different(int a, double b);
無參數
我們的函數也可以沒有參數。那麼在括號裡就不加任何東西。
例如:
string getName() { cout << "請輸入你的名字 : "; string name; cin >> name; return name; }
無返回值的函數
函數也可以沒有返回值,那麼在定義的時候要將函數返回值類型設為void(英語"虛空,無"的意思)。不用加return語句了。例如:
void sayHello() { cout << "Hello!" << endl; // 因為沒有返回值,也就沒有return語句了 }
int main() { sayHello(); // 因為函數sayHello不返回任何值 // 我們調用的時候也不會將其賦給某個變量了 return 0; }
實例
下面我們會一起看幾個函數的實例,以便讀者對函數有更深入的了解。我們盡量展示不同情況,使大家看到可能出現的各種函數類型。
歐元/人民幣轉換
最近歐元兌換人民幣的匯率還是很穩定的在7左右徘徊,為什麼當年(2009)小編出來法國時,歐元的匯率那麼高(10),現在要換人民幣卻只有7。唉,就是辣麼不逢時。
我們就來寫一個函數,用於轉換歐元到人民幣。
查了一下最新的匯率:
1歐元 = 7.0992 人民幣元
#includeusing namespace std; double conversion(double euros) { double rmb = 0; rmb = 7.0992 * euros; return rmb; } int main() { cout << "10 歐元 = " << conversion(10) << "人民幣" << endl; cout << "50 歐元 = " << conversion(50) << "人民幣" << endl; cout << "100 歐元 = " << conversion(100) << "人民幣" << endl; cout << "200 歐元 = " << conversion(200) << "人民幣" << endl; return 0; }
你也可以寫一個人民幣轉換為歐元的小程序。
懲罰
接下來看一個函數,這個函數不會返回任何值,所以類型是void。這個函數會根據傳入的參數在屏幕上顯示一定次數的信息。這個函數只有一個參數,那就是顯示懲罰語句的次數:
#includeusing namespace std; void punish(int lineNumber) { int i; for (i = 0 ; i < lineNumber ; i++) { cout << "我不應該有錢任性" << endl; } } int main(int argc, char *argv[]) { punish(5); return 0; }
顯示結果如下:
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
我不應該有錢任性
矩形面積
矩形的面積很容易計算:長 x 寬。長度乘以寬度。
我們來寫一個求矩形面積的函數,它有兩個參數:矩形的長和矩形的寬。返回值是矩形的面積:
#includeusing namespace std; double rectangleArea(double length, double width) { return length * width; } int main() { cout << "長是10,寬是5的矩形面積是 " << rectangleArea(10, 5) << endl; cout << "長是3.5,寬是2.5的矩形面積是 " << rectangleArea(3.5, 2.5) << endl; cout << "長是9.7,寬是4.2的矩形面積是 " << rectangleArea(9.7, 4.2) << endl; return 0; }
顯示結果:
長是10,寬是5的矩形面積是 50.000000
長是3.5,寬是2.5的矩形面積是 8.750000
長是9.7,寬是4.2的矩形面積是 40.740000
我們可以直接在函數裡顯示 長,寬和計算所得的面積嗎?
當然可以。這樣的情況下,函數就不必返回任何值了,函數計算出矩形面積,然後直接顯示在屏幕上:
#includeusing namespace std; void rectangleArea(double length, double width) { double area = 0; area = length * width; cout << "長為 "<< length << " 寬為 " << width << " 的矩形的面積是 " << area << endl; } int main() { rectangleArea(10, 5); rectangleArea(3.5, 2.5); rectangleArea(9.7, 4.2); return 0; }
我們可以看到,cout在函數體內被調用,顯示的結果和之前把cout放在main函數裡是一樣的。只不過我們用的方法不一樣罷了。
菜單
我們來寫一個餐館菜單的例子。
#includeusing namespace std; int menu() { int choice = 0; while (choice < 1 || choice > 4) { cout << "菜單 :" < > choice; } return choice; } int main() { switch (menu()) { case 1: cout << "您點了北京烤鴨" << endl; break; case 2: cout << "您點了麻婆豆腐" << endl; break; case 3: cout << "您點了魚香肉絲" << endl; break; case 4: cout << "您點了剁椒魚頭" << endl; break; } return 0; }
這個程序還可以改進:你可以在用戶輸入一個錯誤的數字時顯示一個錯誤信息,而不是直接繼續讓其點單。
總結
函數包含具有特定目的的一段代碼。
每個C++程序至少有一個函數:main函數。這是程序的入口。
把程序切分為有特定作用的各個函數是管理代碼的好方式。
程序中,我們可以多次調用同一個函數。
一個函數可以接收數據(通過參數),也可以返回數據(通過return)。
今天的課就到這裡,一起加油吧!
下一課我們學習:傳值引用,文件源頭