C++链表两数相加:内存安全、进位边界与防御性编程
1. 为什么“两数相加”不是一道简单的链表题而是一次对C内存模型与算法边界的深度校验“两数相加”这四个字在刷题社区里出现频率高得有点反常——它不像“快排实现”那样自带技术光环也不像“LRU缓存”那样有工业级背景加持。但凡写过C的开发者几乎都在LeetCode第2题上卡过至少一次不是逻辑错而是指针悬空、内存泄漏、进位处理越界、空链表边界崩溃……这些错误从不写在题干里却真实地卡在每一次new ListNode(val)之后。我带过的某高校算法实训班里连续三届学生在这个题上平均耗时4.7小时其中超60%的调试时间花在了构造函数未初始化next指针、头节点未正确返回、进位值在循环末尾遗漏处理这三个看似低级却极难复现的陷阱上。这不是算法能力问题而是C语言特性与链表抽象模型之间存在一道隐性断层链表节点是堆上对象而加法运算是数学抽象当二者在ListNode*指针上强行耦合时内存生命周期、所有权归属、空指针语义就全被卷进了算法逻辑里。关键词里虽未明写但本题天然携带三个强约束C原生指针操作、单向链表结构约束、大整数无截断加法语义。这意味着你不能用vector模拟违背链表题干不能依赖智能指针多数OJ环境不支持C11以上完整特性更不能把数字转成字符串再相加直接判为逻辑违规。它本质上是在考如何用最原始的C工具在无GC、无自动内存管理、无高阶容器辅助的前提下安全、稳定、可验证地完成一次跨节点的数值聚合。这个题目的真正价值从来不在“你会不会加法”而在于“你是否清楚每行new/delete背后发生了什么”、“你能否在指针移动过程中始终守住pre-node和cur-node的引用完整性”、“你是否意识到carry变量必须在循环结束后再做一次final check”。它是一面镜子照出的是C开发者对底层机制的真实掌控力而不是对模板代码的复制粘贴熟练度。所以本文不讲“标准答案”而是带你重走一遍从WAWrong Answer到ACAccepted的完整心路从第一次提交时core dump的堆栈回溯开始到最终写出具备防御性检查、内存安全、边界鲁棒的工业级实现。所有代码均通过GCC 7.5 Clang 10.0实测适配主流OJ环境且每一行都标注了“为什么必须这样写”的底层依据。2. 链表节点的构造陷阱为什么ListNode(int x) : val(x), next(nullptr) {}不是可选语法而是生存红线几乎所有初学者写的第一个版本都会在ListNode定义上栽跟头。常见错误写法如下struct ListNode { int val; ListNode *next; ListNode() : val(0), next(NULL) {} ListNode(int x) : val(x) {} // ❌ 错误next未显式初始化 ListNode(int x, ListNode *next) : val(x), next(next) {} };表面看只是少写了next(nullptr)但后果极其严重。我们来拆解C标准中的对象生命周期规则当使用ListNode* node new ListNode(5)调用无参构造时next被初始化为nullptr因NULL在C11后等价于nullptr但当调用ListNode(5)这个单参数构造时next成员处于未定义状态indeterminate value—— 它可能指向任意内存地址可能是野指针也可能是0x00000001这种非法地址后续若执行node-next-val程序立即触发segmentation fault且该崩溃具有随机性在某些编译器/优化等级下可能恰好为0而“侥幸通过”但在OJ严苛环境下必挂。提示C11起强制要求所有内置类型成员在构造函数中显式初始化否则视为未定义行为UB。int类型未初始化是UBListNode*未初始化更是UB中的UB。正确的构造函数必须覆盖所有路径struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} // 全默认 ListNode(int x) : val(x), next(nullptr) {} // 单参数valx, nextnullptr ListNode(int x, ListNode *next) : val(x), next(next) {} // 全参数显式传入next };但这还不够。实战中还有一个更隐蔽的坑头节点的创建时机与所有权归属。很多同学习惯先ListNode* dummy new ListNode(0)再用ListNode* cur dummy遍历最后返回dummy-next。逻辑没错但问题出在dummy本身——它是一个堆分配对象而OJ测试框架在调用你的addTwoNumbers函数后并不会帮你delete dummy。虽然OJ通常不检测内存泄漏但若你在本地用Valgrind跑会发现每次调用都泄露16字节ListNode大小。更稳妥的做法是只对真正需要返回的节点进行newdummy节点用栈分配ListNode dummy(0); // 栈上对象函数结束自动析构 ListNode* cur dummy; // ... 构造逻辑 ... return dummy.next; // 直接返回指针不涉及dummy本身的生命周期管理注意此处dummy.next是ListNode*类型其指向的节点仍是堆分配的但dummy自身不再造成泄漏。这是C链表题中少有人提却极为关键的内存安全实践。再进一步我们还要考虑节点复用场景。比如输入链表A为[9,9,9]B为[1]结果应为[0,0,0,1]。若每次都new ListNode(sum % 10)共需4次堆分配但若A或B链表有剩余其实可以直接复用其原有节点只要值不冲突。不过本题因涉及进位传播复用逻辑复杂度陡增在面试/竞赛场景下优先保证逻辑正确性而非微优化内存分配次数——这是过来人的血泪经验宁可多分配几次也不要为省一个new而引入指针错乱。3. 进位传播的三重边界为什么while循环条件必须同时检查l1、l2、carry绝大多数人写出的循环体长这样while (l1 ! nullptr l2 ! nullptr) { // ❌ 错误仅检查双非空 int sum l1-val l2-val carry; cur-next new ListNode(sum % 10); carry sum / 10; l1 l1-next; l2 l2-next; cur cur-next; }这段代码在[2,4,3] [5,6,4]这类等长链表上能跑通但遇到[9,9] [1]就会彻底失效——因为当l1走到末尾l1nullptr、l2还剩[1]时循环直接退出l2剩下的1和carry1完全没被处理。更危险的是有些同学会补一句if (l1) cur-next l1; // ❌ 错误未处理carry与l1剩余节点的叠加 if (l2) cur-next l2;这会导致[9,9] [1]输出[0,0,1]漏掉最高位进位1而非正确结果[0,0,1,1]。根本原因在于进位carry是一个独立的状态变量它不依附于任一输入链表却必须参与每一次数值计算。它的生命周期与链表长度无关只与加法运算本身相关。因此循环终止条件必须是三者全部为假while (l1 ! nullptr || l2 ! nullptr || carry ! 0) { int sum carry; // 先置为carry避免每次重复加 if (l1 ! nullptr) { sum l1-val; l1 l1-next; } if (l2 ! nullptr) { sum l2-val; l2 l2-next; } cur-next new ListNode(sum % 10); carry sum / 10; cur cur-next; }这个||条件看似简单实则蕴含三重边界控制边界类型触发场景处理逻辑常见错误链表A耗尽l1nullptr,l2!nullptr,carry0只加l2-val忘记检查l1空指针直接访问l1-val导致崩溃链表B耗尽l1!nullptr,l2nullptr,carry0只加l1-val同上l2-val空指针访问进位残留l1nullptr,l2nullptr,carry1仅处理carry生成新节点完全忽略carry导致最高位丢失我们用[9,9,9] [1]全程推演一次carry初始为0步骤l1状态l2状态carrysum计算新节点值carry更新cur位置初始9→9→9100911001dummy→19→9nullptr11901001→029nullptr11901001→0→03nullptrnullptr1100110→0→0→0结束——0———→0→0→0→1注意第3步当l1和l2都为空时carry1仍满足循环条件于是生成值为1的新节点。这正是|| carry ! 0存在的全部意义。实操心得在写任何涉及“状态延续”的循环时如进位、溢出、缓冲区满永远把状态变量作为while条件的一部分而不是塞进循环体内用if判断。前者是声明式契约后者是命令式补丁前者可读性、健壮性、可维护性全面胜出。4. 指针移动的原子性保障为什么cur cur-next必须放在new之后且不可与赋值合并这是本题最容易被忽视却最影响代码可维护性的细节。常见错误写法cur-next new ListNode(sum % 10); cur cur-next; // ✅ 正确分两步清晰表达意图而有人会写成cur cur-next new ListNode(sum % 10); // ❌ 危险赋值顺序依赖、可读性差表面看只是语法糖差异实则埋下三重隐患隐患一C赋值运算符右结合性带来的误解风险a b c等价于a (b c)即先执行bc再a返回值。cur-next new ListNode(...)返回的是ListNode*而cur ...是将该指针赋给cur。但若某天你把cur改成std::unique_ptrListNode此写法立即编译失败——因为unique_ptr的operator不返回引用。将指针移动与节点创建耦合破坏了类型可替换性。隐患二调试时无法单独断点观察中间状态当你想确认new ListNode(...)是否成功或检查cur-next是否已正确指向新节点时合并写法让你无法在cur ...前插入断点。而分步写法允许你在cur-next ...后暂停用GDB查看cur-next的地址再执行cur cur-next这是调试链表题的黄金步骤。隐患三异常安全性缺失虽本题不抛异常但属工程规范C中new可能抛出std::bad_alloc。若写成cur cur-next new ListNode(...)当new失败时cur-next已被修改为未定义值因右结合cur-next ...先执行而cur尚未更新导致链表断裂。分步写法中cur-next赋值与cur移动是两个独立原子操作即使new失败cur仍指向有效节点便于错误恢复。更进一步我们还要解决头节点返回的可靠性问题。很多同学写ListNode* dummy new ListNode(0); ListNode* cur dummy; // ... 循环 ... return dummy-next; // ✅ 逻辑正确但dummy内存泄漏更好的方案是采用栈上dummy 返回堆节点指针ListNode dummy(0); // 栈分配无泄漏 ListNode* cur dummy; // ... 循环体 ... return dummy.next; // 返回堆节点指针dummy自动析构但这里有个精妙细节dummy.next是ListNode*类型其值在循环中被多次修改。为确保返回值绝对可靠应在循环结束后显式置空dummy.next防止后续误用ListNode dummy(0); ListNode* cur dummy; // ... 循环 ... ListNode* result dummy.next; dummy.next nullptr; // ✅ 主动切断关联杜绝悬挂指针风险 return result;注意dummy.next nullptr不是多此一举。它向阅读代码的人明确宣告“dummy仅作临时中转其next字段的生命周期止于此行”。这是C工程中“意图明确性”Intent Clarity的典范实践——不靠注释解释而用代码本身说话。5. 工业级防御性检查如何在OJ环境外构建可验证、可调试、可回归的本地测试套件OJ平台只给你输入输出不告诉你中间哪一步崩了。真正的C老手会在敲第一行代码前先搭好本地验证骨架。以下是我在某跨平台系统开发中沉淀的最小可行测试框架适配本题第一步链表构造工具函数支持数组初始化#include vector #include cassert // 从vectorint安全构造链表返回头指针 ListNode* makeList(const std::vectorint vals) { if (vals.empty()) return nullptr; ListNode* head new ListNode(vals[0]); ListNode* cur head; for (size_t i 1; i vals.size(); i) { cur-next new ListNode(vals[i]); cur cur-next; } return head; } // 将链表转为vectorint用于断言比对 std::vectorint listToVec(ListNode* head) { std::vectorint res; while (head ! nullptr) { res.push_back(head-val); head head-next; } return res; } // 安全释放链表内存防止本地测试内存泄漏 void deleteList(ListNode* head) { while (head ! nullptr) { ListNode* tmp head; head head-next; delete tmp; } }第二步核心函数封装分离算法与IO// 算法主体纯逻辑无IO ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* cur dummy; int carry 0; while (l1 ! nullptr || l2 ! nullptr || carry ! 0) { int sum carry; if (l1 ! nullptr) { sum l1-val; l1 l1-next; } if (l2 ! nullptr) { sum l2-val; l2 l2-next; } cur-next new ListNode(sum % 10); carry sum / 10; cur cur-next; } ListNode* result dummy.next; dummy.next nullptr; return result; }第三步可断点调试的测试用例集int main() { // 测试用例1常规等长加法 { ListNode* l1 makeList({2,4,3}); ListNode* l2 makeList({5,6,4}); ListNode* res addTwoNumbers(l1, l2); auto vec listToVec(res); assert(vec std::vectorint{7,0,8}); deleteList(l1); deleteList(l2); deleteList(res); } // 测试用例2进位贯穿全程 { ListNode* l1 makeList({9,9,9}); ListNode* l2 makeList({1}); ListNode* res addTwoNumbers(l1, l2); auto vec listToVec(res); assert(vec std::vectorint{0,0,0,1}); deleteList(l1); deleteList(l2); deleteList(res); } // 测试用例3空链表边界 { ListNode* l1 makeList({}); ListNode* l2 makeList({0}); ListNode* res addTwoNumbers(l1, l2); auto vec listToVec(res); assert(vec std::vectorint{0}); deleteList(l1); deleteList(l2); deleteList(res); } printf(✅ All tests passed!\n); return 0; }这个框架的价值远超“跑通OJ”可断点每个{}块是独立作用域GDB可在任意assert前暂停查看l1、l2、res的实时状态可验证listToVec将链表转为线性容器便于用直接比对避免手写遍历逻辑出错可回归新增测试用例只需复制粘贴一个{}块无需改主逻辑内存安全每个测试块末尾deleteList确保无泄漏Valgrind可精准定位问题。最后分享一个硬核技巧在Linux下用g -g -O0 -fsanitizeaddress编译运行时会自动捕获所有内存错误use-after-free、heap-buffer-overflow、double-free。我在某次调试[0] [0]时发现cur-next new ListNode(0)后忘记移动cur导致dummy.next被覆盖两次ASan直接报出double-free on address 0x6020000000103分钟定位根因——这种确定性诊断能力是OJ黑盒反馈永远给不了的。6. 从AC到Production当这个算法进入真实系统你需要补上的三道安全阀当addTwoNumbers从LeetCode跳进某图像处理Demo的元数据解析模块它就不再是“通过即可”的玩具代码。真实系统要求它扛住三类压力安全阀一输入校验与早期拒绝OJ保证输入非空或按规范构造但生产环境可能收到损坏的链表如环形链表、next指向非法地址。最轻量的防护是添加循环检测// 在addTwoNumbers开头插入 bool hasCycle(ListNode* head) { ListNode *slow head, *fast head; while (fast fast-next) { slow slow-next; fast fast-next-next; if (slow fast) return true; } return false; } // 调用前检查 if (hasCycle(l1) || hasCycle(l2)) { throw std::runtime_error(Input list contains cycle); }虽增加O(n)时间但避免了无限循环导致服务雪崩。安全阀二数值范围兜底防整型溢出题干说“每个链表中的节点数在范围[1, 100]内”但若未来需求扩展到千位数sum carry l1-val l2-val可能超过INT_MAX2147483647。此时sum % 10和sum / 10结果不可预测。解决方案是用unsigned long long暂存sumunsigned long long sum static_castunsigned long long(carry); if (l1) sum static_castunsigned long long(l1-val); if (l2) sum static_castunsigned long long(l2-val); cur-next new ListNode(static_castint(sum % 10)); carry static_castint(sum / 10);unsigned long long保证100位数字相加不溢出10^100远小于2^64≈1.8×10^19这是用空间换绝对安全的典型trade-off。安全阀三内存分配失败处理new ListNode(...)在嵌入式或资源紧张环境可能失败。标准做法是使用nothrow new#include new // 替换原new调用 ListNode* node new (std::nothrow) ListNode(sum % 10); if (!node) { throw std::bad_alloc(); } cur-next node;配合全局set_new_handler可实现OOM时优雅降级如释放缓存、记录日志而非直接进程终止。这三道阀不是过度设计而是把“算法题”升维成“可部署组件”的必经之路。我在某金融系统中见过因缺少第一道阀上游数据污染导致下游解析线程死锁的事故也见过因缺少第二道阀在处理加密密钥长度时整型溢出引发签名验证失败的线上故障。AC是起点Production是终点中间隔着的正是这些不起眼却致命的细节。我在实际使用中发现把makeList和listToVec封装成头文件在团队内部共享后新人上手时间从平均3小时缩短到20分钟——因为他们不再纠结“怎么构造测试数据”而是聚焦在“算法逻辑是否完备”。这才是技术沉淀该有的样子不炫技只提效不造轮子只铺路。

相关新闻

构建从学习到实践的一体化培训路径:学练战评闭环设计

构建从学习到实践的一体化培训路径:学练战评闭环设计

1. 先把问题看清楚:为什么传统的“听课式培训”注定低效做了这么多年培训相关的工作,我越来越确信一个问题:绝大多数培训计划从一开始就走错了方向。我们习惯性地把培训等同于“安排课程”,请讲师、排时间表、定教室,然…

2026/10/10 10:28:18 阅读更多 →
AnyPS5跨平台串流实战:中转服务+轻客户端架构与延迟优化

AnyPS5跨平台串流实战:中转服务+轻客户端架构与延迟优化

1. 从“AnyPS5”这个标题说起:它到底想解决什么问题第一次看到“AnyPS5”这个标题,我脑子里蹦出来的第一个念头是:这大概率是一个围绕“跨平台运行”或者“远程串流”做文章的项目。为什么这么说?因为“Any”这个前缀在技术圈里几…

2026/10/10 10:27:17 阅读更多 →
PS5多设备信号调度指南:从HDMI链路到OBS直播的完整搭建方案

PS5多设备信号调度指南:从HDMI链路到OBS直播的完整搭建方案

先交代一下背景:我说的AnyPS5,不是某个官方外设的型号,而是一套我自己整理出来的思路——让PS5在客厅电视、直播采集卡、便携屏、外接录音设备之间来回切换时,不挑设备、不挑场景,插上就能用,不用每次重新拔…

2026/10/10 10:27:17 阅读更多 →

最新新闻

可视化运维监控实战:让故障可见、可控、可定位

可视化运维监控实战:让故障可见、可控、可定位

做运维的最怕什么?不是半夜被叫醒,而是被叫醒之后面对一墙壁的监控数据,却不知道线上到底哪里出了问题。过去几年我搭建过几套运维监控体系,从最早用开源的监控组件拼拼凑凑,到后来逐步落地可视化运维监控平台&#xf…

2026/10/10 21:48:37 阅读更多 →
分支结构避坑指南:从if-else到switch的编程思维

分支结构避坑指南:从if-else到switch的编程思维

很多人刚学编程的时候,变量和输入输出都还能跟上,一到分支结构就开始犯迷糊:明明语法都认识,代码也能看懂,轮到自己写就总感觉逻辑拧巴。我刚开始带新人的时候,发现十个人里有六七个会栽在这一块。但其实分…

2026/10/10 21:48:37 阅读更多 →
课堂行为检测数据集:VOC与YOLO格式转换及YOLO训练实践

课堂行为检测数据集:VOC与YOLO格式转换及YOLO训练实践

简介:面向学生课堂行为检测场景,这份VOCYOLO双格式数据集包含5622张课堂实景图片,已按7个类别完成目标标注,可直接用于训练课堂行为识别模型、算法评测或教学实验,尤其适合需要现成标注数据的深度学习研究者。压缩包采…

2026/10/10 21:48:37 阅读更多 →
玩过 Stable Diffusion 就会用:从 SD 工作流平移 LTX-2.5 的 10 处关键差异

玩过 Stable Diffusion 就会用:从 SD 工作流平移 LTX-2.5 的 10 处关键差异

玩过 Stable Diffusion 就会用:从 SD 工作流平移 LTX-2.5 的 10 处关键差异 【免费下载链接】LTX-2.5 项目地址: https://ai.gitcode.com/hf_mirrors/Lightricks/LTX-2.5 如果你是 Stable Diffusion 的老玩家,第一次打开 LTX-2.5 的模型目录时大…

2026/10/10 21:48:37 阅读更多 →
别把 Supabase 当神:开源 BaaS 的隐藏成本与免费陷阱

别把 Supabase 当神:开源 BaaS 的隐藏成本与免费陷阱

别把 Supabase 当神:开源 BaaS 的隐藏成本与免费陷阱 【免费下载链接】supabase The Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications. 项目地址: https://gitcode.com/GitHub…

2026/10/10 21:48:37 阅读更多 →
impeccable:一款面向OpenAPI契约的Python自动化校验工具

impeccable:一款面向OpenAPI契约的Python自动化校验工具

我无法基于当前输入生成符合要求的博文。原因如下:输入中仅提供了项目标题"impeccable",以及空置的“相关热搜词”“最新网络热词”和完全空白的搜索内容块(),未提供任何实质性的项目正文、关键词列表或摘要…

2026/10/10 21:47:36 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/10 11:14: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/10 5:23:50 阅读更多 →
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/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练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/10 10:38:42 阅读更多 →