78 GP
[達人專欄] 跟著豬腳 C 起來:程式語言的函數,不只是算數
作者:解凍豬腳│2020-10-14 07:16:23│巴幣:1,154│人氣:4152
今天來講講 C 語言裡的函數吧!這次的篇幅有點長,建議你先把開發環境準備好,一邊看文一邊試著自己動手做。 還記得上次介紹過了數學裡的「函數」 是怎麼一回事。我們知道在研究數學的時候可以自行設計一個函數的規則,然後再把這個函數當成工具來使用,例如我們曾經提到的「攝氏-華氏溫度轉換函數」: 電腦既然最初是為了計算而生,那麼在程式設計的領域裡,自然就少不了函數的蹤影。。在 C 語言要怎麼定義函數? 我們每次試圖憑空用 C 語言寫東西出來的時候,起手式總是這樣的: #include <stdio.h> int main(void) { // code here return 0; } 在 C 語言裡定義函數的做法非常簡單,就好像宣告變數一樣。假如我們想要定義一個從攝氏溫度轉換成華氏溫度的函數,只要決定好資料型態、取好名字,然後放在 main 的上面就可以了: #include <stdio.h> float temp_c_to_f(float c) { return c*9/5+32; } int main(void) { printf("攝氏 56 度等於華氏 %f 度\n", temp_c_to_f(56)); return 0; } 因為程式通常都是從上往下讀取,所以我們得把這些函數擺在 main 的上頭,這樣 main 才能曉得你要呼叫的函數到底是個什麼東西。 我把函數取名為 temp_c_to_f。你可以從現在就開始養成好習慣,我們盡量把函數命名成可以一看就懂的,這樣當以後遇到程式碼累積了成千上百行又需要編輯、維護的情況,我們仍然能夠很快地找到我們想要的東西、馬上弄懂它在幹嘛。 定義好以後,只要在程式裡呼叫 temp_c_to_f(56) 就會得到 132.8,用起來就像數學的函數一樣(這裡的範例圖我改用 %.2f 表示顯示到小數點後第二位): 當我們呼叫 temp_c_to_f(56) 的時候,系統就會開始執行這個函數的內容,並且把這裡面的變數 c 設為 56,然後做了 56*9/5+32 的計算(得到 132.8),接著再將 132.8 這個結果回傳。當 132.8 這個值回傳回來,也就終於知道了這個 temp_c_to_f(56) 的值是 132.8。。函數的型態 我們在定義函數的時候,首先要注意到的是 float temp_c_to_f 這裡的 float。函數開頭的資料型態表示的是 temp_c_to_f(c) 函數值的型態,簡單來說它用來決定我們回傳的結果最後應該要是什麼類型。 比如說,我們知道攝氏溫度轉成華氏溫度的時候可能會有小數點,例如攝氏 56 度轉換成華氏溫度會變成 132.8 度,既然有了這樣的預期,那我們就應該使用 float 或 double 型態來回傳這個 132.8。 如果你把這個函數用 int 的格式來回傳資訊的話,那就會發生強制轉型——小數點以後的資訊都會丟失,本來計算出 132.8 這樣的結果,就會被轉成 132: 再來看看函數定義括號裡的 float c。這裡的 float c 其實跟前面同理,只是這裡的 float 負責規範 c 這個變數的型態。如果我們改用 int c 來定義函數,卻輸入 56.3 這種含有小數點的浮點數,一樣會先被強制轉成 56,以 int 的模式來計算,最後這個計算結果又強制轉成 float 才傳回來: 所以說,無論是輸入的值還是輸出的值,一個優秀的程式設計師都應該要先想好它們的可能情況,然後按照需求決定好應該用什麼型態來傳遞。 注意,這個函數的變數 c 只會在這函數裡面有效,這個概念就好像「在 if、for 等等區塊裡面宣告的變數,只在這個區塊裡有效」一樣,我們稱它為區域變數(local variable);直接在 main 外面宣告的變數,稱為全域變數(global variable),在各個函數之間都可以共用。。原型宣告 有的人就是不喜歡把 main 擺在最下面,我們其實也可以把其他函數移到下面來宣告,但在 main 以前就要預先描述好函數的大致輪廓,好讓系統有所準備,這樣的宣告方式叫做「原型宣告」: #include <stdio.h> float temp_c_to_f(float); int main(void) { printf("攝氏 56 度等於華氏 %f 度\n", temp_c_to_f(56)); return 0; } float temp_c_to_f(float c) { return c*9/5+32; } 這麼做的好處是,你可以從上半部一眼看到你的所有函數會以什麼樣的資料型態來傳遞,如果函數的原型跟你實際寫出來的函數長得不一樣,編譯器也會警告你,好讓你避免一些不必要的錯誤:。多變數函數 有的時候,某些計算也不是只有一個變數那麼單純。 假設我們想要定義一個函數用來計算手上的比特幣(BTC)和以太幣(ETH)的總價值,這時候變數就會有兩個了:比特幣的數量和以太幣的數量。 我們可以使用逗號來分隔多個自變數: double totalAsset(double btcAmount, double ethAmount) { return btcAmount*11500 + ethAmount*385; } 這樣就能透過 totalAsset(1.27, 5) 直接計算出「1.27 顆比特幣和 5 顆以太幣總共價值多少美金」了。。函數,不只是算數 不過,程式語言當中的函數並不只有這麼簡單的功能。它除了「計算」以外,也可以有「行為」的功能,甚至可以不輸入值、不回傳值,連函數的型態都省了: sayHello() { printf("hi\n"); printf("我是豬腳\n"); } 在程式設計的領域裡,由於函數不單只能被用來做純計算用途,因此「函數」也可以被稱為「函式」。實際上我們最早學到的 printf() 也是一個函式,只是它被定義在 stdio.h 這個標頭檔裡面,所以我們平常不會看到它的定義內容。 我們只要呼叫 sayHello(),就會執行函式裡面的程式碼: 不過,一般來說即使沒有任何東西,我們還是會用 void 來維持格式的統一(void 的意思就是「空」,什麼東西也沒有),否則有的函式有寫型態、有的函式沒寫型態,那多難看。 void sayHello(void) { printf("hi\n"); printf("我是豬腳\n"); } 推薦大家遇到這種狀況的時候還是要使用 void,程式碼才會漂亮易讀。 要注意的是,函式裡的程式碼在執行 return 以後就會回去執行外面的程式碼了,也就是說在 return 行為之後的程式碼不會被執行:。遞迴函數 在數學裡面,有些函數的值是參考前項的值而得成的。例如很有名的費波納契數列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610…… 除了 f(1) 和 f(2) 都是 1 以外,每一項的值都是前兩項的總和: f(3) = f(1)+f(2) = 1+1 = 2 f(4) = f(2)+f(3) = 1+2 = 3 f(5) = f(3)+f(4) = 2+3 = 5 f(6) = f(4)+f(5) = 3+5 = 8 f(7) = f(5)+f(6) = 5+8 = 13 …… 我們得到規律: 在數學上,這種以自己作為參考的函數,就稱為遞迴函數。遞迴函數在程式語言的定義跟在數學上的定義方法差不多:我們知道 f(x) = f(x-1)+f(x-2),當程式要求 f(5) 的時候,就會去呼叫 f(4) 和 f(3);想要求 f(4) 的時候,就會去呼叫 f(3) 和 f(2)……如此循環。 當然我們也不可能讓它一直無限循環下去,總不可能讓它在求 f(1) 的時候還跑去接著求 f(0) 和 f(-1) 吧?這樣不就沒完沒了了嗎? 所以,我們既然已經知道頭兩項的結果是 1 了,那我們就得在 f(1) 和 f(2) 的時候把函數值準備好,避免它無限下沉: int fibonacci(int n) { if (n==1 || n==2) { return 1; } return fibonacci(n-1) + fibonacci(n-2); } 當然也可以寫成這樣: int fibonacci(int n) { if (n==1 || n==2) { return 1; } else { return fibonacci(n-1) + fibonacci(n-2); } } 這兩種是沒有差別的,畢竟剛才提到 return 做完就會離開這次的函數計算了,在邏輯上本來就是二選一。 我們呼叫 f(5),程式就會呼叫 f(4)+f(3),然後先從 f(4) 處理,這裡的 f(4) 呼叫 f(3) 和 f(2),接著從這邊的 f(3) 優先處理,呼叫 f(2) 和 f(1),得到了 f(3) = 1+1 = 2,再回頭往上層處理…… 把呼叫順序畫成圖的話會變成這樣(左上角的小數字是順序): 不信的話我們可以做個實驗,每次函數被呼叫就 print 一行字出來,這樣我們就可以知道呼叫的順序了,可以知道它是一層一層堆起來的:。程式模組化 我們每次用 C 語言寫程式的時候總會寫到 int main(void) {...},實際上這個 main 本身就是一個函式了。C 語言把程式編譯起來的時候,會預設先找到叫做 main 的函式,然後從這個函式當作起點開始執行,這就是我們老是要定義 main() 的原因。 這些被呼叫的函式也被稱為「副程式」。我們在 main() 裡面呼叫了 sayHello(),讓電腦去執行這個函式裡的東西,執行完了才又回到 main() 繼續執行下去,這個 sayHello() 就是一個相對於 main() 的副程式。 把時常重複的動作包裝成函式是相當重要的事!假設你想設計一個會問人吃飯了沒或拉屎了沒的程式,而且決定在每次問候以前都先自我介紹,你可以這麼做: 這麼做的好處在於,當你想要修改自我介紹內容的時候,只要修改一個地方就好了: 如果不使用函式來把這些重複的行為包裝起來,程式碼就會看起來很醜又很難集中起來一起修改: 拿到現實生活來看,沒有好好運用函式來包裝各項功能的程式,就會是這副德性:。Main 函式也可以是別人的副程式 如果你寫好的程式 123.exe 被拿來呼叫,那你這個程式就相當於是別人的副程式,你在這程式的 main 函式裡面 return 的值,就會回傳到當初呼叫的來源。這聽起來有點複雜,直接做一次就知道了。 假設我們寫一個程式,讓它完成任務以後回傳一個 5,然後我們把這個程式編譯成執行檔並把執行檔取名叫做 123.exe: 然後從另一個程式呼叫 123.exe: 我們會發現 123.exe 的 main() 也可以成為別人的副程式,也可以接收到來自 123.exe 回傳的資訊。 對於一個程式設計的學習者來說,一般情況下是不會沒事把專案寫成多個 exe 檔啦(這個之後會講到),不過瞭解程式如何呼叫函式、傳遞資料,這點還是蠻重要的。將來如果你想寫的專案相當龐大,那就會需要瞭解這些常識了。 這些就是函式的基本觀念。當然關於「函式」還是有一些延伸的東西沒說到,不過至少有這些已經很夠了。這系列之後看看有什麼東西好講,我再拿出來分享吧。
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=4948064
All rights reserved. 版權所有,保留一切權利
相關創作
同標籤作品搜尋:第一次踏入墳場就上手 |從入門到入墳 |從入門到放棄 |C語言 |程式設計 |C |C++ |寫程式 |程式 |回收業者
留言 共 14 篇留言
習主席萬歲 :
一大早看到這些又想睡了
10-14 08:09
洨布丁 :
學C語言 數學 邏輯 要很強嗎?
10-14 08:14
解凍豬腳 :
邏輯好的話對於寫程式當然有好處
至於「數學」的話比較籠統,不知道你說的數學是指哪些方面
至少我認為寫基本的東西不太需要什麼複雜的數學底子,反倒是流程控制比較吃重
10-14 08:19
洨布丁 :
數學的部分就是函數跟微積分,這兩個很常用到嗎?
豬腳有推薦的C語言入門書嗎?
10-14 08:23
解凍豬腳 :
基礎不可能用到微積分,函數的觀念一定會用到
書本的話我是覺得其實不太需要,我以前開始學寫程式的時候就沒翻過半本書
10-14 08:25
解凍豬腳 :
網路上對於 C 語言大大小小的觀念都可以查得到,你可以試試看直接去 zerojudge 之類的網站做題目當益智遊戲來玩
10-14 08:26
雞塊 :
在C++遇到lambda後就回不去了(X
10-14 08:42
穴穴尼 :
我看到前面的數學公式就開始昏昏欲睡了…看來本人我完全沒有這方面的天賦 OTZ
10-14 09:25
芊芊∣ㄑㄑ :
豬腳產量極高 佩服!
10-14 09:41
解凍豬腳 :
為好久不見的動力感動得痛哭流涕 [e3]
我的小屋上一次發文頻率像現在這麼高已經是七年前的事
10-14 16:41
鄭曉麥 :
人家不懂硬體和流程啦(´・ω・`),雖然我最早學的就是 C
10-14 10:11
燒餅油條 :
每個敘述後面都有那個/n是做什麼用的
10-14 16:31
解凍豬腳 :
是「\n」,換行的意思
10-14 16:37
義式香草焗烤狐 :
突然為主攻js跟python感到感動,直接放棄型別的概念
10-14 18:56
香菇 :
一開始學C覺得根本外星人語 為了逃避它後來甚至只去修硬體相關的課
10-15 10:27
我要留言 提醒:您尚未登入,請先
登入 再留言
送出 78 喜歡 ★johnny860726 可決定是否刪除您的留言,請勿發表違反站規文字。
前一篇:[達人專欄] 微積分到底...
回創作列表 回頂端
後一篇:[達人專欄] 微分的運算 ...