《C专家编程》:指针和数组的区别详解(四)

        C语言编程新手常听到的说法之一就是“数组和指针是相同的”。不幸的是,这是一种非常危险的说法,并不完全正确。
一、什么是声明,什么是定义。
     注意下面声明的区别:

     extern int *x;//声明x是一个int类型的指针;
     extern int y[]; //第二条语句声明y是个int类型的整形数组,长度尚未确定,其存储在别处定义;
问题:我的下面的程序为什么不能运行?有什么错?
文件1:int array[100];
文件2:extern int *array;//error这个申明是有问题的;
就像是:
文件1:int a;
文件2:float a;
上面int和float的例子非常明显,类型不匹配。没人会指望这样的代码能够运行。下面将会有完整详细的解释!但是为什么人们总是认为指针和数组始终应该是可以替换的呢?
 答案是对数组的引用总是可以写成对指针的引用,而且确实存在一种指针和数组定义完全相同的上下文环境。但并非所有情况下都如此。 C语言中的对象必须有且只有一个定义,但是它可以有多个声明extern。定义是一种特殊的声明。
声明相当于普通的声明: 它所说明的并非自身,而是描述其他地方的创建的对象。
定义相当于特殊的声明: 它为对象分配内存;
extern对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处。由于并未在声明中为数组分配内存,所以并不需要提供关于数据长度的信息。extern int array[];//OK,是合法的。

表一:声明与定义的区别
定义只能出现在一个地方确定对象类型并分配内存,用于创建新的对象。例如:int a;
声明可以出现多次描述对象的类型,用于指代其他地方定义的对象(例如在其他文件里的定义:extern int a;)

1、数组的下标引用

char a[10]="Hello world!";
char c=a[i];
编译器符号表具有一个地址8000;
运行时步骤1:取i的值与8000相加
运行时步骤2:取地址(8000+i)的内容;
这就是为什么extern char a[];与extern char a[100];等价的原因;
    这两个声明都提示a是一个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共有多长,因为它只产生偏离起始地址的偏移量。从该数组中取一个字符,只要简单的从符号表显示的a的地址加上下标,需要的字符就位于这个地址中。具体数组的下标引用过程如图一。

图一:数组下表引用
2、对指针的引用
       如果声明的是extern char *p,它将告诉编译器p是一个指针(在许多现代的机器里它是四个字节的对象),它指向的对象是一个字符。为了取得该字符,必须得到p的内容,把它作为字符的地址并从这个地址里取得字符。指针的访问要灵活的多,但需要增加一次额外的提取。
char *p;   c=*p;
编译器符号表有一个符号p,它的地址是4000
运行时步骤一:取地址4000的内容,就是4567;
运行时步骤二:取4567的内容。也就是*p。

如下图二:

图二:指针的引用
3、这时候我们来看看,当你“定义为指针,但是以数组方式引用”会发生什么?
    以数组方式进行引用,需要对内存进行直接的引用。如图一所示,但这时候编译器所执行的却是对内存的间接引用,如图二所示。之所以会如此,因为我们告诉编译器我们拥有的是一个指针。如图三所示:
char *p="Hello workd!";  //p[6]
char p[10]="Hello world!";//p[6]
这两种情况下都能取得字符w,但是其执行路径完全不一样。
当书写了extern char *p;
然后我们用p[6]来引用其中的元素时,其实质是图一和图二的组合。首先进行图二的间接访问,然后通过图一的下标作为偏移量进行直接访问。

文件一:char *p="Hello workd!";  //c=p[6]

编译器符号表示一个p,地址为:4000;

运行步骤一:取地址4000的内容,即4567;

运行步骤二:取i的值,并与4567相加,得到新的地址;

运行步骤三:取c=(4567+i)的内容;

对指针进行下表引用的具体步骤如图三:

图三:对指针进行下表引用
编译器具体执行的步骤:
(1)取得符号p中的地址,提取存储于此处的指针;
(2)把下标作为偏移量与取得的地址值进行相加,得到一个新的地址;
(3)访问得到的新地址,取得数据字符。

    如果定义数组,则告诉编译器p是一个字符序列。p[i]表示从p所指的地址开始,前进i步,每步都是一个字符(即每个元素的长度都是一个字节)。如果是其他的int,double类型,那么步长就不一样。
      如果定义为指针,不管原来p是指针还是数组,都会按照上面的三个步骤来。但是只有原来是一个指针时才能正确执行。

4、如果“原先定义为数组,但是我们声明为指针时”,会发生什么?指针和数组的区别?
       这时候第一步得到的p[6]实际上就是字符w,但是按照指针的规则,规则是不能改的,无规矩不成方圆。此时编译器却将字符当成了一个指针,将ACSII字符解释为地址很显然是牛头不对马嘴。如果此时程序down掉,你应该额手称庆。否则的话,他可能会污染程序地址空间的内容。在以后可能出现莫名其妙的错误。这也是指针和数组的区别。
       所以一个好的习惯是:一定要使声明和定义匹配。
       那么开篇提到的问题解决也十分简单:

文件1:int *x;// 声明x是一个int类型的指针,申请一个地址容纳该指针,x本身始终位于同一个地址,但是内容可以不同;
文件2:extern int x; //声明和定义一致。
文件1:int array[100];//array定义分配了100个int空间,array数组的地址不能改变,它总是100个连续的空间,但是里面的内容可以改变;
文件2:extern int array[];//请保持一致;

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

至此,也应该明白了数组和指针之间的区别了吧!
5、数组与指针的常量初始化问题。
    定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间(一般为4个字节)。除非在定义的时候同时赋给指针一个字符串常量进行初始化。
    例如:
char *p="Hello wirld!"; //次字符串常量被定义为只读。不能修改,否则出现未定义的错误。
    不要指望为int或float类型的数据分配空间:
int *p=4; //error
float *p=3.14;//error;
  数组也可以用字符串常量来初始化 ,但是由字符串常量初始化的数组可以修改。
char p[20]="Hello world!";
strncpy(p,"beautiful",9);
此时数组变为:beautiful world!。
6、左值和右值的区别

       程序的报错和课堂上老师都会告诉我们这样两个概念,左值和右值,下面来看一看它们的区别!

左值和右值的区别x=y
在这个上下文里,x代表的是地址在这个上下文里,y代表的是地址的内容
X被称为左值(由于它位于“左手边”或表示“地点”)y被称为右值(由于它位于“右手边”)
左值在编译器时可知,左值表示存储结果的地方,变量一直存于该地址右值到运行的时候才知道,如无特别说明,右值表示“y的内容”
左值出现在赋值语句的左边,表示内存空间。数组时左值,但是不能赋值。右值就是具体的内容,可以改变




  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值