--- GA: G-RZYLL0RZGV --- ###### tags: `大一程設-下` `東華大學` `東華大學資管系` `基本程式概念` `資管經驗分享` 進階探討 pointer 雙重指標!與 2-d dynamic array === [TOC] ## 前言 在真的探討二維的動態陣列之前,有一個必須知道的基本知識就是雙重指標,而他的概念有點不直覺,我們一起來看看。 在開始閱讀這篇筆記前,請確定你閱讀過運算子的優先權了哦! -> [我是傳送門](https://hackmd.io/@ndhu-programming-2021/HJAkAf8AF) > 第一次看請不要灰心,一定會看不懂,多看幾次吧QAQ。 > [name=Orange] ## 雙重指標變數 複習一下,一般的指標變數如下: ``` int *p; ``` 那既然會說是雙重指標變數,長相如下: ``` int **p; ``` 可以看到有兩顆 `*`,那該怎麼理解呢? 我們先從名詞解釋開始吧。 還記得我們稱 `int *p` 為一個指標變數,會儲存一個指標(記憶體位址)。 不失一般性,`int **p` 仍為一個指標變數,也會儲存一個指標(記憶體位址),但他儲存的指標是一個指標變數的指標(記憶體位址)。 所以就會有我們常聽到的,雙重指標變數就是「<span style="color:red">**指向指標變數的指標變數**</span>」。 我們馬上看個實際例子。 ```cpp= int a = 5; int *p1 = &a; int **p2 = &p1; ``` 以 `int *p1` 來看,他是一個指標變數,儲存一個變數 a 的指標。 以 `int **p2` 來看,他是一個指標變數,<span style="color:red">**儲存一個指標變數的指標**</span>。 如果你覺得很饒口,看一下下面的圖幫助你理解。 ![](https://i.imgur.com/91ksWfZ.png) 核心觀念千萬不要忘記,無論是一般的指標變數,還是雙重指標變數,都是儲存記憶體位址,就算今天變成三重指標變數,四重指標變數都一樣哦! 而這邊宣告他們是 int 型態就與上一篇筆記在講一般指標變數時相同,代表該指標變數最後指向的記憶體位址所儲存的值必須要是 int 型態。 ### 小提醒 今天你宣告一個指標變數是幾層指標,他就必須要指向幾次,可以參考上圖,雙重指標變數(0x300)會根據記憶體位址指兩次,一般指標變數(0x200)只有指一次。 如果你把指向次數不同的位址指派錯人,程式是會報錯的哦,像下面這樣。 ```cpp= int a = 5; int *p1 = &a; int **p2 = &a; // error!! ``` 你會看到像下面這樣的錯誤訊息。沒有辦法把一重指標變數指派給雙重指標變數,所以請一定要明確了解這邊的運作方式哦。 ![](https://i.imgur.com/cv4u84y.png) ### precedence 優先權與結合性 還記得請各位去看一下運算子的優先權,這邊想跟大家討論的原因是,`int **p`這邊有兩個 `*,亦稱 dereference 運算子`,請問要先看哪個星號,優先權是如何? * <span style="color:red">dereference 的結合性是右結合</span>,會先看右邊 * 所以宣告雙重指標其實是像這樣 `int *(*p2)` 看到 `*` 號(左邊那顆)代表宣告指標變數,但因為是右結合,所以 p2 與 右邊的 `*` 優先結合,這樣由左至右就可以翻譯成,宣告一個指標變數其指向一個一重指標變數。 ## 雙重指標變數的取值與取址 ```cpp= int main(){ int a = 5; int *p1 = &a; int **p2 = &p1; //a 取值 cout << "[變數] a 存的值: " << a << endl; //a 取址 cout << "[變數] a 所在的記憶體位址: " << &a << endl; //p1 取值 cout << "[指標變數] p1 指向的位址儲存的值: " << *p1 << endl; //p1 取址 cout << "[指標變數] p1 指向的位址: " << p1 << endl; cout << "[指標變數] p1 所在的記憶體位址: " << &p1 << endl; //p2 取值 cout << "[指標變數] p2 指向的位址所儲存的值(位址): " << *p2 << endl; cout << "[指標變數] p2 指向的位址所指向的位址儲存的值: " << **p2 << endl; //p2 取址 cout << "[指標變數] p2 指向的位址: " << p2 << endl; cout << "[指標變數] p2 所在的記憶體位址: " << &p2 << endl; return 0; } ``` ![](https://i.imgur.com/H1gAAN2.png) > 對齊工具 setw 與 left ->小題外話[(1)](https://dotblogs.com.tw/v6610688/2013/11/05/cplusplus_output_align_setw_set_field_width)、[(2)](https://www.cnblogs.com/wxxweb/archive/2011/06/01/2065671.html) 相信都是文字有點不直覺,我們再看張圖吧! ![](https://i.imgur.com/PrvB2Cv.png) 已經講到最簡單摟,希望大家多多思考,有問題鼓勵發問! ## 動態配置二維陣列 在真的配置動態二維陣列之前,有一些觀念要跟大家討論。 二維動態陣列配置的記憶體運作方式是像這樣的,請看下圖。 > <span style="color:red">**圖片來源**</span> -> [here](https://mropengate.blogspot.com/2015/12/cc-dynamic-2d-arrays-in-c.html) > ![](https://3.bp.blogspot.com/-2hkNUAq93Ds/VmsO7aLrQTI/AAAAAAAA_P0/FNmVMTZNgUk/s1600/array_2d_dynamic.png) * 最上層,雙重指標變數 p * 會是由外往內配,先配置最上層(圖中最左)的雙重指標,讓它往下指,而我們知道一個指標變數就存一個記憶體位址,所以它要存下一層一維陣列的陣列頭 p[0],剩下的 p[1]、p[2] 則是依靠我們之前說的位移量去遍歷它。 * 第二層的 p[0]、p[1]...,因為要能夠指到最內層的陣列,所以每一個(p[0]、p[1]...p[n])都會被視為指向一維陣列頭(記憶體位址)的指標變數 * 因此 p[0] 會衍生出一條一維陣列,p[1] 會衍生出一條一維陣列,依此類推。 * 而對於 p[0] 而言,p[0][0]、p[0][1] 則是依靠位移量來達到。 所以第二層我們稱它為<span style="color:red">**指標變數陣列**</span>,陣列中的每一格都儲存一個指標變數。我們後面來說明它。 > 就像**整數陣列**每格**存整數**,**浮點數陣列**每格**存浮點數**。 > **指標變數陣列**每格**存指標變數**,就這麼簡單~<br> > 所以你只要搞懂什麼是指標變數你就會了。 > [name=Orange] ### 動態配置二維陣列的語法 既然已經知道上面那張圖的看法跟建立陣列的方式,我們來看 CODE。 ```cpp= int main(){ int row = 3, col = 2; int **p = new int*[row]; for(int i = 0; i < row; i++){ p[i] = new int[col]; } } ``` * 第 3 行 * 宣告一個雙重指標變數 p * 因為要配置出下一層的指標變數陣列,讓每一格指標變數都能指出一條陣列(這邊假設 row = 3,所以有 3 條) -> 3 條一維陣列建構出一個二維陣列。 * 第 4 行 * 假設我們要配置出一個 3 * 2 的陣列,在第 3 行已經配置了三格儲存 3 個指標變數,接下來要為這三個指標變數分別指向一個陣列的陣列頭。 * 第 5 行 * 每條一維陣列有 2 格(col = 2) * 因為在第 3 行已經配置一個長度為 3 的一維指標變數陣列,所以 p[i] 代表配置出的陣列的位移量,去取得該記憶體位址內的值。如果這一行混淆你,我們返璞歸真看一下下面。 * **提醒** : p[i] 跟 p[i][j] 的位移量影響的是不同的方向哦。 別忘記之前講的。 ```cpp= int a = 5; int *p1 = &a; int **p2 = &p1; cout << p2 << endl; ``` `cout << p2` 代表印出指標變數 p2 所指向的位址。 那換成下面這樣。 ``` int **p2 = new int*[3]; cout << p2 << endl; ``` `cout << p2` 會印出指標變數 p2 所指向的陣列頭的記憶體位址。 而 `cout << p2[0]` 會取出 p2 這個指標變數陣列第一格所儲存的值,因為每一格都是指標變數,所以會印出第一格指標變數的記憶體位址。 `cout << p2[0][0]` 會取出 p2 這個指標變數陣列第一格所儲存的值(位址)所儲存的值。 以下依此類推... 畫個圖就像這樣。 ![](https://i.imgur.com/3pYgejm.png) 上面這張圖可以用下面這個範例玩一下,確保你知道圖上每個取值取址的不同處哦! ```cpp= int main(){ int **p = new int*[3]; int i = 0, j = 0; // 配置 for(i = 0; i < 3; i++){ p[i] = new int[2]; } // 取址 for(i = 0; i < 3; i++){ for(j = 0; j < 2; j++){ cout << &p[i][j] << " "; } cout << endl; } // 附值 int k = 0; for(i = 0; i < 3; i++){ for(j = 0; j < 2; j++){ p[i][j] = k; k++; } } // 取值 cout << p << " " << &p[0] << " " //p equal to &p[0] << p[0] << " " << &p[0][0] << " " //p[0] equal to &p[0][0] << p[0][0] << endl; return 0; } ``` 所以其實一個 3 * 2 的動態陣列,就配置了 10 個記憶體位址,你說它真的非常有效率嗎?真的端看需求而定! ### 動態配置記憶體,用完請回收 上面的例子動態配置了 3 * 2 的陣列,但配出來的這些記憶體,必須要被收回去,語法先來。 ```cpp= //配置 int **p = new int*[3]; for(int i = 0; i < 3; i++){ p[i] = new int[2]; } //回收 for(int i = 0; i < 3; i++){ delete[] p[i]; } delete[] p; ``` 我想在看配置的時候,應該有點感覺,我們是從外到內一直分配記憶體出來,所以在回收的時候呢,要先把最內部的記憶體刪乾淨,才能把外面的刪掉。 * 最內層 * 第 7 + 8 行,因為總共配置 3 條,每條 2 格,所以是先把 p[0 ~ 2][0 ~ 1] 共 6 格的記憶體刪掉 * > 語法沒有 p[0 ~ 2][0 ~ 1] 這種寫法,這邊只是說明! > 這樣寫絕對大扣分<br>[name=Orange] * 外層 * 第 10 行 * 接者再把 p[0~2] 共 3 格,的記憶體給刪掉 <span style="color:green;font-size:20px">**到這邊的內容是參考書的內容,要考過考試大家要學會,下面是補充。**</span> --- <span style="color:green;font-size:20px">**以下為補充~**</span> ### [補充 - 自行閱讀]指標變數陣列 > 我左思右想,我雖然會,但我決定依靠大神們來講解 > [name=Orange] * 這篇請閱讀文章內 3(a)(b) * [簡單搞懂指標(pointer)、指標陣列(pointers of array, int *foo[]) 與指向陣列的指標 (pointer to array, int (*bar)[])](http://hackgrass.blogspot.com/2018/03/c-pointerint-foo-int-bar.html) * > 雖然語法是 C,不是 C++,但核心概念不變,可以自己用 C++ 的語法試試看 > 裡面有一些小地方寫錯,麻煩自己判別哦 * 接著請閱讀這篇 * > 上面那篇要你們看的地方看得懂的話,這篇一定看得懂 * [C / C++ 函式傳遞二維陣列 範例與解說](https://charlottehong.blogspot.com/2017/11/c-c.html) * 最後請閱讀這篇 * > 雖然是大陸用語,但我相信聰穎的你一定知道對應到我們常說的甚麼。 > 不會歡迎來找我討論哦~ * [C/C++编程语言中char** a和char* a[]介绍](https://blog.csdn.net/liitdar/article/details/80972088) ## [補充 - 自行閱讀] 雙重指標的運算 (double pointer arithmetic) * [雙重指標運算與二維陣列](https://edisonshih.pixnet.net/blog/post/27344592) 給大家一些例子自己閱讀吧~ 有問題歡迎來問我~ ### 一重指標的二維陣列運算 ```cpp= int main(){ int array[2][3] = {1,2,3,4,5,6}; int *p1 = &array[0][0]; for(int i = 0; i < 2; i++){ for(int j = 0; j < 3; j++){ cout << *(p1+i*3+j); } } } ``` ### 雙重指標的二維陣列運算 如果你真的搞懂了 precedence 跟結合性,請問第 11 行怎麼解讀呢? ```cpp= #include <iostream> #include <cmath> using namespace std; void input(double (*wh)[2], int sizeOfRow){ int j = 0; double t = 0; for(int i = 0; i < sizeOfRow; i++){ for(j = 0; j < 2; j++){ cout << "student" << i+1 << " - "<<j<<":"; cin >> t; *(*(wh+i)+j) = t; // 如果寫 *(*wh+i+j) 結果會一樣嗎? } if(*(*(wh+i)+j) <= 0) break; } } string calculation(){ double wh[3][2]; input(wh, 3); double max = 0, mini = 0, bmi = 0; for(int i = 0; i < 3; i++){ bmi = wh[i][0]/(pow(wh[i][1], 2)); cout << bmi << endl; if(i==0) mini = bmi; if(max < bmi) max = bmi; if(mini > bmi) mini = bmi; } string a = "max and bmi is "; // to_string 只支援 C++11 a = a + to_string(max) + "," + to_string(mini); return a; } void show(string a){ cout << a << endl; } int main(int argc, char** argv) { show(calculation()); return 0; } ``` ## Reference * [[C語言] 指標教學[七]: 多重指標](https://medium.com/@racktar7743/c%E8%AA%9E%E8%A8%80-%E6%8C%87%E6%A8%99%E6%95%99%E5%AD%B8-%E4%B8%83-%E5%A4%9A%E9%87%8D%E6%8C%87%E6%A8%99-89854e6eae0b) * [指標與位址](https://openhome.cc/Gossip/CGossip/Pointer.html) 很累吧,看個搞笑的文章放鬆一下 * [台灣工程師常唸錯的英文單字](https://blog.privism.org/2012/06/blog-post.html)