轉載:http://blog.csdn.net/walkinginthewind/article/details/7044380
C語言中數組和指針是一種很特別的關系,首先本質上肯定是不同的,本文從各個角度論述數組和指針。
一、數組與指針的關系
數組和指針是兩種不同的類型,數組具有確定數量的元素,而指針只是一個標量值。數組可以在某些情況下轉換為指針,當數組名在表達式中使用時,編譯器會把數組名轉換為一個指針常量,是數組中的第一個元素的地址,類型就是數組元素的地址類型,如:
int a[5]={0,1,2,3,4};
數組名a若出現在表達式中,如int *p=a;那么它就轉換為第一個元素的地址,等價於int *p=&a[0];
再來一個:
int aa[2][5]={0,1,2,3,4,
5,6,7,8,9};
數組名aa若出現在表達式中,如int (*p)[5]=aa;那么它就轉換為第一個元素的地址,等價於int (*p)[5]=&aa[0];
但是int (*p)[5]=aa[0]; 這個就不對了,根據規則我們推一下就很明了了,aa[0]的類型是int [5],是一個元素數量為5的整型數組,就算轉化,那么轉化成的是數組(int [5])中第一個元素的地址&aa[0][0],類型是 int *。所以,要么是int (*p)[5]=aa;要么是int (*p)[5]=&aa[0];
只有在兩種場合下,數組名並不用指針常量來表示--就是當數組名作為sizeof操作符或單目操作符&的操作數時,sizeof返回整個數組的長度,使用的是它的類型信息,而不是地址信息,不是指向數組的指針的長度。取一個數組名的地址所產生的是一個指向數組的指針,而不是指向某個指針常量值的指針。
如對數組a,&a表示的是指向數組a的指針,類型是int (*) [5],所以int *p=&a;是不對的,因為右邊是一個整形數組的指針int (*)[5],而p是一個整形指針int *;
數組的sizeof問題會在下面中仔細討論。
二、數組與指針的下標引用
int a[5]={0,1,2,3,4};
如a[3],用下標來訪問數組a中的第三個元素,那么下標的本質是什么?本質就是這樣的一個表達式:*(a+3),當然表達式中必須含有有效的數組名或指針變量。
其實a[3]和3[a]是等價的,因為他們被翻譯成相同的表達式(頂多順序不同而已),都是訪問的數組a中的元素3。
指針當然也能用下標的形式了,如:int *p=a; 那么p[3]就是*(p+3),等同於3[p](不要邪惡。。。3P,3P),同樣訪問數組a中的元素3。
根據這一規則,我們還能寫出更奇怪的表達式,如:
int aa[2][5]={0,1,2,3,4,
5,6,7,8,9};
1[aa][2],這個看起來很別扭,首先 1[aa],就是*(1+aa),那么1[aa][2]就是*(*(1+aa)+2),也就是aa[1][2]。
1[2][aa],這個就不對了,因為前半部分1[2]是不符合要求的。
當然在實際中使用這樣的表達式是沒有意義的,除非就是不想讓人很容易的看懂你的代碼。
三、數組與指針的定義和聲明
數組和指針的定義與聲明必須保持一致,不能一個地方定義的是數組,然后再另一個地方聲明為指針。
首先我們解釋一下數組名的下標引用和指針的下標應用,它們是不完全相同的,從訪問的方式來講。
int a[5]={0,1,2,3,4};
int *p=a;
對於a[3]和p[3]都會解析成*(a+3)和*(p+3),但是實質是不一樣的。
首先對於a[3],也就是*(a+3):
(1)把數組名a代表的數組首地址和3相加,得到要訪問數據的地址,這里注意,數組名a直接被編譯成數組的首地址;
(2)訪問這個地址,取出數據。
對於p[3],也就是*(p+3):
(1)從p代表的地址單元里取出內容,也就是數組首地址,指針名p代表的是指針變量的存儲地址,變量的地址單元里存放的才是數組的首地址;
(2)把取出的數組首地址和3相加,得到要訪問的數據的地址;
(3)訪問這個地址,取出數據。
下面給出一個例子來說明若定義和聲明不一致帶來的問題:
設test1.cpp中有如下定義:
char s[]="abcdefg";
test2.cpp中有如下聲明:
extern char *s;
顯然編譯是沒有問題的。
那么在test2.cpp中引用s[i]結果怎樣呢?如s[3],是‘d’嗎?好像是吧
下面我們對test2.cpp中的s[3]進行分析:
s的地址當然是由test1.cpp中的定義決定了,因為在定義時才分配內存空間的;
我們根據上面給出的指針下標引用的步驟進行計算
(1)從s代表的地址單元的取出內容(4個字節),這里實際上是數組s中的前4個元素,這個值是“abcd”,也就是16進制64636261h,到這一步應該就能看出來問題了;
(2)然后把取出的首地址和3相加,得到要訪問的數據的地址64636261h+3,這個地址是未分配未定義的;
(3)取地址64636261h+3的內容,這個地址單元是未定義的,訪問就會出錯。
下面給出分析的代碼(可只需觀察有注釋的部分):
#include<iostream> using namespace std; extern void test(); char s[]="abcdefg"; int main() { 002E13A0 push ebp 002E13A1 mov ebp,esp 002E13A3 sub esp,0D8h 002E13A9 push ebx 002E13AA push esi 002E13AB push edi 002E13AC lea edi,[ebp+FFFFFF28h] 002E13B2 mov ecx,36h 002E13B7 mov eax,0CCCCCCCCh 002E13BC rep stos dword ptr es:[edi] char ch; int i=3; 002E13BE mov dword ptr [ebp-14h],3 ch = s[i]; 002E13C5 mov eax,dword ptr [ebp-14h] 002E13C8 mov cl,byte ptr [eax+011F7000h] /* s直接翻譯成數組首地址和i(eax)相加,得到操作數地址,然后作為byte ptr類型取內容,傳給cl */ 002E13CE mov byte ptr [ebp-5],cl /* cl的內容傳給ch(ebp-5) */ test(); 002E13D1 call 002E1073 return 0; 002E13D6 xor eax,eax } 002E13D8 pop edi 002E13D9 pop esi 002E13DA pop ebx 002E13DB add esp,0D8h 002E13E1 cmp ebp,esp 002E13E3 call 002E113B 002E13E8 mov esp,ebp 002E13EA pop ebp 002E13EB ret
test2.cpp // 運行錯誤
extern char *s; void test() { 011F1470 push ebp 011F1471 mov ebp,esp 011F1473 sub esp,0D8h 011F1479 push ebx 011F147A push esi 011F147B push edi 011F147C lea edi,[ebp+FFFFFF28h] 011F1482 mov ecx,36h 011F1487 mov eax,0CCCCCCCCh 011F148C rep stos dword ptr es:[edi] char ch; int i=3; 011F148E mov dword ptr [ebp-14h],3 ch=s[i]; 011F1495 mov eax,dword ptr ds:[011F7000h] /* ds沒有影響,因為windows中所有的段基址都為0,取011F7000h單元的內容,這里是數組中前四個字節(指針是四個字節)組成的整數,也就是64636261h,也就是這里,把s所指的單元計算成了64636261h */ 011F149A add eax,dword ptr [ebp-14h] /* 然后把地址和i相加,也就是64636261h+3,這個地址是未分配定義的,訪問當然會出錯 */ 011F149D mov cl,byte ptr [eax] /* 訪問錯誤 */ 011F149F mov byte ptr [ebp-5],cl return; } 011F14A2 pop edi 011F14A3 pop esi 011F14A4 pop ebx 011F14A5 mov esp,ebp 011F14A7 pop ebp 011F14A8 ret
若test2.cpp中這樣聲明:
extern char s[];
這樣就正確了,因為聲明和定義一致,訪問就沒問題了。
所以千萬不要簡單的認為數組名與指針是一樣的,否則會吃大虧,數組的定義和聲明千萬要保持一致性。
四、數組和指針的sizeof問題
數組的sizeof就是數組的元素個數*元素大小,而指針的sizeof全都是一樣,都是地址類型,32位機器是4個字節。
下面給出一些例子:
測試程序:
#include<iostream> using namespace std; int main() { int a[6][8]={0}; int (*p)[8]; p=&a[0]; int (*pp)[6][8]; pp=&a; cout<<sizeof(a)<<endl; // 192 cout<<sizeof(*a)<<endl; // 32 cout<<sizeof(&a)<<endl; // 4 cout<<sizeof(a[0])<<endl; // 32 cout<<sizeof(*a[0])<<endl; // 4 cout<<sizeof(&a[0])<<endl; // 4 cout<<sizeof(a[0][0])<<endl; // 4 cout<<sizeof(&a[0][0])<<endl; // 4 cout<<sizeof(p)<<endl; // 4 cout<<sizeof(*p)<<endl; // 32 cout<<sizeof(&p)<<endl; // 4 cout<<sizeof(pp)<<endl; // 4 cout<<sizeof(*pp)<<endl; // 192 cout<<sizeof(&pp)<<endl; // 4 system("pause"); return 0; }
VS2010在32位windows7下的運行結果(VC6.0不符合標准):
192
32
4
32
4
4
4
4
4
32
4
4
192
4
下面對程序做逐一簡單的解釋:
(1) sizeof(a); a的定義為int a[6][8],類型是int [6][8],即元素個數為6*8的二維int型數組,它的大小就是6*8*sizeof(int),這里是192;
(2) sizeof(*a); *a這個表達式中數組名a被轉換為指針,即數組第一個元素a[0]的地址,'*'得到這個地址所指的對象,也就是a[0],總的來說*a等價於*(&a[0]),a[0]的類型int [8],即大小為8的一維int型數組,它的大小就是8*sizeof(int),這里是32;
(3) sizeof(&a); '&'取a的地址,類型是int (*)[6][8],地址類型,這里大小是4;
(4) sizeof(a[0]); a[0]的類型int [8],即大小為8的一維int型數組,它的大小就是8*sizeof(int),這里是32;
(5) sizeof(*a[0]); *a[0]這個表達式中數組名a[0]被轉換為指針,即數組的第一個元素a[0][0]的地址,'*'得到這個地址所指的元素,也就是a[0][0],總的來說*a[0]等價於*(&a[0][0]),a[0][0]的類型是int,它的大小就是sizeof(int),這里是4;
(6) sizeof(&a[0]); '&'取a[0]的地址,類型是int (*)[8],地址類型,這里大小是4;
(7) sizeof(a[0][0]); a[0][0]的類型是int,它的大小就是sizeof(int),這里是4;
(8) sizeof(&a[0][0]); '&'取a[0][0]的地址,類型是int *,地址類型,這里大小是4;
(9) sizeof(p); p的類型是int (*)[8],指向一個元素個數為8的int型數組,地址類型,這里大小是4;
(10)sizeof(*p); *p取得p所指的元素,類型是int [8],大小為8*sizeof(int),這里是32;
(11)sizeof(&p); '&'取p的地址,類型是int (**) [8],地址類型,這里大小是4;
(12)sizeof(pp); pp的類型是int (*)[6][8],指向一個大小為6*8的二維int型數組,地址類型,這里大小為4,
(13)sizeof(*pp); *pp取得pp所指的對象,類型是int [6][8],即元素個數為6*8的二維int型數組,它的大小就是6*8*sizeof(int),這里是192;
(14)sizeof(&pp); '&'取pp的地址,類型是int (**)[6][8],地址類型,這里大小是4;
五、數組作為函數參數
當數組作為函數參數傳入時,數組退化為指針,類型是第一個元素的地址類型。“數組名被改寫成一個指針參數”,這個規則並不是遞歸定義的。數組的數組會被改寫為“數組的指針”,而不是“指針的指針”。
下面給出幾個例子:
fun1(char s[10])
{
// s在函數內部實際的類型是char *;
}
fun2(char s[][10])
{
// s在函數內部的實際類型是char(*) [10],即char [10]數組的指針;
}
fun3(char *s[15])
{
// s在函數內部的實際類型是char **,字符型指針的指針;
}
fun4(char(*s)[20])
{
// s在函數內部的實際類型不變,仍然是char(*) [20],即char [20]數組的指針;
}
以上可以簡單的歸納為數組作為參數被改寫為指向數組的第一個元素(這里的元素可以是數組)的指針。數組作為參數必須提供除了最左邊一維以外的所有維長度。我們還要注意char s[][10]和char ** s作為函數參數是不一樣的,因為函數內部指針的類型不一樣的,尤其在進行指針加減運算以及sizeof運算時。
總結:
總結了這么多,應該對數組和指針有個較深入的理解了。這些問題的歸根原因還是來自於指針問題,這也正是c語言的精華所在,不掌握這些根本不算掌握c語言,不過掌握了這些也不敢說就等於掌握了c語言:)