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被称为右值(由于它位于“右手边”) |
左值在编译器时可知,左值表示存储结果的地方,变量一直存于该地址 | 右值到运行的时候才知道,如无特别说明,右值表示“y的内容” |
左值出现在赋值语句的左边,表示内存空间。数组时左值,但是不能赋值。 | 右值就是具体的内容,可以改变 |