目录
什么是数组(Array)?
🔍为什么数组的下标要从 0 开始?
一、内存地址与偏移量的关系:从 0 开始是最自然的映射
二、指针的起点就是第 0 个元素的地址
三、历史原因:BCPL → B → C → C++
数组的内存体现
数组的声明
数组的访问方式
什么是数组(Array)?
数组(Array)是 C++ 中的一种线性数据结构,用于存储多个相同类型的变量,并且这些变量在内存中是连续排列的。
你可以把它想象成一个排好队的储物柜,每个柜子有编号(下标),每个柜子里放着一个值。例如:
int arr[5] = {10, 20, 30, 40, 50};
这表示我们声明了一个包含 5 个 int
类型的数组,它依次存储:
-
arr[0] = 10
-
arr[1] = 20
-
arr[2] = 30
-
arr[3] = 40
-
arr[4] = 50
注意:数组的下标从 0 开始,而不是 1。
🔍为什么数组的下标要从 0 开始?
虽然最初很多人觉得「从 1 开始」更符合直觉,但数组从 0 开始其实是有深刻的底层原因和效率考量,它与 指针、地址计算、语言设计哲学 有关。我们来系统解释这个设计逻辑。
一、内存地址与偏移量的关系:从 0 开始是最自然的映射
在 C/C++ 中,数组实际上是指针加偏移(pointer arithmetic)。
例子:
int A[4] = {10, 20, 30, 40};
假设数组 A
从地址 0x1000
开始,且每个 int
占 4 字节。
下标 i | 内存地址 | 数学计算 |
---|---|---|
A[0] | 0x1000 | A + 0 → 0x1000 + 0 * 4 |
A[1] | 0x1004 | A + 1 → 0x1000 + 1 * 4 |
A[2] | 0x1008 | A + 2 → 0x1000 + 2 * 4 |
访问 A[i]
实际上是计算:
*(A + i) // 指针 + 偏移量
👉 如果下标从 1 开始,那就必须写成:
*(A + (i - 1))
这样会多一个运算(减法),无论在运行效率还是语义上都不自然。
二、指针的起点就是第 0 个元素的地址
当你声明:int A[5];
数组名 A
实际上是指向 A[0]
的地址。不是 A[1]
,不是别的起点。
所以,
*A == A[0]
*(A+1) == A[1]
*(A+2) == A[2]
如果下标从 1 开始,就会出现“偏移一格”的矛盾,代码会更难维护。
三、历史原因:BCPL → B → C → C++
🧬 C语言起源于 B 和 BCPL
最早的语言 BCPL 和 B语言 中没有数组的概念,只有“地址 + 偏移”。C 语言继承了这种偏移访问模型,所以自然地,数组从 0
开始偏移。
Dennis Ritchie(C 语言的设计者)就是遵循这个简洁、底层直观的设计哲学。
现代语言很多也从 0 开始
大多数现代语言也继承了这个设计:
语言 | 数组是否从 0 开始 |
---|---|
C | ✅ 是 |
C++ | ✅ 是 |
Java | ✅ 是 |
Python | ✅ 是 |
JavaScript | ✅ 是 |
Rust | ✅ 是 |
虽然也有一些语言(如 Fortran、Lua)允许你从 1 开始索引,但这并不常见。
数组的内存体现
数组的核心特征是:所有元素在内存中是挨着排放的,没有任何间隔。
我们用一个直观的内存图解来说明:
假设 int
类型占用 4 字节(常见情况),数组如下:
int arr[4] = {100, 200, 300, 400};
如果 arr[0]
存储在内存地址 0x1000
,那么在内存中是这样的:
内存地址 值
0x1000 arr[0] = 100
0x1004 arr[1] = 200
0x1008 arr[2] = 300
0x100C arr[3] = 400
▶️ 特点总结:
-
每个元素都紧挨着上一个,偏移量是
sizeof(类型)
。 -
编译器知道数组是连续的,所以可以通过起始地址和偏移快速定位任意元素:
arr[i]
等价于*(arr + i)
数组的声明
在 C++ 中,声明数组就是告诉编译器我们要创建一个连续内存区域,用于存储多个相同类型的数据项。声明时必须指定类型和元素数量。
1. int A[5];
含义:
-
声明一个整型数组
A
,包含 5 个元素。 -
未初始化,每个元素的值是未定义的垃圾值(在局部变量中)。
注意:
-
在函数内部声明的数组(局部数组)不会自动清零。
-
在全局或静态作用域中声明的数组会被自动初始化为 0。
2. int A[5] = {2, 4, 6, 8, 10};
含义:
-
声明一个大小为 5 的整型数组,并完全初始化所有元素。
-
A[0] = 2
,A[1] = 4
, ...,A[4] = 10
特点:
-
初始化列表刚好填满数组,无自动补零。
-
所有元素值由你控制。
3. int A[5] = {2, 4};
含义:
-
声明一个大小为 5 的数组,只初始化前两个元素。
-
剩下的元素会被自动补零。
结果是:
A[0] = 2
A[1] = 4
A[2] = 0
A[3] = 0
A[4] = 0
4. int A[5] = {0};
含义:
-
声明一个大小为 5 的数组,仅第一个元素初始化为 0。
-
其余元素也会被自动补零。
-
快速清零的技巧:用
{0}
初始化整个数组。
实际效果:
A[0] = 0
A[1] = 0
A[2] = 0
A[3] = 0
A[4] = 0
5. int A[] = {2, 4, 6, 8, 10};
含义:
-
不指定大小,由初始化列表的元素数量自动推断大小为 5。
-
效果与
int A[5] = {2, 4, 6, 8, 10};
相同。
编译器推断出:int A[5]; // ← 实际等价形式
更简洁,特别是在你明确初始化所有元素的情况下。
数组的访问方式
通过索引访问数组元素(Index)
这是最常见、最直接的方式。
A[index]
-
index
是整数类型,从0
开始。 -
索引值必须在合法范围内:
0
到数组大小 - 1
。
用指针访问数组元素
数组名与指针的关系:
在大多数表达式中,数组名会自动退化为指向第一个元素的指针:
int A[5] = {1, 2, 3, 4, 5};
int* p = A; // A 就是 &A[0]
此时:
-
p
和A
都指向数组开头 -
你可以用指针访问元素
✅ 使用 *(pointer + index)
:
int A[5] = {1, 2, 3, 4, 5};
int* p = A;cout << *(p + 0); // 输出 1
cout << *(p + 3); // 输出 4*(p + 2) = 100; // 修改 A[2] 为 100
🟰 等价关系:
表达式 | 含义 |
---|---|
A[i] | 访问数组第 i 个元素 |
*(A + i) | 使用数组名当指针 |
*(p + i) | 使用指针访问数组元素 |
p[i] | 指针变量也支持 [] 下标运算(语法糖) |