--- title: 'C++ 函數、暫時變數、Reference' disqus: kyleAlien --- C++ 函數、暫時變數、Reference === 真正程式設計來自,STL & BOOT C++ 函式庫的結合,C 語言相關基礎請看 [**指標 & Array & typedef**](https://hackmd.io/bzOkHGC_TDGN-RF4lK18Pw?view#%E6%8C%87%E6%A8%99-amp-Array-amp-typedef) ## OverView of Content [TOC] ## 函數 ### 函數呼叫 ```c= int a = 0; void func_B(int v) { a = v - a; } void func_A() { func_B(10); printf("a value: %d\n", a); } ``` * 當呼叫 func_A() 時,電腦會如何操作 1. 儲存進入函數前的下一個的記憶體位置 ( Line 9 ) 2. Copy 引數 10 的值到堆疊 3. 跳到 func_B(int) 的記憶體位置,並處理程序 4. 回到先前儲存的記憶體位置 ( Line 9 ) ### 函數回傳值 * 與一般 Java 語言相同 ```cpp= #include <iostream> using namespace std; int sum(int a, int b) { return a + b; } int main() { int res = sum(1, 2); cout << res << endl; return 0; } ``` :::warning * C++ 特點,不能回傳 Array 類型,可以使用 ptr 替代 ```cpp= int array[6] = {0, 1, 2, 3, 4, 5}; int[6] returnArray() { return array; } ``` > ![](https://i.imgur.com/NJHI03r.png) ::: :::success * 函數回傳值 回傳值複製到 CPU 的暫存器中 or 記憶體中,然後呼叫程式才會檢查此記憶體位置的數值,在賦予到 Stack 中 ::: ### 函數原型 - Prototype * 函數原型是幫助 Compiler 編譯用,功能有 ^1.^ 判斷函數入參、^2.^ 判斷函數回傳值 ```cpp= // 1. 入參兩個 int 類型 // 2. 回傳 int 類型 int sum(int a, int b); ``` * 為何需要 函數 Prototype ? 1. 效率,否則就須要將所檔案都遍歷並收集所有函數 2. 函數可能不在自身檔案中,也就是說如果使用 lib 會導致找不到函數,這時使用函數原型,就可以確保該函數會被實現 :::info * 如果每有引數 (入參) 可以使用 `(void)` 替代,如果有引數,但不確定引數長度,則可以使用 `(...)` 替代 > 像是 `printf` 函數 ::: ### 函數引數 * 呼叫函數時,傳入的數值是 `實際引數 (actual argument)`,當真正進入函數時,函數接收到的參數又稱為 `形式引數` 實際引數進入形式引數時,是經過 **複製**,**函數接收到的參數只是副本 !** ```cpp= int sum(int a, int b) { // a、b 是形式引數 return a + b; } int main() { int A = 1, B = 2; // A、B 是實際引數 int res = sum(A, B); cout << res << endl; return 0; } ``` > ![](https://i.imgur.com/O63yevU.png) ## 函數 & Array * 當函數接收一個 Array 類型的參數時,其實 **接收到的是一個指標,並不是實際整個 Array 的複製**,所以如果改變該指標指向的內容,原來的數據也會一起更改 > 可以使用 const 來避免資料被修改 ```cpp= #include <iostream> using namespace std; void printArray(int array[], int len) { for(int i = 0; i < len; i++) { if(i == 2) { array[i] = 3333; } cout << array[i] << endl; // [] 符號相對來說就是 + i 再取值 } } int main() { int array[5] = { 1, 2, 3, 4, 5 }; printArray(array, 5); return 0; } ``` > ![](https://i.imgur.com/mTKwygr.png) ### Array & Pointer * 在函數內,我們也可以使用指標來操控該陣列,**結果相同**;**But~ array 不可以代替指標** 1. 接收形參為 array,使用 ptr 操控 ```cpp= void printArrayByPtr(int array[], int len) { cout << "printArrayByPtr" << endl; for(int i = 0; i < len; i++) { if(i == 2) { *(array + i) = 3333; } cout << *(array + i) << endl; } } ``` > ![](https://i.imgur.com/edmtAn6.png) 2. 接收形參為 ptr,使用 ptr 操控 ```cpp= void printArrayByPtr_2(int* array, int len) { cout << "printArrayByPtr_2" << endl; for(int i = 0; i < len; i++) { if(i == 2) { *(array + i) = 3333; } cout << *(array + i) << endl; } } ``` > ![](https://i.imgur.com/WUuu9gU.png) :::info * Function 內參數的形參為 指標的第一個元素,但有幾個點需要注意 1. 陣列宣告時,用陣列名稱宣告記憶體 ```cpp= int main() { int array[5] = { 1, 2, 3, 4, 5 }; cout << "array: " << array << endl; cout << "&array[0]: "<< &array[0] << endl; } ``` > ![](https://i.imgur.com/XFrmezh.png) 2. 使用 sizeof 關鍵字來計算陣列大小,代表了整個陣列的大小 (`Byte 為單位`) ```cpp= int main() { int array[5] = { 1, 2, 3, 4, 5 }; cout << "sizeof(array): "<< sizeof(array) << endl; cout << "sizeof(&array): "<< sizeof(&array) << endl; } ``` > ![](https://i.imgur.com/HivsHiB.png) 3. **如果使用 `&` 配合 `陣列宣告時的 Symbol`,代表的是整個陣列的地址**,所以 `&陣列 + 1` 是加了整個陣列的大小 ```cpp= int main() { int array[5] = { 1, 2, 3, 4, 5 }; cout << "&array: "<< &array << endl; cout << "array + 1: "<< array + 1 << endl; // 指標前進一個 cout << "&array + 1: "<< &array + 1 << endl; // 加 20 Byte } ``` > ![](https://i.imgur.com/wpRvsI9.png) ::: * 函數中陣列的大小計算:傳入函數後的陣列形參,它代表的是一個陣列的 Header 指標,不能使用 sizeof 算出整個陣列的大小,**所以陣列大小必須另外傳入** ```cpp= void printArray(int array[], int len) { // 只能算出指標 int* 的大小 cout << "sizeof(array): "<< sizeof(array) << endl; for(int i = 0; i < len; i++) { if(i == 2) { array[i] = 3333; } cout << array[i] << endl; } } ``` > ![](https://i.imgur.com/uSIrSNM.png) ### 二維 Array & Pointer * 二維陣列的第二個元素必須要給予值,基本型態為 `int *[<長度>]`,並且同樣可以使用指標操控 ```cpp= #include <iostream> using namespace std; int sum(int array[][3], int size) { int res = 0; for(int i = 0; i < size; i++) { for(int j = 0; j < 3; j++) { res += *(*(array + j) + i); // 使用 指標 操控 // res += array[i][j]; // 一般 Array 寫法 } } return res; } int main() { int array[][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; cout << sum(array, 3) << endl; return 0; } ``` > ![](https://i.imgur.com/V0nifu0.png) ## Pointer ### Pointer & Const 這邊就不再提及 Pointer & Const 的一般情節,來看看 C++ 中 Pointer & Const 的差異之處 ```cpp= // 一般的三種情況 int value = 200; const int *pV = &value; // 指向值不可改 int const * pv = &value; // 同上 int * const pv = &value; // 指標指向不可改 const int * const pv = &value; // 指標指向不可改、值也不可改 ``` * **C++ 禁止將非 const 指標,指向 const 數值** (可以使用 `const_cast` 運算子改變) ```cpp= void invalidConst() { const int value = 200; int * pV = &value; cout << *pV << endl; } ``` > ![](https://i.imgur.com/axOORgK.png) :::info C++ 對於 const 的檢查會比較嚴格,但這也是一個好處,避免資料被意外修改 ::: ### Pointer & Function * 只要知道函數的原型後,就可以透過指標,指向該類型函數 函數指標基本使用 ```cpp= #include <iostream> using namespace std; int sub(int, int); // 函數原型宣告 int sub(int a, int b) { return a - b; } int main() { int (*method)(int, int); method = sub; // 兩種寫法都可以 cout << "method(10, 4): " << method(10, 4) << endl; cout << "*method(50, 4): " << (*method)(50, 4) << endl; return 0; } ``` > ![](https://i.imgur.com/0Q1DUam.png) * C++ 有另外一個常用的特性,就使用區域變數的 **auto 自動推導**,auto 也可以使用在 函數指標 ```cpp= int main() { auto method = sub; // 自動推導類型 cout << "method(10, 4): " << method(10, 4) << endl; cout << "*method(50, 4): " << (*method)(50, 4) << endl; return 0; } ``` :::danger * 自動推導只能跟單一值賦予使用,不能推倒超過一個值 ::: ## C++ 特色函數 C++ 有 C 語言中沒有的特色函數類型 ### 內嵌函數 - inline function * 一般函數在呼叫時會進入靜態 Stack 中,過程如下 1. 將當前位置 PC 紀錄到 LR 暫存器中,LR 原本的數值進入 SP 暫存器 2. 複製函數需要的實際引數到 Stack 上 3. 執行函數 4. SP Pop 出上一個指令位置到 LR,而 LR 原本數據給予 PC 暫存器,PC 繼續執行函數位置計數 * 從一般的函數呼叫可看得出來,記憶體的 **跳轉會導致系統負擔**,而內嵌函數則不需跳躍記憶體,但 **inline 要付出更多的記憶體當代價** > **inline 最好使用在程式碼較少的地方** (建議 1 ~ 2 行) * inline 是否適用,這由編譯器自己判斷,如果它覺得該 **函數過於龐大,不符合效益它就會使用記憶體轉跳** > sayHelloWorld() 這個 func 會**複製到每個呼叫它的地方** > > ![](https://i.imgur.com/5GZWyJ7.png) ```cpp= #include <iostream> using namespace std; inline int sum_3(int a, int b, int c) { return a + b + c; } int main() { cout << sum_3(1, 2, 3) << endl; return 0; } ``` > ![](https://i.imgur.com/nSyr5n6.png) ### Reference - 變數 :::success 關於 Reference 可以想做一個變數、對象的別名,它並 **不會產生一個新的地址**,這與指標相向,但又不全等於指標 ::: * 如果用在變數,不管是參數還是變數,都代表了別名,其地址是相同的 ```cpp= #include <iostream> using namespace std; void expressRef() { int apple = 100; int& refApple = apple; cout << "apple: " << apple << ", &address of apple: " << &apple << endl; cout << "refApple: " << refApple << ", &refApple of apple: " << &refApple << endl; } ``` > ![](https://i.imgur.com/uTLaTdV.png) * Reference 必須在宣告時就指定其數值,不可以在另外指定,這有點類似 const 修飾 Pointer,不讓指標肆意更動 ```cpp= void refInit() { int apple = 100; int& refApple = apple; int* const ptrApple = &apple; // 使用 Pointer 表達類似於 ref 的方法 } ``` :::warning * 如果沒有初始化,則會編譯失敗 > ![](https://i.imgur.com/9BqSEa2.png) ::: * Reference 又類似於自帶 `*` 解析指標符號的表示法,如果透過 ref 修改了變數,則會修改到原數值 (這應該不意外,因為它指向了源數據的地址) ```cpp= void refChangeValue() { int apple = 100; int& refApple = apple; refApple = 90; cout << "apple: " << apple << endl; cout << "refApple: " << refApple << endl; } ``` > ![](https://i.imgur.com/SfzoT5a.png) ### 暫時變數、Reference、const * C++ 有規定暫時變數的產生時機是,**++`實際引數` & `Reference` 引數不符合時++**,就會產生 **暫時變數**;以當前來講就是 函數為 const 的 Reference 時 > 並不是每個形參都會產生暫時變數 ```cpp= int refConst(const int& a) { return a * a; } void temporaryVal() { int a = 3; int aRes = refConst(a); // 不產生暫時變數 int b_ref = a; int bRes = refConst(b_ref); // 不產生暫時變數 int* c_ptr = &b_ref; int cRes = refConst(*c_ptr); // 不產生暫時變數 // ------------ 以下由於實際參數 & Reference 形參不同型態,所以都會產生暫時變數 long d_long = 3.9L; int dRes = refConst(*c_ptr); int eRes = refConst(5.0); int fRes = refConst(10 + 5.0); } ``` :::info * 暫時變數的釋放 ? 由編譯器 Compiler 自動幫我們完成 * 為何限制 const reference 形參 ? 因為 const reference 形參代表其值在函數內絕對不會被修改,所以可以大膽放心使用暫時變數,否則可能造成有要修改傳入參數的函數失敗 ```cpp= void swaper(int &a, int &b) { // 收到的變成暫時變數 int tmp = a; a = b; b = a; } int main() { long a = 10.0L, b = 20.0L; swaper(a, b); // 由於型態不符合,導致產生暫時變數,最終交換的是暫時變數 ! return 0; } ``` ::: ### Reference 傳遞 & 返回 1. Reference 傳遞:使用 Reference 如同指標,可以修改原來傳入的資料 ```cpp= #include <iostream> using namespace std; struct ClzInfo { string name; int studentCount; }; void show(const struct ClzInfo & clz) { cout << "Class name: " << clz.name << ", student count: " << clz.studentCount << endl; } void merge(struct ClzInfo & target, struct ClzInfo & clz) { target.studentCount += clz.studentCount; clz.name = "Non"; clz.studentCount = 0; } int main() { struct ClzInfo clz_a {"A Class", 10}; struct ClzInfo clz_b {"B Class", 9}; merge(clz_a, clz_b); show(clz_a); show(clz_b); return 0; } ``` > ![](https://i.imgur.com/NWR2YV1.png) 2. 回傳 Reference:如果需要操控回傳的數據,就可以回傳 Reference,而是否需要視情況而定 :::success 回傳的 Reference 實際上就是原來數據的別名 (類似指標地址,但是帶有了 `*` 符號) ::: ```cpp= struct ClzInfo & merge_2(struct ClzInfo & target, struct ClzInfo & clz) { target.studentCount += clz.studentCount; clz.name = "Non"; clz.studentCount = 0; return target; } int main() { struct ClzInfo clz_a {"A Class", 10}; struct ClzInfo clz_b {"B Class", 9}; struct ClzInfo clz_c {"C Class", 11}; merge_2(merge_2(clz_a, clz_c), clz_b); show(clz_a); show(clz_b); show(clz_c); return 0; } ``` > ![](https://i.imgur.com/H9Pg2HD.png) :::danger * 避免回傳暫時變數 (local) 的指標、Reference,因為區域變數分配在 Stack 上 ```cpp= struct ClzInfo & badAction_1(struct ClzInfo & clz) { struct ClzInfo clz_x {"X Class", 10}; return clz_x; } struct ClzInfo& badAction_2() { struct ClzInfo clz_xx {"XX Class", 10}; struct ClzInfo & res = clz_xx; return res; } ``` ::: * 如果不希望回傳的 Reference 被修改,可以使用 const 對其修飾 ```cpp= const struct ClzInfo & merge_3(struct ClzInfo & target, struct ClzInfo & clz) { target.studentCount += clz.studentCount; clz.name = "Non"; clz.studentCount = 0; return target; } void test3() { struct ClzInfo clz_a {"A Class", 10}; struct ClzInfo clz_b {"B Class", 9}; struct ClzInfo clz_c {"C Class", 11}; merge_3(merge_3(clz_a, clz_c), clz_b); // ERR show(clz_a); show(clz_b); show(clz_c); } ``` > ![](https://i.imgur.com/mrUdd7s.png) ### 預設參數 * C++ 對於函數的參數,可以有預設值,當然該預設值是可以覆蓋的;^1.^ 預設值的順序是由左至右,^2.^ 預設值不可在沒預設值之前 ```cpp= void default_params_valid_1(int a, int b = 10, int c = 20); void default_params_valid_2(int a = 10, int b = 20, int c = 30); void default_params_invalid_1(int a = 10, int b, int c = 10); void default_params_invalid_2(int a, int b = 10, int c = b + 10); ``` :::warning 只有 **函數原型** 可以設置預設值,物件導向來說就是 Class 對象中的函數 ::: > ![](https://i.imgur.com/HnkrXIh.png) ### 函數重載 - Overloading * 在 C 語言中,函數的名稱是唯一,就算是接收參數數量不同也不可以有相同名稱 ```c= void Hello() { } void Hello(int offset) { // Error: redefine } ``` * C++、Java 語言則可以進行函數重載,函數重載的判斷標準為 **函數簽名 (signature)** ```cpp= #include <iostream> #include <string> using namespace std; void Hello() { cout << "Hello" << endl; } void Hello(int offset) { char res[] = "Hello"; string s(res + offset); cout << s << endl; } int main() { Hello(); Hello(1); return 0; } ``` > ![](https://i.imgur.com/jwCjaOv.png) * **函數簽名 (signature)** 細節: 1. 可以判斷 指標、Reference 是否是 const 參數 ```cpp= // ------------------------------------ Pointer void Hello(const int* offset) { char res[] = "Hello"; string s(res + *offset); cout << s << endl; } void Hello(int* offset) { char res[] = "Hello"; string s(res + *offset); cout << s << endl; } // ------------------------------------ Reference void Hello(const int& offset) { char res[] = "Hello"; string s(res + offset); cout << s << endl; } void Hello(int& offset) { char res[] = "Hello"; string s(res + offset); cout << s << endl; } ``` :::warning * Reference 重載會跟 原來沒修飾的 const 數據 衝突 ```cpp= void Hello(const int& offset) { // 兩者的簽名相同 char res[] = "Hello"; string s(res + offset); cout << s << endl; } void Hello(int offset) { char res[] = "Hello"; string s(res + offset); cout << s << endl; } ``` ::: 2. **無法判斷返回值的不同**,必須要參數值不同 ```cpp= int Hello(int a, int b) { return a + b; } long Hello(int a, int b) { // 無法判斷不同返回值 return a + b; } ``` > ![](https://i.imgur.com/ZQYh93p.png) ### 一般函數樣板 - 隱式實體化 * 我們知道 Java 有泛型,用來簡易化抽象 (不示範嘞~ );而 C++ 也有泛型,只是表示的方式不同,C++ 泛型的關鍵字有兩種 (任意種都可以) 1. `template<typename [t]>`:推薦使用 typename ```cpp= template<typename T> void _swap(T &t1, T &t2) { T tmp = t1; t1 = t2; t2 = tmp; } ``` 2. `template<class [t]>` ```cpp= template<class T> void _swap2(T &t1, T &t2) { T tmp = t1; t1 = t2; t2 = tmp; } ``` * **隱式實體化**:由函數自動推定;呼叫時可以直接使用 泛型函數 ```cpp= #include <iostream> using namespace std; template<typename T> void _swap(T &t1, T &t2) { T tmp = t1; t1 = t2; t2 = tmp; } int main() { int a = 10, b = 20; cout << "Before a: " << a << ", b: " << b << endl; _swap(a, b); cout << "After a: " << a << ", b: " << b << endl; return 0; } ``` > ![](https://i.imgur.com/lIiC3ry.png) ### 特定化模板 - explicit 顯示 * 特定化 (顯示化) 函數會覆寫一般的樣板,如果有符合的限定函數呼叫,就會進入特定化函數 (請見下方範例) :::info 可以當成指定某個泛型要如何操作 ::: * 特定化 (顯示化) 泛型的 **原型宣告、實現,前置都必須有 `tamplate<>` 關鍵字**,並 **提及特定化的型態名稱 !** ```cpp= struct Hello { string say; int offset; }; // 1. 一般函數原型宣告 (非模板函數) void __swap(struct Hello &, struct Hello &); // 2. 模板函數 template <typename T> void __swap(T &t, T &t); // 3. 特定化模板 template <> void __swap<struct Hello>(struct Hello &, struct Hello &); ``` * 範例,交換 Hello 結構中的 offset,但不交換 say ```cpp= #include <iostream> using namespace std; struct Hello { char say[10]; int offset; }; // 一般模板 (泛型) template <typename T> void __swap(T &, T &); // 一般模板 (泛型) - 實現 template <typename T> void __swap(T &t1, T &t2) { T tmp = t1; t1 = t2; t2 = tmp; } // 特定化模板 (泛型) template <> void __swap<struct Hello>(struct Hello &, struct Hello &); // 特定化模板 (泛型) - 實現 template <> void __swap<struct Hello>(struct Hello &h1, struct Hello &h2) { cout << "--------- explicit ---------" << endl; // 指交換 offset 成員,不交換 say 成員 int tmp = h1.offset; h1.offset = h2.offset; h2.offset = tmp; } void show(struct Hello &h) { cout << h.say + h.offset << endl; } int main() { struct Hello h1{"Hello", 1}; struct Hello h2{"World", 2}; cout << "Before swap: " << endl; show(h1); show(h2); __swap(h1, h2); // 交換過後 offset 數值不同 cout << "After swap: " << endl; show(h1); show(h2); return 0; } ``` > ![](https://i.imgur.com/PZAN2Ri.png) ### 實體化模板 - explicit 顯示 :::success `實體化模板` 與 `特定化模板` 最大的不同是,template 不需要 `<>` 符號 ::: * **顯式**:由函數自動推定;呼叫時可以直接使用 泛型函數 1. 使用時直接指定需要的模板 ```cpp= #include <iostream> using namespace std; template <typename T> T _add(T a, T b) { return a + b; } int main() { int a = 10, b = 20; cout << _add<long>(a, b) << endl; return 0; } ``` > ![](https://i.imgur.com/jbTScZC.png) 2. 先宣告需要實體化的樣板格式,在後面使用時,對應到相對的格式,就會使用特定化樣板 ```cpp= template double _add<double>(double, double); int main() { int c = 10, d = 20; cout << _add(c, d) << endl; // 隱式實體化 double e = 10.5, f = 20.7; cout << _add(e, f) << endl; // 顯式實體化 return 0; } ``` > ![](https://i.imgur.com/tGBmWY3.png) ## Appendix & FAQ :::info ::: ###### tags: `C++` `C/C++`