多维数组与指针之间的关系详解
先介紹一下簡單的一維數組:
列如:
int a[3] = {0,1,2};[3]和類型int則明確表示編譯器應該為這個棧分配多大的內存,也就是三個int大小!
在內存中示意圖是:
在CPU看來內存是一組連續的地址空間,所以當我們對一維數組進行操作時只需要知道數組首地址,就可以通過地址偏移加減運算方式來求得每個元素位于內存中的文件映射出來的數據段虛擬地址!
不過要注意不要越界,其實你在自己的地址空間內訪問超出數組大小的空間也不會有問題,因為你是在自己的地址下進行訪問的,不會被內核卡擦掉,只不過那一段內存可能被對齊了,是未知不被使用的數據!
詳細內存對齊參見:詳解C語言內存對齊
使用方法也非常簡單:
- 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位數那么會造成硬件中斷!
關于CPU尋址詳解: 深度理解“CPU內部尋址方式”
最后在補充一點為什么說要經常使用指針?
答:指針節省內存,使用指針并通過malloc分配內存可以節省編譯后內存,并且棧也是有限的!
[轉](https://blog.csdn.net/bjbz_cxy/article/details/79962349)總結
以上是生活随笔為你收集整理的多维数组与指针之间的关系详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mat,Iplimage,vector,
- 下一篇: 处有未经处理的异常:0xC0000005