CPU眼里的:数组

技术博客 (226) 2023-12-01 09:01:01

千万不要被数组简单的外表迷惑,它的事,比你想象的多

01

提出问题

数组可能是最具欺骗性的数据类型。仅从语法定义上看,它非常简单;似乎用法跟普通变量相同,仅仅只有数量上的差异。但真正使用数组的时候,却完全是两回事

02

一维数组

让我们先从最简单的开始,打开Compiler Explorer,写一个最简单的对数组赋值的函数:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第1张

通过对应的汇编指令,可以很容易猜到:这个赋值过程,就是在对一段连续的内存赋值。因为 int 类型是 4 字节长度,所以,每个元素的地址间隔都是 4 个字节。

同理,如果是 char 类型的数组,每个元素的地址间隔就是 1 个字节。

所以,我们可以脑补一下数组 a 在内存中的样子,大概就是长这个样子:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第2张

看到这种连续的内存,你会想到什么?是的,又是指针。因为,只要知道数组 a 的首地址和长度;一样可以准确的定位:数组 a 所在的内存空间。

不要以为,这是阿布的一厢情愿。实际上,很多编译器就是这么干的!让我们写一个最简单的用数组变量作参数的函数;再写一个最简单的用指针变量作参数的函数:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第3张

没想到吧?它们的汇编指令竟然是一模一样的!

所以,在传递数组参数的时候,无论你的数组有多大,编译器都不会像普通变量那样,为你在“堆栈”中,构建一个相同大小的临时数组变量。

而是,简简单单的传递一个数组的内存首地址,像传递指针变量一样,把数组的内存首地址,传递给:被调函数。而对数组的读、写操作,则等价于对指针的 * 操作。

再写一个函数,作一下调用:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第4张

如你所见,这 3 种调用形式,对应的CPU指令完全一致!它们本质上完全相同,都是在传递数组的内存首地址,而非构建临时数组。

我们需要习惯这种传递指针的形式,很多知名的开源软件(操作系统、协议栈)也是这么作的。当然,为了防止越界,还会增加一个参数,表示数组的长度:

void func(int* array, int length)

03

多维数组

最后是 二维、三维 数组,可能你心中的:一维、二维、三维数组长这个样子:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第5张

但很遗憾,计算机的内存条是一维的,在CPU眼里,多维数组,都是在当一维数组来处理。事实胜于雄辩,下面3个函数,分别在给一维、二维、三维数组赋值:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第6张

但显然,它们的汇编指令,完全相同!所以,无论数组是一维的还是高维的,它都是一段连续的一维内存。

04

总结

  1. 数组是一段连续的内存,除了常规读、写数组元素的方法;也会用指针来表示数组,并用指针的 * 操作来读、写数组元素。
  2. 传递数组参数,本质上是传递指针,所以,在函数内改变数组的值,也会改变函数外,数组的值。
  3. 高维数组本质上还是一维数组,只是索引的方式不同,应用的场景不同,特别是3D领域,用多维数组编程,会给开发者带来诸多方便。

05

热点问题

Q1:为什么经常看见把指针,当数组一样使用呢?可以认为指针和数组是等价的吗?

A1:这是一个很好的问题!在编程实践中,我们经常看见把指针当作数组使用的情况。但可以确定的是指针和数组是不等价的。

指针变量只是记录了一个内存地址。如果是32位的CPU,这个内存地址会占据4字节的内存空间,如果是64位的CPU,这个内存地址则占用8字节的内存空间。

而数组可能是多组数据,其占用的内存空间,往往不止4或8个字节。虽然指针经常变形成数组使用,但它的真实意图,还是在作地址偏移。例如下面的代码:

CPU眼里的:数组 (https://mushiming.com/) 技术博客 第7张

通过Compiler Explorer,我们可以发现:函数func1和函数func2对应的CPU指令是完全一致的。所以p[1]实质上是在作一个字节的内存地址偏移:(p + 1),然后再对新地址,做读操作:*(p + 1)。

虽然p[1]的写法更加简洁,但同样误导性也比较强,会让开发者误以为p是一个数组。当然,不仅一维数组如此,二维数组也存在类似的问题,我们未来也会看到类似的案例。

THE END

发表回复