惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
L
LINUX DO - 最新话题
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
Know Your Adversary
Know Your Adversary
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
Lohrmann on Cybersecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy & Cybersecurity Law Blog
K
Kaspersky official blog
The Last Watchdog
The Last Watchdog
Webroot Blog
Webroot Blog
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
WordPress大学
WordPress大学
L
LINUX DO - 热门话题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
V
Visual Studio Blog
O
OpenAI News
AI
AI
Hacker News: Ask HN
Hacker News: Ask HN
V2EX - 技术
V2EX - 技术
GbyAI
GbyAI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
Spread Privacy
Spread Privacy
Y
Y Combinator Blog
I
InfoQ
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
F
Fortinet All Blogs
C
CERT Recently Published Vulnerability Notes
T
The Blog of Author Tim Ferriss
C
Check Point Blog
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
N
News and Events Feed by Topic
Project Zero
Project Zero
小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
B
Blog
G
Google Developers Blog

皓子的小站

网站字体应用之坑——font-family 篇 糟糕的 meta name="theme-color" How to Automatically Track Newly Supported ESLint Rules in Oxlint How to Gradually Migrate from ESLint to Oxlint (Without Breaking Everything) 静态资源预压缩:零运行时开销,极致节省带宽 CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash Halo CMS Comment Backend 自定义网页鼠标指针——一段曲折的旅程 CVE-2025-70886 漏洞复现/PoC | 一个脚本让 Halo CMS 评论后台瘫痪 2025 年终总结 & 博客两周年 老用户专享!已有 Halo 专业版授权可免费升级商业版 自动追踪 Oxlint 对 ESLint 规则的新增支持 Halo 贡献者证书与实体周边盲盒开箱 博客评论系统指南 CDN 回源跟随配置导致登录异常问题排查 SCDN 免费赞助计划:助力高质量博客&博客圈 锐捷校园网:网络共享与带宽叠加方案(哈理工案例) ESLint 到 Oxlint 渐进式迁移快速上手指南 Fixing Vite Breaking Inline JS & CSS in Thymeleaf Templates 解决 Vite 破坏 Thymeleaf 模板内联 JS & CSS 的方法 我的博客,为什么是月更? 博客俱乐部一周年纪念品开箱 网站字体加载之坑——format 篇 经历 1000000000 次 DDoS 请求攻击后,我总结了三条经验 题解分享:[AtCoder Beginner Contest 414 E] Count A%B=C 题解分享:[蓝桥杯 2025 国 Python A] 杨辉三角 P12876 FiF口语训练破解刷分教程(适用于 Windows) 不要与蠢人辩论 小心网络地雷·续篇 当心网络组织“开往”·续篇 当心网络组织“开往” 小心网络地雷 在 Halo CMS 中通过模板实现随机推荐多篇文章 回应某博客上有关“开往”组织在内部会议上对我的污蔑(上) 再遭 DDoS 攻击:CDN 流量限额拦不住请求计费飙升 2025年团体程序设计天梯赛 - Python 个人赛时暴力题解 网站流量分析工具 Umami 中国大陆合规版发布 把握当下,享受生活 Folo 认证功能反成漏洞?警惕 RSS 订阅源所有权被恶意盗取风险 在 Halo CMS 中通过模板实现随机文章跳转功能 从沉默展柜到时代回响:731部队罪证陈列馆参访手记 脚本分享:CDN 流量触顶后自动切换至 Cloudflare 官方 BUG 快薅!Halo 永久/买断用户可额外获得一份买断许可 老用户专享!已有 1Panel 永久授权/买断许可用户可再领免费买断许可证! 限时特惠!1¥ 立即解锁 1Panel 专业版全功能 61 天 混合云架构下的动态 IPv6 智能加速实践:从家庭网络到全球分发的低成本部署指南 如何用 AI 全方面辅助博客创作 - 从博文之内到博文之外 Random Number Generation and Formatting in Thymeleaf Thymeleaf 随机数生成与格式化详解 列举我维护的项目和发布的软件包 快速部署教程 - 通过 Webhook 触发邮件通知的开源自托管服务 Going Against the Grain - Four Websites That Make Your URL Longer 反其道而行 - 四个让你网址变得更长的网站 2024 年终总结 & 博客一周年 基于 OneBot API 的简易异步 QQ 群批量发消息脚本 如何快速完成中文排版优化 如何给文章分类和打标签 群规之敏感违禁词对抗 一图展示​ ​RSS3 世界 - The Open(RSS3) Universe 开源项目发到 bilibili 后可能获得的 PR Fast Methods to Convert Uppercase Letters to Lowercase in C++ C++ 中快速将字符串大写字母转为小写的方法 Fixing GitHub Actions push to ghcr '403 Forbidden' error Fixing docker pull image "ghcr.io..." Unauthorized error 本站“算法题练习/题解分享”相关文章汇总 不怕流氓多,就怕流氓有文化——站长应该防范的几个问题及对应解决方法分享 题解分享:HRBUST 1132 水数 新手程序员必备的三项技能 如何快速排查插件(模组)的兼容性问题(基于二分法和分治思想) 通过 Umami 最新功能观察定时发布文章的最佳时段 踩坑分享:如何一个人搬运 126.7 斤的书坐动车 两篇文章分享:别像弱智一样提问 & 提问的智慧 论下落式音游/定轨音游中的优质粪铺(文末附铺面推荐) 为什么选择无纸化阅读?为什么买墨水屏电子书?为什么 Kindle“失败”了? 我维护的 Halo 主题,现已发布到 Halo 应用商店! OSU!Standard(osu!std)/OSU!Taiko 转 OSU!Mania 铺面转换器发布 我常用的 OSU!Mania 皮肤分享 电荷守恒辨析:为什么一个离子带多个电荷要乘上所带电荷数而不是除以所带电荷数 注意!align-items: flex-start 会导致 overflow-wrap: break-word 失效 惊人 98 元限定T恤!1Panel, Halo 开发商开启推广积分奖励活动 Minecraft Spigot Bukkit 服务器插件开发 教程收集 中文博客倡议 Windows 10 遇到“只能重装系统”但不想重装的解决方案 Windows 10 文件系统错误 -2144926975 -2147219196 修复 Windows 10 升级 Windows 11 简明教程 CSS 预处理器如何正确的复用样式(stylus 中使用 @extend 继承的坑) 请一定要回复博客下的匿名评论!对于博主和评论者有关匿名评论的倡议 一图解释单一质点运动波动图像和振动图像的关系 解决 Docker pull image "ghcr.io..." Unauthorized error 解决 GitHub Actions push to ghcr '403 Forbidden' error 解决 Umami API 更新延迟问题(如何修改开源项目代码并构建 Docker 镜像-以 Umami 为例) 在国家公祭日自动将网站变灰 中文博客圈列表 解决 Halo CMS 的 Umami Plugin CORS&CSP 跨域报错问题 Windows 创建存储池报错 无法创建池 不支持该请求 (0x00000032) Windows 显示器突然黑白 桌面突然黑白 鼠标和截图还是彩色的 通过指令过滤出正确的公网 IPv6 地址(记一次折腾 IPv6 DDNS) 我的世界免费科技地址 1.7-1.21 网站 Favicon 下载工具发布 键盘 Bios 内可用 系统内无法输入 设备管理器无黄色感叹号 设备管理器 网卡一堆黄色感叹号 例如 WAN, Miniport
C/C++:为什么 a[i][j] 等价于 *(*(a+i)+j)
HowieHz · 2024-11-25 · via 皓子的小站

前言

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

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

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

编译以下代码

#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 加上 整数表达式 的结果)。

后记

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