Learning Objectives
- 寫出函式的定義與原型,說明回傳型別、參數與回傳值
- 追蹤函式呼叫:每次呼叫產生一個新的堆疊框架,返回時框架消失
- 說明傳值呼叫:參數是引數的複本,修改參數不影響呼叫端
- 說明區域變數的作用域,以及 static 區域變數如何在呼叫之間保留值
- 使用預設引數與函式多載,並判斷呼叫會選到哪一個多載版本
為什麼要寫函式
程式變長之後,同一段計算常常要在好幾個地方用到。把它包成一個函式(function),取一個名字,需要時「呼叫」它就好:
- 不用重複寫:算平方、印分隔線這類動作寫一次,到處都能用
- 好讀:
int total = sumOfSquares(3, 4);一看就知道在做什麼,不用讀每一行運算 - 好修改:規則改變時只要改函式裡面,所有呼叫的地方都跟著更新
你從第一課就在用函式:main 本身就是一個函式,程式從它開始執行。
int square(int n) { // 回傳型別 名稱(參數列表)
return n * n; // return:把結果交回給呼叫的地方
}
- 回傳型別:函式算完交回什麼型別的值;不回傳任何值就寫
void - 參數(parameter):函式需要的輸入,每個都要寫型別
- 呼叫時放進去的值叫做引數(argument),例如
square(5)的 5
函式原型與呼叫堆疊
C++ 編譯器由上往下讀程式,呼叫一個函式之前,必須先看過它的宣告。如果想把函式的定義寫在 main 後面,
就要在前面先寫一行函式原型(prototype),也就是只有標頭、以分號結尾的宣告:
int square(int n); // 原型:告訴編譯器「有這個函式,長這樣」
int main() { cout << square(4); }
int square(int n) { return n * n; } // 定義:真正的內容
原型裡的參數名稱可以省略(int square(int);),但寫上比較好讀。
每次呼叫函式,系統會在堆疊(stack)上配置一塊新的框架(frame),放這次呼叫的參數與區域變數;函式 return 之後,框架就被移除,
程式回到呼叫的地方繼續執行。函式裡再呼叫函式,框架就一層一層疊上去。下面的動畫右邊會顯示這些框架,最上面那一個是正在執行的函式。
呼叫、回傳與堆疊框架
看右邊的框架:呼叫 square(x) 時多出一個 square 框架,參數 n 拿到 x 的值 3;return 之後框架消失,回傳值存進 y。sumOfSquares 裡又呼叫 square,這時會同時有三層框架。
傳值呼叫與作用域
C++ 預設是傳值呼叫(pass by value):呼叫時,引數的值被複製一份給參數。函式裡怎麼改參數,改的都是複本, 呼叫端的變數不受影響。
變數的作用域(scope)是「能使用這個名字的範圍」:
- 函式裡宣告的變數(包括參數)是區域變數,只在這個函式裡有效,函式返回時就消失
- 不同函式可以有同名的區域變數,它們是互不相干的兩個變數
- 一對大括號
{ }就是一個區塊,區塊裡宣告的變數離開區塊就消失(for迴圈的i也是) - 在作用域外使用變數,是編譯錯誤
想讓函式修改呼叫端的變數,要用第 8 課的指標或第 9 課的參考。
改的是複本
main 和 tryToDouble 各有一個 x,在右邊是兩個不同框架裡的兩個變數。tryToDouble 把自己的 x 改成 20,回到 main 之後,main 的 x 還是 10。
想一想:呼叫、傳值與作用域
下面的程式會印出什麼?
void addFive(int n) {
n = n + 5;
}
int main() {
int a = 1;
addFive(a);
cout << a;
}
下面的程式會印出什麼?
int twice(int n) {
return n * 2;
}
int main() {
int a = 3;
twice(a);
cout << a << twice(a);
}
下面的程式會印出什麼?
int makeTotal() {
int sum = 5;
return sum;
}
int main() {
makeTotal();
cout << sum;
}
預設引數
參數可以給預設值,呼叫時省略的引數就用預設值:
void greet(string name, string word = "你好", int times = 1);
greet("小明"); // word = "你好",times = 1
greet("Amy", "Hi"); // times = 1
greet("Ken", "嗨", 2);
規則:
- 有預設值的參數必須放在最後面。一旦某個參數有預設值,它後面的參數也都要有,例如
void f(int a = 1, int b);是編譯錯誤 - 呼叫時引數由左往右對應,不能跳過中間的:想指定
times就一定要先給word - 預設值只寫一次:有原型的話,寫在原型裡,定義時就不要再寫
本站的逐步執行器目前只認得寫在定義上的預設值,所以下面的範例把定義放在
main前面、不另寫原型。用 g++ 編譯時,預設值寫在原型、定義放在後面也可以。
省略引數時用預設值
每次呼叫時看右邊框架裡的 word 與 times:呼叫時沒有給的,框架裡出現的就是預設值。
函式多載
C++ 允許好幾個函式用同一個名字,只要它們的參數列表不同(參數個數或型別不同),這叫做函式多載(function overloading):
int area(int side); // 正方形
int area(int width, int height); // 長方形
double area(double radius); // 圓形
呼叫時,編譯器依引數的個數與型別挑出最適合的版本,這個過程叫做多載解析(overload resolution)。大致的優先順序是:
- 型別完全相符
- 只需要「提升」,例如
char、bool變成int,float變成double - 需要其他轉換,例如
int變成double、double變成int,或字串字面常數變成string
注意:
- 只有回傳型別不同不算多載,
int f(int);和double f(int);同時存在是編譯錯誤 - 如果有兩個版本一樣適合,編譯器無法決定,會報呼叫有歧義(ambiguous)的編譯錯誤。例如只有
f(int)和f(double)時呼叫f(5L)(long),轉成哪一個都需要轉換
同一個名字,依引數選版本
注意每次呼叫時,箭頭跳到哪一個 area 或 show。最後一個呼叫 show('A'):沒有 show(char),char 提升成 int 比轉成 string 更適合,所以呼叫 show(int),印出的是 'A' 的字元編碼 65。
static 區域變數
一般的區域變數在每次呼叫時重新建立、返回時消失,所以函式「記不得」上一次呼叫的事。
在區域變數前面加上 static,它就變成靜態區域變數:
- 只在第一次執行到宣告時初始化一次,之後的呼叫跳過初始化
- 函式返回後不會消失,值一直保留到程式結束
- 作用域仍然只在這個函式裡,其他函式看不到它
適合用在計數器、流水號這類「需要記住狀態、但不想讓整個程式都能改」的情況。
記得上一次的值
比較 nextTicket 的 number 和 countCalls 的 calls:每次進入 countCalls,calls 都從 0 重來;number 則接著上一次的值往上加。右邊最上面的「全域/靜態變數」區可以看到,number 在呼叫之間一直存在,不屬於任何一次呼叫的框架。
想一想:預設引數、多載與 static
下面的程式會印出什麼?
int add(int a, int b = 10, int c = 100) {
return a + b + c;
}
int main() {
cout << add(1) << " " << add(1, 2);
}
下面的程式會印出什麼?
void p(int x) { cout << "I"; }
void p(double x) { cout << "D"; }
int main() {
p(2);
p(2.0);
p('x');
}
下面的程式會印出什麼?
int next() {
static int c = 0;
c++;
return c;
}
int main() {
next();
next();
cout << next();
}
下列哪一個函式宣告無法通過編譯?
動手寫:兩個版本的 maxOf
完成兩個多載的 maxOf:兩個參數的版本回傳較大的數;三個參數的版本呼叫兩個參數的版本來找出最大的數。main 已經寫好。
重點回顧
- 函式由回傳型別、名稱、參數列表與本體組成;不回傳值用
void - 呼叫前編譯器要先看過宣告;定義寫在後面時,先寫函式原型
- 每次呼叫產生一個新的堆疊框架,放參數與區域變數;返回時框架消失
- 預設是傳值呼叫:參數是複本,修改參數不影響呼叫端
- 區域變數只在所屬的函式或區塊裡有效;
static區域變數只初始化一次,值在呼叫之間保留 - 預設引數必須放在參數列表最後面
- 函式多載:同名但參數列表不同;編譯器依引數挑最適合的版本,兩個一樣適合時是歧義錯誤