# [APCS] 函式和指標 ###### tags: `APCS` ## 函式 **函式**是一段程式敘述的集合,並且會被給予一個**名稱**。C和C++的函式可區分為兩種: * 系統提供的標準函式 需要將函式庫以標頭檔的方式 include 進來。 * 自訂函式 可以依照使用者的需求自行建立。 ### 函式原型宣告 由於 C 和 C++ 在編譯的時候是由上而下,因此在呼叫函式前必須先看到這個函式的**宣告**(而不需要先定義),否則會發生編譯器的錯誤。宣告函式的語法如下: ```cpp= 回傳資料型態 函式名稱(引數列); ``` 其中引數列必須包含每個引數的資料型態,引數名稱可有可無。例如: ```cpp= // 兩種等價的宣告 int sum(int a, int b); int sum(int, int); ``` 如果函數不回傳值,則回傳的資料型態要寫成`void`。如果函數不接受引數,也可以在引數列寫`void`,或著什麼都不寫: ```cpp= // 兩種等價的宣告 void boo(void); void boo(); ``` 如果呼叫函式的程式碼位於函式定義主體以後,那就可以省略函式原型的宣告。但是,為了程式的可讀性,通常會建議先宣告函式,再把函式定義寫在程式的最後面。 ### 定義函式主體 函式的定義方式和撰寫一般程式碼類似: ```cpp= 回傳資料型態 函式名稱(引數列){ // 函式主體 return 回傳值; // 若回傳資料型態為void則可有可無 } ``` 在這邊的引數列就必須包含每個引數的資料型態和引數名稱。 **函式名稱**可以依個人喜好命名,但不能和保留字撞名。一般會採取全部小寫的命名慣例,最好根據函式功能取有意義的名稱,以增加可讀性。 **函式主體**是由合法指令組成,也可以加入註解來說明函式的作用。return 的回傳值型態必須和前面定義的回傳資料型態相同。 當回傳資料型態為`void`時,可以不包含`return`,也可以在需要結束函式時只寫一個`return`而不包含回傳值。 ### 例題 * 以下程式輸出為何? ```cpp= int G (int B){ B = B * B; return B; } int main (){ int A = 0, m = 5; A = G (m); if (m < 10){ A = G(m) + A; } else{ A = G(m); } printf("%d\n", A); return 0; } ``` :::spoiler 解答 50 ::: * 給定一陣列`a = {1, 3, 9, 2, 5, 8, 4, 9, 6, 7}`,以`f(a, 10)`呼叫以下函式,回傳值為何? ```cpp= int f (int a[], int n){ int index = 0; for (int i = 0; i <= n - 1; i = i + 1){ if (a[i] >= a[index]){ index = i; } } return index; } ``` :::spoiler 解答 7。這題其實是要求最大數字所在的index,而且如果有多個最大數字,回傳最後一個的index。 ::: * 給定函式如下,哪個選項是錯的? ```cpp= void A1 (int n){ F(n / 5); F(4 * n / 5); } void A2 (int n){ F(2 * n / 5); F(3 * n / 5); } void F(int x){ int i; for (i = 0; i < x; i = i = 1){ printf("*"); } if (x > 1){ F(x / 2); F(x / 2); } } ``` (A) `A1(5)`印的`*`比`A2(5)`多 (B) `A1(13)`印的`*`比`A2(13)`多 \(C\) `A1(14)`印的`*`比`A2(14)`多 (D) `A1(15)`印的`*`比`A2(15)`多 :::spoiler 解答 (C) ::: * `______`要填入什麼,才會使得這個函式回傳`m`和`n`的最大公因數? ```cpp= int fun(int m, int n){ if (n == 0){ return m; } return ______; } ``` (A) `fun(m, n % m)` (B) `fun(m % n, n)` \(C\) `fun(n % m, m)` (D) `fun(n, m % n)` :::spoiler 解答 (D)。想一下輾轉相除法就可以知道答案了。 ::: ## 指標 **指標**(pointer)可以說是初學 C 和 C++ 的大魔王關卡。指標確實是當中最難駕馭的技術課題之一,但是它同時也是讓 C 和 C++ 成為高階語言中擁有最高效能和最大彈性的關鍵。在許多人的心目中,對指標的熟稔程度甚至成為衡量一個 C 和 C++ 程式設計師功力的衡量標準。 指標的複雜度來自它對實體記憶體位置的操作,大多數的高階語言基於系統的安全性和程式的高階抽象化設計的理念,通常都不允許對實體記憶體位置進行操作,因為一旦對實體記憶體進行不當的存取,就有可能造成程式的異常當機,甚至波及系統程式的穩定性。但從另一個角度看,適當的使用指標可以讓工程師更貼近硬體(記憶體)的層次,因此將有利於設計出更節省資源、更高效能的程式。 在效率及功能不受影響的情況下,我們儘可能不使用指標,以維程式的簡易和直覺特性。但是有三種應用場合,使用指標仍然是必要的。第一是字串(string)的宣告;第二是動態記憶體配置(dynamic memory allocation);第三是函式的陣列傳遞。 一個簡單的關於變數和指標之間的關係的說明可以參考[這篇文章](https://kopu.chat/c%E8%AA%9E%E8%A8%80-%E8%B6%85%E5%A5%BD%E6%87%82%E7%9A%84%E6%8C%87%E6%A8%99%EF%BC%8C%E5%88%9D%E5%AD%B8%E8%80%85%E8%AB%8B%E9%80%B2%EF%BD%9E/)。 ### 取址運算 在進入指標之前,我們要先來知道什麼是記憶體位址。 我們知道,變數是用來儲存數值,但實際上在電腦裡面,變數要儲存在記憶體的哪個地方呢?這個問題我們可以使用`&`運算子來回答: ```cpp= printf("%p", &變數名稱); ``` 上面的這行指令,就會印出該名稱對應的變數所佔據的記憶體位址。 在一般情況下我們不太會對記憶體位址直接去做運算,因為變數名稱本身就包含有位址的資訊了,它會告訴程式該到記憶體的哪個位址去存取數值。 ### 指標是什麼? 指標的意義,說穿了,其實就只是儲存記憶體位址罷了。並沒有許多人想像得那麼複雜。 其宣告方式為 ```cpp= 資料型別 *指標變數名稱; ``` 資料型別通常是基本的資料型別,如`int`, `float`, `char`等,變數名稱與一般變數的命名法則相同,差異處在於變數名稱前加上一個星號`*`。只要在變數宣告時,在變數名稱前面加上一個星號,該變數就是指標變數,例如: ```cpp= int *p; ``` 以上的宣告具有三個意義: 1. `p`是一個指標變數 2. 一旦宣告`p`是指標變數,則`p`代表一個記憶體位址,而`*p`代表此位址的資料內容 3. `p`所指向記憶體位址內的資料的變數型態為整數`int` ![](https://hackmd.io/_uploads/B1lzMBBW3.png) 在指標宣告後,如果沒有直接指定初值(初始化),則指標指向的記憶體是未知的。我們不能對未初始化的指標進行運算,因為那會產生不可預期的後果。可以用以下方式初始化一個指標: ```cpp= int a = 10; int *ptr; ptr = &a; ``` ![](https://hackmd.io/_uploads/BkhQdrBZn.png) 在這段程式裡面,我們把一個整數`num`的位址存放在指標`p`裡面。 我們不能直接將指標變數的初始值設定為數值,這是不合法的,編譯器會報錯: ```cpp= int *p = 10; // 不合法 ``` 只透過這樣簡單的說明,我們很難明白為何要在程式中使用指標。要一直到自己去搭建鏈結串列(linked list),並明白不使用連續記憶體位址的好處時,才能了解指標的威力。 ### 多重指標 ![](https://hackmd.io/_uploads/r1wAgrSWn.png) 有了指標之後,我們當然也可以有指向指標的指標,也就是**雙重指標**;甚至是三重指標,也就是指向雙重指標的指標: ```cpp= int num = 10; // 整數 int *ptr1 = &num; // 指向整數 num 的指標 ptr1 int **ptr2 = &ptr1; // 指向指標 ptr1 的指標 ptr2 int ***ptr3 = &ptr2; // 指向雙重指標 ptr2 的指標 ptr3 ``` ### 指標和陣列 指標既然是記憶體位址,那我們如果使用`++`或`--`,是不是可以存取隔壁的記憶體呢?答案是肯定的,但在這邊的`++`和`--`可不是`+1`或`-1`那麼簡單,實際遞增或遞減的值要視指標所在位址儲存的變數型態而定: ```cpp= int num = 10; int *ptr = &num; // ptr = 0x2004 ptr++; // ptr = 0x2008 ``` 注意這邊因為整數佔據4個位元組,所以`ptr++`遞增了4單位的記憶體位址。 那麼指標跟陣列又有什麼關係呢?我們之前有說過,陣列在記憶體中是以連續記憶體的方式儲存的。在宣告陣列之後,使用到陣列變數時,會取得首元素的位址。也就是說,陣列`arr`與`&arr[0]`的值是相同的: ```cpp= int arr[10] = {0}; printf("arr :\t\t%p\n", arr); printf("&arr[0] :\t%p\n", &arr[0]); ``` 輸出結果為: ``` arr : 0x7ffdf5171e70 &arr[0] : 0x55be34205eb0 ``` 事實上,陣列索引是相對於首元素位址的位移量。也就是說,透過指標的運算,我們也可以依序存取陣列裡面的元素。在這個程式中,將陣列的首元素位址指定給`p`,然後對`p`遞增運算,每遞增一個單位,陣列相對應索引的元素之位址都相同。: ```cpp= #include <stdio.h> #define LEN 10 int main(void) { int arr[LEN] = {0}; int *p = arr; for(int i = 0; i < LEN; i++) { printf("&arr[%d]: %p", i ,&arr[i]); printf("\t\tptr + %d: %p\n", i, p + i); } return 0; } ``` 其執行結果為: ``` &arr[0]: 0x7fffa455aec0 ptr + 0: 0x7fffa455aec0 &arr[1]: 0x7fffa455aec4 ptr + 1: 0x7fffa455aec4 &arr[2]: 0x7fffa455aec8 ptr + 2: 0x7fffa455aec8 &arr[3]: 0x7fffa455aecc ptr + 3: 0x7fffa455aecc &arr[4]: 0x7fffa455aed0 ptr + 4: 0x7fffa455aed0 &arr[5]: 0x7fffa455aed4 ptr + 5: 0x7fffa455aed4 &arr[6]: 0x7fffa455aed8 ptr + 6: 0x7fffa455aed8 &arr[7]: 0x7fffa455aedc ptr + 7: 0x7fffa455aedc &arr[8]: 0x7fffa455aee0 ptr + 8: 0x7fffa455aee0 &arr[9]: 0x7fffa455aee4 ptr + 9: 0x7fffa455aee4 ``` 也可以利用指標運算來取出陣列的元素值,如以下的程式所示: ```cpp= #include <stdio.h> #define LEN 5 int main(void) { int arr[LEN] = {10, 20, 30, 40, 50}; // 以指標方式存取資料 for(int i = 0; i < LEN; i++) { printf("*(arr + %d): %d\n", i , *(arr + i)); } return 0; } ``` 那麼,如果是二維陣列呢? 事實上在記憶體裡面二維陣列就是陣列的陣列,而三維陣列則是陣列的陣列的陣列,依此類推 舉個例子來說,一個`[3][2]`的陣列代表的是有一個大小 3 的陣列裡面有大小 2 的陣列,總共佔據 6 格,參考下圖即可清楚看出,裡面的數字代表的就是程式取值時需給的二維陣列位址,橘色代表的是第一層陣列,藍色代表第二層陣列(陣列的陣列): ![](https://hackmd.io/_uploads/B127itU-n.png) 在實作上我們可以這樣存取二維陣列裡面的元素: ```cpp= int main(){ int a[3][2] = {{10, 20}, {30, 40}, {50, 60}}; int *b = a[0]; printf("a[0][0] = %d\n", a[0][0]); printf("a[0][1] = %d\n", a[0][1]); printf("a[1][1] = %d\n", a[1][1]); printf("a[2][1] = %d\n", a[2][1]); printf("*(b + 0) = %d\n", *(b + 0)); printf("*(b + 1) = %d\n", *(b + 1)); printf("*(b + 3) = %d\n", *(b + 3)); printf("*(b + 5) = %d\n", *(b + 5)); return 0; } ``` 執行結果如下: ``` a[0][0] = 10 a[0][1] = 20 a[1][1] = 40 a[2][1] = 60 *(b + 0) = 10 *(b + 1) = 20 *(b + 3) = 40 *(b + 5) = 60 ``` ### 指標和字串 我們前面提到,字串是由字元構成的特殊陣列,所以既然指標可以用在陣列上,當然也適用於字串。事實上,我們在宣告一個字串的時候,也可以使用這種方法: ```cpp= char *str = "Hello world!"; ``` 這種方式靈活很多,也很簡潔。 ### 例題 * 以下程式中,假設`a`、`a_ptr`和`a_ptrptr`都有被正確宣告,且`G()`函式呼叫時的參數為`a_ptr`和`a_ptrptr`。則`(a)`和`(b)`分別應填入什麼型別? ```cpp= void G((a) a_ptr, (b) a_ptrptr){ ... } int main(){ int a = 1; // 加入 a_ptr 和 a_ptrptr 的宣告 a_ptr = &a; a_ptrptr = &a_ptr; G(a_ptr, a_ptrptr); } ``` :::spoiler 解答 `a_ptr`是指向`int`的指標,故`(a)`應填入`int*`。 `a_ptrptr`是指向「指向int的指標」的雙重指標,故`(b)`應填入`int**`。 ::: * 以下程式輸出結果為何? ```cpp= #include <stdio.h> int main(){ int iVal = 10; double dVal = 123.45; int *piVal = NULL; piVal = &iVal; double *pdVal = &dVal; printf("%d", *piVal); *piVal = 20; printf("%d", iVal); *pdVal = 18.8; printf("%2.2f", dVal); return 0; } ``` :::spoiler 解答 10 20 18.80 ::: ## 參考 現在我們要來講一個只有C++才有的東西:**參考**(reference)。C是沒有參考的,相較於指標,參考顯得平易近人多了。參考其實是別名(alias)的意思,它不像指標記錄著變數的記憶體位置,它只是變數的別名。下面用個簡單的例子講解: ```cpp= int iValue = 2; int &iReference = iValue; cout << iReference << endl; //會印出2 cout << &iReference << endl; //會印出iValue 的記憶體位置 cout << &iValue << endl;    //會印出iValue本身自己的記憶體位置 ``` 使用參考有個很重要的好處:因為是別名,所以可以避免複製大量的變數到函數去。因為就算是pointer函數,也會複製。傳遞參考到函式裡面的這個動作,就是所謂的call by reference。 下面是一個簡單的參考函數範例: ```cpp= void fnReference(int &iValue){ iValue = iValue + 1; cout << iValue << endl; } int main(){ int iValue = 2; fnReference(iValue); cout << iValue << endl; return 0; } ``` 在上面的範例我們宣告了一個函數`fnReference`,其傳遞參數`&iValue`為參考型態。在主程式`main`中,傳遞了單純的整數變數進去。跟傳遞指標不一樣,傳遞指標的時候我們是傳記憶體位置進去。call by reference的情況,就直接傳普通的變數進去,我們在函數中把傳遞進去的參數+1,然後印出3。回到主程式以後,再印出iValue,一樣會印出3。