指针

指向二维数组的指针 以及一些 指针地址相减的问题

前面我们讲到了指向变量的指针和指向一维数组的指针,这篇文章我们来讨论一下指向二维数组的指针。

对于二维数组指针,需要注意的是对于不同情况下的下标变化,指针保存的地址值增加的长度(步长)是不同的,以下我们看一下不同情况下指针步长有什么规律(部分内容可能涉及简单的汇编来帮助理解)。

我们先看以下代码:

编译运行结果:

接下来我们一点一点的分析为什么是这些结果。

首先我们定义并初始化了一个二维数组,它在内存中可以表示为如下形式:

这里我们假设的是数组起始地址值为0x10(实际地址虽然也打印出来了,不过太长就不用了),其中蓝色的是数据,黑色的是数据所在的内存地址,紫色是数组的行号和列号,即下标。(其实它们在内存上是线性排列的,这样画是为了便于理解)

接着我们定义了一个指向该二维数组的指针p,并将num的值(0x10)赋值给了它,现在p=0x10。

对于上述代码,可以看成这样 int[3]  *p; 也就是我们定义了一个*p, 指向的是包含3个int型元素的数组。因为*操作是从右向左结合的,而小括号优先级最高,所以*p会结合成一对,这样理所当然的int和[3]结合在一起了。

扩展一下,如果我们定义成如下形式又表示什么意思呢?

根据前面提到的结合方向,我们可以得出上述代码可以理解成int* p[3];也就是我们定义了一个数组,且该数组有3个元素,元素类型为int*,即数组里的每个元素都是指向int型的指针,这个我会在后面指针剩余部分提到,那就是另一篇文章了,也就是所谓的指针数组了。

点到为止,我们继续分析一下原来的代码。

假设p的地址为0x30,此时的数据分布可以用下图表示:

可以看到,num是一个地址,值为0x10,它即是作为第一行三个int型数据数组的首地址,也是整个二维数组的首地址。

以上代码中的p[1],会首先获得指针p里面存的内容0x10,然后在根据下标获得真正目标的地址值,具体这个下标1表示的步长究竟是多少呢?我们再看一下定义时候的代码:

由上面的讨论我们知道,我们定义的是一个指针*p,该指针指向了一个包含3个int型变量的数组。即,我们每次对p加1,实际需要跨过的地址便是1*sizeof(int[3])=4*3=12字节了,所以p[1]会和数组的首地址相差12个字节。

对应会实际代码的运行结果,我们算一下到底是不是这样呢?
num = 0x7ffc54dc4920
p[1] = 0x7ffc54dc492c

则可得出差值 = 0x7ffc54dc4920 – 0x7ffc54dc492c = 0xc,转化成10进制的话,就是12了,与我们推测计算的一样!

承接上面的结论,既然p[1]是num的地址基础上再跨越了3个int型数据,那么它的地址就是第4个int型数据的地址了,即使下图的第4个元素,值为4的地址,再取*操作,就得到了该地址下面的值为4了。

而这句为什么打印的不是4呢?

因为我们加了一个小括号,使指针p和*号先结合起来了,这就会先完成*p操作,获得num的首地址0x10,又因为数组num是int型数据的,所以会再根据下标1,完成了地址偏移量计算1*sizeof(int)=4,再计算出最终目标地址是0x10+0x04=0x14,这时就有点像数组索引了,因为*p就是数组的首地址,再加一个下标值,便会取出对应地址0x14的数据了,所以得到的结果便是(*p)[1])=2了。

再看一下最后一句:

按照上面一条讲的那样,*p取得了num的地址,再计算出地址偏移量=5*sizeof(int)=20(即0x14);那么目标地址即是:0x10+0x14=0x24,对应的值就是6了。

我们再推测一下,如果是num[5]是不是得到一样的结果呢?

毕竟刚刚*p可就是num的值啊。转化一下不就是(*p)[5] = num[5]吗?

其实,不是这样的,数组和指针还是有区别的,毕竟它们本身就是不同的数据类型,只是在某些情况下有一些互通的地方,加之数组名代表了数组的一个地址,不可以给它赋值,而指针是可以的,其次,当用sizeof(num)和sizeof(p) 时得到的分别是数组的大小(而不是num代表的地址值的数据大小)和指针的大小(8字节,占用64位)。

所以如果是num[5]的话,那么第一个下标指的是行,而每行是3个int型元素,下标每加1,代表跨过了一整行,那就是sizeof(int[3])=12字节了,num[5]相比于首地址偏移地址量就为12*5=60了。

而(*p)[5]是取得了首地址num,而num是int型数组,每个元素占用sizeof(int)=4字节,所以偏移地址量就是4*5=20了,相比于num[5]的60,实在相差甚远。

为了验证上面的的推论,我们再写一段代码,打印出num+60的值和&num[5]的值进行比较,如果相等就说明推论正确,如果不相等,我们就得再推敲推敲了。

Continue reading...

指针

指针其实也是一个变量,只是这个变量比较特殊,专门存放其他变量在内存上的位置值(叫做地址值)。

就像一本书上的目录,它不包含每篇文章的内容,但它携带了对应文章的页码,比如我们要查看哪一篇文章在哪一页,去看一下目录找到对应的文章标题,它的末尾就写明了这篇文章在哪一页,然后再翻到那一页,我们就能读取文章内容了。

我们定义一个指针,让它指向一个变量,再把他打印出来:

运行结果:

我们来详细讲解一下,指针是怎么运作的。

首先我们定义了一个int型的变量 num, 并赋初值26. 代码运行后便会在内存的某个地址中初始化这个变量,假设该地址在0x1a处。如下图:

当我们执行如下操作:

&num会获得num在内存中的地址0x1a, 再赋值给指针变量p,之后p=0x1a,假设p变量被放在内存中的0x20处,那么指针变量的示意图可以表示为如下:

再执行printf打印*p时, 实际的操作过程是取得p的值(0x1a), 再去找到内存0x1a处,获取里面的数据26,再调用printf打印出这个值。

那么如果我们只打印p会得到什么呢?

我们继续看如下代码:

运行结果(注意每次运行的结果可能不同,因为开辟的内存地址可能不同):

其中%p表示目标以地址的形式输出。

我们可以看到,p直接以%d的格式输出的话,值为一个负数-818621124
,那么它与0x7ffecf34d53c有什么关系呢?

其实,-818621124是0x7ffecf34d53c的二进制的低32位转化成10进制后的值,-818621124在计算机中是以补码的形式存在的,其为负数,表示最高位(符号位,正号为0,负号为1)为1。

因为 源码的补码=源码的反码+1;

所谓反码就是在二进制的情况下,除了符号位,对每一位取反。

知道了上面的规律,我们就可以反推出-818621124在计算机中存储时候的二进制情况了。

先计算一下-818621124的二进制表示形式:

再看一下0x7ffecf34d53c的二进制形式:

对比-818621124的补码和0x7ffecf34d53c的二进制有没有发现什么?

没错-818621124的32位二进制补码和0x7ffecf34d53c的低32位完全相同。

但指针是8字节的,8*8=64位,那么问题又来了,为什么是指针值的低32位才相同,而不是64位呢?

那是因为int型是4字节,即4*8=32位,而我们打印出p的时候,用的是%d对应的就是int型,所以它才会把p的值的低32位转化成10进制给打印出来,当然就不是取64位转化打印出来了。

END~…

Continue reading...

一维数组指针

所谓数组指针,即是指向数组的指针。指针变量用来存放数组的地址值,通过星号操作 ‘ * ‘ 来间接访问该地址下的数据。

如我们定义一个int型数组:

那么便会在内存中(栈)开辟一个地址连续的空间,用来存放这三个数据,我们假设起始地址为0xff0,那么这三个数在内存中会按如下排列,其中地址每次加4,是因为一个int型数据占用了四个字节。

我们再定义一个数组指针,用来指向这个数组:

因为数组本质上就是通过地址索引的,如上面的图示中,如果要读取出第二个数据num[1],那么从汇编上来看,需要知道这个数组的首地址0xff0,再根据下标[1],计算出它的真正地址:0xff0+1*sizeof(int) = 0xff0 + 4 = 0xff4,再取目标地址0xff4里面的数据,即为2。

而在C语言中,数组名就代表了数组的首地址,也就是说在本例中,num即代表了0xff0。

我们用 int *p = num这个操作,将num的地址0xff0赋值给p,而p也是一个变量,它也需要存放在内存中,而内存是需要地址去访问的,所以p在内存中也是有一个自己的地址的。p内部存放的数据就是刚刚赋值的0xff0,假设p在内存地址为0xfe8处,那么用图的形式表示为:

由上图我们可以看到p的值就是数组的首地址值0xff0,如果我们要访问数组的3个值,就可以不用直接操作数组了,可以通过如下方式获得:

因为num即代表了数组的首地址,而num的值(0xff0)赋值给了p,所以num[1]和p[1]两者表达的意思是一样的。

对于*(p+1)这样的操作为什么也可以呢?是因为p+1操作对应的汇编上,相当于这样一段指令(当然以下不是真的汇编指令,而是写成了C格式的汇编操作过程):

一番操作后我们就得到了数组第二个数的地址值eax=0xff4了,此时*(p+1) 就等价于 *eax, 星 * 操作后即获得地址值0xff4对应的内容2。

代码示例:

运行结果:

Continue reading...