您的位置:首页 > 编程语言

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

2016-05-24 10:21 344 查看
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的内容”
左值出现在赋值语句的左边,表示内存空间。数组时左值,但是不能赋值。右值就是具体的内容,可以改变
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: