C/C++数组和指针详解

/****************************************************************/

/*            学习是合作和分享式的!

/* Author:Atlas                    Email:[email protected]   

/*  转载请注明本文出处:

*  http://blog.csdn.net/wdzxl198/article/details/9087497

/****************************************************************/

1.引文

     本文不会对数组和指针的基本概念和操作做讲解,不是很清楚的请看c/c++相关书籍,这里默认读者对这些内容熟知。

首先看一个例子:以下两个程序,程序1使用整形变量,程序2使用整形指针,但是在编译运行后,会是什么样的?

program 1:

   1: #include <iostream>
   2: int main(){
   3:     int int_input;
   4:     cin>>int_input;
   5:     cout<<(int_input + 4)<<endl;
   6:     return 0;
   7: }
program 2:
   1: #include <iostream>
   2: int main(){
   3:     int *int_ptr = new int[1];
   4:     cin>>*int_ptr;
   5:     cout<< (*int_ptr + 4)<<endl;
   6:     delete(int_ptr);
   7:     return 0;
   8: }

       答案显而易见,都是需要你输入一个整数,然后输出刚才输入的值。问题就来了,这两个程序运行结果是一样的,你能说这两个程序是完全一样的么?? Eexactly,NO! Why?我想当你看完这篇文章,肯定豁然开朗(分析见最后)。

       通过以上简单的引入例子,可以对数组和指针有个简单认识,接下来本文从数组和指针在访问方式上的区别、在函数调用上的区别以及数组和指针内在的联系上进行分析。

2.在内存访问方式上,数组和指针的区别

    本小节讲述对数组的引用和对指针的引用的不同之处,学习资料来源于《expert c programing》。

为了对数组和指针对内存访问方式的理解,首先对“地址y”和“地址y的内容”做简单的区别,详细内容如下图:

(ps:这个图中文字描述上,我觉的语言欠妥。什么是X所代表的地址?左值在编译时可知,左值表示存储结果的地方?等等,我看了原版,感觉也不是翻译的问题,让人非似懂的。大家的意见呢?)

         编译器为每个变量分配地址(左值)。这个地址在编译时可知且一直存在,而它的右值在运行时才能知道。通俗的说:每个变量都有一个地址,这个地址在编译时可以知道,而地址里存储的内容(也就是变量的右值)只有在运行时才能知道。如果需要用到变量的值(也就是已知地址存储的值),那么编译器发出指令从指定地址读入变量值并放入相应寄存器中。

(1)数组的访问方式

        关键之处在于每个符号的地址在编译时可知,如果编译器需要一个地址(比如说加上偏移量)来执行某种操作,可以可以直接操作。相反、对于指针,必须在运行时取得它的地址,然后才能

对它进行接触引用操作。下图A展示了对数组下标的引用,体现数组的访问形式:

      看到这里,就明白为什么extern char a[]和extern char a[100]相同的原因了。这两个声明都是提示a是一个数组,也就是一个内存地址,数组内的字符可以由这个地址(加偏移量)找到。

(2)指针的访问方式

      若声明的是一个指针,如  extern char *p,它表示p指向一个字符,为了取得这个字符,必须知道地址p的内容,把它作为字符的地址并从这个地址中取得这个字符。如下图B所示,体现指针访问的形式。

(3)若“定义为指针,但是以数组的方式引用”,这样会发生什么?

     当一个外部数组的实际定义是一个指针,但却以数组的方式对其引用时,会发生些什么?会向图A中方式直接的引用访问?事实是编译器所执行的是图B中的对内存间接引用。因为我们告诉编译器拥有的一个指针,如图C所示。

我们这里对照图C中的访问方式:

   1: char *p = "abcdefgh";  ...  p[3]; 

与图A中的访问方式:

   1: char a[] = "abcdefgh";   ...  a[3];

这两种方式都可以取得字符‘d’,但是途径不一致。

(4)数组和指针的一些特点对比

表1 数组和指针的区别

指针

数组

保存数据的地址 保存数据
间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。 
如果指针有一个下标[i],就把指针的内容加上i作为地址,从中提取数据
直接访问数据,a[i]只是简单的以a+i为地址取得数据
通常用于动态数据结构 通常用于存储固定数目且数据类型相同的元素
相关的函数为malloc(),free() 隐式分配和删除
通常指向匿名数据 自身即为数据名

      数组和指针可以在定义中用字符串常量进行初始化,尽管看上去一样,底层的机制却不同。

     定义指针时,编译器并不为它所指向的对象分配空间,只为指针本身分配空间。除非在定义同时付给一个指针一字符窜常量进行初始化。

           如:char *p = "breadfruit";   (注意:只有对字符串常量才是如此,不能指望为浮点数之类的常量分配空间例如:float *p = 3.1415 ,!!!这是错误的)

     一般情况下初始化指针时创建的字符串变量被定义为只读。如果试图修改就会出现未定义的行为。

     数组可以用字符串常量进行初始化:

          char a[] = “gooseberry“;

    与指针相反,由字符串常量初始化的数组可以修改的。

3再论数组和指针

     在内存访问方式上,数组和指针存在区别。意识到数组和指针是不同的,但是在实际应用中有时候数组和指针却是相同的!?多么神奇!如果你是编程老手,就会知道在实际应用中数组和指针可以互换的情形要比两者不可互换的情形更为常见。

(1)“声明”和“使用”情形下

       声明本身还可以进一步分为三种情况:

          1)外部数组的声明(external array)

          2)数组的定义(它是声明的一种特殊情况,它分配内存空间,并可能提供一个初值)

          3)函数参数的声明

       所有作为函数参数的数组名在编译时是会转换为指针(有种说法是“退化为指针”,一个意思),而其他情况的声明,数组的声明就是数组 ,指针的声明就是指针,两者不混淆。但是在使用数组时,数组总是可以写成指针的形式,两者可以互换。如下图所示:

     上图在声明和使用上说明了数组和指针在什么情况下可以互换使用,即是相同的。然而,数组和指针在编译器处理时是不同的,在运行时的表现形式也是不同的,并可能产生不同的代码,这点是需要牢记的!具体原因,如果看了前面的第一部分内容,很容易理解。

(2)什么时候数组和指针是相同的

   在C语言标准中,对此有如下说明

1)表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针;

   在表达式中,指针和数组是可以互换的,因为他们在编译器中最终形式都是指针。如何理解这句话,请看下面的这个例子。

  例子:

   1: int a[10],*p,i = 2;

可以通过以下任一种方式访问a[i]

p=a;   

p[i];      

p = a;  

*(p+i);  

p = a+i;

*p;

原因是对数组的引用如a[i]在编译时总是被编译器改写成*(a+i)的形式。

2)下标总是与指针的偏移量相同;

             C语言中,把数组下标作为指针的偏移量。根本原因是指针和偏移量是底层硬件所使用的基本模型。

3)在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针;

      理解这句话首先要明白形参和实参的区别,

术语 定义 例子
形参(parameter) 它是一个变量,在函数定义或者函数声明的原型中定义。又称“形式参数(formal parameter)” int power(int base,int n);
base 和 n 都是形参
实参(argument) 在实际调用一个函数时所传递给函数的值。又称“实际参数(actual parameter)” i = power(10,j);
10和j都是实参。

在函数参数这种特殊情况下,编译器必须把数组名当作指向该数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。

(3)数组和指针可交换型总结

a)对于a [i]这种形式的访问数组,通常被解释为指针形式*(a + i)   也就是上文中所说的“表达式”的情形

b)指针就是指针,没有说指针转化为数组的情况,你可以用下标的形式去访问指针,但一般都是指针作为函数参数时,而且传入的是一个数组

c)在函数参数的声明中,数组可以看做指针,(也只有这种情况)

d)当把一个数组定义为函数参数时,可以定义为数组,也可以是指针

e)其他的所有情况,声明和定义必须匹配。如果定义了一个数组,在其他文件中对它也必须声明为数组。指针也一样。

(4)数组和指针参数是如何被编译器修改的

      看到文章这里,可以说是到一个结束段落了,你基本理解了数组和指针的不同和相同,学习更多知识需要自己的实践操作!!!哦,忘了还有对引文中问题的解答,看下第四部分吧。

上部分中提到编译器对数组到指针的修改,所以这里补充一点东西。

“数组名被改写成一个指针参数”规则不是递归定义的。就是说数组的数组会被改写成“数组的指针”,而不是“指针的指针”。

实参

所匹配的形式参数

数组的数组                        char  c[8][10]; char(*)[10];                       数组指针
指针数组                            char *c[15]; char **c;                           指针的指针
数组指针(行指针)         char (*c)[64]; char (*c)[64];                   不改变
指针的指针                        char **c; char **c;                           不改变

4 对引文中问题的解答

      这里我们在内存中方式的不同来看这个问题。查看其汇编代码答案就一目了然了。

第一个程序:整型变量的

   1: #include <iostream>
   2: int main(){
   3:     int int_input;
   4:     cin>>int_input;
   5:     cout<<(int_input + 4)<<endl;
   6:     return 0;
   7: }
汇编程序:
   1: 2212: main(){
   2: 00401000   push        ebp
   3: 00401001   mov         ebp,esp
   4: 00401003   sub         esp,44h
   5: 00401006   push        ebx
   6: 00401007   push        esi
   7: 00401008   push        edi
   8: 2213:      int int_input;
   9: 2214:      cin>>int_input;
  10: 00401009   lea         eax,[ebp-4]
  11: 0040100C   push        eax
  12: 0040100D   mov         ecx,offset cin (00414c58)
  13: 00401012   call        istream::operator>> (0040b7c0)
  14: 2215:      cout<<(int_input+4)<<endl;
  15: 00401017   push        offset endl (00401070)
  16: 0040101C   mov         ecx,dword ptr [ebp-4]
  17: 0040101F   add         ecx,4
  18: 00401022   push        ecx
  19: 00401023   mov         ecx,offset cout (00414c18)
  20: 00401028   call        ostream::operator<< (0040b3e0)
  21: 0040102D   mov         ecx,eax
  22: 0040102F   call        ostream::operator<< (00401040)
  23: 2216:      return 0;
  24: 00401034   xor         eax,eax
  25: 2217: }

第二个程序:整形指针的

   1: #include <iostream>
   2: int main(){
   3:     int *int_ptr = new int[1];
   4:     cin>>*int_ptr;
   5:     cout<< (*int_ptr + 4)<<endl;
   6:     delete(int_ptr);
   7:     return 0;
   8: }
汇编程序:
   1: 2212: main(){
   2: 00401000   push        ebp
   3: 00401001   mov         ebp,esp
   4: 00401003   sub         esp,4Ch
   5: 00401006   push        ebx
   6: 00401007   push        esi
   7: 00401008   push        edi
   8: 2213:      int *int_ptr = new int[1];
   9: 00401009   push        4
  10: 0040100B   call        operator new (004011b0)
  11: 00401010   add         esp,4
  12: 00401013   mov         dword ptr [ebp-8],eax
  13: 00401016   mov         eax,dword ptr [ebp-8]
  14: 00401019   mov         dword ptr [ebp-4],eax
  15: 2214:      cin>>*int_ptr;
  16: 0040101C   mov         ecx,dword ptr [ebp-4]
  17: 0040101F   push        ecx
  18: 00401020   mov         ecx,offset cin (00414c38)
  19: 00401025   call        istream::operator>> (0040b8a0)
  20: 2215:      cout<< (*int_ptr + 4)<<endl;
  21: 0040102A   push        offset endl (004010a0)
  22: 0040102F   mov         edx,dword ptr [ebp-4]
  23: 00401032   mov         eax,dword ptr [edx]
  24: 00401034   add         eax,4
  25: 00401037   push        eax
  26: 00401038   mov         ecx,offset cout (00414bf8)
  27: 0040103D   call        ostream::operator<< (0040b4c0)
  28: 00401042   mov         ecx,eax
  29: 00401044   call        ostream::operator<< (00401070)
  30: 2216:      delete(int_ptr);
  31: 00401049   mov         ecx,dword ptr [ebp-4]
  32: 0040104C   mov         dword ptr [ebp-0Ch],ecx
  33: 0040104F   mov         edx,dword ptr [ebp-0Ch]
  34: 00401052   push        edx
  35: 00401053   call        operator delete (00401120)
  36: 00401058   add         esp,4
  37: 2217:      return 0;
  38: 0040105B   xor         eax,eax
  39: 2218: }

通过汇编代码来看,在变量定、输入以及输出时看出整型变量不同于指向整形的指针。

 


本文为《c++内存管理系列》的知识补充。

参考文献:c++内存管理学习纲要

Edit by Atlas  

Time:2013/6/13  15:39

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。