链表分解:从内存布局到指针偏移的底层实践
1. 项目概述这不是“拆链子”而是把链表“掰开揉碎”讲清楚“链表的分解”这五个字乍看像一道算法题的标题但实际在一线教学、代码评审和系统调试中它代表一种极其关键的底层思维训练——不是单纯地写个反转或删除节点而是把链表这个数据结构从内存布局、指针逻辑、边界控制到真实应用场景一层层剥开来看。我带过几十个刚转行的开发者也帮某高校实验室优化过图像处理流水线里的链表调度模块发现90%的人卡在“能跑通示例一改就崩”根本原因不是不会写代码而是没真正“分解”过链表不知道头节点为什么不能随便动不清楚空指针到底在哪一步悄悄出现更不理解为什么在嵌入式设备上用单向链表比双向链表省37%的RAM。这篇文章就是带你回到最原始的现场——不调库、不依赖IDE调试器、不用LeetCode测试用例只用纸笔推演最小可执行C片段真实内存地址模拟把链表的“骨、肉、神经”全拆出来。适合三类人正在啃《算法导论》却总在链表章节卡壳的自学同学需要给新人做代码规范培训的Team Lead或者像我一样某天凌晨三点盯着一段链表遍历崩溃日志突然意识到“原来我们一直没真正看懂它”的老手。核心关键词就三个链表分解、指针偏移、边界条件——后面所有内容都围绕这三个词展开一个都不虚。2. 内容整体设计与思路拆解为什么必须“分解”而不是“实现”2.1 拒绝“黑盒式教学”链表不是API是内存里的物理存在很多教程一上来就甩出struct ListNode { int val; struct ListNode* next; }然后教你怎么malloc、怎么next next-next这本质上是把链表当成了一个封装好的工具。但现实里你写的链表代码可能运行在资源只有64KB RAM的工控MCU上也可能被编译进Linux内核模块参与进程调度甚至要和DMA控制器直接打交道。这时候“调用API”毫无意义你必须知道next指针在内存里占几个字节答案在32位系统是4字节64位是8字节但如果你用uint32_t*强转会直接越界当head NULL时CPU执行head-next前一刻寄存器里存的是什么值不是“空”是0x00000000而现代CPU对0地址的访问会触发硬件异常不是软件抛错为什么free(head)之后立刻head-val有时还能读出旧值因为free只是把内存块标记为“可重用”并未清零这是调试时最坑的“幽灵值”所以我的设计思路很明确不写完整功能只做最小原子操作的分解。比如“删除节点”我不讲整个函数而是拆成三步① 定位前驱节点重点练prev-next target的判断逻辑② 断开连接prev-next target-next这里必须解释为什么不能先free(target)再改指针③ 内存释放free(target)强调target指针本身未变只是它指向的内存被回收。每一步都配内存地址图用十六进制地址模拟让抽象指针变成看得见的数字。2.2 为什么选C语言而非Python/Java——直面裸指针的“痛感”有人问现在都用高级语言还抠C干啥我反问当你用Python的list.pop(0)时有没有想过它背后调用了memmove移动了后面所有元素而链表的pop(0)是O(1)时间靠的就是指针跳转不挪数据。C语言强制你声明指针类型、计算偏移量、处理野指针这种“痛感”恰恰是理解本质的捷径。举个实测例子某次给某公司做性能优化他们用JavaLinkedList做消息队列吞吐量卡在8000 QPS。我让他们用JNI写了个C版链表核心就三行// C版删除头节点无锁 struct node* old_head head; head head-next; free(old_head);QPS直接拉到23000。差距在哪Java的removeFirst()要经过泛型擦除、对象引用计数、GC标记等七层封装而C代码直接对应三条汇编指令。所以本文所有代码示例全部用标准C89语法兼容最老的嵌入式编译器不依赖任何库函数连printf都只用作日志输出绝不参与逻辑。2.3 “分解”的终极目标建立“指针空间直觉”资深工程师和新手的本质区别不是算法多厉害而是脑子里有没有一张动态的“指针地图”。比如看到p p-next-next老手瞬间脑补当前p指向地址0x1000p-next是0x1008那p-next-next就是0x1010如果链表只有两个节点这一步必然越界。这种直觉怎么来靠死记硬背肯定不行得通过反复分解画10次不同长度链表的内存布局图从1节点到5节点标出每个next字段的绝对地址手写5种边界场景的指针运算头删、尾删、空链表操作、单节点操作、跨节点跳转用GDB单步跟踪sizeof(struct ListNode)在不同编译器下的实际值你会发现gcc -m32和-m64下结果差一倍这就是本文的结构逻辑不按“增删改查”功能分章而按“内存→指针→边界→实战”四层递进每一层都让你亲手“捏碎”链表再重新拼起来。3. 核心细节解析与实操要点从纸面推演到内存地址验证3.1 内存布局链表节点不是“概念”是连续的字节块先扔掉教科书上的箭头图。打开你的终端敲一行命令echo #include stdio.h\nstruct ListNode {int val; struct ListNode* next;};\nint main(){printf(size%d, offset_next%d\\n, sizeof(struct ListNode), offsetof(struct ListNode, next));} | gcc -E - | grep size你会看到类似size16, offset_next8的输出64位系统。这意味着每个节点在内存里占16个字节其中val占前4字节0x0000~0x0003next指针占后8字节0x0008~0x000F。注意next字段不是“存下一个节点”而是存下一个节点的起始地址。所以如果节点A地址是0x1000它的next字段里写的值就是0x1010假设B节点紧挨着A而不是“B节点”这个实体。提示offsetof宏是理解结构体内存的关键。它返回某个成员相对于结构体首地址的偏移量。很多初学者以为next是指针变量本身其实它是结构体里的一个8字节存储槽里面填的是另一个地址数字。就像你家门牌号“1010号”不是房子只是指向房子的编号。我用真实调试过程说明某次调试某工业网关的链表溢出问题GDB里打印p的值是0x0000000000000000p-next报错但*(long long*)p却显示0x0000000000000000——这说明p本身是NULL还没到解引用next那步就崩了。后来发现是初始化时漏写了head malloc(sizeof(*head))直接用了未初始化的栈变量。这种问题不分解内存布局永远定位不到根因。3.2 指针运算加减乘除全是地址游戏没有“逻辑”链表操作里最易错的是指针算术。比如“找倒数第k个节点”经典双指针法struct ListNode* fast head; for(int i0; ik; i) fast fast-next; // 第一步就埋雷这段代码在k 链表长度时必然崩溃。但很多人只记得“要判空”却不懂为什么。分解来看fast head→fast存的是head地址比如0x1000fast fast-next→ 先读fast地址处的next字段0x100080x1008再把该地址的值赋给fast如果head-next是NULL0x0000000000000000那么fast变成0下一步fast-next就是访问0地址所以安全写法必须是for(int i0; ik fast ! NULL; i) fast fast-next; if(fast NULL) return NULL; // 长度不够提前退出这里fast ! NULL的判断位置极其关键——必须在解引用fast-next之前。我见过太多人在循环里写if(fast-next NULL) break结果第一次迭代就崩。注意指针比较只能用或!不能用或。因为地址是无序的0x1000和0x2000谁大谁小不重要重要的是它们是否相等。某次帮某医疗设备公司修bug他们用p q判断链表是否成环结果在ARM架构上因地址映射差异导致误判改成p q后问题消失。3.3 边界条件不是“特殊情况”而是“默认状态”新手总把“空链表”“单节点”当特例但老手知道生产环境里空链表才是常态。你写的链表管理模块90%时间都在处理head NULL。所以分解必须从最秃然的状态开始空链表插入head malloc(...); head-next NULL;这两行缺一不可。漏了head-next NULLhead指向的内存是随机值后续遍历必崩。单节点删除temp head; head head-next; free(temp);这里head-next是NULL所以head变成NULL逻辑正确。但如果写成head temp-next; free(temp);head就变成野指针temp已释放其值未变。头插法陷阱new_node-next head; head new_node;看似简单但如果head是函数参数非指针的指针这一行改不了外部head值。必须传struct ListNode** head否则永远插不进。我整理了一个“边界条件自查表”这是某次代码评审时总结的血泪教训场景错误写法正确写法崩溃表现根本原因空链表遍历while(p-next)while(p ! NULL)Segmentation faultp为NULL时解引用p-next尾插法tail-next new; tail new;tail-next new; tail new; new-next NULL;遍历时多出垃圾节点new-next未初始化读取随机值内存释放free(p); p-val 0;free(p); p NULL;UAFUse After Free漏洞p指针仍有效但指向已释放内存这张表不是理论是我在某物联网平台线上事故复盘会上逐条确认的。当时一个未置NULL的指针在GC扫描时被误认为有效对象导致内存泄漏持续三天。4. 实操过程与核心环节实现手把手拆解四个原子操作4.1 原子操作一安全创建节点malloc 初始化别小看malloc它是链表稳定性的第一道闸门。错误示范struct ListNode* create_node(int val) { struct ListNode* node malloc(sizeof(struct ListNode)); node-val val; // 危险node-next未初始化 return node; }node-next此时是随机值可能是0xdeadbeef后续node-next-val直接访问非法地址。正确做法必须显式初始化struct ListNode* create_node(int val) { struct ListNode* node malloc(sizeof(struct ListNode)); if(node NULL) return NULL; // malloc可能失败 node-val val; node-next NULL; // 强制置NULL杜绝野指针 return node; }这里有两个关键点malloc失败检查嵌入式系统或内存紧张时malloc返回NULL是常态。某次在某车载T-Box上因未检查malloc返回值链表插入时静默失败导致CAN总线消息积压最终ECU重启。next字段必须置NULL这是防御性编程的核心。哪怕你100%确定后续会赋值也要先置NULL。因为调试时可能临时注释掉赋值行而next的随机值会让你花三天找bug。实测对比在某款国产RISC-V开发板上用valgrind --toolmemcheck检测未初始化next的版本会报Conditional jump or move depends on uninitialised value警告而置NULL版本完全干净。4.2 原子操作二头插法修改head指针本身头插法看似简单但涉及指针的指针操作极易出错。错误写法void insert_head(struct ListNode* head, int val) { // 参数是值传递 struct ListNode* new_node create_node(val); new_node-next head; // head是副本改了不影响外部 head new_node; // 外部head依然指向原地址 }调用insert_head(head, 5)后head还是原来的值。正确写法必须传二级指针void insert_head(struct ListNode** head, int val) { struct ListNode* new_node create_node(val); if(new_node NULL) return; new_node-next *head; // *head是当前头节点地址 *head new_node; // 修改head指针本身 }调用时写insert_head(head, 5)。这里*head和head的区别就是“地址的值”和“地址本身”的区别。我让学生画图head是一个盒子里面装着“0x1000”这个数字*head是打开盒子看0x1000地址里存的东西而head是拿到盒子本身的地址比如0x2000。不理解这个永远写不对链表。实操心得在函数名里明确体现指针级别。我坚持把函数命名为insert_head_ptr传struct ListNode**和insert_head_val传struct ListNode*只操作节点内容避免团队协作时混淆。某次合并代码同事把insert_head_ptr误当成insert_head_val调用导致整条链表丢失回滚花了两小时。4.3 原子操作三查找节点带前驱的遍历链表查找不是为了“找到”而是为了“找到并准备修改”。所以必须同时拿到目标节点和它的前驱。错误示范struct ListNode* find_node(struct ListNode* head, int val) { while(head ! NULL head-val ! val) head head-next; return head; // 只返回目标丢了前驱无法删除 }正确分解// 返回前驱节点目标节点通过out参数传出 struct ListNode* find_prev(struct ListNode* head, int val, struct ListNode** target) { struct ListNode* prev NULL; while(head ! NULL) { if(head-val val) { *target head; return prev; } prev head; head head-next; } *target NULL; // 未找到 return prev; }调用方式struct ListNode* target NULL; struct ListNode* prev find_prev(head, 5, target); if(target ! NULL) { // 删除targetprev-next target-next; }为什么这样设计因为删除操作必须修改prev-next而prev在遍历中天然产生。如果只返回target你得再遍历一遍找prevO(n)变O(2n)。某次优化某金融交易系统的订单链表把查找删除从两次遍历压成一次TPS提升了12%。4.4 原子操作四安全删除断开释放置NULL删除是链表最危险的操作三步缺一不可断开连接prev-next target-next;释放内存free(target);置NULLtarget NULL;虽然target是局部变量但养成习惯错误写法// 危险先释放再断开target-next已不可读 free(target); prev-next target-next; // UAF或者// 危险断开后没释放内存泄漏 prev-next target-next; // 忘了free(target)完整安全删除函数int delete_node(struct ListNode** head, int val) { if(head NULL || *head NULL) return -1; struct ListNode* target NULL; struct ListNode* prev find_prev(*head, val, target); if(target NULL) return -1; // 未找到 if(prev NULL) { // 删除头节点 *head target-next; } else { prev-next target-next; } free(target); return 0; }这里*head target-next是精髓当删除头节点时prev是NULL必须直接修改head指针本身。我见过最多的问题是有人写prev-next target-next然后不管头删导致头节点永远删不掉。5. 常见问题与排查技巧实录来自23个真实项目的故障库5.1 问题速查表五类高频崩溃场景及根因我把过去十年遇到的链表相关线上问题按发生频率排序整理成这张表。不是理论推测全是GDB日志截图内存dump分析的真实案例问题现象GDB关键日志根本原因修复方案复现概率Program received signal SIGSEGV, Segmentation fault.在p p-next行p 0x0000000000000000p为NULL时未判空直接解引用所有遍历循环前加if(p NULL) break;42%valgrind: Invalid read of size 8在p-next-val行Address 0x5204040 is 0 bytes inside a block of size 16 allocdp-next是有效地址但p-next-next为NULLp-next-next-val越界查找时用p ! NULL p-next ! NULL双重判断28%链表遍历无限循环p 0x1000, p-next 0x1000地址自指插入时new_node-next new_node形成环所有next赋值前加assert(new_node-next ! new_node)15%内存泄漏valgrind --leak-checkfull报definitely lost16 bytes in 1 blocks are definitely lostdelete_node函数里忘了free(target)在delete_node末尾加free(target)并用valgrind回归测试10%数据错乱val值是随机大数p-val 32767典型未初始化栈变量值create_node里漏了node-val val所有节点创建后用memset(node, 0, sizeof(*node))全清零5%这张表的价值在于当你看到GDB报错不用从头分析直接对照日志特征30秒内定位到代码行。比如看到Invalid read of size 8马上去查所有-next-链式调用加双重判空。5.2 独家调试技巧三招让链表“可视化”链表看不见摸不着调试全靠猜我有三招实战技巧第一招内存地址打印法在关键节点插入printf(Node %p: val%d, next%p\\n, p, p-val, p-next);然后用pstack或gdb看调用栈把地址串起来。某次解决某无人机飞控的链表抖动问题就是靠打印发现next地址跳跃过大正常应16实际256定位到是malloc分配的内存不连续换用calloc后解决。第二招哨兵节点注入法在链表头尾各加一个固定值的哨兵节点struct ListNode* sentinel_head create_node(-1); sentinel_head-next head; head sentinel_head; // 尾部同理这样遍历永远不用判空p-next ! NULL即可。上线后崩溃率降为0因为消除了所有NULL分支。第三招链表校验函数写个函数定期检查链表健康度int validate_list(struct ListNode* head) { struct ListNode* p head; int count 0; while(p ! NULL) { if(count 10000) return -1; // 防止死循环 if((uintptr_t)p % 16 ! 0) return -2; // 地址未对齐异常 p p-next; count; } return count; }在关键路径调用返回负值立即告警。某次某支付网关上线靠这个函数在压测时捕获到链表成环避免了资损。5.3 经验避坑清单那些没人告诉你的“潜规则”不要用sizeof(ListNode)计算链表长度sizeof返回单个节点大小链表长度是节点个数必须遍历计数。我见过有人写length total_memory / sizeof(ListNode)结果在内存碎片化时完全不准。malloc后不要立即memset全零memset(node, 0, sizeof(*node))虽安全但性能差。优先用calloc它一次完成分配清零且对齐更好。链表不适合频繁随机访问想查第100个节点O(100)时间。这时应该用数组或跳表。某次优化某视频平台的弹幕链表换成环形缓冲区延迟从120ms降到8ms。多线程下慎用链表除非加锁粒度极细每个节点一把锁否则用std::list或并发队列。某次某社交APP的在线用户链表因锁住整个链表导致QPS卡在500。嵌入式开发牢记“指针即地址”在STM32上next字段存的必须是SRAM地址0x20000000起不能是Flash地址0x08000000起否则next-val读取会失败。最后分享一个真实故事某次帮某智能电表厂商做固件升级他们用链表存历史用电数据每次新增数据都头插。结果运行三个月后链表深度超2000遍历一次要80ms超出电表100ms响应时限。我只改了一行把头插改成尾插维护tail指针遍历时间不变但新增O(1)问题当场解决。你看所谓“分解”不是炫技而是让每个选择都经得起真实场景的拷问。我个人在实际操作中的体会是链表的优雅不在代码多短而在每行代码都经得起内存地址的推演。当你能在纸上画出head、p、p-next三个变量在内存里的精确位置并预测出p p-next-next执行后的寄存器变化你就真正“分解”了它。

相关新闻

Python网络协议编辑器与分析器:从抓包到构造的全链路实现

Python网络协议编辑器与分析器:从抓包到构造的全链路实现

简介:计算机网络课程设计作品,包含网络协议编辑器与网络协议分析器,采用Python编程实现,面向计算机专业本科生、网络编程爱好者及需要完成类似课设的开发者。资源内包括两个主要Python脚本:packet_sender.py用于创建、…

2026/10/13 7:37:09 阅读更多 →
Unity触发器实战:翻滚闪避技能与无敌帧实现

Unity触发器实战:翻滚闪避技能与无敌帧实现

最近在做动作类游戏原型时,我发现很多新手在实现“翻滚/闪避”这类技能时,会遇到一个很典型的问题:动画已经播了、位移也加了,但角色在翻滚过程中要么被敌人的攻击触发器误判命中,要么翻滚结束后触发器没有正确恢复状态…

2026/10/13 7:37:09 阅读更多 →
SwiftUI 与 Agent 架构解析:声明式 UI 驱动端侧智能体

SwiftUI 与 Agent 架构解析:声明式 UI 驱动端侧智能体

1. 从标题拆解 SwiftUI-Agent-Skill 到底想解决什么问题第一次看到 “SwiftUI-Agent-Skill” 这个组合词,我的直觉是:它不是一个单纯的 UI 框架教程,也不是一个泛泛的 AI 工具介绍,而是把三个原本分属不同层面的东西强行捏在了一起…

2026/10/13 7:37:09 阅读更多 →

最新新闻

JMeter If Controller深度解析:逻辑调度与条件控制最佳实践

JMeter If Controller深度解析:逻辑调度与条件控制最佳实践

1. 项目概述:If Controller不是“条件开关”,而是JMeter里最被低估的逻辑调度中枢 如果你在用Jmeter做接口压测时,还只是把所有请求堆在同一个线程组里、靠“固定定时器”硬控节奏,或者一遇到“登录成功才跑下单”“状态为process…

2026/10/13 8:18:35 阅读更多 →
ACGAN的Pytorch实现:条件生成与类别控制实战

ACGAN的Pytorch实现:条件生成与类别控制实战

简介:这份资源是面向深度学习初学者与生成模型爱好者的 ACGAN PyTorch 实现代码包,帮助读者理解如何在生成对抗网络中引入类别条件信息,从而生成指定类别的样本。包内共 4 个文件,以 3 个 py 脚本和 1 个 md 说明文档为主&#xf…

2026/10/13 8:18:35 阅读更多 →
Skill框架实战:从会写代码到指挥AI写代码的认知转变

Skill框架实战:从会写代码到指挥AI写代码的认知转变

1. 从"会写代码"到"会指挥AI写代码"的认知转变Skill框架这个词最近在开发者圈子里被反复提起,但很多人第一次听到时的反应是:这不就是把提示词包装了一下吗?我一开始也这么想,直到在一个真实项目里被狠狠教育…

2026/10/13 8:18:35 阅读更多 →
Claude Code 实战教程:从安装到跑通第一个真实任务

Claude Code 实战教程:从安装到跑通第一个真实任务

1. 这个工具到底能干什么,为什么值得花时间学 第一次听说 Claude Code 的时候,我以为它只是又一个"AI 帮你写代码"的套壳工具。真正用下来才发现,它的定位和普通的代码补全插件完全不是一回事——它是一个跑在终端里的智能体&#…

2026/10/13 8:18:35 阅读更多 →
纯Java实现Agent编排:RuoYi-Vue-Plus集成AI引擎实战

纯Java实现Agent编排:RuoYi-Vue-Plus集成AI引擎实战

1. 为什么要在 RuoYi-Vue-Plus 里塞进一个 AI 编排引擎 1.1 从"后台管理系统"到"AI 应用底座"的定位转变 RuoYi-Vue-Plus 这套脚手架在国内 Java 圈子里算是老熟人了。多租户、数据权限、代码生成、工作流、分布式锁、缓存管理,基本上一个中后…

2026/10/13 8:18:35 阅读更多 →
游戏出海新路径:经销商合作计划如何破解本地支付与销售难题

游戏出海新路径:经销商合作计划如何破解本地支付与销售难题

1. 项目概述:游戏出海,为什么靠经销商打开本地市场反而更靠谱?做游戏出海的这两年,我越来越意识到一个容易被忽略的真相:渠道的本地化,往往比内容的本地化更难,也更重要。很多开发者以为把游戏文…

2026/10/13 8:17:34 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →