深入理解C/C++数组和指针

http://bbs.lupaworld.com/thread-183216-1-1.html
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][1],类型是 int *。
只有在两种场合下,数组名并不用指针常量来表示--就是当数组名作为sizeof操作符或单目操作符&的操作数时,sizeof返回整个数组的长度,使用的是它的类型信息,而不是地址信息,不是指向数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是指向某个指针常量值的指针。
如对数组a,&a表示的是指向数组a的指针,类型是int (*)a,所以int *p=&a;是不对的;
数组的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相加,得到要访问数据的地址;
(2)访问这个地址,取出数据。
对于p[3],也就是*(p+3):
(1)从p代表的地址单元里取出内容,也就是数组首地址;
(2)把取出的数组首地址和3相加,得到要访问的数据的地址;
(3)访问这个地址,取出数据。
下面给出一个例子来说明若定义和声明不一致带来的问题:
设test1.cpp中有如下定义:
char s[]="abcdefg";
test2.cpp中有如下声明:
extern char *s;
显然编译是没有问题的。
那么在test2.cpp中引用s结果怎样呢?如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的内容,这个地址单元是未定义的,访问就会出错。
下面给出分析的代码(可只需观察有注释的部分):

view plain

  • #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;  
  • 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 // 运行错误

view plain

  • 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;  
  • 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个字节。
下面给出一些例子:
测试程序:
view plain

  • #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 *,指向一个int型元素,地址类型,这里大小是4;
(10)sizeof(*p); *p取得p所指的元素,类型是int,大小为sizeof(int),这里是4;
(11)sizeof(&p); '&'取p的地址,类型是int **,地址类型,这里大小是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语言:)
posted @ 2011-12-07 18:54  wdliming  阅读(111)  评论(0编辑  收藏  举报