Minghsin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

明新科大 AQI 70 24°C PM2.5 19
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 06

函式

定義與呼叫、傳值、預設引數與函式多載
Interaction實作 25 minute(s)

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。

想一想:呼叫、傳值與作用域

1.

下面的程式會印出什麼?

void addFive(int n) {
    n = n + 5;
}
int main() {
    int a = 1;
    addFive(a);
    cout << a;
}
2.

下面的程式會印出什麼?

int twice(int n) {
    return n * 2;
}
int main() {
    int a = 3;
    twice(a);
    cout << a << twice(a);
}
3.

下面的程式會印出什麼?

int makeTotal() {
    int sum = 5;
    return sum;
}
int main() {
    makeTotal();
    cout << sum;
}
第 1 次作答

預設引數

參數可以給預設值,呼叫時省略的引數就用預設值:

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)。大致的優先順序是:

  1. 型別完全相符
  2. 只需要「提升」,例如 char、bool 變成 int,float 變成 double
  3. 需要其他轉換,例如 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

1.

下面的程式會印出什麼?

int add(int a, int b = 10, int c = 100) {
    return a + b + c;
}
int main() {
    cout << add(1) << " " << add(1, 2);
}
2.

下面的程式會印出什麼?

void p(int x)    { cout << "I"; }
void p(double x) { cout << "D"; }
int main() {
    p(2);
    p(2.0);
    p('x');
}
3.

下面的程式會印出什麼?

int next() {
    static int c = 0;
    c++;
    return c;
}
int main() {
    next();
    next();
    cout << next();
}
4.

下列哪一個函式宣告無法通過編譯?

第 1 次作答

動手寫:兩個版本的 maxOf

完成兩個多載的 maxOf:兩個參數的版本回傳較大的數;三個參數的版本呼叫兩個參數的版本來找出最大的數。main 已經寫好。

你的輸出
預期輸出

重點回顧

  • 函式由回傳型別、名稱、參數列表與本體組成;不回傳值用 void
  • 呼叫前編譯器要先看過宣告;定義寫在後面時,先寫函式原型
  • 每次呼叫產生一個新的堆疊框架,放參數與區域變數;返回時框架消失
  • 預設是傳值呼叫:參數是複本,修改參數不影響呼叫端
  • 區域變數只在所屬的函式或區塊裡有效;static 區域變數只初始化一次,值在呼叫之間保留
  • 預設引數必須放在參數列表最後面
  • 函式多載:同名但參數列表不同;編譯器依引數挑最適合的版本,兩個一樣適合時是歧義錯誤
這一課完成了!
Next lesson:陣列與字串
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。