Crack C语言之-指针之四

本文深入探讨了C语言中数组和指针的概念及其区别,包括数组名作为左值和右值的不同含义,以及&a[0]、&a和a的细微差别。通过实例解释了指针加减运算的具体含义。

省政府和市政的区别----&a[0]&a 的区别

这里&a[0]&a 到底有什么区别呢?a[0]是一个元素,a 是整个数组,虽然&a[0]&a的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。举个例子:湖南的省政府在长沙而长沙的市政府也在长沙。两个政府都在长沙,但其代表的意义完全不同。这里也是同一个意思。

 

 

数组名a 作为左值和右值的区别

简单而言,出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值。

比如,x=y

左值:在这个上下文环境中,编译器认为x 的含义是x 所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。

右值:在这个上下文环境中,编译器认为y 的含义是y 所代表的地址里面的内容。这个内容是什么,只有到运行时才知道。

C 语言引入一个术语-----“可修改的左值”。意思就是,出现在赋值符左边的符号所代表的地址上的内容一定是可以被修改的。换句话说,就是我们只能给非只读变量赋值。既然已经明白左值和右值的区别,下面就讨论一下数组作为左值和右值的情况:

a 作为右值的时候代表的是什么意思呢?很多书认为是数组的首地址,其实这是非常错误的。a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组的首地址。这是两码事。但是注意,这仅仅是代表,并没有一个地方(这只是简单的这么认为,其具体实现细节不作过多讨论)来存储这个地址,也就是说编译器并没有为数组a分配一块内存来存其地址,这一点就与指针有很大的差别。

a 作为右值,我们清楚了其含义,那作为左值呢?

a 不能作为左值!这个错误几乎每一个学生都犯过。编译器会认为数组名作为左值代表的意思是a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a当左值。其实我们完全可以把a 当一个普通的变量来看,只不过这个变量内部分为很多小块,我们只能通过分别访问这些小块来达到访问整个变量a 的目的。

 

 

a &a 的区别

通过上面的分析,相信你已经明白数组和指针的访问方式了,下面再看这个例子:

main()

{

int a[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

这些分析我相信大家都能理解,但是在授课时,学生向我提出了如下问题:

Visual C++6.0 Watch 窗口中&a+1 的值怎么会是(x0012ff6d0x0012ff6c+1)呢?

 

 

 

 

 

上图是在Visual C++6.0 调试本函数时的截图。

a 在这里代表是的数组首元素的地址即a[0]的首地址,其值为0x0012ff6c

&a 代表的是数组的首地址,其值为0x0012ff6c

a+1 的值是0x0012ff6c+1*sizeofint),等于0x0012ff70

问题就是&a+1 的值怎么会是(x0012ff6d0x0012ff6c+1)呢?

按照我们上面的分析应该为0x0012ff6c+5*sizeofint)。其实很好理解。当你把&a+1放到Watch 窗口中观察其值时,表达式&a+1 已经脱离其上下文环境,编译器就很简单的把它解析为&a 的值然后加上1byte。而a+1 的解析就正确,我认为这是Visual C++6.0 的一个bug。既然如此,我们怎么证明证明&a+1 的值确实为0x0012ff6c+5*sizeofint)呢?很好办,printf 函数打印出来。这就是我在本书前言里所说的,有的时候我们确实需要printf 函数才能解决问题。你可以试试用printf("%x",&a+1);打印其值,看是否为0x0012ff6c+5*sizeofint)。注意如果你用的是printf("%d",&a+1);打印,那你必须在十进制和十六进制之间换算一下,不要冤枉了编译器。

另外我要强调一点:不到非不得已,尽量别使用printf 函数,它会使你养成只看结果不问为什么的习惯。比如这个列子,*(a+1)*(ptr-1)的值完全可以通过Watch 窗口来查看。平时初学者很喜欢用“printf("%d,%d",*(a+1),*(ptr-1));”这类的表达式来直接打印出值,如果发现值是正确的就欢天喜地。这个时候往往认为自己的代码没有问题,根本就不去查看其变量的值,更别说是内存和寄存器的值了。更有甚者,printf 函数打印出来的值不正确,就措手无策,举手问“老师,我这里为什么不对啊?”。长此以往就养成了很不好的习惯,只看结果,不重调试。这就是为什么同样的几年经验,有的人水平很高,而有的人水平却很低。其根本原因就在于此,往往被一些表面现象所迷惑。printf 函数打印出来的值是对的就能说明你的代码一定没问题吗?我看未必。曾经一个学生,我让其实现直接插入排序算法。很快他把函数写完了,把值用printf 函数打印出来给我看。我看其代码却发现他使用的算法本质上其实是冒泡排序,只是写得像直接插入排序罢了。等等这种情况数都数不过来,往往犯了错误还以为自己是对的。所以我平时上课之前往往会强调,不到非不得已,不允许使用printf 函数,而要自己去查看变量和内存的值。学生的这种不好的习惯也与目前市面上的教材、参考书有关,这些书甚至花大篇幅来介绍scanf printf 这类的函数,却几乎不讲解调试技术。甚至有的书还在讲TruboC 2.0 之类的调试器!如此教材教出来的学生质量可想而知。

再论a &a 之间的区别

既然这样,那问题就来了。前面我们讲过a &a 之间的区别,现在再来看看下面的代

码:

int main()

{

char a[5]={'A','B','C','D'};

char (*p3)[5] = &a;

char (*p4)[5] = a;

return 0;

}

上面对p3 p4 的使用,哪个正确呢?p3+1 的值会是什么?p4+1 的值又会是什么?

毫无疑问,p3 p4 都是数组指针,指向的是整个数组。&a 是整个数组的首地址,a是数组首元素的首地址,其值相同但意义不同。在C 语言里,赋值符号“=”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。在Visual C++6.0 上给出如下警告:warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。还好,这里虽然给出了警告,但由于&a a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过我仍然警告你别这么用。

 

 

 

 

 

 

摘自 C语言深度剖析

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值