C/C++:为什么 a[i][j] 等价于 *(*(a+i)+j)
前言
如果你较熟悉前置相关知识可直接跳转 第三章。
初探:指针的 +、-、++、-- 操作
自增操作对于指针所指向地址的影响
编译以下代码
#include <bits/stdc++.h>
using namespace std;
const int M = 3;
int main() {
int a[M] = {1, 2, 3};
int *p = a;
// 输出指针 p 所指向的地址
cout << p << endl;
// 输出 p 所指向的值
cout << *p << endl;
// 对 p 进行自增操作
p++;
// 输出 p 所指向的地址
cout << p << endl;
// 输出 p 所指向的值
cout << *p << endl;
}
运行结果(输出的地址随实际运行情况变化)
0x3a2f7ff93c
1
0x3a2f7ff940
2
可以看见,输出了 a[0]
和 a[1]
对应的值,以及两个地址。
两个地址为 0x
开头,说明是十六进制数。
按示例中 3a2f7ff940 - 3a2f7ff93c
可得十进制下 4,而 4 Byte 正好是笔者编译环境中 int 类型的数据大小。(编译环境:x86_64-w64-mingw32 gcc_version:13.2.0)
我们很容易从中得出结论:指针类型自增操作就是,指向的内存地址增加一个数 x。这个 x 就是指针所指向数据类型的大小。
类似的,我们可以得出 +、-、-- 对于指针所指向地址的影响。
深入:数组和指针
一维数组
#include <bits/stdc++.h>
using namespace std;
const int M = 3;
int main() {
int a[M] = {1, 2, 3};
int *p = a;
// 输出 a 所指向的地址
cout << a << endl;
// 输出 a 所指向的值
cout << *a << endl;
// 输出 p 所指向的地址
cout << p << endl;
// 输出 p 所指向的值
cout << *p << endl;
}
运行结果(输出的地址随实际运行情况变化)
0x2fafbffd9c
1
0x2fafbffd9c
1
从上面的代码我们可以看出来:
虽然变量 a
不是指针,但在很多情况下,数组名 a
是可以被当作指向其第一个元素的指针使用。
数组 a
每项存储的是 int
类型的数据,而变量 p
是 int *
类型的(int *
表示一个指向 int
类型变量的指针)。
二维数组
接下来让我们尝试编译以下代码
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int *p = a;
// 输出指针 p 所指向的地址
cout << p << endl;
return 0;
}
得到报错
文件路径\文件名.cpp: In function 'int main()':
文件路径\文件名.cpp:8:14: error: cannot convert 'int (*)[4]' to 'int*' in initialization
8 | int *p = a;
| ^
| |
| int (*)[4]
正确可编译的版本为
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[N] = a;
// 输出指针 p 所指向的地址
cout << p << endl;
return 0;
}
显然数组 a
每项存储的是 包含 4 个 int 元素的数组
类型的数据,而变量 p
是 int (*)[4]
类型的(int (*)[4]
表示一个指向 包含 4 个 int 元素的数组
的指针)。
a
赋值给 int *
会因为类型问题无法通过编译。
有同学就想到,既然 a
能赋值到 int (*)[4]
类型的变量了 ,那 *a
岂不是就能赋值给 int *
的变量上。
我们尝试编译以下代码
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[N] = a;
int *p2 = *a;
int v = **a;
// 输出指针 p 所指向的地址
cout << p << endl;
// 输出指针 p 所指向的值
cout << *p << endl;
// 输出指针 p2 所指向的地址
cout << p2 << endl;
// 输出指针 p2 所指向的值
cout << *p2 << endl;
// 输出变量 v
cout << v << endl;
return 0;
}
运行结果(输出的地址随实际运行情况变化)
0xd5e61ffc20
0xd5e61ffc20
0xd5e61ffc20
1
1
可以看见,的确如同这个同学猜测的一样,代码顺利编译运行了。
那二维数组下的自增操作是怎样的情况呢,其实我们从在 第一章 得出的结论也能推断出实际结果。实践出真知,我们来验证下这个结论是否正确。
#include <bits/stdc++.h>
using namespace std;
int main() {
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[4] = a;
// 输出指针 p 所指向的地址
cout << p << endl;
// 输出 p 所指向的值
cout << *p << endl;
// 对 p 进行自增操作
p++;
// 输出 p 所指向的地址
cout << p << endl;
// 输出 p 所指向的值
cout << *p << endl;
}
运行结果(输出的地址随实际运行情况变化)
0xb9ff5ff6b0
0xb9ff5ff6b0
0xb9ff5ff6c0
0xb9ff5ff6c0
可见,指向的地址增大了 b9ff5ff6c0 - b9ff5ff6b0
等于十进制下 16。
符合我们在 第一章 得出的结论:指针类型自增操作就是,指向的内存地址增加一个数 x。这个 x 就是指针所指向数据类型的大小。
(int (*)[4]
表示一个指向 包含 4 个 int 元素的数组
的指针)
此处变量 p
是 int (*)[4]
类型的,指向的数据类型 x 为 包含 4 个 int 元素的数组
,而这个数组大小为 4×int 数据类型大小
,即为 4×4Byte 为 16Byte。
实际运行结果也是增加了 16。
分析:为何 a[i][j] 等价于 *(*(a+i)+j)
在这一章,笔者将通过代码逐步拆解来解释 *(*(a+i)+j)
。
以下代码为较简洁版本,仅仅是拆步执行,没有详细检验和解释。
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int i = 2, j = 3;
// 输出 a[i][j] 的值
cout << a[i][j] << endl; // 应为 12
// 输出 *(*(a+i)+j) 的值
cout << *(*(a + i) + j) << endl; // 应为 12
// 拆解 *(*(a+i)+j) 操作
// 赋值给 p,以能进行运算操作
int(*p)[N] = a;
// 第一步
p += i; // 对应最内层的 a+i
// 第二步
int *q = *p; // 对应 *(a+i) 的 *( ) 操作
// 第三步
q += j; // 对应 *(a+i)+j 的 +j 操作
// 第四步
int res = *q; // 对应 *(*(a+i)+j) 最外层 *( ) 操作
// 输出操作结果
cout << res << endl; // 应为 12
return 0;
}
接下来,笔者将在上面代码的基础上,再补充更详细的解释。请注意代码注释。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int i = 2, j = 3;
cout << boolalpha; // 设置直接输出 true 或 false 而不是 1 或 0
// 输出 a[i][j] 的值
cout << a[i][j] << endl; // 应为 12
// 输出 *(*(a+i)+j) 的值
cout << *(*(a + i) + j) << endl; // 应为 12
// 拆解 *(*(a+i)+j) 操作
// 赋值给 p,以能进行运算操作
int(*p)[N] = a;
// 此处 p 指向 a[0]
int(*temp)[N] = p; // 临时变量,用于存储 p 指向的地址
// 第一步
p += i; // 对应最内层的 a+i
// 此处 p 操作后指向 a[i]
// p 此处操作后指向的地址增加了 i×(N×int数据长度)
// 检查 p 指向地址增加的值是否为 i×(N×int数据长度)
cout << (ull(p) - ull(temp) == i * (N * sizeof(int))) << endl; // 应为 true
// 第二步
int *q = *p; // 对应 *(a+i) 的 *( ) 操作
// 此处 q 指向 a[i][0]
// 检查 q 指向的值 是否为 a[i][0]
cout << (*q == a[i][0]) << endl; // 应为 true
int *temp2 = q; // 临时变量,用于存储 q 指向的地址
// 第三步
q += j; // 对应 *(a+i)+j 的 +j 操作
// 此处 q 操作后指向 a[i][j]
// q 此处操作后指向的地址增加了 j×int数据长度
// 检查 q 指向地址增加的值是否为 j×int数据长度
cout << (ull(q) - ull(temp2) == j * sizeof(int)) << endl; // 应为 true
// 检查 q 指向的值 是否为 a[i][j]
cout << (*q == a[i][j]) << endl; // 应为 true
// 第四步
int res = *q; // 对应 *(*(a+i)+j) 最外层 *( ) 操作
// 输出操作结果
cout << res << endl; // 应为 12
return 0;
}
运行结果
12
12
true
true
true
true
12
参考文献
-
- 根据定义,下标运算符
E1[E2]
与*((E1)+(E2))
完全相同。如果 指针表达式 是一个数组表达式,它将进行 左值到右值转换 并成为指向数组第一个元素的指针。 - 由于 指针和整数之间的加法 的定义,结果是数组中索引等于 整数表达式 结果的元素(或者,如果 指针表达式 指向某个数组的第 i 个元素,则结果的索引为 i 加上 整数表达式 的结果)。
- 根据定义,下标运算符
后记
笔者能力有限,文章可能有纰漏错误,欢迎在评论区补充、纠错、讨论。
如果您觉得文章写得好,也欢迎留言鼓励笔者 👍