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


轉載: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語言:) 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM