Chinaunix首页 | 论坛 | 博客
  • 博客访问: 261769
  • 博文数量: 94
  • 博客积分: 526
  • 博客等级: 中士
  • 技术积分: 687
  • 用 户 组: 普通用户
  • 注册时间: 2012-02-09 10:02
文章存档

2014年(1)

2013年(10)

2012年(83)

分类:

2012-09-07 23:57:00

原文地址:深入理解数组与指针 作者:xu48169172

在大一刚开始学习C的我们也许并没有真正的理解数组与指针,其实C的精华部分便是指针与内存的分配这一块。

那时充其量我们能够知道数组与指针肯定不是完全等价的,相同点就是:对数组的引用总是转化为对指针的引用,而不同点呢就是数组名是常量而指针是变量仅此而已,随着我们资历不断的提升,我们更加进一步的去理解它,从他的本质去理解,即内存的分配与访问去理解它。
          好了,首先呢我们必须明白一个概念在C语言中,一个变量的声明和定义有什么区别。
         我们知道定义只是一个特殊的声明。
         定义:只能出现在一个地方,创建新对象,同时确定对象的类型并分配内存
         声明:可以出现多次。 描述对象的类型,用于指代其他地方定义的对象。它所说明的并非本身。
extern 对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行,由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。
所以下来我们看一个例子:
文件1:int mango[100];
文件2:exitern int *mango;
在我们运行时总是会出错,为什么呢?
首先我们在看一下内存对数组和指针的引用:
比如说:char a[9]="abcdefgh"; c=a[i];
编译器符号表具有一个地址 9980
              运行时第一步:取i的值,将它与9980相加
              运行时第二步:取地址(9980+i)的值。
但是当:char *a="abcdefgh";     c=a[i];时
编译器符号表具有一个a,地址为4624
              运行时第一步:取地址 4624的内容,如:5081(注意这里:因为a是一个指针),我们知道指针存的是地址(也就是4624单元内的内容仍然是一个地址),而相对于本题来说它所存的地址就是字符串的地址。)
              运行时第二步:去i的值,并将它与5081相加。
              运行时第三步:取地址[5081+i]的值。
我们很容易看到上面对数组内存是直接访问的,而对于指针是间接访问的。(虽然最终都可以取得所要的元素,但是访问途径是不一样的!)
当extern char *a;然后用a[i]来引用其中的一个元素时,就是对:char *a="abcdefgh";     c=a[i];的访问步骤。
所以既然把a声明为指针,那么不管a原先是定义为指针还是数组,都会按照指针形式访问元素的,但是只有当a原来定义为指针时这个方法才是有效的,得到正确引用。
然后我们分析开始的那个为什么运行时总是出错?mango 被声明为extern char *mango,而它原先的定义却是char mango[100],这种情形,当用mango[i]这种形式提取这个声明的内容时,实际上得到的是一个字符。但按照上面的说法,编译器却把它当成是一个指针,把ACSII码解释为地址,显然是牛头不对马嘴么。
即:先取出mango这个地址存的内容为首地址的内容,再把首地址的内容当成指针,再加上i个偏移量。
解决方案很容易:即声明为:extern int mango[];

还有就是如我们定义一个:char *p="abcdefgh";
此时这是一个字符串常量,这个字符串常量被定义为只读。如果试图通过指针去修改这个字符串的内容时,就会出现未定义的行为。
           
与指针相反的是:被字符串常量初始化的数组是可以修改其内容的。

下面说一句C程序员的名言:千万千万不要忘了C语言在表达式中把一个类型为T的数组的左值当作是指向该数组第一个元素的指针。

什么时候数组和指针可以相同呢?

所有作为函数参数的数组名总是可以通过编译器转换为指针。在其他所有情况下(最有趣的情况就是“在一个文件中定义为数组,在另一个文件中声明为指针”),数组的声明就是数组,指针的声明就是指针,两者不能混淆。但在是用数组(在语句或表达式中引用)时,数组总是可以写成指针的形式,两者可以互换。

对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址。

总结一下相同点的地方和不同点的地方:
声明方面:1、extern 如extern char a[];不能写成指针形式。

                  2、定义,如 char a[10];不能该写成指针形式。

                  3、函数的参数,如fun(char a[])或fun(char *a)随便写,因为当数组作为函数
                
                  参数时,编译都将把它转化为指针形式处理。

在表达式中使用:如c=a[i]也可以随便写,数组形式或指针形式都可以。

标准C规定了三条规则:数组与指针相同

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

2、下标总是与指针的偏移量相同。

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

在表达式中,指针和数组是可以互换的,因为他们在编译器里的最终形式都是指针,并且都可以进行取下标运算。当然也有几个极少见的例外:在下列情况下,对数组的引用不能用指向该数组第一个元素的指针来代替:
(1)数组作为sizeof()的操作数显然此时需要的是整个数组的长度,而不是指向第一个元素的指针。
(2)使用&取数组的地址,它所取的是整体数组的一个地址。
(3)初始值时,数组是一个字符串常量。

C语言把数组下标改写成指针偏移量的根本原因就是指针和偏移量是底层硬件所使用的基本模型。

标准规定作为“类型的数组”的形惨的声明应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第一个元素的指针形式,编译器只向函数传递数组的地址,而不是整个数组的拷贝。

把作为形惨的数组和指针等同起来是处于效率原因的考虑。
在C语言中,所有非数组形式的数据实参均以传值形式(对实参作一份拷贝传递给调用的函数,函数不能修改作为实参的实际变量的值,而是只能修改传递给他的那份拷贝)调用。然而,如果要拷贝整个数组,无论是在时间上还是空间上的开销都是很大的,而且在大多数情况下你并不需要拷贝整个数组,只要告诉函数那个地址就可以了。

我们看一下数组形惨是如何被引用的:
fun(char p[])或fun(char *p)         c=p[i]
编译器符号表显示p可以取址,从堆栈指针SP偏移14个位置
运行时步骤1:从SP偏移14个位置找到函数的活动记录,取出参数
运行时步骤2:取i的值,并于5081相加
运行时步骤3:取出地址(5081+i)的内容


注意:有一样操作只能在指针里进行而无法在数组中进行,那就是修改它的值。数组名是不可修改的左值,他的值是不能改变的。


如下面三个函数在同一个文件中:

点击(此处)折叠或打开

  1. fun1(int *ptr)
  2. {
  3.       ptr[1]=3;
  4.       *ptr=3;
  5.       ptr=array2;//可以把另一个数组名赋给ptr,因为它是一个指针
  6. }
  7. fun2(int arr[])
  8. {
  9.       arr[1]=3;
  10.       *arr=3;
  11.       arr=array2;//也可以,因为arr编译器是按照指针的形式处理的
  12. }


  13. int arrary[100],array2[100];
  14. main()
  15. {
  16.          array=array2;//编译错误"无法修改数组名"
  17. }
有关数组和指针的异同就到此为止吧,其实还有很多内容,它们是C的精华,也是难理解的一部分!
阅读(606) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~