本站在允许 JavaScript 运行的环境下浏览效果更佳


C/C++:为什么 a[i][j] 等价于 *(*(a+i)+j)

90

前言

如果你较熟悉前置相关知识可直接跳转 第三章

初探:指针的 +、-、++、-- 操作

自增操作对于指针所指向地址的影响

编译以下代码

#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 类型的数据,而变量 pint * 类型的(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 元素的数组 类型的数据,而变量 pint (*)[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 元素的数组 的指针)
此处变量 pint (*)[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

参考文献

  1. cppreference.com 指针声明

  2. cppreference.com 数组到指针转换

  3. cppreference.com 算术运算符-加法性运算符-指针算术

  4. cppreference.com 成员访问运算符

    • 根据定义,下标运算符 E1[E2]*((E1)+(E2)) 完全相同。如果 指针表达式 是一个数组表达式,它将进行 左值到右值转换 并成为指向数组第一个元素的指针。
    • 由于 指针和整数之间的加法 的定义,结果是数组中索引等于 整数表达式 结果的元素(或者,如果 指针表达式 指向某个数组的第 i 个元素,则结果的索引为 i 加上 整数表达式 的结果)。

后记

笔者能力有限,文章可能有纰漏错误,欢迎在评论区补充、纠错、讨论。
如果您觉得文章写得好,也欢迎留言鼓励笔者 👍