1. 程式人生 > >詳解多維陣列與指標之間的關係

詳解多維陣列與指標之間的關係

先介紹一下簡單的一維陣列:

列如:

int a[3] = {0,1,2};

[3]和型別int則明確表示編譯器應該為這個棧分配多大的記憶體,也就是三個int大小!

在記憶體中示意圖是:


在CPU看來記憶體是一組連續的地址空間,所以當我們對一維陣列進行操作時只需要知道陣列首地址,就可以通過地址偏移加減運算方式來求得每個元素位於記憶體中的檔案映射出來的資料段虛擬地址!

不過要注意不要越界,其實你在自己的地址空間內訪問超出陣列大小的空間也不會有問題,因為你是在自己的地址下進行訪問的,不會被核心卡擦掉,只不過那一段記憶體可能被對齊了,是未知不被使用的資料!

使用方法也非常簡單:

int a[3] = { 0, 1, 2 };
	printf("%d", a[0]);	//列印第0個數據

列印結果:


使用指標方式:

注意陣列即本身就是一個地址,而指標可以直接操作地址,所以指標可以使用陣列的方式來表示:

int a[3] = { 0, 1, 2 };
	int *p = a;
	printf("%d", p[0]);	//列印第0個數據

編譯器會自動根據表示式所使用的運算子來自動彙編解引用程式碼!

列印結果:


也可以顯示的使用指標解引用:

int a[3] = { 0, 1, 2 };
	int *p = a;
	printf("%d", *p+1);	//列印第1個數據

這裡p已經指向了a陣列的首地址,也就是a[0],想列印第一個元素的值,只需要對其*解引用並+1讓其進行地址偏移一個型別單位(int,編譯器會根據表示式在結合型別自動進行彙編上的地址偏移量add)!

二維陣列:

int a[3][3] = {{0,1,2},{3,4,5}};    //定義一個二維陣列

上面的表達方式即:定義一個有3列且每列有3行資料的一個二維陣列!


上面只是抽象的表達方式,其實底層就是一個一維陣列:


長度是每行的集合,只是C語言上對其更加抽象的區分開了,根據第一個[]操作符裡的值將其分成多少個段!根據[]後的[]確定每段記憶體能存下多少位元組,根據型別來確定該記憶體用來儲存什麼樣的型別資料運算時呼叫alu(整數運算器)還是fpu(浮點數運算器)

浮點數是由單獨的儲存方式的,所以需要嚴格的區分開!

而且在底層是沒有型別這一區分的全部都是二進位制數,所以在編譯階段編譯器就會檢查型別之間的讀寫,所以型別限制是由編譯器來維護的!

使用方法:

int a[3][3] = { { 0, 1, 2 }, {3,4,5} };
	printf("%d", a[0][1]);	//列印第0行第1個數據

列印結果:


下面來介紹一下使用指標的方法和問題:

首先先來看一下:

下面程式碼為什麼會報錯?

int a[3][8] = {{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8}}; 
int **p = a;

原因很簡單,二級指標只能用來指向int*指標,而int a是一個二維陣列,兩個型別一開始指向的實際型別就不對,在其次,雙方佔用的記憶體大小也不同!

列如

int a[3][8]佔用的是int*3*8個位元組大小

而*p僅佔用4個位元組(取決於編譯器位數)

那問題又來了,為什麼一維陣列就可以?

其原因如下:

在C/C++編譯器當中一維陣列隱式是一個指標,換句話來說,陣列就是指標,陣列本身就是一個地址,無需二次定址,指標和陣列區別在於陣列不用解引用,而且陣列名也是一個常量,無法直接賦值!

最經典的列子就是當你將陣列作為引數時,編譯器會自動將陣列轉化為指標,其原因是為了剩記憶體!

而二維陣列則被隱式的宣告成:int *a[8];

所以我們如果想指向一個二維陣列時候就要宣告成int (*p)[8] = a; // 一個指向有8個整型陣列的指標;

如果不相信的話,我們來修改一下程式碼看看:

int a[3][8] = {{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8}}; 
int (*p)[5] = a;    //這裡將int (*p)[4]改成int (*p)[5]看看會報什麼錯誤

報如下錯誤:


可以看到:int a[3][8]被隱式的轉換成:int(*)[8]了!

修改一下:

int (*p)[8] = a; //一個指向有8個整型陣列的指標;

解引用方法:

最簡單的就是我們可以直接將該指標當做陣列來使用,因為:二維陣列實則上並沒有並轉換成int (*)[8]只是隱式的型別轉換,實際記憶體還是位於棧中!(*p)指向的是:一個隱式的int *a,而int *a指向a[3]這個第一維陣列的首元素也就是首地址a[0],要知道陣列地址是連續的可以通過解引用隱式的*a+1得到下一個元素的地址!而後面的[8]則表示每個一維陣列中有多少個元素!

也就是說說int a[3][8]被隱式的轉換成int *a[8],*a指向原本的a[3]的首地址,而後面的[8]則是告訴*a每個元素的偏移量是多少!

則也就是說[8]為8個int!

其實更為明確的表示方法就是 int a[3][8] = 3個int[8]

其實我們也不需要對其進行解引用,因為使用[]括號編譯器會把該指標作為陣列一樣使用,而陣列自己就是一個地址,所以編譯器會自動將該指標轉化為地址!

printf("%d", p[1][1]);    //列印第一維第一個資料

上面這張方法是最為簡單的,

還有一種方法:

printf("%d", *(*(p+1)+1));    //列印第一維第一個資料

下面來詳細的分解一下上面的解引用過程

首先第一步需要對p進行解引用,這裡不在當做陣列使用所以需要顯示的對其進行解引用,上面說過*p指向隱式的*a,這裡對其解引用實則上是對找到了*a的地址並對其進行+1

*(p+1)

這裡加上括號是因為*取值運算子優先順序要高於+號運算子,注意乘法*不高於+號運算子,而取值*會高於+號運算子,編譯器會根據表示式來確定*號的用途。

下面再在來看p+1,上面說過(*p)指向的是隱式的*a地址,而*a指向陣列的首地址也就是a[0],這裡p+1也就是讓*a+1,加上括號()讓其優先對地址進行+1在解引用,否則會直接對*a解引用然後在對該元素值+1!即操作*a棧地址裡儲存的地址+1而非真正的陣列地址,如果不解引用的話那就是p本身地址+1了!

補充一個小知識:

指標也是有自己的地址的,指標存在於棧中,一般指標的棧記憶體儲存的是堆或棧地址!

然後又在*(p+1)的外面加了一個括號(*(p+1)),最後並讓其+1再次解引用:*(*(p+1)+1)

下面來詳細解釋一下:

第一,當我們通過*(p+1)找到了隱式*a的地址,注意只是找到了隱式*a的地址而非陣列的地址,需要再次對*a解引用找到*a棧記憶體儲存的陣列地址:

**p    這樣的寫法才是真正以指標的形式找到二維陣列的寫法!

不信我們試一下:

printf("%d", **p);

列印結果為:1

而**p+1就是對a指向的陣列地址+1,要知道二維實則上也是一維陣列,都是地址都是線性排序的所有**p+1,就是指向第二個元素,不需要加括號是因為**優先順序高於+,按照這個優先順序來算表示式,會先對p解引用找到隱式的*a,在對*a解引用找到陣列地址+1則下一個元素的地址:

printf("%d", **p+1);

列印結果:2

通過上面的介紹,就應該很容易理解這段程式碼了:

*(*(p+1)+1)

首先對*(p+1)解引用找到也就是隱式的*a並對其地址進行解引用,然後在對其+1(這裡+1加的是int*位元組大小的偏移地址)也就是找到指向a[1]的*a偏移地址,在對其進行+1,也就是找到數組裡的元素,然後在對其進行解引用,在解引用之前要加上括號,上面也說了,優先順序的原因,否則會找到a[1]首元素然後對該值+1

所以正確的指標引用寫法是:

*(*(p+1)+1)

下面說說三維陣列應該怎樣使用:

列如:

int nA[2][2][2];

對於這樣的三維陣列並不難理解:

int nA[2][2][2];

實則上就是

在每行多增加了一個行寬

列如:

int nA[2][2] = { { 1, 2 }, { 3, 4 }}; 

更改為三維陣列之後:

int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三維陣列 

三維陣列可以被認為是二維陣列,而二維陣列也可以被認為是一維陣列,因為在計算機當中陣列的地址是連續的,只有行沒有列,維陣列只是一種抽象的表達方式!

三維則是給每行增加額外的行寬


更明確的表達方式就是:int a[2][2][2] = 2個int[2][2]

更加明確的表達方式其實就是:int a[2][2][2] = 有列,每個列上有兩行,每行可以放2個數據!


注意這裡不是畫畫,沒有高度,所以在更底層的表達方式三維實則上是給每行增加行寬!

使用方法:

int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三維陣列 
	int(*p)[2][2] = nA;
	printf("%d\n", p[0][1][1]);	//列印第0列第一行第1個行寬

注意三維的初始化必須用{}括起來!


即表示每行寬


列印結果:


可以看到打印出第列印第0列第一行第1個行寬第1個元素資料:4


堆疊下標是從0開始的所以索引是1!

下面介紹如何使用指標的形式來訪問:

int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三維陣列 
	int(*p)[2][2] = nA;
	printf("%d\n", *(*(*p)+1));	//列印第0列第0行第1個行寬

列印結果:


下面來解釋一下上面的指標解引用過程:

*(*(*p)+1)

*p首先解引用是對p指向的*nA指標解引用找到*nA指標,在*解引用是找到*nA指向的指向的nA[2]的首地址解引用,注意這個時候必須再次解引用,因為行寬已經被分成了兩個,nA[2][2]也已經被隱式的宣告成一個指標**nA指向該陣列的首地址也就是nA[2][2]的首地址,我們要對其解引用確定要對哪個地址進行訪問***p 這種解引用方式則是對nA元素第0行第0列第0個元素進行訪問,如果+1則是對第0行第1列第0個元素訪問***p+1,如果想訪問其中的每個元素需要進行括號優先順序運算,上面也說過:

(*p)解引用*nA

*(*p)解引用*nA指向的陣列元素首地址

*(*(*p)) 上面說過nA[2][2]已經被隱式的宣告成了一個指標指向每個行寬,所以這步操作是對該指標進行解引用則每行的首地址

*(*(*p)+1)    對指標進行加減運算,讓指標向下個地址偏移一個指標變數單位也就是一個int的大小,指向下一個元素

所以列印的是:

第0行第0列第1個元素:2


如果想列印第0行第1列第0個元素只需要對*p+1即可

*(*(*p+1))

 

其指標概念較多,容易混淆,下面是幾種指標的宣告方式:

1、  一個整型數;

int a;

 

2、  一個指向整型數的指標;

int *a;

 

3、  一個指向指標的指標,它指向的指標是指向一個整型數;

int **a;

 

4、  一個有10個整型數的陣列;

int a[10];

 

5、  一個有10個指標的陣列,該指標是指向一個整型數的;

int *a[10];

 

6、  一個指向有10個整型陣列的指標;

int (*a)[10];

 

7、  一個指向函式的指標,該函式有一個整型引數並返回一個整型數;

int (*a)(int);

 

8、  一個指向陣列的指標,該陣列有10個指標,每個指標指向一個整型數;

int *(*a)[10];

 

9、  一個有10個指標的陣列,給指標指向一個函式,該函式有一個整型引數並返回一個整型數;

int (*a[10])(int);

 

10、         一個指向函式的指標,該函式有一個整型引數並返回一個指向函式的指標,返回的函式指標指向有一個整型引數且返回一個整型數的函式;

int (*(*a)(int))(int);

其實指標和陣列並沒有本質上的區別,區別在於陣列在初始化之後就不能作為右值進行運算修改陣列大小或指向其它陣列地址,所以陣列為什麼被稱為陣列就是地址?因為陣列在宣告之後就是一個常量,其地址就是整個陣列的起始地址,而指標則可以隨意指向,當然除了被const修飾符修飾的指標!

而且陣列名是不能參與運算的,必須通過下標顯示指明要參與運算的元素!

那麼又來了一個問題,上面說的陣列名就是陣列的首地址那為何還要用[]來指明下標才能運算?

答:因為在C/C++編譯器規定陣列名雖然是首地址,但是隻能被作為右值運算,如果想要被作為左值參與運算必須顯示指定下標確定操作哪個元素,而陣列名則對應整個陣列的首地址,如果對陣列名操作不指明對哪個元素操作,即對整個陣列操作,那麼對於編譯器來說如果這個陣列大於CPU位數那麼會造成硬體中斷!

最後在補充一點為什麼說要經常使用指標?

答:指標節省記憶體,使用指標並通過malloc分配記憶體可以節省編譯後記憶體,並且棧也是有限的!