C++ 数组下标语法
C++ 数组下标语法
C++ 的数组下标运算看起来简单,但背后其实牵涉到 数组、指针、指针算术、表达式求值、运算符重载。你前面看到的 2[1[a]],正是这些规则叠加产生的结果。
1. 最基本的数组下标语法
最常见的是:
int a[5] = {10, 20, 30, 40, 50};
a[0] // 10
a[1] // 20
a[4] // 50
语法形式:
表达式1[表达式2]
对于内置数组和指针,C++ 中它的核心含义可以理解为:
a[i]
等价于:
*(a + i)
所以:
a[2]
相当于:
*(a + 2)
假设:
int a[] = {10, 20, 30};
内存可以概念上理解成:
a
↓
+------+------+------+
| 10 | 20 | 30 |
+------+------+------+
0 1 2
a + 2 指向第三个元素,然后:
*(a + 2)
取出这个位置的值,也就是:
30
2. 为什么 1[a] 居然合法?
这是最有意思的地方。
因为:
a[i]
等价于:
*(a + i)
而指针加整数时:
a + i
和:
i + a
都是合法的。
所以:
*(a + i)
也可以写成:
*(i + a)
因此:
a[i]
和:
i[a]
对于内置数组/指针来说效果相同。
例如:
#include <iostream>
int main() {
int a[] = {10, 20, 30};
std::cout << a[1] << '\n';
std::cout << 1[a] << '\n';
}
输出:
20
20
也就是说:
a[1] == 1[a]
这不是编译器的特殊彩蛋,而是下标运算本身的定义导致的。
3. 2[1[a]] 到底发生了什么?
假设:
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
正常写法:
a[1][2]
得到:
7
现在看:
2[1[a]]
先算:
1[a]
因为:
1[a] == a[1]
所以:
2[1[a]]
变成:
2[a[1]]
再次利用:
x[y] == y[x]
得到:
a[1][2]
所以:
2[1[a]]
等价于:
a[1][2]
最终结果:
7
完整展开:
2[1[a]]
== 2[a[1]]
== a[1][2]
== *(*(a + 1) + 2)
这最后一个形式很重要:
*(*(a + 1) + 2)
它展示了二维数组下标背后的真实逻辑。
4. 一维数组的下标实际上是指针运算
看下面:
int a[] = {10, 20, 30, 40};
int x = a[2];
可以理解成:
int x = *(a + 2);
这里:
a
在很多表达式中会发生所谓的 array-to-pointer conversion(数组到指针转换)。
也就是:
a
通常会转换为:
&a[0]
类型大致从:
int[4]
变成:
int*
因此:
a + 2
表示:
&a[2]
于是:
*(a + 2)
就是:
a[2]
5. 指针也可以直接使用 []
因为下标运算本身就是建立在指针运算之上的,所以指针也能用:
int a[] = {10, 20, 30};
int* p = a;
std::cout << p[0] << '\n';
std::cout << p[1] << '\n';
std::cout << p[2] << '\n';
输出:
10
20
30
并且:
p[2]
就是:
*(p + 2)
所以:
p[2]
a[2]
*(p + 2)
*(a + 2)
在这里都是同一个元素。
6. 数组下标不一定从“数组变量”开始
例如:
int a[] = {10, 20, 30, 40};
int* p = &a[1];
那么:
p[0] // 20
p[1] // 30
p[2] // 40
因为:
p
本身指向:
a[1]
因此:
p[1]
其实是:
*(p + 1)
也就是:
a[2]
这说明:
[]的本质并不是“访问某个数组变量”,而是“基于一个指针进行偏移后解引用”。
7. 为什么 a + 1 不是地址加 1 字节?
这是指针算术特别重要的规则。
例如:
int a[3];
假设:
sizeof(int) == 4
如果:
a
地址是:
1000
那么:
a + 1
不是:
1001
而是:
1004
因为指针算术会自动按元素大小移动。
所以:
a + i
实际地址大致是:
起始地址 + i * sizeof(int)
因此:
a[i]
可以方便地访问第 i 个元素。
8. 二维数组为什么可以写 a[i][j]?
例如:
int a[3][4];
它不是一个特殊的“二维数组对象”。
从类型角度,它是:
一个长度为 3 的数组
每个元素又是一个长度为 4 的 int 数组
也就是:
int [3][4]
可以想象成:
a
│
├── a[0] -> int[4]
├── a[1] -> int[4]
└── a[2] -> int[4]
所以:
a[1][2]
是分两步:
第一步:
a[1]
得到第二行:
int[4]
第二步:
a[1][2]
取第二行中的第三个元素。
展开:
a[1]
就是:
*(a + 1)
因此:
a[1][2]
就是:
(*(a + 1))[2]
继续展开:
*(*(a + 1) + 2)
因此:
a[i][j]
本质上就是:
*(*(a + i) + j)
10. 下标可以不是常量
当然不需要写死:
a[0]
a[1]
a[2]
可以写:
int i = 2;
std::cout << a[i];
也可以:
a[i + 1]
甚至:
a[f()]
只要表达式最终产生一个合适的整数索引即可。
例如:
int i = 1;
int j = 2;
a[i + j]
等价于:
a[3]
11. 下标可以是负数吗?
语法上可以。
比如:
p[-1]
等价于:
*(p - 1)
这有时候是合法的。
例如:
int a[] = {10, 20, 30};
int* p = &a[1];
std::cout << p[-1];
这里:
p
指向:
a[1]
于是:
p[-1]
就是:
a[0]
输出:
10
所以负下标本身并不是非法语法。
关键问题不是:
下标是不是负数?
而是:
运算后得到的地址是否仍然属于允许访问的数组范围?
12. 越界是未定义行为
这是 C++ 中非常重要的一点。
int a[3] = {10, 20, 30};
std::cout << a[100];
C++ 通常不会自动检查越界。
这属于:
Undefined Behavior
也就是未定义行为。
可能发生:
输出垃圾值
程序崩溃
看起来正常
修改其他内存
优化器做出意想不到的优化
都可能。
同样:
a[-1]
通常也是越界,因此也是未定义行为。
13. [] 本身不会做边界检查
比如:
std::vector<int> v = {10, 20, 30};
v[100]
std::vector::operator[] 也通常不进行边界检查。
如果希望检查:
v.at(100)
则会抛出:
std::out_of_range
所以:
v[i]
优点是速度直接,缺点是无边界检查。
而:
v.at(i)
会进行检查。
14. 内置数组的 [] 和 std::vector 的 [] 不完全是一回事
这里有一个非常重要的区别。
对于:
int a[10];
这里:
a[i]
使用的是语言内置的下标运算。
本质上:
*(a + i)
但是:
std::vector<int> v;
这里:
v[i]
实际上调用的是成员函数:
v.operator[](i)
概念上类似:
v.operator[](i)
因此对于类类型来说,[] 可以被重载。
15. 为什么 1[a] 对数组成立,但 1[v] 对 vector 不成立?
非常关键。
数组:
int a[3];
可以:
1[a]
因为使用的是内置规则:
*(1 + a)
但:
std::vector<int> v = {1, 2, 3};
你不能写:
1[v]
因为 std::vector 的下标访问依赖:
v.operator[](1)
operator[] 是 vector 的成员函数。
1 显然没有:
operator[](v)
因此:
v[1] // OK
1[v] // 错
所以以前说:
x[y] == y[x]
必须加限定:
对内置数组/指针下标运算来说成立,不是所有 C++ 类型都成立。
17. 为什么 a[i] 可以放在赋值左边?
例如:
int a[3];
a[1] = 100;
为什么可以?
因为:
a[1]
相当于:
*(a + 1)
解引用得到的是那个真实元素本身,而不仅仅是它的一个临时副本。
所以:
a[1] = 100;
修改的是数组里的元素。
同样:
++a[1];
也是可以的。
以及:
int& r = a[1];
也可以。
18. &a[i] 和 a + i
因为:
a[i] == *(a + i)
所以取地址:
&a[i]
基本就是:
a + i
例如:
int a[] = {1, 2, 3};
int* p1 = &a[2];
int* p2 = a + 2;
那么:
p1 == p2
为真。
这也是一个非常实用的关系:
&a[i] == a + i
19. a、&a[0] 和 &a 不是完全相同的东西
这个地方是初学 C++ 时非常容易混淆的。
假设:
int a[5];
通常表达式中:
a
会转换成:
int*
即:
&a[0]
但是:
&a
类型是:
int (*)[5]
也就是:
指向“整个长度为 5 的数组”的指针。
所以:
a
和:
&a
数值地址往往看起来相同,但类型和指针算术完全不同。
例如:
a + 1
跳过一个 int。
而:
&a + 1
跳过整个:
int[5]
也就是 5 个 int。
21. 数组名不是“永远等于指针”
这是另一个常见误解。
很多教程会说:
数组名就是指针。
严格来说不对。
应该说:
数组表达式在很多情况下会自动转换为指向首元素的指针。
例如:
int a[10];
a 的真实类型是:
int[10]
并不是:
int*
证明之一:
sizeof(a)
得到整个数组大小:
10 * sizeof(int)
如果 a 真的是 int*,那么:
sizeof(a)
就只会得到指针大小。
比如 64 位平台通常是 8。
22. 哪些情况下数组不会退化成指针?
几个典型情况:
sizeof(a)
不会退化。
还有:
&a
也不会。
另外模板和引用中,也能保留真正的数组类型:
template <std::size_t N>
void f(int (&a)[N]) {
}
这里可以直接知道数组长度。
23. 字符串字面量也可以下标
例如:
"hello"[1]
结果是:
'e'
因为字符串字面量本质上是字符数组:
const char[6]
内容是:
h e l l o \0
所以:
"hello"[0] // 'h'
"hello"[1] // 'e'
甚至按照前面的内置下标规则:
1["hello"]
也是:
'e'
虽然正常代码绝对不建议这么写。
25. 指针加法不是普通整数加法
下面不合法:
int* p;
int* q;
p + q; // 错
两个指针不能直接相加。
但:
p + 3
3 + p
都合法。
所以:
p[3]
3[p]
也都合法。
这就是对称性的来源。
26. 两个指针可以相减
例如:
int a[10];
int* p = &a[2];
int* q = &a[7];
auto d = q - p;
那么:
d == 5
它表示两个指针之间相差多少个元素,而不是多少字节。
这和数组下标的设计是统一的。
27. 数组下标的类型通常是整数
常见:
int i;
unsigned i;
long i;
std::size_t i;
都可以用作下标。
实际标准库中经常使用:
std::size_t
例如:
for (std::size_t i = 0; i < v.size(); ++i) {
std::cout << v[i];
}
原因是容器大小通常使用:
std::size_t
表示。
28. 但要小心无符号整数
例如:
std::size_t i = 0;
--i;
它不会变成:
-1
而是发生无符号回绕,变成一个非常大的数字。
因此:
a[i]
很可能严重越界。
这也是 C++ 下标相关 bug 的常见来源之一。
29. [] 的优先级很高
例如:
*p[i]
解析成:
*(p[i])
而不是:
(*p)[i]
因为 [] 的优先级比一元 * 高。
如果你真的想:
(*p)[i]
必须写括号。
类似地:
a[i].x
先做:
a[i]
再访问:
.x
30. 多维数组的内存是连续的
例如:
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
通常可以理解为连续排列:
1 2 3 4 5 6
但类型结构仍然是:
a
├── int[3]
└── int[3]
因此:
a + 1
不是跳过一个 int,而是跳过一整行:
int[3]
所以:
a + 1
指向第二行。
这也解释了:
a[1]
为什么可以直接得到第二行。
31. int** 和二维数组不是同一种东西
这个问题和下标语法关系很大。
例如:
int a[3][4];
a 在表达式中退化为:
int (*)[4]
即:
指向 int[4] 的指针
而不是:
int**
所以:
int** p = a;
是不合法的。
因为:
int (*)[4]
和:
int**
完全是不同的类型。
虽然它们都可以写:
x[i][j]
但内部结构不一样。
32. p[i][j] 为什么对 int** 也能工作?
假设:
int** p;
那么:
p[i][j]
仍然按两个步骤进行:
p[i]
得到:
int*
然后:
p[i][j]
得到:
int
也就是:
*(*(p + i) + j)
语法一样,但底层内存结构不一定像真正二维数组那样连续。
33. C++23 对多参数 operator[] 有扩展
现代 C++ 里,用户定义类型的 operator[] 能力比传统写法更灵活。
传统上通常是:
obj[i][j]
通过两次 operator[] 实现。
较新的 C++ 允许类定义带多个参数的下标运算符,例如概念上:
matrix[i, j]
对于自定义类型可以有特殊意义。
不过这属于现代 C++ 的运算符重载特性,和内置数组:
a[i][j]
的机制不是一回事。

浙公网安备 33010602011771号