C语言指针动态内存管理:从malloc到内存泄漏实战排查
C语言里指针动态内存的申请和释放是我在排查一个线上问题后彻底想清楚的事。当时某个服务端模块跑上几天就崩溃内存占用一路上涨最后在一个看似毫不相关的地方段错误退出。用工具定位之后发现根因就是某个错误处理分支里malloc出来的结构体没有完整释放还有一个free之后继续读的野指针。从那以后我再看到“动态内存”三个字脑袋里想的就不只是malloc和free两个函数而是一整套关于生命周期、所有权、边界检查的心智模型。这篇文章就把这些内容完整拆开为什么必须用动态内存、四个内存函数各自的设计逻辑、指针和堆内存的关系、一个真实的排查过程以及工程上怎么把动态内存管得比较稳。适合正在学C语言指针的入门读者也适合已经在项目里被内存问题折腾过的开发者。我会尽量用实际踩坑经验来讲而不是把教科书上的定义再抄一遍。1. 为什么非要动态内存先弄清楚三个硬伤1.1 长度未知编译期做不到的事写C代码时int arr[100]这种写法最直观但前提是你已经知道100这个数字从哪来。真实项目中程序要读取的配置文件行数、网络包长度、日志条数几乎都不可能提前写死在数组大小里。你可以把数组开得很大比如char buf[65536]但开小了会溢出开大了又浪费内存尤其在嵌入式平台上2KB的RAM可能一半都被一个数组占掉。动态内存的核心价值就是让“运行时才能确定大小”这件事变得可行。你可以在函数里读取文件头确认有多少条记录再通过malloc精确申请对应大小的内存。这个能力对链表、哈希表、动态数组这类数据结构来说更是刚需——它们的大小本来就会随着增删操作不断变化。1.2 生命周期受限栈上内存留不住这是静态分配最容易被忽视的坑。看这段代码int *get_buf(void) { int arr[64]; return arr; // 返回指向已失效栈内存的指针 }编译器通常会给出警告但很多人还是会这样写。arr的生命周期随着函数返回就结束了之后你通过返回的指针去读写属于未定义行为可能碰巧能跑也可能在某个时刻突然崩溃。正确做法是把内存挪到堆上int *get_buf(void) { int *p malloc(64 * sizeof(int)); return p; }但这样写的代价是调用方必须记得free。这正是动态内存和静态内存的本质区别——栈内存由作用域自动管理堆内存的生命周期完全由程序员手动控制。很多人从“数组”迈到“指针动态内存”这一步时最大的不适感就来自这里再也没有人替你回收了。1.3 栈空间太小大块数据放不进去默认栈大小在不同平台差别很大常见配置下Linux上可能是8MBWindows上可能是1MB某些嵌入式RTOS甚至只有几千字节。栈上放一个小型结构体数组还好如果数据结构一多直接把函数调用链的栈空间耗尽程序会以stack overflow告终。堆的大小则受系统虚拟内存限制宽松得多。所以处理大块数据、大数组、大数据量缓存时几乎只能走堆。这也是为什么服务端程序里很少见到大数组局部变量而malloc出现的频率非常高。理解这个背景你就不会觉得“能用数组就不该用malloc”这句话在所有场景下都成立——它只对小数据量、固定大小、短生命周期的情况成立。1.4 申请多少个字节sizeof的正确打开方式动态内存申请里sizeof用错的频率比想象中高。比如int *p malloc(10 * sizeof(int)); // 正确10个int int *q malloc(sizeof(int) * 10); // 同样正确 int *r malloc(10 * 4); // 不推荐int不一定就是4字节 int *s malloc(10); // 危险10个字节不是10个int建议写sizeof(int) * 10或者更稳妥地写成sizeof(*p) * 10这样即使以后p的类型从int *改成long *申请大小也会跟着变不容易漏改。2. malloc、calloc、realloc、free四个函数的设计取舍2.1 malloc和calloc都要内存差别你可能没想过malloc负责分配指定字节数的内存不保证内容calloc负责分配count * size字节的内存并把每个字节初始化为0。开发中经常有人问“我该用哪个”答案取决于你是否介意初始状态不确定。calloc存在的意义有两个。第一清零可以避免读到残留数据。你申请一块内存后如果没有完全初始化所有字段万一某条逻辑路径意外读到一个没赋值的字段malloc出来的内存里可能是任何值行为不可复现而calloc至少保证首次读取时是0。第二清零带有安全考量程序中有些敏感数据不希望在新进程里被他人通过内存扫描看到。还有一个不容易注意到的设计calloc用两个参数表示count和size而不是直接接收相乘后的结果。好的实现会在内部先检查count * size是否溢出如果溢出就返回NULL。但不要把这个当成所有平台的默认行为工程上仍然要自己确认这个乘积不会超过size_t的表示范围。另外要提醒一下calloc清零不等于C语言意义上的“零值”——大多数常见平台上int 0、浮点0.0、空指针都是全0位模式但C标准并不保证这一点。2.2 realloc的两种结局原地扩还是搬家realloc的意图很好把一块内存调大或调小如果原地空间足够就在原地扩展并返回原地址如果原地空间不够就另找一块更大的内存把旧数据复制过去再释放旧内存。问题在于标准并没有承诺一定原地扩展所以你不能假设指针在realloc之后还是原来的值。一个常见错误是这样写void *p malloc(100); p realloc(p, 200); // 如果realloc失败原来的p丢失当realloc返回NULL时原来的内存块仍然有效但直接赋值会让p变成NULL旧地址找不到了这块内存泄漏连恢复的空间都没有。正确写法是用临时变量接住返回值void *newp realloc(p, 200); if (newp ! NULL) { p newp; } else { // 原来的p还在可以继续用或者在这里自行处理 }另外realloc(p, 0)这个边界行为在不同实现里不一致有的直接释放并返回NULL有的返回一个非NULL但不可解引用的指针。这类写法在工程代码里没有任何收益只会给自己和接手的人增加负担能避免就避免。2.3 free的边界释放了不等于清零了free(p)把p指向的堆内存块标记为可用还给分配器管理但它不会修改p变量本身也不会把内存内容清零。这意味着三点第一free之后p仍然保存着旧地址你如果再去使用它就踩到了悬垂指针第二内存块里的字节大概率还留着旧数据直到分配器把它分配给新的申请者并写入新内容第三对同一指针调用两次free是未定义行为可能直接崩溃也可能悄无声息地破坏分配器的内部结构过很久才在其他地方爆发。所以工程上常见的做法是free(p); p NULL;。这不能根治悬垂指针问题因为可能还有另一个指针也指向同一块内存但至少主引用变成NULL后误用时的表现更容易被发现和调试。2.4 每次malloc之后都要检查返回值吗严格来说应该检查。malloc申请失败会返回NULL不检查就解引用基本立刻段错误。但现实中不同场景的处理方式不一样。如果是小型工具代码或快速原型我倾向于用一个封装函数统一处理申请失败就打印错误并退出避免每个函数里都写一堆if (p NULL)。如果是库代码或服务端模块场景就复杂了申请失败时可能需要回滚已有资源可能要把错误码返回给上一层也可能只能直接终止。你应该根据业务的性质决定策略而不是机械地面面俱到。有一点需要特别小心别写那种“申请失败就重试一次”的循环因为内存耗尽往往是系统层面的问题重试大概率还是失败还可能让进程卡在无意义的循环里。3. 指针动态内存的三大心智模型所有权、深拷贝、悬垂指针3.1 指针变量和堆内存是两回事先看这段代码int *p malloc(100); p malloc(100);问题在哪如果你脑子里能清晰区分“指针变量”和“被指内存”答案就很明显第一次malloc返回的100字节在p被重新赋值后再也没有任何指针指向它泄漏了。C语言没有垃圾回收这块内存直到进程结束都不会回来。这类bug的特点是不崩溃、不报错程序能跑内存却一直在涨。而且这种泄漏往往不是大数据块而是每隔一段时间就发生一次的小泄漏累积到一定程度才让人注意到。想避免问题你得确保每一块malloc出来的内存在生命周期结束前至少有一个指针能访问到它一旦没有指针指向它就是泄漏的开始。3.2 谁申请谁释放所有权必须在接口层讲清楚动态内存管理说到底是所有权问题。谁负责free、什么时候free需要在设计阶段约定好而不是等到编码时随机发挥。工程里常见的约定有这么几种函数内申请并通过返回值返回调用方用完负责释放。很多返回字符串副本的函数就是这样。调用方提供缓冲区函数只写入数据不负责分配也不负责释放。这类接口的边界最清晰。数据结构内部持有指针由容器的销毁接口统一释放。比如链表节点的数据域链表销毁时递归释放所有节点。最怕的是同一块内存被两个模块都认为“归我管”于是double free或者都认为“不归我管”于是泄漏。我的经验是跨模块传递指针时一定要在接口注释里写清楚内存的归属约定。接口越清晰后面的内存问题越少。3.3 结构体里的指针浅拷贝与深拷贝结构体复制是另一个容易踩坑的地方。看这个例子struct Student { char *name; }; struct Student a, b; a.name malloc(32); strcpy(a.name, zhang); b a; // 浅拷贝b.name和a.name指向同一块内存 free(a.name); // 问题来了b.name立刻变成悬垂指针b a只是把b.name这个指针变量里的地址值复制给了b.name并没有复制name指向的32字节内容。如果想要两个结构体各自持有独立的数据必须手动深拷贝b.name malloc(32); strcpy(b.name, a.name);这个坑在链表复制、容器扩容、结构体返回时非常常见。尤其在动态数组扩容时realloc只拷贝结构体本身不会递归拷贝结构体里嵌套的指针数据这一点需要额外处理。3.4 悬垂指针的三种典型来源悬垂指针是动态内存中最危险的状态归纳下来主要有三种来源。第一种是free之后没有置NULL代码继续使用旧指针。第二种是函数返回了局部栈变量的地址外部还当成有效指针继续用。第三种是一个对象的多个副本指针中有一个被释放了其他副本仍然存在比如上面浅拷贝的例子。排查悬垂指针时比泄漏更头疼因为悬垂访问并不一定马上崩溃可能只是读到一批“看起来合理”的数据直到某个内部状态被破坏后才暴露。所以我对团队的建议是尽量少用裸指针到处传能用free NULL的地方就做能在接口边界明确所有权的就明确不要把悬垂问题留给运行时去发现。4. 一次真实的内存泄漏与野指针排查实录4.1 现场程序跑几天后突然崩溃我曾在某个跨平台系统的服务端模块里处理过一个问题模块负责接收网络数据并写入本地文件刚启动时一切正常跑了两三个小时后内存占用越来越高最终在一块业务逻辑复杂的路径上触发段错误。当时第一反应是怀疑某个数组越界写坏了堆内存。因为这类崩溃往往不是当场出错而是过了一段时间后在不相关的位置爆发。数组越界可能会破坏相邻的内存管理元数据让后续任意一次malloc或free都变得不可靠。带着这个怀疑我开始用工具验证而不是靠肉眼翻代码。4.2 用Valgrind把问题“逼”出来Valgrind是排查内存问题的经典工具它的原理是拦截并检查程序的所有内存访问能比较准确地报告非法读写、重复释放、内存泄漏。代价是程序运行会慢很多但排查这种问题慢一点完全值得。我用类似这样的命令跑了一个较小规模的回归测试valgrind --leak-checkfull --show-leak-kindsall ./your_program结果很快出来了一块内存属于“definitely lost”确定泄漏另有一次“invalid read”发生在free之后。这两个信号基本把问题类型锁定了——不是因为堆元数据被破坏而是代码逻辑里存在释放路径不完备和释放后继续访问的问题。4.3 根因错误处理分支释放不完整顺着Valgrind给出的调用栈回到源码问题变得很清晰。在某个错误处理分支里代码先malloc了一个结构体然后在判断条件不满足时直接将结构体释放了但结构体内部嵌套的另一块动态内存没有释放导致一部分内存泄漏。更隐蔽的是非法读取同一条错误路径上结构体的一个成员被提前释放但后续代码仍然通过这个成员指针去读取数据。平时这个错误处理分支很少被触发所以内存泄漏的累积效应要跑很久才达到临界值野指针访问也一直没有暴露直到某次错误分支被触发且数据布局“恰好”不对才直接段错误。4.4 修复后的三个补充动作修复本身并不复杂把遗漏的free补上把提前释放的那行挪到结构体最终使用完之后。但真正有价值的是修复后我做的三件事。第一写了一个专门触发错误处理分支的单元测试确保释放路径在测试里被覆盖到而不是只靠运气碰巧执行。第二在调试构建里默认开启AddressSanitizer它靠编译期插桩工作能直接定位到崩溃行比Valgrind更适合日常开发中反复跑。第三在团队约定里加了一条涉及动态内存的错误处理分支代码评审时专门检查释放路径是否完备。这件事给我留下的印象很深。内存问题靠“多想想”不一定能发现因为你的注意力往往集中在正常路径上错误路径却长期得不到检查。借助工具把隐藏路径暴露出来反而比反复读代码高效得多。5. 工程级动态内存管理从能跑到跑得稳5.1 分配与释放的封装xmalloc是怎么帮你省事的很多人觉得封装malloc是多此一举但真实项目中如果每个调用点都写一遍if (p NULL)代码会非常啰嗦处理逻辑也难以一致。我常用的方式是在工具类代码里做一个简单封装static void *xmalloc(size_t size) { void *p malloc(size); if (p NULL) { fprintf(stderr, out of memory\n); exit(EXIT_FAILURE); } return p; }然后代码里直接p xmalloc(sizeof(*p));。这个封装的适用场景是内存耗尽时进程已经处于危险状态继续执行大概率还会出错所以直接终止比让错误一路扩散更可控。但如果你写的是库代码或者业务允许申请失败后返回错误码继续运行就不要用这个封装而应该走更精细的错误处理路径。除了封装分配我还建议大家给每一块动态内存配一个明确的释放接口。比如定义buffer_new和buffer_free使用者不需要知道内部申请了几块内存只需要成对调用构造和销毁函数。这其实是“谁构造谁销毁”原则的落地能大幅减少释放遗漏。5.2 内存池高频小对象要不要上如果一个服务端程序每秒要创建几千个几十字节的小对象每次都malloc和free分配器的锁竞争和元数据管理开销会变成实际问题。内存池可以提前从堆里申请一大块内部再按小块分配批量释放从而减少对系统分配器的依赖。但内存池不是银弹。它有三个代价实现复杂度高出问题后更难排查池内分配的峰值内存可能比实际需求高如果池的释放时机设计错了整个池的泄漏会以更大的规模隐藏起来。我的建议是先用最直接的malloc/free把功能跑通再用性能分析工具确认malloc确实占用明显比例最后才考虑内存池。过早优化很容易把简单的程序改复杂却解决不了真正的问题。5.3 一个自查清单最后分享一个我在代码评审和自我检查时常用的清单不一定多高深但很管用所有指针变量初始化了吗局部裸指针不初始化就是野指针的起点。每个malloc都有对应的free吗错误路径上的free有没有遗漏free之后指针有没有被置NULL还有没有其他指针指向同一块内存结构体复制时需要的是浅拷贝还是深拷贝如果是深拷贝后续有没有对应的释放跨模块传递指针时所有权归属在接口注释里写清楚了吗调试构建里有没有开启AddressSanitizer后台程序有没有监控内存占用曲线内存占用持续上涨对后台程序来说就是泄漏的早期信号等它触发段错误再去查成本要高得多。最后再说点个人体会。我见过很多C语言初学者在学指针和动态内存时把精力放在背函数原型上但真正重要的其实是“内存的生命周期由谁负责”这个习惯。函数原型几天就记住了所有权意识需要靠真实项目去练。如果你现在正被一个莫名其妙的段错误折磨别只顾着看代码逻辑先想想是不是有一块动态内存在某个角落被用错了。把这个问题想通你会突然发现以前那些玄学般的崩溃其实都有迹可循。

相关新闻

foobox-cn:5步装好foobar2000中文DUI皮肤,含组件集成

foobox-cn:5步装好foobar2000中文DUI皮肤,含组件集成

foobox-cn:5步装好foobar2000中文DUI皮肤,含组件集成 【免费下载链接】foobox-cn DUI 配置 for foobar2000 项目地址: https://gitcode.com/GitHub_Trending/fo/foobox-cn foobox-cn 是 foobar2000 的 DUI 皮肤配置,基于 JSplitter 组…

2026/10/15 2:06:13 阅读更多 →
Vale 基准测试实战:从 bench.md 看 Markdown 散文检查的性能保障体系

Vale 基准测试实战:从 bench.md 看 Markdown 散文检查的性能保障体系

开发工具代码质量CLI 【免费下载链接】vale :pencil: A markup-aware linter for prose built with speed and extensibility in mind. 项目地址: https://gitcode.com/gh_mirrors/va/vale 点击查看 免费下载 bench.md 是 Vale(一个面向散文的 markup-a…

2026/10/15 2:06:13 阅读更多 →
SpringBoot公交查询系统架构设计与性能优化

SpringBoot公交查询系统架构设计与性能优化

1. 项目背景与核心价值公交线路查询系统是城市公共交通信息化建设的基础设施之一。随着城市规模扩大和公交网络复杂化,传统纸质线路图和简单电子查询已无法满足市民出行需求。这个基于SpringBoot的系统正是为解决以下痛点而生:实时性差:传统查…

2026/10/15 2:06:13 阅读更多 →

最新新闻

Electron + Vue 项目结构搭建:主进程、渲染进程与IPC通信实践

Electron + Vue 项目结构搭建:主进程、渲染进程与IPC通信实践

我最早是在做一个后台管理系统的时候碰上了 Electron。当时需求很明确:系统本身是 Vue 写好的网页,但客户希望在 Windows 桌面上能直接双击打开、离线可用、还能调用一些浏览器里做不了的系统能力,比如文件拖拽和本地目录读写。如果单独做一套…

2026/10/15 2:49:27 阅读更多 →
生成函数在算法竞赛中的应用:解锁计数与递推难题

生成函数在算法竞赛中的应用:解锁计数与递推难题

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

2026/10/15 2:49:27 阅读更多 →
ASM1083数据表解读:PCIe转PCI桥片硬件设计与调试指南

ASM1083数据表解读:PCIe转PCI桥片硬件设计与调试指南

简介:ASM1083 PCIe转PCI芯片数据表面向硬件工程师、嵌入式开发者与主板维修人员,用于解决PCIe接口与传统PCI设备之间的桥接设计问题。该芯片由ASMedia出品,支持x1至x16多种PCI Express lanes,数据传输速率最高2.5Gb/s,…

2026/10/15 2:49:27 阅读更多 →
JUnitCore方法级运行为何比类级更慢?揭秘Runner生命周期开销

JUnitCore方法级运行为何比类级更慢?揭秘Runner生命周期开销

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

2026/10/15 2:49:27 阅读更多 →
U8g2单色屏图形库应用笔记:移植、中文显示与刷新提速

U8g2单色屏图形库应用笔记:移植、中文显示与刷新提速

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

2026/10/15 2:49:27 阅读更多 →
ESXi 升级到 8.0 后 VM 快照删不掉?一篇讲透 “in progress“ 死循环的 3 种解法

ESXi 升级到 8.0 后 VM 快照删不掉?一篇讲透 “in progress“ 死循环的 3 种解法

本文适用版本:ESXi 7.0 U3 → 8.0 U2 升级场景(vSphere 6.7 升级也会遇到) 阅读时长:约 8 分钟 难度:⭐⭐⭐(操作不可逆,生产 VM 强烈建议先备份再动)一、为什么写这篇文章?最近私信里又有几位网友问:ESXi 7.0 升到 8.0 之后,VM 的快照怎么都删不掉,vSphere Client 里一直显示…

2026/10/15 2:48:27 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/14 7:53:34 阅读更多 →