九味书屋 > 激情辣文电子书 > [免费下载 c语言深度解剖[1] >

第18部分

[免费下载 c语言深度解剖[1]-第18部分

小说: [免费下载 c语言深度解剖[1] 字数: 每页4000字

按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!



都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型
的具名+匿名访问。一定要注意的是这个“以 
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,指针数组和数组指针的内存布局
初学者总是分不出指针数组与数组指针的区别。其实很好理解:

指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
决定。它是“储存指针的数组”的简称。

数组指针:首先它是一个指针,它指

返回目录 上一页 下一页 回到顶部 0 0

你可能喜欢的