前言
时间与空间复杂度以后还会继续深入,目前可以先搁置一边。
数据结构与算法中,最基本的两个结构就是:数组、链表。二者最大的区别在于:数组存储地址是连续的,链表存储地址不是(完全)连续的。
数组可以说的没有多少,一是因为简单;二是因为有很多文章都讲的很详细。所以我们不再赘述,本片对数组引出的两个问题进行探讨:
- 栈的生长方向以及数组的偏移
- 大小端问题
栈的生长方向及数组偏移
引用一张 linux 进程内存结构的图,其中 stack 栈的生长方向是向下生长的,也就是向低地址生长,即比如从 0x10008 - 0x00010。windows的栈也是向下生长。而我们都知道,数组的元素和其对应的地址是往大增长的:
比如:int a[2] = {0}
, 其中数组 a 的地址为0x00010,那么a[1] 的地址应该是 0x00010 + 1 * sizeof(int)
。
正是因为这种区别,考虑到如下的 C 程序 :
#include <stdio.h>
int main() {
int i = 5;
int a[3] = {0};
int j = 6;
j = a[3]; // (1)
printf("j is : %d", j); // (2)
return 0;
}
复制代码
运行后,输出为:
j is 5
为什么?
因为上述程序在执行 (1)之前,由于(1)之前的变量都是局部变量,所以存储在栈中。而上文说过了,栈的生长方向是向下的,且数组的元素地址是递增的,所以(1)之前的内存(栈)布局为:
所以,当数组越界访问 a[3]
时,就越界到了 i = 5
的位置,所以就会将 5 赋值给 j。
当然,实现上述结果的前提还有下面的两点:
- i 和 数组 a 类型相同,或者说二者的类型占用字节数相同
- 所选用语言没有对数组越界做出限制
这个程序还可以进行扩展,留到下次进行分析。
大小端问题
上面是数组如何存储的,那么数组中的某一个元素又是如何存储的?比如 int i = 5
,其又是如何存储的呢?
栈是一种数据结构,其本质是被操作系统管理的内存空间。变量 i 的存储肯定不是依靠操作系统来完成的,那么是谁呢?
我们写好了某个语言的程序,程序需要转换成机器语言即01010这种二进制语言才能够被计算机识别,这一过程可能是由编译器来完成也可能是由翻译器来完成。
比如 i 转换成机器语言后可以用十六进制表示成 “0x00000005”(展开就是01010二进制),CPU需要读取指令并执行,那么当它收到这个数字时,又是如何解读的呢?
假设 int 为 4 字节大小,那么一种解读规则是:内存中的低位地址存储 i 的高位字节,即:
0x00 | 0x1000 |
---|---|
... | ... |
0x05 | 0x1003 |
这种存储模式就称为 大端存储
而另外一种模式则反过来,如下表:
0x05 | 0x1000 |
---|---|
... | ... |
0x00 | 0x1003 |
这种存储模式就称作为 小端存储
。
只有编译器(解释器)和 CPU 对于多字节数据的存储模式相统一,才能够正确执行程序。
拓展阅读
- 《深入理解计算机系统》
- 大小端
- 进程模型