您的位置:首页 > 其它

指针,数组转换漫谈

2014-07-19 12:42 393 查看
1.一维数组向指针转换

先给出几个定义:

int a [10] :即是说明 a 是一个连续的内存块,有10个结构,这个结构被看成 int 去解析。

int * b = a;即是说明 b 是一个内存块,有 1 个结构,这个结构被看成一个 int* (指向 int 地址)去解析。即告诉编译器,要把 b 里面存放的数值当成一个地址去解析。

先说明这两者的不同及相同。

前者是一个数组,它是编译器的天生支持数据结构,只要这样写,就等于告诉编译器,我需要的是一个数组,然后编译器就会安排内存,并且返回一个你可以操纵整个数组的入口地址,即将这个内存块的首地址返回给 a 去接受。其实也可以不用返回首地址,也可以返回尾地址什么的,这是因为人最习惯的思维是加法,况且,从某种意味上来说,生成一个数组,是一个从无到有的过程,我们返回最起始的东西,也更符合人的思维方式。就好比,你去取钱,如果钱是连号的,那么,往往是号最小的钱放在上面。好了,扯了一些无关的话题,让我们回到正题。数组
a 的每一个元素是一个 int ,它占的内存空间是确定的,即 sizeof(int),当使用 a[2] 就是取第 3 个 int,这是自然语言,必须要转化为机器语言才能让机器处理,即,要把那个请求翻译为机器语言,前面说了,数组是连续的,(先不要考虑二维数组),那么,我们将数组的首地址向后面偏移 2 个 int 就正好得到第 3 个 int 的地址,也即:

a[2] = *(a + 2);
//式1

等等,a 不是一个地址吗,地址的单位是字节,这个地址加上 2 字节,怎么会是将 a 向后偏移 2 个 int 的地址呢?按理说,这里应该是

a[2] = *(a + 2 * sizeof(int));//式2

是的,没错。但是编译器为了使上面的表达式看起来更加自然,它帮我们自动将式1转换为了式2,因而能够正确地寻址。我暂且把这个过程叫作“地址运算纠正”。

既然,a 就是数组的首地址,我们知道,地址在内存里面占的大小即刚好是机器位数,那么它就刚好与 int 占的位数相等,因此,我们可以用一个 int 类型的变量去接受这个地址,即:

int c = a;

此时,c 里面存放的就是数组的首地址,我们可以将式1改写如下:

a[2] = *(c + 2);
//式3

如果这样写,一经调试,就会发现等式右边的值并不等于左边,这是为什么呢?

问题出现在了地址 c + 2 没有被编译器正确地转换为地址 c + 2 * sizeof(int) 。

现在就引出了问题,为什么式1能够被编译器进行地址处理,而式3不能呢?

c + 2 是一个 int 与另一个 int 的加法,这是一个普通的加法运算,编译器没有理由进行纠正。

实际上,如果一个运算不被编译器识别为地址计算,那么它就是普通的运算,编译器不会进行地址运算纠正!

因此,上面的问题就解释清楚了。正确的写法应该是:

a[2] = *(c + 2 * sizeof(int));

如果数组指针用这种方式转换,那也太麻烦了,取一个元素要写这么长的代码!这个代码看起来比较长都是 2 * sizeof(int)造成的,有没有方法把这个 sizeof(int) 去掉呢?是有的,根据上面的分析,我们只要告诉编译器,我们是在进行地址运算,那么,它就会自动帮我们进行地址运算纠正。

所以,我们可以用一个"地址类型"去接受数组的首地址,可是没有地址类型这一种类型,我们只有指针。(注意,指针不是类型,因为指针只有说明是指向何类型的指针才有意义)

int *b = a;

此时,b 里面存放的数值内容就能自然地被看成一个地址。

因此有 a[2] = *(b + 2);

再等等,前面提到过,指向任何类型的指针都占(机器位数,如32位机器,指针就是 32 位)。如果我们这样做:

char *b = a;

a[2] = *(b + 2);

还是正确的吗?答案是不正确的,因为 b 是一个指向 char 的指针,计算 b + 2 的地址时,是要将 b 向后移动 2 个 char 大小,即 2 个字节。而求 a[2] 是将首地址向后面偏移 2 个 int 。明显不正确。

实际上,在进行地址运算纠正时,它根据需要纠正的地址类型进行纠正。

让编译器以什么类型去解析一个地址对应的内存里面的数据是非常重要的!

2.二维数组向指针的转换
int a[2][3] 声明了一个二维数组,前面提到过,一维数组是连续的内存,二维数组呢?我们先认为是连续的。这个问题在后面回答。如图1,二维数组内存分布图



图1 二维数组

可以把 a 看成是有两个元素的一维数组: a[0] 和 a[1] ,它们各自是一个数组的首地址。因此,一个二维数组就是一个一维数组,这个一维数组的每一个元素是一个数组的首地址。即 a 是一个一维指针数组。如图2所示:



图2 二维数组其实就是一维数组

int **b 声明了一个二维指针,如图 3 ,二维指针内存分布图



图3 二维指针

我们先来想一下,编译器如何求 a[1][2] 。数组 a 有两个子数组,a[0] 和 a[1],它们的值是一个地址,分别是一个数组的首地址。所以,a[1][2] 实际上就是求数组 a[1] 的第 2 个元素。
a[1][2] = (a[1])[2] = *(a[1] + 2);
而 a[1] 这个地址的值是多少呢?a 可以被当成一维数组,a[0] 和 a[1] 都是它的元素。自然 a[0] 等于首地址。求 a[1] 可以使用一维数组的逻辑,
a[1] = *(a + 1);
这里就有一个问题,根据图1的内存分布图,a[1] 应该是 a 向后面偏移 a[0] 数组大小。即:
a[1] = *(a + sizeof(a[0]));
sizeof(a[0])是多少呢?此处,a[0] 是一个数组。如果片面地认为,数组等价于指针,那么,很有可能就会弄错,认为 sizeof(a[0]) = sizeof(int *) = 4;
很明显,为了让数组能够正确地寻址,必须要让 sizeof(a[0]) = (数组 a[0] 占的字节数)。
因此,编译器会再次施展“地址运算纠正”大法,使得处理逻辑与一维数组的处理逻辑一致。
a[1] = *(a + sizeof(a[0])) = *(a + 1);

我们分两步来考虑如何将一个二维数组转换为二维指针。
1.将二维数组转换为一维指针数组 p。
2.将一维指针数组转换为二维指针 s。
为了得到 p,只需要走访 a[0], a[1] 即可,将它们的值放入 p 即可。惟一的问题在于,确定 p 中指针的是指向什么类型的。的确,如果只是单纯地保存一个地址,使用 int * 的数组足矣,但是,我们考虑到把 p 转换为 s 时,遍历原数组的每个元素,现在只能采用从数组首地址加 1 的方式,如果把 p 中的指针声明为 int *,则加 1 只相当于向后偏移 4 个字节,而正确的应该是偏移sizeof(a[0]),所以不能声明为 int
*,而应该声明为指向大小为 sizeof(a[0]) 的数组。可以这样声明 p,
int (*p)[3];
如果令 p = a;那么 p + 1 就等于 a[1] 的首地址,逻辑是正确的。

由 p 得到 s,我们只需要根据一维数组转换为一维指针的规则,使用 int ** s = (int**)p;就完成了。
整个的转换如下:

最后再论述,为什么二维数组的内存也是连续的。我们假设不连续,看会怎么样。
考虑编译器如何寻址 a[1][2] ,编译器已知的内容只有 a 这个首地址,要访问 a[1][2],必然只能通过偏移。首先,一个数组里面必然是连续的内存,这个在第一节里面描述过了。那么,不连续只能是子数组之间不是连续的(即 a[0] 和 a[1] 在内存上面不连续)。此时,编译器就没有办法通过 a 偏移到 a[1],因为它不知道这个不连续的空间有多大。或者你可能认为,编译器可能有某种算法,仅仅根据二维数组的某些特征去决定每个子数组之间的偏移,或者对每个二维数组增加管理以表示偏移,是的,这或许行得通,但编译器没有理由不采用连续内存这种简单,直接而快速的寻址方法,而去用那些蹩脚而低速的方式。
因此,二维数组,乃至多维数组内存都是连续的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: