深刻理解C/C++數組和指針

C語言中數組和指針是一種很特別的關係,首先本質上確定是不一樣的,本文從各個角度論述數組和指針。ios

1、數組與指針的關係
數組和指針是兩種不一樣的類型,數組具備肯定數量的元素,而指針只是一個標量值。數組能夠在某些狀況下轉換爲指針,當數組名在表達式中使用時,編譯器會把數組名轉換爲一個指針常量,是數組中的第一個元素的地址,類型就是數組元素的地址類型,如:
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問題會在下面中仔細討論。
2、數組與指針的下標引用
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]是不符合要求的。
固然在實際中使用這樣的表達式是沒有意義的,除非就是不想讓人很容易的看懂你的代碼。
3、數組與指針的定義和聲明
數組和指針的定義與聲明必須保持一致,不能一個地方定義的是數組,而後再另外一個地方聲明爲指針。
首先咱們解釋一下數組名的下標引用和指針的下標應用,它們是不徹底相同的,從訪問的方式來說。
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的內容,這個地址單元是未定義的,訪問就會出錯。windows

下面給出分析的代碼(可只需觀察有註釋的部分):數組

  1. #include<iostream>           
  2. using namespace std;  
  3. extern void test();  
  4. char s[]="abcdefg";  
  5. int main()  
  6. {  
  7. 002E13A0  push        ebp    
  8. 002E13A1  mov         ebp,esp    
  9. 002E13A3  sub         esp,0D8h    
  10. 002E13A9  push        ebx    
  11. 002E13AA  push        esi    
  12. 002E13AB  push        edi    
  13. 002E13AC  lea         edi,[ebp+FFFFFF28h]    
  14. 002E13B2  mov         ecx,36h    
  15. 002E13B7  mov         eax,0CCCCCCCCh    
  16. 002E13BC  rep stos    dword ptr es:[edi]    
  17.     char ch;  
  18.     int i=3;  
  19. 002E13BE  mov         dword ptr [ebp-14h],3    
  20.     ch = s[i];  
  21. 002E13C5  mov         eax,dword ptr [ebp-14h]    
  22. 002E13C8  mov         cl,byte ptr [eax+011F7000h]  /* s直接翻譯成數組首地址和i(eax)相加,獲得操做數地址,而後做爲byte ptr類型取內容,傳給cl */  
  23. 002E13CE  mov         byte ptr [ebp-5],cl          /* cl的內容傳給ch(ebp-5) */  
  24.     test();  
  25. 002E13D1  call        002E1073    
  26.     return 0;  
  27. 002E13D6  xor         eax,eax    
  28. }  
  29. 002E13D8  pop         edi    
  30. 002E13D9  pop         esi    
  31. 002E13DA  pop         ebx    
  32. 002E13DB  add         esp,0D8h    
  33. 002E13E1  cmp         ebp,esp    
  34. 002E13E3  call        002E113B    
  35. 002E13E8  mov         esp,ebp    
  36. 002E13EA  pop         ebp    
  37. 002E13EB  ret    


test2.cpp // 運行錯誤 函數

  1. extern char *s;  
  2. void test()  
  3. {  
  4. 011F1470  push        ebp    
  5. 011F1471  mov         ebp,esp    
  6. 011F1473  sub         esp,0D8h    
  7. 011F1479  push        ebx    
  8. 011F147A  push        esi    
  9. 011F147B  push        edi    
  10. 011F147C  lea         edi,[ebp+FFFFFF28h]    
  11. 011F1482  mov         ecx,36h    
  12. 011F1487  mov         eax,0CCCCCCCCh    
  13. 011F148C  rep stos    dword ptr es:[edi]    
  14.     char ch;  
  15.     int i=3;  
  16. 011F148E  mov         dword ptr [ebp-14h],3    
  17.     ch=s[i];  
  18. 011F1495  mov         eax,dword ptr ds:[011F7000h]  /* ds沒有影響,由於windows中全部的段基址都爲0,取011F7000h單元的內容,這裏是數組中前四個字節(指針是四個字節)組成的整數,也就是64636261h,也就是這裏,把s所指的單元計算成了64636261h */  
  19. 011F149A  add         eax,dword ptr [ebp-14h]       /* 而後把地址和i相加,也就是64636261h+3,這個地址是未分配定義的,訪問固然會出錯 */   
  20. 011F149D  mov         cl,byte ptr [eax]             /* 訪問錯誤 */  
  21. 011F149F  mov         byte ptr [ebp-5],cl    
  22.     return;  
  23. }  
  24. 011F14A2  pop         edi    
  25. 011F14A3  pop         esi    
  26. 011F14A4  pop         ebx    
  27. 011F14A5  mov         esp,ebp    
  28. 011F14A7  pop         ebp    
  29. 011F14A8  ret    

若test2.cpp中這樣聲明:
extern char s[];
這樣就正確了,由於聲明和定義一致,訪問就沒問題了。
因此千萬不要簡單的認爲數組名與指針是同樣的,不然會吃大虧,數組的定義和聲明千萬要保持一致性。
4、數組和指針的sizeof問題
數組的sizeof就是數組的元素個數*元素大小,而指針的sizeof全都是同樣,都是地址類型,32位機器是4個字節。
下面給出一些例子:
測試程序:測試

  1. #include<iostream>                          
  2. using namespace std;  
  3. int main()  
  4. {  
  5.     int a[6][8]={0};  
  6.     int (*p)[8];  
  7.     p=&a[0];      
  8.     int (*pp)[6][8];   
  9.     pp=&a;  
  10.   
  11.     cout<<sizeof(a)<<endl;        // 192  
  12.     cout<<sizeof(*a)<<endl;       // 32  
  13.     cout<<sizeof(&a)<<endl;       // 4  
  14.     cout<<sizeof(a[0])<<endl;     // 32  
  15.     cout<<sizeof(*a[0])<<endl;    // 4  
  16.     cout<<sizeof(&a[0])<<endl;    // 4  
  17.     cout<<sizeof(a[0][0])<<endl;  // 4  
  18.     cout<<sizeof(&a[0][0])<<endl; // 4  
  19.     cout<<sizeof(p)<<endl;        // 4  
  20.     cout<<sizeof(*p)<<endl;       // 32  
  21.     cout<<sizeof(&p)<<endl;       // 4  
  22.     cout<<sizeof(pp)<<endl;       // 4  
  23.     cout<<sizeof(*pp)<<endl;      // 192  
  24.     cout<<sizeof(&pp)<<endl;      // 4   
  25.   
  26.     system("pause");  
  27.     return 0;  
  28. }  

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;
5、數組做爲函數參數
當數組做爲函數參數傳入時,數組退化爲指針,類型是第一個元素的地址類型。「數組名被改寫成一個指針參數」,這個規則並非遞歸定義的。數組的數組會被改寫爲「數組的指針」,而不是「指針的指針」。
下面給出幾個例子:
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語言: spa

相關文章
相關標籤/搜索