当前位置: 首页 > news >正文

超硬核c语言编程随想笔记:深挖cint**二级指针-核心多级指针的内存陷阱,彻底终结多级指针恐惧症

3刷模拟算法的时候,发现一个returnColumnSizes的内存问题,

二级指针这里的问题为什么会这样?????????

--------------------------------------------------------------------------------------------------------------------------------------------------------------------更新与25.10.13

导语: 兄弟们,如果你在刷力扣或者牛客时遇到需要返回动态二维数组的 C 语言题,你一定被这个**“邪恶”**的函数签名吓到过:int** function(..., int* returnSize, int** returnColumnSizes)

别装了,我懂你!你看着 int** returnColumnSizes 里的两个星号,心里一定在骂娘:为什么这么复杂?为什么非要用指针的指针?为什么我写的 $\texttt{malloc}$ 就是错的?

没关系,今天咱们不聊算法,只聊 C 语言最底层的内存和指针哲学。这个矩阵旋转问题,就是 C 语言给你下的“战书”!我们不仅要实现功能,更要像一个写操作系统的工程师那样,彻底搞懂每一个星号背后的内存机制。

近重构项目里的旋转矩阵功能,本来以为逻辑挺简单 —— 不就是根据旋转次数映射坐标嘛?结果卡在了 returnColumnSizes 这个参数上,那段 returnColumnSizes[i] = malloc(...) 的代码调试时崩得我怀疑人生。后来翻源码、画内存图、问了公司的老大哥才彻底搞懂,今天把这个踩坑过程捋清楚,给同样卡壳的朋友避避坑。

本文将为你彻底解剖 C 语言中最复杂的回参机制,帮你建立起“多级指针”的底层信仰! 适合所有想精通 C 语言、突破嵌入式面试瓶颈的程序员!

一、先交代背景:我写的 “看似没问题” 的错误代码

先上我最初写的核心代码(就是崩掉的版本),大家可以先找找问题在哪:

c

运行

// 旋转逻辑都对,就栽在returnColumnSizes这里
*returnSize = newRow;
// 我当时觉得这步没问题啊:给每行分配列数
for (int i = 0; i < newRow; i++) {returnColumnSizes[i] = (int *)malloc(sizeof(int));  // 崩溃触发点*(returnColumnSizes[i]) = newCol;
}

这段代码一跑就触发内存访问错误,调试器指向 returnColumnSizes[i] 这句。当时我百思不解:returnColumnSizes 是 int** 类型,不就是指向数组的指针吗?直接给数组元素赋值怎么就错了?

后来才发现,我根本没搞懂 int** returnColumnSizes 的真实结构 —— 它不是 “指向普通 int 数组的指针”,而是 “指向指针数组的指针”,这俩差了一个维度!

二、先补基础:别搞混 “指针数组” 和 “指向指针的指针”

要搞懂 returnColumnSizes,必须先分清两个容易混淆的概念,这是我踩坑的根源:

1. 指针数组:本质是 “数组”,装的是指针

比如 int* arr[3],这是一个能装 3 个 int* 指针的数组。可以理解成一个有 3 个格子的架子,每个格子里放的不是具体数值,而是一张写着 “数值地址” 的纸条。

2. 指向指针的指针(int**):本质是 “指针”,指向指针数组的首地址

比如 int** p = arr,这里的 p 就是指向刚才那个指针数组的指针。因为数组名 arr 会退化为数组首元素的地址,而首元素是 int* 类型,所以指向它的指针自然是 int** 类型 —— 相当于手里拿着一张写着 “架子地址” 的总纸条。

而题目里的 returnColumnSizes 就是这个 int** 类型的 “总纸条”,它的作用是让调用者通过这张总纸条,找到放着 “列数地址” 的架子(指针数组),再从架子上拿到具体的列数。

三、我的错误根源:没给 “架子” 分配空间就往格子里放东西

回到我最初的错误代码,问题就出在 “没有先创建架子,就直接往架子的格子里放纸条”

1. 错误逻辑拆解

returnColumnSizes 是 int** 类型的 “总纸条”,但我没给它赋值任何 “架子地址”—— 也就是说,这张总纸条是空白的,根本不知道架子在哪。这时候直接写 returnColumnSizes[i],相当于 “对着空气说‘把纸条放进架子第 i 格’”,内存能不崩溃吗?

2. 正确的逻辑应该是 “先搭架子,再放纸条”

要让 returnColumnSizes 能正常工作,必须分两步走,对应内存里的两层结构:

  1. 给 “架子”(指针数组)分配空间:让 returnColumnSizes 这张总纸条,指向一个真实存在的架子;
  2. 给架子的每个格子分配 “数值地址纸条”:在架子的每个格子里,放一张写着具体列数地址的纸条。

对应到代码就是这样(这是正确写法):

c

运行

*returnSize = newRow;
// 第一步:创建架子(指针数组),让returnColumnSizes指向这个架子
*returnColumnSizes = (int**)malloc(newRow * sizeof(int*));
// 第二步:给架子的每个格子放“数值地址纸条”
for (int i = 0; i < newRow; i++) {// 给具体列数分配内存(相当于写一张数值纸条)(*returnColumnSizes)[i] = (int*)malloc(sizeof(int));// 把列数写进数值内存里*((*returnColumnSizes)[i]) = newCol;
}

四、逐行拆解正确代码:从内存视角看究竟发生了什么

为了让大家看得更清楚,我用 newRow=3(3 行)、newCol=4(每行 4 列)的场景,画了个内存结构图(地址是虚构的,方便理解):

plaintext

// 总纸条:returnColumnSizes(int**类型,地址0x0010)
+------------------+
| 存储:0x1000     |  // 指向架子(指针数组)的地址
+------------------+↓
// 架子:*returnColumnSizes(指针数组,地址0x1000开始)
+------------------+  +------------------+  +------------------+
| 0x2000           |  | 0x3000           |  | 0x4000           |  // 3个格子,每个放int*指针
+------------------+  +------------------+  +------------------+↓                ↓                ↓
// 数值内存:每个int*指向的具体列数
+------------------+  +------------------+  +------------------+
| 4                |  | 4                |  | 4                |  // 真正的列数
+------------------+  +------------------+  +------------------+

现在逐行拆解代码对应的内存操作:

1. 第一步:创建架子(指针数组)

c

运行

*returnColumnSizes = (int**)malloc(newRow * sizeof(int*));
  • 作用:在堆上分配一块能装 newRow 个 int* 指针的内存(也就是创建架子);
  • 赋值:把这个架子的首地址(比如 0x1000)写进 returnColumnSizes 指向的内存里 —— 相当于给总纸条写上架子的地址;
  • 为什么用 *returnColumnSizes?因为 returnColumnSizes 是 int** 类型,解引用(*)才能拿到它指向的 “架子地址存储位”,把架子地址写进去。

2. 第二步:给架子格子放 “数值地址纸条”

c

运行

(*returnColumnSizes)[i] = (int*)malloc(sizeof(int));
  • 作用:给第 i 个格子分配一块存 int 类型的内存(比如 0x2000),用来放具体的列数;
  • 赋值:把这个数值内存的地址(0x2000)写进架子的第 i 个格子里 —— 相当于给架子第 i 格放一张写着数值地址的纸条;
  • 为什么用 (*returnColumnSizes)[i]?因为 *returnColumnSizes 是架子本身(指针数组),用 [i] 就能访问到第 i 个格子,给格子赋值。

3. 第三步:给数值内存写具体列数

c

运行

*((*returnColumnSizes)[i]) = newCol;
  • 作用:把 newCol(比如 4)写进数值内存里;
  • 为什么要加两个 *?第一个 * 解引用拿到架子第 i 格的数值地址(比如 0x2000),第二个 * 解引用这个地址,才能拿到存数值的内存,然后赋值。

五、灵魂拷问:为什么要设计得这么复杂?直接返回 int * 不行吗?

这是我当时最困惑的问题,后来老大哥一句话点醒我:“复杂是为了适配更灵活的场景”

如果只是返回固定行数、固定列数的矩阵,确实可以用 int* 直接返回一个连续的列数数组(比如 int* cols = malloc(newRow*sizeof(int)))。但题目设计 int** 有两个核心原因:

1. 适配 “不规则矩阵” 场景

实际开发中,矩阵可能是不规则的(比如第 1 行 3 列,第 2 行 4 列)。如果用 int* 返回连续数组,没法给不同行分配不同的列数 —— 因为连续数组的每个元素是直接存数值,改一个会影响其他。而用 int** 设计的两层结构,每个列数都有独立的内存,想改哪行改哪行,灵活得多。

2. 符合 C 语言 “通过指针返回动态数据” 的规则

C 语言里,函数要返回动态分配的数组,不能直接返回数组名(数组名退化为指针,函数结束后会失效),必须通过指针参数传递。而返回 “动态数量的独立指针”(也就是架子上的格子),只能用 int** 类型 —— 因为它是 “指向指针数组的指针”,能承载架子的地址。

六、完整修正后的代码(带关键注释)

最后把完整的旋转矩阵代码放出来,重点标注了 returnColumnSizes 相关的修正部分,方便大家对照:

c

运行

/*** 旋转矩阵n次(每次90度顺时针)* @param mat 输入矩阵* @param matRowLen 输入矩阵行数* @param matColLen 输入矩阵列数指针* @param n 旋转次数* @param returnSize 输出矩阵行数指针* @param returnColumnSizes 输出矩阵每行列数指针(核心修正部分)* @return 旋转后的矩阵*/
int** rotateMatrix(int** mat, int matRowLen, int* matColLen, int n, int* returnSize, int** returnColumnSizes) {// 边界检查if (mat == NULL || matRowLen == 0 || matColLen == NULL || *matColLen == 0) {*returnSize = 0;*returnColumnSizes = NULL;return NULL;}// 简化旋转次数(4次旋转回到原状态)n = (n % 4 + 4) % 4;int rowLen = matRowLen;int colLen = *matColLen;int newRow, newCol;// 确定新矩阵的行数和列数if (n == 0 || n == 2) {newRow = rowLen;newCol = colLen;} else {newRow = colLen;newCol = rowLen;}// 分配结果矩阵内存int** res = (int**)malloc(newRow * sizeof(int*));for (int i = 0; i < newRow; i++) {res[i] = (int*)malloc(newCol * sizeof(int));}// 旋转逻辑(这部分之前是对的,保留)if (n == 0) {for (int i = 0; i < rowLen; i++) {for (int j = 0; j < colLen; j++) {res[i][j] = mat[i][j];}}} else if (n == 1) {for (int i = 0; i < rowLen; i++) {for (int j = 0; j < colLen; j++) {res[j][rowLen - 1 - i] = mat[i][j];}}} else if (n == 2) {for (int i = 0; i < rowLen; i++) {for (int j = 0; j < colLen; j++) {res[rowLen - 1 - i][colLen - 1 - j] = mat[i][j];}}} else {  // n == 3for (int i = 0; i < rowLen; i++) {for (int j = 0; j < colLen; j++) {res[colLen - 1 - j][i] = mat[i][j];}}}// --------------- 核心修正部分 ---------------*returnSize = newRow;// 1. 先创建架子(指针数组),分配newRow个int*的空间*returnColumnSizes = (int**)malloc(newRow * sizeof(int*));// 2. 给每个架子格子分配数值内存,并存列数for (int i = 0; i < newRow; i++) {(*returnColumnSizes)[i] = (int*)malloc(sizeof(int));*((*returnColumnSizes)[i]) = newCol;}// -------------------------------------------return res;
}

七、踩坑总结:这 3 点让我彻底记住了 int** 的用法

  1. 先搭架子再放东西:遇到 int** 类型的输出参数,先给它指向的指针数组分配空间(*p = malloc(...)),再操作数组元素;
  2. 两层指针对应两层内存int** 永远对应 “指针数组 + 数值内存” 的两层结构,解引用一次拿到指针数组,解引用两次拿到具体数值;
  3. 设计不是为了复杂:看似嵌套的 int**,本质是为了适配动态、灵活的场景,理解背后的需求比死记语法更重要。

这次踩坑让我明白,C 语言的指针问题,光看语法没用,必须画内存图、拆执行步骤。希望我的经历能帮大家少走点弯路,要是有其他指针问题,评论区一起交流!

http://www.dtcms.com/a/541829.html

相关文章:

  • 邯郸建立网站费用乐清网站开发
  • JAVA面试入职笔记 | linux常用基本指令快速查看
  • 网站设计公司苏州erp系统的功能包括哪些
  • 【2025-系统规划与管理师】第16章:资源与工具管理
  • 天津网站建设案例wordpress怎么导入自己的php
  • 东莞网站优化方法有哪些哈尔滨大型网站设计公司
  • 模板的网站都有哪些公司网站建设公
  • 避免踩坑!三星打印机SCX3401驱动安装详细步骤解析
  • 有哪些网站开发技术甘肃省建设工程造价信息网站
  • 建网站 选安全网页设计代码单元格内容怎么居中
  • 双牌网站建设app网站样式
  • 模型参数大小计算
  • AI智能体连载(9)绘制智能体的工作流
  • 0.4、向量、向量维度、向量比较、向量搜索和相关算法
  • 无SDK API,可自定义API C++开发的脚本语言源码编译过程
  • 广州网站搭建哪家好公司网站报价
  • 网站 单页做网站需要用到什么
  • 硬件与软件交互全解析:协议、控制与数据采集实践
  • 国内外网站建设2017php网站怎么做的
  • 离石古楼角网站建设合肥有哪些做网站的公司
  • 二叉树的锯齿形层序遍历
  • Java8:新日期时间
  • Java_String对象特性
  • 网站做app的软件有哪些360安全浏览器
  • 网站建设 互成网络amp 网站开发
  • 网站app免费生成软件下载免费 片
  • USB基础知识--Endpoint与pipe
  • SpringBoot拦截器实战与原理剖析
  • 把握智能语音风口:云蝠智能【声・纪元】VoiceAgent 实时语音智能论坛邀您同行
  • 一文吃透二叉树、完全平衡树、红黑树原理及C语言实现