Try   HackMD
tags: 大一程設-下 東華大學 東華大學資管系 基本程式概念 資管經驗分享

進階探討 pointer 雙重指標!與 2-d dynamic array

前言

在真的探討二維的動態陣列之前,有一個必須知道的基本知識就是雙重指標,而他的概念有點不直覺,我們一起來看看。

在開始閱讀這篇筆記前,請確定你閱讀過運算子的優先權了哦! -> 我是傳送門

第一次看請不要灰心,一定會看不懂,多看幾次吧QAQ。
Orange

雙重指標變數

複習一下,一般的指標變數如下:

int *p;

那既然會說是雙重指標變數,長相如下:

int **p;

可以看到有兩顆 *,那該怎麼理解呢? 我們先從名詞解釋開始吧。
還記得我們稱 int *p 為一個指標變數,會儲存一個指標(記憶體位址)。

不失一般性,int **p 仍為一個指標變數,也會儲存一個指標(記憶體位址),但他儲存的指標是一個指標變數的指標(記憶體位址)。

所以就會有我們常聽到的,雙重指標變數就是「指向指標變數的指標變數」。

我們馬上看個實際例子。

int a = 5; int *p1 = &a; int **p2 = &p1;

int *p1 來看,他是一個指標變數,儲存一個變數 a 的指標。
int **p2 來看,他是一個指標變數,儲存一個指標變數的指標

如果你覺得很饒口,看一下下面的圖幫助你理解。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

核心觀念千萬不要忘記,無論是一般的指標變數,還是雙重指標變數,都是儲存記憶體位址,就算今天變成三重指標變數,四重指標變數都一樣哦!

而這邊宣告他們是 int 型態就與上一篇筆記在講一般指標變數時相同,代表該指標變數最後指向的記憶體位址所儲存的值必須要是 int 型態。

小提醒

今天你宣告一個指標變數是幾層指標,他就必須要指向幾次,可以參考上圖,雙重指標變數(0x300)會根據記憶體位址指兩次,一般指標變數(0x200)只有指一次。

如果你把指向次數不同的位址指派錯人,程式是會報錯的哦,像下面這樣。

int a = 5; int *p1 = &a; int **p2 = &a; // error!!

你會看到像下面這樣的錯誤訊息。沒有辦法把一重指標變數指派給雙重指標變數,所以請一定要明確了解這邊的運作方式哦。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

precedence 優先權與結合性

還記得請各位去看一下運算子的優先權,這邊想跟大家討論的原因是,int **p這邊有兩個 *,亦稱 dereference 運算子,請問要先看哪個星號,優先權是如何?

  • dereference 的結合性是右結合,會先看右邊
    • 所以宣告雙重指標其實是像這樣 int *(*p2)

看到 * 號(左邊那顆)代表宣告指標變數,但因為是右結合,所以 p2 與 右邊的 * 優先結合,這樣由左至右就可以翻譯成,宣告一個指標變數其指向一個一重指標變數。

雙重指標變數的取值與取址

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; }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

對齊工具 setw 與 left ->小題外話(1)(2)

相信都是文字有點不直覺,我們再看張圖吧!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

已經講到最簡單摟,希望大家多多思考,有問題鼓勵發問!

動態配置二維陣列

在真的配置動態二維陣列之前,有一些觀念要跟大家討論。

二維動態陣列配置的記憶體運作方式是像這樣的,請看下圖。

圖片來源 -> here

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 最上層,雙重指標變數 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] 則是依靠位移量來達到。

所以第二層我們稱它為指標變數陣列,陣列中的每一格都儲存一個指標變數。我們後面來說明它。

就像整數陣列每格存整數浮點數陣列每格存浮點數
指標變數陣列每格存指標變數,就這麼簡單~

所以你只要搞懂什麼是指標變數你就會了。
Orange

動態配置二維陣列的語法

既然已經知道上面那張圖的看法跟建立陣列的方式,我們來看 CODE。

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] 的位移量影響的是不同的方向哦。

別忘記之前講的。

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 這個指標變數陣列第一格所儲存的值(位址)所儲存的值。
以下依此類推

畫個圖就像這樣。

上面這張圖可以用下面這個範例玩一下,確保你知道圖上每個取值取址的不同處哦!

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 的陣列,但配出來的這些記憶體,必須要被收回去,語法先來。

//配置 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] 這種寫法,這邊只是說明!
      這樣寫絕對大扣分
      Orange

  • 外層
    • 第 10 行
    • 接者再把 p[0~2] 共 3 格,的記憶體給刪掉

到這邊的內容是參考書的內容,要考過考試大家要學會,下面是補充。


以下為補充~

[補充 - 自行閱讀]指標變數陣列

我左思右想,我雖然會,但我決定依靠大神們來講解
Orange

[補充 - 自行閱讀] 雙重指標的運算 (double pointer arithmetic)

給大家一些例子自己閱讀吧~
有問題歡迎來問我~

一重指標的二維陣列運算

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 行怎麼解讀呢?

#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

很累吧,看個搞笑的文章放鬆一下