都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型
的具名+匿名访问。一定要注意的是这个“以
XXX的形式的访问”这种表达方式。
另外一个需要强调的是:上面所说的偏移量
4代表的是
4个元素,而不是
4个
byte。只
不过这里刚好是
char类型数据
1个字符的大小就为
1个
byte。记住这个偏移量的单位是元
素的个数而不是
byte数,在计算新地址时千万别弄错了。
4。3。2,a和&a的区别
通过上面的分析,相信你已经明白数组和指针的访问方式了,下面再看这个例子:
main()
{
inta'5'={1;2;3;4;5};
int*ptr=(int*)(&a+1);
printf(〃%d;%d〃;*(a+1);*(ptr…1));
}
打印出来的值为多少呢?这里主要是考查关于指针加减操作的理解。
对指针进行加
1操作,得到的是下一个元素的地址,而不是原有地址值直接加
1。所以,
一个类型为
T的指针的移动,以
sizeof(T)为移动单位。因此,对上题来说,
a是一个一
维数组,数组中有
5个元素;
ptr是一个
int型的指针。
&a
+1:取数组
a的首地址,该地址的值加上
sizeof(a)的值,即
&a
+5*sizeof(int),也
就是下一个数组的首地址,显然当前指针已经越过了数组的界限。
(int*)(&a+1):则是把上一步计算出来的地址,强制转换为
int*类型,赋值给
ptr。
*(a+1):
a;&a的值是一样的,但意思不一样,a是数组首元素的首地址,也就是
a'0'的
首地址,
&a是数组的首地址,a+1是数组下一元素的首地址,即
a'1'的首地址;&a+1是下一
个数组的首地址。所以输出
2
*(ptr…1):因为
ptr是指向
a'5',并且
ptr是
int*类型,所以
*(ptr…1)是指向
a'4',
输出
5。
这些分析我相信大家都能理解,但是在授课时,学生向我提出了如下问题:
在
VisualC++6。0的
Watch窗口中&a+1的值怎么会是(x0012ff6d(0x0012ff6c+1)呢?
上图是在
VisualC++6。0调试本函数时的截图。
a在这里代表是的数组首元素的地址即
a'0'的首地址,其值为
0x0012ff6c。
&a代表的是数组的首地址,其值为
0x0012ff6c。
a+1的值是
0x0012ff6c+1*sizeof(int),等于
0x0012ff70。
问题就是&a+1的值怎么会是(x0012ff6d(0x0012ff6c+1)呢?
按照我们上面的分析应该为
0x0012ff6c+5*sizeof(int)。其实很好理解。当你把
&a+1
放到
Watch窗口中观察其值时,表达式
&a+1已经脱离其上下文环境,编译器就很简单的把
它解析为&a的值然后加上
1byte。而
a+1的解析就正确,我认为这是
VisualC++6。0的一个
bug。既然如此,我们怎么证明证明
&a+1的值确实为
0x0012ff6c+5*sizeof(int)呢?很好办,
用
printf函数打印出来。这就是我在本书前言里所说的,有的时候我们确实需要
printf函数
才能解决问题。你可以试试用
printf(〃%x〃;&a+1);打印其值,看是否为
0x0012ff6c+5*sizeof(int)。注意如果你用的是
printf(〃%d〃;&a+1);打印,那你必须在十进制和十六进制之间换算
一下,不要冤枉了编译器。
另外我要强调一点:不到非不得已,尽量别使用
printf函数,它会使你养成只看结果不
问为什么的习惯。比如这个列子,*(a+1)和*(ptr…1)的值完全可以通过
Watch窗口来查看。
平时初学者很喜欢用“
printf(〃%d;%d〃;*(a+1);*(ptr…1));”这类的表达式来直接打印出值,
如果发现值是正确的就欢天喜地。这个时候往往认为自己的代码没有问题,根本就不去查
看其变量的值,更别说是内存和寄存器的值了。更有甚者,
printf函数打印出来的值不正确,
就措手无策,举手问“老师,我这里为什么不对啊?”。长此以往就养成了很不好的习惯,
只看结果,不重调试。这就是为什么同样的几年经验,有的人水平很高,而有的人水平却
很低。其根本原因就在于此,往往被一些表面现象所迷惑。
printf函数打印出来的值是对的
就能说明你的代码一定没问题吗?我看未必。曾经一个学生,我让其实现直接插入排序算
法。很快他把函数写完了,把值用
printf函数打印出来给我看。我看其代码却发现他使用的
算法本质上其实是冒泡排序,只是写得像直接插入排序罢了。等等这种情况数都数不过来,
往往犯了错误还以为自己是对的。所以我平时上课之前往往会强调,不到非不得已,不允
许使用
printf函数,而要自己去查看变量和内存的值。学生的这种不好的习惯也与目前市面
上的教材、参考书有关,这些书甚至花大篇幅来介绍
scanf和
printf这类的函数,却几乎不
讲解调试技术。甚至有的书还在讲
TruboC2。0之类的调试器!如此教材教出来的学生质量
可想而知。
4。3。3,指针和数组的定义与声明
4。3。3。1,定义为数组,声明为指针
文件
1中定义如下:
chara'100';
文件
2中声明如下(关于
extern的用法,以及定义和声明的区别,请复习第一章):
externchar*a;
这里,文件
1中定义了数组
a,文件
2中声明它为指针。这有什么问题吗?平时不是总说数
组与指针相似,甚至可以通用吗?但是,很不幸,这是错误的。通过上面的分析我们也能
明白一些,但是“革命尚未成功,同志仍需努力”。你或许还记得我上面说过的话:数组就
是数组,指针就是指针,它们是完全不同的两码事!他们之间没有任何关系,只是经常穿
着相似的衣服来迷惑你罢了。下面就来分析分析这个问题:
在第一章的开始,我就强调了定义和声明之间的区别,定义分配的内存,而声明没有。
定义只能出现一次,而声明可以出现多次。这里
extern告诉编译器
a这个名字已经在别的文
件中被定义了,下面的代码使用的名字
a是别的文件定义的。再回顾到前面对于左值和右值
的讨论,我们知道如果编译器需要某个地址(可能还需要加上偏移量)来执行某种操作的
话,它就可以直接通过开锁动作(使用“*”这把钥匙)来读或者写这个地址上的内存,并不
需要先去找到储存这个地址的地方。相反,对于指针而言,必须先去找到储存这个地址的
地方,取出这个地址值然后对这个地址进行开锁(使用“*”这把钥匙)。如下图:
这就是为什么externchara''与externchara'100'等价的原因。因为这只是声明,不分配这就是为什么externchara''与externchara'100'等价的原因。因为这只是声明,不分配
空间,所以编译器无需知道这个数组有多少个元素。这两个声明都告诉编译器
a是在别的文
件中被定义的一个数组,
a同时代表着数组
a的首元素的首地址,也就是这块内存的起始地
址。数组内地任何元素的的地址都只需要知道这个地址就可以计算出来。
但是,当你声明为
externchar*a时,编译器理所当然的认为
a是一个指针变量,在
32位系
统下,占
4个
byte。这
4个
byte里保存了一个地址,这个地址上存的是字符类型数据。虽
然在文件
1中,编译器知道
a是一个数组,但是在文件
2中,编译器并不知道这点。大多数
编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然
a实际
大小为
100个
byte,但是在文件
2中,编译器认为
a只占
4个
byte。
我们说过,编译器会把存在指针变量中的任何数据当作地址来处理。所以,如果需要
访问这些字符类型数据,我们必须先从指针变量
a中取出其保存的地址。如下图:
4。3。3。2,定义为指针,声明为数组
显然,按照上面的分析,我们把文件
1中定义的数组在文件
2中声明为指针会发生错误。
同样的,如果在文件
1中定义为指针,而在文件中声明为数组也会发生错误:
文件
1
char*p
=
“abcdefg”;
文件
2
externcharp'';
在文件
1中,编译器分配
4个
byte空间,并命名为
p。同时
p里保存了字符串常量“abcdefg”
的首字符的首地址。这个字符串常量本身保存在内存的静态区,其内容不可更改。在文件
2
中,编译器认为
p是一个数组,其大小为
4个
byte,数组内保存的是
char类型的数据。在
文件
2中使用
p的过程如下图:
p
0x0000FF00
p
0x000x000x000xFFpcharp'0'
p'1'p'2'p'3'
4char0x000x000xFF0x00p'i'p
4。3。4,指针和数组的对比
通过上面的分析,相信你已经知道数组与指针的的确确是两码事了。他们之间是不可
以混淆的,但是我们可以“以
XXXX的形式”访问数组的元素或指针指向的内容。以后一
定要确认你的代码在一个地方定义为指针,在别的地方也只能声明为指针;在一个的地方
定义为数组,在别的地方也只能声明为数组。切记不可混淆。下面再用一个表来总结一下
指针和数组的特性:
指针数组
保存数据的地址,任何存入指针变量
p的数
据都会被当作地址来处理。p本身的地址由
编译器另外存储,存储在哪里,我们并不知
保存数据,数组名
a代表的是数组首元素的
首地址而不是数组的首地址。&a才是整个数
组的首地址。a本身的地址由编译器另外存
道。储,存储在哪里,我们并不知道。
间接访问数据,首先取得指针变量
p的内容,
把它作为地址,然后从这个地址提取数据或
向这个地址写入数据。指针可以以指针的形
式访问*(p+i);也可以以下标的形式访问
p'i'。
但其本质都是先取
p的内容然后加上
i*sizeof(类型)个
byte作为数据的真正地址。
直接访问数据,数组名
a是整个数组的名字,
数组内每个元素并没有名字。只能通过“具
名+匿名”的方式来访问其某个元素,不能把
数组当一个整体来进行读写操作。数组可以
以指针的形式访问*(a+i);也可以以下标的形
式访问
a'i'。但其本质都是
a所代表的数组首
元素的首地址加上
i*sizeof(类型)个
byte作为
数据的真正地址。
通常用于动态数据结构通常用于存储固定数目且数据类型相同的元
素。
相关的函数为
malloc和
free。隐式分配和删除
通常指向匿名数据(当然也可指向具名数据)自身即为数组名
4。4,指针数组和数组指针
4。4。1,指针数组和数组指针的内存布局
初学者总是分不出指针数组与数组指针的区别。其实很好理解:
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指