---
title: 進階語法-指標與參考 # 簡報的名稱
tags: 7th 教學 # 簡報的標籤
---
# 進階語法-指標與參考
#### Author: PixelCat
## 概要
1. 前言
2. 指標
3. 指標與`const`關鍵字
4. 函數指標
5. 參考
6. 有獎徵答
7. 結語
## 前言
這篇的出發點來自11/1我在群組發的一個有獎徵答

於是,最後還是決定亂寫一篇,介紹跟這坨東西相關的小知識。這篇講義的內容會偏淺,只提供最一般的認識,因為這些語法博大精深,深究下去都有太廣的內容可以討論,以我寫c++的經驗(我甚至沒做過任何一個有規模的專案)完全沒把握可以完整的解釋清楚,假如需要進一步了解的話請自行查閱資料。
當然,現實應用中你不會(也不應該)遇到這麼奇怪的東西,就當作科普小知識看一看吧。
## 指標
記憶體由很多很多byte組成,一個byte由8個bit組成,1個bit是一個0或1的單元,例如1GB的記憶體可以有
$$
1024^3=1,073,741,824\text{ bytes}=8,589,934,592\text{ bits}
$$
的空間可以儲存資料。
我們知道每個變數就是一塊記憶體,那要怎麼知道這塊記憶體在哪裡?我們需要一個「位址」來記錄你的變數在記憶體的什麼位置,如果把這個位址放進一個變數,這個變數稱為「指標」。你可以想像記憶體是一座城市,每個變數有自己的房子(記憶體),房子分別有門牌號碼(位址),只要有門牌號碼就可以找到變數在哪裡。
```cpp=
int a = 0; //一般的變數
int* ptr; //指向整數的指標
ptr = &a; //讓ptr指向a
int b = *ptr;
```
其中,第二行的`int*`代表ptr的型別是「指向整數變數的指標」,也就是說在`ptr`紀錄的位址,那塊記憶體應該要是一個整數,同理,`char*`代表指向字元變數的指標,`string*`代表指向字串變數的指標。第三行的`&a`代表取得變數a的位址,同理`&var`代表取得任意變數var的位址。第四行的`*ptr`代表取得ptr指向的資料。
### 經典應用: 交換變數
今天我們想交換兩個整數變數的資料
```cpp=
// 函數有兩個參數,型別是整數
void swap(int a,int b){
int t = a;
a = b;
b = t;
}
int main(){
int a = 4, b = 9;
cout << a << " " << b << "\n";
swap(a, b);
cout << a << " " << b << "\n";
return 0;
}
```
可是呼叫完`swap`,a和b的資料沒有改變,為什麼?因為呼叫函數的時候,參數會被複製一份給函數使用,此時`a,b`稱為區域變數,只在這個函數裡面有作用,所以函數裡面的`a,b`和`main`裡面的`a,b`在記憶體裡是不一樣的。
```cpp=
// 函數有兩個參數,型別是整數指標
void swap(int* a,int* b){
int t = *a;
*a = *b;
*b = t;
}
int main(){
int a = 4, b = 9;
cout << a << " " << b << "\n";
swap(&a, &b);
// swap( a, b); 會編譯失敗,為什麼?
cout << a << " " << b << "\n";
return 0;
}
```
現在`a,b`正確的被交換了,因為我們讓函數直接衝到他們家把他們拖出來~~暴打~~。
### 多重指標
指標裝的是別人家門牌,可是他自己也需要幾間房子存這個門牌號碼,所以理所當然指標自己也會有一個位址。
```cpp=
// int
int a = 0;
// pointer of int
int* ptr1 = &a;
// pointer of pointer of int
// aka. "double" pointer of int
int** ptr2 = &ptr1;
int*** ptr3 = &ptr2;
```
像這樣「指向整數指標的指標」,或者整數的雙重指標,是可行的。當然你可以繼續製造三重、四重指標,只要在型別後面繼續加星星就好了。
### 指標的大小與內容
我們知道`sizeof`關鍵字可以輸出一個變數占用多少記憶體,那一個指標占多大空間?
在大部分的電腦上,一個指標占8 bytes,這和你的電腦是32位元還是64位元有關。關於x86、x32、x64的差異博大精深,我也不會,請自己查資料QQ
```cpp=
int* ptr;
cout << sizeof(ptr) << "\n"; // 8 (or 4)
```
至於這八個byte裡面裝什麼?我們可以cout出來看看。
```cpp=
int a = 48763;
int* ptr = &a;
cout << *ptr << "\n"; // 48763
cout << ptr << "\n"; // 0x7efe18
```
你應該會在第二行看到一個`0x`開頭的十六進位數字,不一定跟我的輸出一樣。事實上指標的本質就**只是一個無號長整數**(uint64_t/unsigned long long),**跟他指向的型別完全無關**。
### 指標與陣列
我們知道陣列是一塊連續的記憶體,分成很多格,每一格都是一樣的型別一樣的大小。
```cpp=
int arr[4] = {1,2,3,4};
cout << arr[0] << "\n"; // 1
cout << &arr[0] << "\n"; // 0x7efe00
cout << arr << "\n"; // 0x7efe00
```
啊,陣列看起來就像一個指標!
一半對,一半錯。陣列的行為非常像指標,比如說你把陣列當作參數傳給一個函數時,不會整個陣列複製一份過去,而是傳一個指標。此外,你可以`*a`來得到`a[0]`,`*(a+1)`是`a[1]`,以此類推,不過...
```cpp=
cout << arr << "\n"; // 0x6ffe00
cout << * arr << "\n"; // 1
cout << arr+1 << "\n"; // 0x6ffe04
cout << *(arr+1) << "\n"; // 2
```
我們確實看到`arr[0]`和`arr[1]`被正確的印出來,但`arr`和`arr+1`相差了四不是一。這是因為arr指向整數,因此arr+1其實是加上了一個整數的大小,更一般的說法是,假如有一個指向T的指標`ptr`,則`ptr+i`的值和`ptr`會相差`i*sizeof(T)`;同時,`ptr[i]`會完全等同於`*(ptr+i)`。
目前為止陣列的行為完全等同於指標,那前面為什麼說「陣列等於指標一半錯」?因為陣列除了提供「位址」的資訊,還會伴隨一個「長度」的資訊。我們可以嘗試用`sizeof`檢查一個陣列占用的空間。
```cpp=
int* ptr;
int arr[100];
cout << sizeof(ptr) << "\n"; // 8
cout << sizeof(arr) << "\n"; // 400
```
因此就算arr可以當指標來用,詢問他的大小仍然會得到100個整數的大小。
另外一個不同是,陣列不能被放在等號左邊直接更改(類似一個常數指標),但一般指標可以。
```cpp=
int a = 49;
int* ptr;
int arr[100];
ptr = &a;
arr = &a; // error: incompatible types
```
當然還有還有其他的不同,就不繼續深究了。
## 指標與`const`關鍵字
對指標有一些認識之後,來看看一些奇形怪狀的指標。
我們都知道`const`關鍵字(修飾子),被`const`修飾的變數不能更改,也就是說這個變數是「唯讀(read-only)」的。
```cpp=
const int a = 0;
a = 49;
// error: assignment of read-only variable
```
那用`const`修飾一個指標,代表的是他自己不能被更改,還是他指向的資料不能被更改?
| `int n;` | `ptr = &n;` | `*ptr = n;` |
| -------- | -------- | -------- |
| `int* ptr` | 可以 | 可以 |
| `const int* ptr` | 可以 | 不行 |
| `int* const ptr` | 不行 | 可以 |
| `const int* const ptr` | 不行 | 不行 |
這裡有一個小技巧,你可以把指標的型別由後往前讀,以`const int*`為例:
1. `*`: 一個指標,指向...
2. `int`: ...整數...
3. `const`: ...(唯讀的)
所以`const int*`是*一個指標,指向唯讀的整數*,代表他指向的內容不能修改,但可以讓他指向不同人。
這個技巧對於多重指標也是有用的,例如`int*const*`代表
1. `*` 一個可修改的指標,指向
2. `*const` 不可修改的指標,指向
3. `int` 可修改的整數
「一個可修改,指向 {一個不可修改,指向 (可修改的整數) 的指標} 的指標」。
## 函數指標
前面的指標都是指向一個變數,那函數指標是什麼?在C/C++裡函數又不能當變數用。
:::spoiler `std::function`不這麼覺得
[C++先不要吵](https://en.cppreference.com/w/cpp/utility/functional/function),請體諒純C的同胞=w=
:::
</br>
然而,在編譯出來的執行檔裡面,一個函數會變成一串最低階的、機器看得懂的指令(接近組合語言的概念),仍然佔用了一塊記憶體,所以我們當然可以把函數的位址塞進變數裡面。
```cpp=
void out(int a){
cout << "value = " << a << "\n";
}
void(*ptr)(int);
ptr = &out;
ptr = out; // without '&' also works
```
於是,`ptr`現在是一個指向`out`函數的變數。我們把第五行拆開看看發生了什麼事
1. `void` 這是函數的回傳值
2. `(*ptr)` 是變數的名字,括號加上星星代表這是一個函數指標
3. `(int)` 這是參數(們)的型別
最令人難以接受的是,正常的型別和變數名稱都是分開的,函數指標的名字居然包在中間。
不過重點是,搞函數指標有啥用?他可以讓你把函數裝進一個變數裡面傳來傳去,在某個你滿意的時間地點呼叫這個函數。要呼叫函數也很簡單,把指向他的函數指標當成一個函數,加上括號和參數們就好。
```cpp=
// ...續上面的例子
out(1); // value = 1
ptr(1); // value = 1
// ptr is now an alias for out()
```
```cpp=
void execute(void(*func)(int), int value){
func(value);
}
execute(out, 49); // what happens? why?
```
其實你高機率已經用過函數指標了!
[前面提過](https://hackmd.io/@nehs-iced-7th/rk7moqETd#/4/9),C++內建的排序函數可以自己提供一個比較函數,在`std::sort`的函數原形中,第三個參數cmp(比較函數)就是一個類似函數指標的東西。所以你當然可以像這樣彷做一個對整數陣列的排序函數
```cpp=
void sort(int* begin, int* end, bool(*cmp)(int, int)){
// some SORT of sorting algorithm
// inaff.
}
```
:::spoiler 他跟真正的`std::sort`有什麼不一樣?
1. `std::sort`可以排序任意型別,所以會是一個模板(`template`)函數
2. `std::sort`訪問元素的方法是透過封裝過的[`RandomAccessIterator`](https://www.cplusplus.com/reference/iterator/RandomAccessIterator/)
3. `std::sort`的比較函數除了函數指標,還可以是一個[`std::function`](https://en.cppreference.com/w/cpp/utility/functional/function)物件
~~哭啊,那不就全部都不一樣了嗎~~
:::
</br>
最後,函數當然可以返回一個函數指標,這樣應該要怎麼寫?
那假如有一個函數指標,他指向的函數返回函數指標,又應該怎麼表示?
```cpp=
void out(int n) { cout << "value: " << n << "\n"; }
// getFuncPtr is a function that
// - takes in no parameters
// - returns a function pointer void(*ptr)(int)
void(* getFuncPtr() )(int){
return out;
}
void(*(*ptr)())(int) = getFuncPtr;
// ptr is a function pointer pointing to getFuncPtr()
```
還是那句話,現實應用中你不會(也不應該)遇到這麼奇怪的東西,就當作茶餘飯後小知識看一看吧。
## 參考
「參考」,有人稱為「引用」,也就是reference,總是與`&`符號一起出現。
讓我們再次召喚前面變數交換的函數。
```cpp=
void swap(int* a, int* b){
int t = *a;
*a = *b;
*b = t;
}
int a = 487, b = 63;
swap(&a, &b);
```
我們之所以需要指標,是因為呼叫函數的時候,所有參數會被複製一份,也就是說裡面的參數們跟外面傳進去的參數們是不同的變數。但是我們真的真的很想修改他們,指標指來指去又搞得大家頭很痛,於是參考出現了。
```cpp=
void swap2(int& a, int& b){
int t = a;
a = b;
b = t;
}
int a = 487, b = 63;
swap2(a, b);
```
星星全部不見了!
參考可以視為幫變數另外取一個名字,使用方式跟正常的變數完全一模一樣,可是換個名字,走到哪裡還是同一個變數同一塊記憶體。使用參考有兩個主要的效果:
1. 可以直接修改原來的變數本體
2. 避免不必要的資料複製
1.就像上面的swap2演示的,2.的效果可以由這段程式體現
```cpp=
// call by value
double valueAt1(vector<int> v, int i){
return v[i];
}
// call by reference
double valueAt2(vector<int>& v, int i){
return v[i];
}
vector<int> v(1000000); // v.size() == 1e6
valueAt1(v);
valueAt2(v);
```
先不論為什麼不要直接中括號就好(我想不到好範例qq),呼叫兩種版本的valueAt,效率是天壤之別。一次兩次可能不明顯,假如跑十萬次,就會感受到執行時間的明顯差距,但是我們知道他們做的事情一模一樣。這是因為每次呼叫valueAt1,整條陣列v會被完整的複製一次,但透過參考的使用,valueAt2每次使用的都是同一個、第11行宣告的那個v。
### 各種參考
參考又可以分為三種。
1. 左值參考 lvalue reference
2. 右值參考 rvalue reference
3. 常數參考 const reference
我要道歉,用文字實在很難好好解釋清楚(才不是因為我懶呢),因此放個影片希望有解釋到。
(影片是英文的,個人認為解釋的不錯,聽到什麼move semantics之類的高科技就忽略就好了)
{%youtube fbYknr-HPYE %}
## 有獎徵答
最後,開頭那一串到底是什麼型別?貼心的幫你再貼一次圖片。

```
var是,
一個不可修改的指標,指向
一個不可修改的函數指標,指向
一個函數,參數有,
一個整數指標
一個布林常數參考
回傳值是,
一個函數指標,指向
一個函數,參數有,
一個字元右值參考
一個函數指標,指向
一個函數,參數有,
沒有參數
回傳值是,
一個指標,指向
不可修改的指標,指向
虛無(void)
回傳值是,
一個指標,指向
虛無(void)
```
現實生活根本不會也不該碰到這種妖魔鬼怪,不過還是恭喜`橘子#2164`在隔天成功回答!

## 結語
C\+\+博大精深,永遠都有{新的高科技|黑魔法}可以學習。
大家一起朝著C\+\+大師之路邁進吧!