为什么数组从索引0开始:内存层面的解释

发布: (2026年1月4日 GMT+8 22:52)
7 min read
原文: Dev.to

Source: Dev.to

📌 目录

连续内存块的数组

在 C/C++ 中,数组本质上是一个 固定大小、元素类型相同的集合,存储在 连续的内存位置。当你声明

int arr[100];

编译器会为 100 个连续的整数 分配空间。

在大多数现代系统上:

  • 一个 int 通常占用 4 字节(在 32‑ 或 64‑ 位架构上)。
  • 因此整个数组占用 400 字节,在内存中紧密排列。

Source:

arr[i] 的工作原理:指针算术解释

数组从索引 0 开始的真正原因与计数或约定无关。它来源于语言 定义 数组下标的方式。

当你写 arr[i] 时,它会 直接翻译*(arr + i)

这不是实现细节;它是 C/C++ 语言定义的一部分。

拆解 *(arr + i)

部分含义
arr数组的 基地址(即 &arr[0])。
+ i指针算术 —— 加上 i × sizeof(元素类型) 字节,而不是 i 字节。
*对计算得到的地址解引用,以读取或写入该值。

所以 arr[i] 的字面意思是:从数组起始位置向后移动 i 个元素,然后访问存放在那里的值

示例

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40};

    // 直接数组访问
    printf("arr[1]: %d\n", arr[1]);          // 输出: 20

    // 等价的指针写法
    printf("*(arr + 1): %d\n", *(arr + 1));  // 同样输出: 20

    return 0;
}

两个语句都打印 20,因为它们实际上是完全相同的操作。

为什么这会导致索引从 0 开始

第一个元素 并不是“相隔一步”——它位于 基地址 本身。

  • 与基地址的距离 = 0
  • 偏移量 = 0
  • 索引 = 0

因此,第一个元素的访问方式是:

arr[0] == *(arr + 0)   // no adjustment needed

每个后续元素通过在内存中向前移动来访问:

arr[1] == *(arr + 1)   // skip 1 element (4 bytes for int)
arr[2] == *(arr + 2)   // skip 2 elements (8 bytes)

索引仅仅是以 元素为单位的偏移量。偏移量从 0 开始,因为没有任何东西可以比原点的距离更近。这直接源于内存寻址和指针算术的工作方式。

如果数组从索引 1 开始?

假设数组是 基于 1 的索引,像 MATLAB 那样,首个元素将通过 arr[1] 访问。

指针运算本身 不会 改变;arr[i] 仍然会被翻译为 *(arr + i)。直接套用这条规则会得到:

arr[1] → *(arr + 1)   // 指向 *第二* 个元素,而不是第一个

为了让基于 1 的索引工作,编译器需要把每一次访问重写为:

arr[i] → *(arr + (i - 1))

额外的减法会导致 语义不匹配,与硬件自然的 “基址 + 偏移” 寻址模型相冲突,增加了边界推理的复杂度,并且模糊了 “相对于基址的偏移” 这一简单的思维模型。虽然现代编译器可以将减法优化掉,但这一步概念上的额外处理是没有必要的。

Source:

为什么 arr[i]i[arr] 意思相同

C 标准将下标运算符定义为:

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

因为 加法是交换律的a + b == b + a),我们同样有:

*(a + b) == *(b + a)

于是:

a[b] == b[a]

不是技巧 或未定义行为;它是语言定义的直接结果。

演示

#include <stdio.h>

int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};

    // 普通的数组下标
    printf("arr[3] = %d\n", arr[3]);   // 输出: 4

    // 等价但不常见的下标写法
    printf("3[arr] = %d\n", 3[arr]); // 输出: 4
    return 0;
}

两个语句都会打印 4,因为 arr[3]3[arr] 计算得到的是同一个地址。

结论

  • 零基索引 与 C/C++ 定义数组下标的方式(*(base + offset))完全一致。
  • 第一个元素位于 基地址,因此它的偏移量是 0
  • 使用一基索引则需要在每次访问时额外进行 -1 的调整,增加不必要的复杂性。
  • 定义 a[b] == *(a + b) 也解释了为什么 arr[i]i[arr] 可以互换使用。

理解这些底层细节可以阐明,看似奇怪的数组索引从 0 开始实际上是对语言和底层硬件最自然、最高效的选择。

#include <stdio.h>

int main(void) {
    int arr[] = {1, 2, 3, 4};
    int i = 3;
    printf("%d\n", i[arr]); // Output: 4
    return 0;
}

注意: 虽然 i[arr] 在 C 中是合法的,但在实际代码中很少使用,因为它会降低可读性。它之所以存在,仅仅是因为数组下标是用指针算术来定义的。

结论

在 C/C++ 中,数组索引并不是在计数位置,而是测量相对于基地址的偏移量。

Back to Blog

相关文章

阅读更多 »