---
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;
}
```
> 
:::
:::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;
}
```
> 
## 函數 & 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;
}
```
> 
### 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;
}
}
```
> 
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;
}
}
```
> 
:::info
* Function 內參數的形參為 指標的第一個元素,但有幾個點需要注意
1. 陣列宣告時,用陣列名稱宣告記憶體
```cpp=
int main() {
int array[5] = {
1, 2, 3, 4, 5
};
cout << "array: " << array << endl;
cout << "&array[0]: "<< &array[0] << endl;
}
```
> 
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;
}
```
> 
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
}
```
> 
:::
* 函數中陣列的大小計算:傳入函數後的陣列形參,它代表的是一個陣列的 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;
}
}
```
> 
### 二維 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;
}
```
> 
## 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;
}
```
> 
:::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;
}
```
> 
* 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 會**複製到每個呼叫它的地方**
>
> 
```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;
}
```
> 
### 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;
}
```
> 
* Reference 必須在宣告時就指定其數值,不可以在另外指定,這有點類似 const 修飾 Pointer,不讓指標肆意更動
```cpp=
void refInit() {
int apple = 100;
int& refApple = apple;
int* const ptrApple = &apple; // 使用 Pointer 表達類似於 ref 的方法
}
```
:::warning
* 如果沒有初始化,則會編譯失敗
> 
:::
* Reference 又類似於自帶 `*` 解析指標符號的表示法,如果透過 ref 修改了變數,則會修改到原數值 (這應該不意外,因為它指向了源數據的地址)
```cpp=
void refChangeValue() {
int apple = 100;
int& refApple = apple;
refApple = 90;
cout << "apple: " << apple << endl;
cout << "refApple: " << refApple << endl;
}
```
> 
### 暫時變數、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;
}
```
> 
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;
}
```
> 
:::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);
}
```
> 
### 預設參數
* 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 對象中的函數
:::
> 
### 函數重載 - 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;
}
```
> 
* **函數簽名 (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;
}
```
> 
### 一般函數樣板 - 隱式實體化
* 我們知道 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;
}
```
> 
### 特定化模板 - 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;
}
```
> 
### 實體化模板 - 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;
}
```
> 
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;
}
```
> 
## Appendix & FAQ
:::info
:::
###### tags: `C++` `C/C++`