--- title: 進階語法-指標與參考 # 簡報的名稱 tags: 7th 教學 # 簡報的標籤 --- # 進階語法-指標與參考 #### Author: PixelCat ## 概要 1. 前言 2. 指標 3. 指標與`const`關鍵字 4. 函數指標 5. 參考 6. 有獎徵答 7. 結語 ## 前言 這篇的出發點來自11/1我在群組發的一個有獎徵答 ![](https://i.imgur.com/FEph8J8.png) 於是,最後還是決定亂寫一篇,介紹跟這坨東西相關的小知識。這篇講義的內容會偏淺,只提供最一般的認識,因為這些語法博大精深,深究下去都有太廣的內容可以討論,以我寫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 %} ## 有獎徵答 最後,開頭那一串到底是什麼型別?貼心的幫你再貼一次圖片。 ![](https://i.imgur.com/FEph8J8.png) ``` var是, 一個不可修改的指標,指向 一個不可修改的函數指標,指向 一個函數,參數有, 一個整數指標 一個布林常數參考 回傳值是, 一個函數指標,指向 一個函數,參數有, 一個字元右值參考 一個函數指標,指向 一個函數,參數有, 沒有參數 回傳值是, 一個指標,指向 不可修改的指標,指向 虛無(void) 回傳值是, 一個指標,指向 虛無(void) ``` 現實生活根本不會也不該碰到這種妖魔鬼怪,不過還是恭喜`橘子#2164`在隔天成功回答! ![](https://i.imgur.com/BxwOzmq.png) ## 結語 C\+\+博大精深,永遠都有{新的高科技|黑魔法}可以學習。 大家一起朝著C\+\+大師之路邁進吧!