某日,看到一道比较恶心的C语言的试题,考了很多比较绕的知识点,嘴脸如下:
int main(void)
{
int a[4] = {1, 2, 3, 4};
int *ptr1=(int *)(&a+1);
int *ptr2=(int *)((int)a+1);
printf("%x, %x/n", ptr1[-1], *ptr2);
return 0;
}
问,在x86平台下输出啥?
题目虽然恶心了点,但作为一个例子来分析,还是挺好玩的。学过C语言的朋友可以暂且不看下文,自己试着分析一下,看看结果跟我的是否一样,也不失为一件趣事。
下面把这道题牵涉到的所有边边角角的问题梳理一遍,详细讨论如下:
1、&a+1
首先明确,a是一个具有4个整型变量的数组的名字,具体地说是这种数组的首元素的首地址,而&a是数组的首地址,请注意措辞。而关于指针加1,则需要指针运算的知识。没学过指针运算或者已经忘记了这个知识点的朋友们,下面就是关于指针运算你需要知道的事实:
就像上面的例子那样,式子&a+1表示的是指针加法运算,而不是普通的数值加法运算,之所以会这样是因为&a是一个指针而非普通数值(虽然它本质上也是一个整数)。那么你会问:加入此时&a=0xFFFF5700,那么&a+1是多少呢?答案是:取决于&a的类型 。
a) 如果&a是一个指向char型的指针,那么&a+1 = 0xFFFF5701
b) 如果&a是一个指向short型的指针,那么&a+1 = 0xFFFF5702
c) 如果&a是一个指向int型的指针,那么&a+1 = 0xFFFF5704 (32位机器)
d) 如果&a是一个指向某种结构体struct foo的指针,那么&a+1 = 0xFFFF5700+sizeof(struct foo)
……
还没看出端倪吗?没错,指针加1不是指针内容简单地加1,而是让指针指向下一个数据 ,加2就是让指针指向下两个数据,这个数据的类型就是指针指向的类型,所以指针的加法究竟会让这个指针指向哪里,取决于这个指针指向的数据类型。
因此,综上所述,当&a与整数1做加法时,实际上是指针的加法,加1的含义是:令指针a指向下一个数据 ,下一个数据是啥?当然是紧挨着的下一个具有4个整型变量的数组了(因为&a的类型是指向具有4个整型变量的数组的指针嘛),于是a的指向了4的下一个地址,在用此值初始化ptr1,因此ptr1的指向如图所示:
由于在ptr1初始化的时候,令&a+1强制转换成整型指针(关于类型转换的详细讨论请参考http://blog.csdn.net/seton040/archive/2009/10/19/4699869.aspx),因此ptr1[-1]相当于把ptr1往前挪一个整型大小,即4个字节。 如下图:
显然,打印出的第一个数字是a[3]的内容,即数值4.
当然,我们还必须说明一个事实:数组下标是可以为负数的,实际上,取下标符“[ ]”的内部实现,就是指针运算!比如a[2],等价于*(a+2),即以a地址为基址,取偏移量为2的地址的值。所以ptr1[-1]等价于*(ptr-1)。
2、(int *)((int)a+1)
至于指针ptr2的处理更恶心一点,呵呵!它先是把数组名a强制转换成整型变量,然后再加1,然后再强制转换成整型指针!真罗嗦,但不要紧,咱有的是耐性,无非就是让ptr2指向a[0]的第二个字节罢了,此时ptr2指向如下图所示:
显然,此时打印的内容就是ptr2所指向的往后4个字节的内容了,也就是a{0}的后三个字节和a[1]的第一个字节,那究竟会打印出啥玩意儿呢?上面的图没有画出里面的内容,要是把每个字节的内容都画出来就好了,呵呵!
要把上图中每一个字节的内容都打印出来没问题,但是你先要知道字节序 的概念,字节序分两种,一种叫大端字节序(big-endian) ,当然除此之外必然有小端 字节序(little-endian) ,让我们用一个问题,来引出字节序的概念,然后再来搞定这两个小鬼吧!
问题是这样的:对多字节存储的变量,机器是如何做出解释的??请看下图:
假如这是一个普通的int变量,地球人都知道,在32机器上int占用4个字节存储数据,就像上图中显示的那样,在4个字节中放置了一堆数字,但是机器究竟会把这个数解释成0x0103070F呢,还是解释成0x0F070301呢?答案是:都有可能!
官方解释:
1、所谓大端(big-endian)序,就是高优先位 对应高有效位 。
2、所谓小端(little-endian)序,就是高优先位 对应低有效位 。
民间解释:
1、所谓大端(big-endian)序,就是读取或者存放数据时,最低 位 对应 高地址 。
2、所谓小端(big-endian)序,就是读取或者存放数据时,最低 位 对应 低地址 。
如,要把0x0103070F存放进存储器中时,如果把0E放进高地址处则是小端序,如果把0F放进低地址处则是大端序。照此,上图中存放的数据如果被机器理解成0x0103070F则是该机器是大端序的,否则若被理解成0x0F070301则是小端序的。
回到原来的问题,此时ptr2指向了a[0]的第二个字节。我们以x86平台为例(小端序),此时其内部数据分布是这样的:
由于x86平台是小端序的,根据咱刚刚讨论过的理论,小端序的存取时最低 位 对应 低地址 ,因此将会打印出0200 0000,如果题目中没有说明在x86平台,那答案是不确定的,取决于具体的平台,例如ARM平台就是大端序的。
到此这个试题基本算是圆满解决了,但是我还想再罗嗦一下,关于代码中的那个a,以及那个&a,估计还是有很多朋友心里不怎么平坦吧,每次遇到数组啊指针啊,虽然程序好像仿佛貌似也没出啥毛病,但总感觉是混过去的,这次我们要彻底解决它。记得某人说过,一个真正的程序员,必须对自己写的代码的每一个细节了如指掌。
再来考虑这个定义:
int a[4] = {1, 2, 3, 4};
这时我们必须明确,编译器根据我们提供的类型和数组大小,为我们分配了适当大小的存储区域,并且把这块存储区域叫做a,请注意 :&a,就像我们上面提到的,它的类型是“指向具有4个整型变量数组的指针“,简而言之&a是一个数组指针。这个数组的名字a,当它作为右值(简单而言,出现在赋值号“=”左边的就是左值,出现在赋值号“=”右边的就是右值)时代表的是数组首元素的首地址,而不是数组的首地址。此时a的意义跟&a[0]是等价的,都是指向首元素的指针。记住咯!
数组的名字会在另一个地方与指针等价,那就是函数参数表,当出现在函数的参数表里面的时候,不管你写的是数组,还是指针,一旦进入函数内部,通通变成指针。