C語言中文網 目錄

數組和指針的區別,C語言數組和指針的區別

在 C 語言中,對數組的引用總是可以寫成對指針的引用,而且也確實存在一種指針和數組定義完全相同的上下文環境。因此,給大家帶來指針和數組應該是可以互換的錯覺,大家也會自然地歸納并假定在所有的情況下數組和指針都是等同的。實際上,并非所有情況下都是如此。

簡單地講,數組就是數組,指針就是指針,它們之間沒有任何關系,只是經常穿著相似的衣服來迷惑你罷了。因此,“數組和指針是相同的”這種說法是危險的,是不完全正確的。

回顧前面對于左值和右值的討論,編譯器為每個變量分配一個地址(左值),這個地址在編譯時可知,而且該變量在運行時一直保存于這個地址。相反,存儲于變量中的值(右值)只有在運行時才可知。如果需要用到變量中存儲的值,編譯器就發出指令從指定地址讀入變量值并將它存于寄存器中。

這里需要注意的是,由于編譯器為每個變量分配一個地址(左值),這個地址在編譯時可知,因此,如果編譯器需要一個地址(可能還需要加上偏移量)來執行某種操作,它就可以直接進行操作,并不需要增加指令首先取得具體的地址。示例代碼如下所示:
char a[6]="hello";
...
c=a[i];
對于上面的示例代碼,在定義數組 a 時,編譯器就在某個地方保存了 a 的首元素的首地址,這里假設地址為 10000(即 a 是一個地址,編譯器會為數組 a 分配一個空間,但不會為 a 本身分配空間),如圖 1 所示。


圖 1 對數組下標的引用

現在,要取 a[i] 的內容可以分為如下兩步進行:
  1. 計算 a[i] 的地址:10000+i*sizeof(char)。
  2. 取 10000+i*sizeof(char) 地址上的內容。

除了圖 1 之外,我們還可以通過下面一段匯編代碼來清楚地看見其執行步驟(Microsoft Visual Studio 2010 的 Debug 模式):
char a[6]="hello";
00A13718  mov         eax,dword ptr [string "hello" (0A157A0h)]
00A1371D  mov         dword ptr [ebp-10h],eax
00A13720  mov         cx,word ptr ds:[0A157A4h]
00A13727  mov         word ptr [ebp-0Ch],cx
char a0=a[0];
00A1372B  mov         al,byte ptr [ebp-10h]
00A1372E  mov         byte ptr [ebp-19h],al
char a1=a[1];
00A13731  mov         al,byte ptr [ebp-0Fh]
00A13734  mov         byte ptr [ebp-25h],al
char a2=a[2];
相反,對指針變量而言,編譯器要為之分配一個空間。在對一個指針變量進行引用的時候(比如(*p)),編譯器首先需要得到 p 的地址,從中取值,然后把得到的值作為地址,再取值。示例代碼如下所示:
char *p="hello";
...
c=*p;
對比前面的數組,它的執行步驟如圖 2 所示。


圖 2 對指針的引用
  1. 這里假設 p 的地址為 1001,取出地址 1001 上的內容 2001。
  2. 再取出地址 2001 上的內容。

匯編代碼如下(Microsoft Visual Studio 2010 的 Debug模式):
00D313D5  mov         eax,dword ptr [p]
00D313D8  mov         cl,byte ptr [eax]
很顯然,對數組而言,指針的訪問要靈活得多,但需要增加一次額外的提取。

通過上面的闡述,相信你對數組與指針之間的區別已經有了一定的了解,現在繼續看下面兩種情況。

定義為數組,聲明為指針

這里需要特別強調一下變量的聲明與定義之間的區別。簡單地講,聲明就是告訴編譯器存在著這么一個變量,但是編譯器并不會為它分配任何內存(即并不實現它)。而定義則是實現這個變量,真正在內存(堆或棧)中為此變量分配空間。定義只能出現一次,而聲明可以出現多次。

理解了這些內容,來看下面的示例代碼:
/*文件1:f1.c*/
int a[3]={0,1,2};
/*文件2:f2.c*/
int main(void)
{
    extern int *a;
    printf ("%d\n", a[0]);
    return 0;
}
在上面的示例代碼中,我們在 f1.c 中定義了一個數組 a,在 f2.c 中聲明了一個指針變量 a。其中,語句“extern int*a”告訴編譯器:a 這個名字已經在別的文件中被定義了,下面的代碼使用的名字 a 是別的文件定義的。因此,編譯器此時一定不會做什么分配內存的事,因為它就是聲明,僅僅表明下面的代碼引用了一個符號。

但是,當你聲明“extern int*a”時,編譯器理所當然地認為 a 是一個指針變量,在 32 位系統下,占 4 字節。這 4 字節保存了一個地址,這個地址上保存的是 int 類型的數據。雖然在 f1.c 中,編譯器知道 a 是一個數組,但是在 f2.c 中,編譯器并不知道這點。大多數編譯器是按文件分別編譯的,編譯器只按照本文件中聲明的類型來處理。所以,雖然 a 在 f1.c 中實際大小為 12 字節,但是在 f2.c 中,編譯器認為 a 只占 4 字節。

由此可見,“extern int*a”這種聲明方法是完全錯誤的,它會按照指針的方法來引用數組。同時,在一些編譯器中也會給出報錯提示,如在 Microsoft Visual Studio 2010 中將給出錯誤提示:“error C2372:'a':redefinition;different types of indirection”。

正確的聲明方法應該是:
extern int a[];
/*或者*/
extern int a[3]
示例代碼如下所示:
int main(void)
{
    extern int a[];
    printf ("%d\n", a[0]);
    return 0;
}
這里的 extern int a[] 與 extern int a[3] 是等價的。因為這只是聲明,不分配空間,所以編譯器無需知道這個數組有多少個元素。這兩個聲明都告訴編譯器:a 是在別的文件中被定義的一個數組,a 同時代表著數組 a 的首元素的首地址,也就是這塊內存的起始地址。數組內任何元素的地址都只需要知道這個地址就可以計算出來。

定義為指針,聲明為數組

根據上面的分析不難看出,如果在文件 1 中定義為指針,而在文件 2 中聲明為數組,也同樣會發生錯誤,示例代碼如下所示:
/*文件3:f3.c*/
char *pa = "hello";
/*文件4:f4.c*/
int main(void)
{
    extern char pa[];
    printf ("%c\n", pa[0]);
    return 0;
}
同樣,在 Microsoft Visual Studio 2010 將給出錯誤提示:“error C2372:'pa':redefinition;different types of indirection”。因為本來針對數組需要對內存進行直接引用,但是這時編譯器所執行的卻是對內存進行間接引用。

正確的聲明方法應該是:

extern char *pa

由此可見,聲明與定義應該完全相匹配。同時,這也證明了“數組和指針是相同的”這種說法的錯誤性。數組和指針在編譯器處理的時候是不同的,在運行時的表示形式也不一樣。對編譯器而言,一個數組就是一個地址,一個指針就是一個地址的地址,你應該根據情況進行選擇。表 3 列出了指針與數組的常見區別。

表 3 指針與數組的區別
指針 數組
保存數據的地址,任何存入指針變量 p 的數據都會被當作地址來處理 保存數據,數組名 a 代表的是數組首元素的首地址,&a 是整個數組的首地址
間接訪問數據,首先取得指針變量 p 的內容,把它當做地址,然后從這個地址提取數據或向這個地址寫入數據。

指針可以以指針的形式訪問 "*(p+i)" 也可以以下標的形式訪問 "p[i]"。但其本質都是先取 p 的內容后加上“i*sizeof(類型)”字節作為數據的真正地址。
直接訪問數據,數組名 a 是整個數組的名字,數組內每個元素并沒有名字。只能通過"具名+匿名"的方式來訪問其某個元素,不能把數組當一個整體進行讀寫操作。

數組可以以指針的形式訪問"*(a+i)",也可以以下標的形式訪問"a[i]"。但其本質都是 a 所代表的數組首元素的首地址加上"i*sizeof(類型)"字節來作為數據的真正地址
通常用于動態數據結構 通常用于存儲固定數目且數據類型相同的元素
需要 malloc 和 free 等相關的函數進行內存分配 隱式分配和刪除
通常指向匿名數據 自身即為數組名

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo