“千万不要被数组简单的外表迷惑,它的事,比你想象的多”
01
提出问题
数组可能是最具欺骗性的数据类型。仅从语法定义上看,它非常简单;似乎用法跟普通变量相同,仅仅只有数量上的差异。但真正使用数组的时候,却完全是两回事
02
一维数组
让我们先从最简单的开始,打开Compiler Explorer,写一个最简单的对数组赋值的函数:
通过对应的汇编指令,可以很容易猜到:这个赋值过程,就是在对一段连续的内存赋值。因为 int 类型是 4 字节长度,所以,每个元素的地址间隔都是 4 个字节。
同理,如果是 char 类型的数组,每个元素的地址间隔就是 1 个字节。
所以,我们可以脑补一下数组 a 在内存中的样子,大概就是长这个样子:
看到这种连续的内存,你会想到什么?是的,又是指针。因为,只要知道数组 a 的首地址和长度;一样可以准确的定位:数组 a 所在的内存空间。
不要以为,这是阿布的一厢情愿。实际上,很多编译器就是这么干的!让我们写一个最简单的用数组变量作参数的函数;再写一个最简单的用指针变量作参数的函数:
没想到吧?它们的汇编指令竟然是一模一样的!
所以,在传递数组参数的时候,无论你的数组有多大,编译器都不会像普通变量那样,为你在“堆栈”中,构建一个相同大小的临时数组变量。
而是,简简单单的传递一个数组的内存首地址,像传递指针变量一样,把数组的内存首地址,传递给:被调函数。而对数组的读、写操作,则等价于对指针的 * 操作。
再写一个函数,作一下调用:
如你所见,这 3 种调用形式,对应的CPU指令完全一致!它们本质上完全相同,都是在传递数组的内存首地址,而非构建临时数组。
我们需要习惯这种传递指针的形式,很多知名的开源软件(操作系统、协议栈)也是这么作的。当然,为了防止越界,还会增加一个参数,表示数组的长度:
void func(int* array, int length)
03
多维数组
最后是 二维、三维 数组,可能你心中的:一维、二维、三维数组长这个样子:
但很遗憾,计算机的内存条是一维的,在CPU眼里,多维数组,都是在当一维数组来处理。事实胜于雄辩,下面3个函数,分别在给一维、二维、三维数组赋值:
但显然,它们的汇编指令,完全相同!所以,无论数组是一维的还是高维的,它都是一段连续的一维内存。
04
总结
05
热点问题
Q1:为什么经常看见把指针,当数组一样使用呢?可以认为指针和数组是等价的吗?
A1:这是一个很好的问题!在编程实践中,我们经常看见把指针当作数组使用的情况。但可以确定的是指针和数组是不等价的。
指针变量只是记录了一个内存地址。如果是32位的CPU,这个内存地址会占据4字节的内存空间,如果是64位的CPU,这个内存地址则占用8字节的内存空间。
而数组可能是多组数据,其占用的内存空间,往往不止4或8个字节。虽然指针经常变形成数组使用,但它的真实意图,还是在作地址偏移。例如下面的代码:
通过Compiler Explorer,我们可以发现:函数func1和函数func2对应的CPU指令是完全一致的。所以p[1]实质上是在作一个字节的内存地址偏移:(p + 1),然后再对新地址,做读操作:*(p + 1)。
虽然p[1]的写法更加简洁,但同样误导性也比较强,会让开发者误以为p是一个数组。当然,不仅一维数组如此,二维数组也存在类似的问题,我们未来也会看到类似的案例。