一道试题引发的血案 int *ptr2=(int *)((int)a+1);

某日,看到一道比较恶心的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]是等价的,都是指向首元素的指针。记住咯!

数组的名字会在另一个地方与指针等价,那就是函数参数表,当出现在函数的参数表里面的时候,不管你写的是数组,还是指针,一旦进入函数内部,通通变成指针。

时间: 2024-11-07 06:12:24

一道试题引发的血案 int *ptr2=(int *)((int)a+1);的相关文章

一道python面试题引发的血案

这里说的是一道阿里校招的面试题:一行代码实现对列表a中的偶数位置的元素进行加3后求和? 今天去面试同样遇到了这个题目,这道题考察的是对python高阶函数map/filter的灵活运用(具体的使用方法可以参考'廖雪峰的官方网站').作为一个小白的我对高阶函数的运用本就不多,当时连高阶函数的名字都记不清了(书到用时方恨少),妥妥的被虐了个无路可走.无奈记下题目回来求助于度娘了,没想到是阿里的校招题目,网上也给出了答案,但是很明显该答案是存在一些问题的,具体什么问题在这里就不讲了,大家可以自行查找,

一道Javascript面试题引发的血案

文章首发于szhshp的第三边境研究所,转载请注明 先来看几道面试题,公司的开发们都尝试做了一下,然而基本没有人能够全部答对. 覆盖的考点很多,也有一些难度,题目挺有意思建议手动执行一边玩玩. Question 1 for (var i = 0; i <5 ; i++) { setTimeout(function(){ console.log(i) ),1000} } console.log(i) Q:这道题目会输出什么? A:这道题目还比较简单,如果对Javascript稍微有一点深入的同学都

【C语言】【笔试题】编写一个函数itob(int n,char s[], int b),将整数n转换为以b进制的数。保存到s中。

#include <stdio.h> static int i=0; int itob(int n,char s[],int b) { if(n<2) { s[i]=n+'0'; } else { itob(n/2,s,b); //递归 i++; n=n%2; s[i]=n+'0'; } s[i+1]='\0';//结束标志 return 0; } int main () { char s[20]; int num=0; scanf("%d",&num); i

一个无锁消息队列引发的血案(六)——RingQueue(中) 休眠的艺术 [续]

目录 (一)起因 (二)混合自旋锁 (三)q3.h 与 RingBuffer (四)RingQueue(上) 自旋锁 (五)RingQueue(中) 休眠的艺术 (六)RingQueue(中) 休眠的艺术 [续] 开篇 这是第五篇的后续,这部分的内容同时会更新和添加在 第五篇:RingQueue(中) 休眠的艺术 一文的末尾. 归纳 紧接上一篇的末尾,我们把 Windows 和 Linux 下的休眠策略归纳总结一下,如下图: 我们可以看到,Linux 下的 sched_yield() 虽然包括了

openstack运维实战系列(十三)之glance更改路径引发的&quot;血案&quot;

1. 背景说明 glance在openstack中负责镜像相关的服务,支持将运行的虚拟机转换为快照,镜像和快照都存储在glance中,glance的后端支持多种存储方式,包括本地的文件系统,http,glusterfs,ceph,swift等等. 默认情况下,glance采用本地文件系统的方式存储image,存储的路径为/var/lib/glance/images,随着时间的推移,当镜像越来越多的时候,根目录的空间将会越来越大,所以对于glance的路径来说,需要提前做好规划和准备,如划分一个单

模板链接与前置声明引发的血案

模板链接与前置声明引发的血案 模板链接与前置声明引发的血案 现象 问题原型 模板參数类型类 使用类模板的类 分析 objdump -S TemplateLink SUPERSUBCLASS 分析 objdump -S UsingBaseo objdump -S UsingChildo 问题解答 解答问题一 解答问题二 解决方式 类型萃取辅助类 应用 不足 现象: 有一个类模板,它会依据模板类型參数T的实际类型,调用不同的实例化泛型函数子去处理实际事情. 在程序运行时.发如今不同的模块中用相同的类

一个Sqrt函数引发的血案

我们平时经常会有一些数据运算的操作,需要调用sqrt,exp,abs等函数,那么时候你有没有想过:这个些函数系统是如何实现的?就拿最常用的sqrt函数来说吧,系统怎么来实现这个经常调用的函数呢? 虽然有可能你平时没有想过这个问题,不过正所谓是"临阵磨枪,不快也光",你"眉头一皱,计上心来",这个不是太简单了嘛,用二分的方法,在一个区间中,每次拿中间数的平方来试验,如果大了,就再试左区间的中间数:如果小了,就再拿右区间的中间数来试.比如求sqrt(16)的结果,你先试

一个由内存泄漏引发的血案-Square

一个内存泄漏引发的血案-Square 原文链接 : A small leak will sink a great ship 原文作者 : Pierre-Yves Ricau 译文出自 : 开发技术前线 www.devtf.cn.未经允许,不得转载! 译者 : chaossss 校对者: 这里校对者的github用户名 状态 : 完成 在开发 LeakCanary 时我发现一处奇怪的内存泄漏,为了搞清楚到底是什么原因导致这个问题我一边 Debug,一边在邮件中和小伙伴们沟通,最后形成了这篇博文.

Replication的犄角旮旯(六)-- 一个DDL引发的血案(上)(如何近似估算DDL操作进度)

原文:Replication的犄角旮旯(六)-- 一个DDL引发的血案(上)(如何近似估算DDL操作进度) <Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Replication的犄角旮旯(三)--聊聊@bitmap Replication的犄角旮旯(四)--关于事务复制的监控 Replication的犄角旮旯(五)--关于复制identity列 Replicati