关于数组

数组就是一片连续的内存空间,并且被划分成大小相等的小空间。

为什么数组下标从0开始?

寻址公式:i_address = base_addr + i * sizeof(element)

  • 如果下标从 0 开始,第一个元素 arr[0] 的地址为 A + 0 * S = A,这与数组的基地址相同,无需额外的计算。
  • 如果下标从 1 开始,第一个元素 arr[1] 的地址会是 A + (1-1) * S = A,虽然结果相同,但在逻辑上需要多一层计算处理,即计算 (1-1) 的结果。

从来只有一维数组

1
2
3
4
5
int data[2][2] = { 1,1,1,1 };

// 等价于

int data[2][2] = { {1,1},{1,1} }; // 可读性强

数组名称是 data ,数据类型是 int[2][2]。 data[0] 或 data[1] 的数据类型是 int[2]。 data[0][0] 或 data[0][1] 的数据类型是 int。

数组数据类型.png

因此,这就能很好理解数组计算长度的公式了。

1
sizeof(data) / sizeof(data[0][0])

我们说数据类型指示数据的范围,data 的数据类型代表的范围是 4 个 int 类型,而 data[0][0] 的数据类型代表的范围是 1 个 int 类型。

小心数组退化为指针

因为数组作为实参传递给函数的时候,回退化为指针,而我们遍历数组需要长度,这个时候你再用前面的方式计算数组长度就会出现错误,所以我们希望你在传递数组作为实参的时候,记得提前计算长度并也作为实参传递进去。

于此同时,我们应该在传递数组作为实参的时候,类型应该写成 指针,这样读者就会认为这是传递进来的一个数组,且已退化为指针。如果你不是这样做,而是像这样 int[] data 或 int[2] data ,那么读者虽然能看出这是一个数组,但是容易忘记它已经退化为指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int func(int *data, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", data[i]);
}
}

int main(void) {

int data[4] = { 1,2,3,4 };

int len = sizeof(data) / sizeof(data[0]);

func(data, len);

return 0;
}

如果你非要这么做,即以数组的视角去定义形参,那么访问数组元素需要用如下方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int func(int data[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", data[i]);
}
}

int main(void) {

int data[4] = { 1,2,3,4 };

int len = sizeof(data) / sizeof(data[0]);

func(data, len);

return 0;
}

你可能想问,如果是多维数组,我们该如何传递长度呢?只需要传递可以被省略的那个数即可,比方说 data[2][2] 可以省略为 data[][2],也就是说第一个可以被省略。或者,你直接把行和列的长度都传进去,因为我们通常不会超过二维数组,所以这不是什么难考虑的事情。