C++ unique_ptr release与reset性能差异及使用场景深度解析
1. 项目概述从一次性能调优引发的思考最近在做一个对性能要求极高的C后台服务在Review一段核心的内存管理代码时我和同事就一个unique_ptr的使用产生了分歧。代码大致是这样的我们需要在一个高频调用的循环里根据条件动态地替换一个unique_ptr所管理的对象。同事习惯性地写成了ptr.reset(new Resource(...))而我则倾向于先auto old ptr.release()再ptr.reset(new Resource(...))。我们俩都坚信自己的写法更“高效”争论不下最后决定写个基准测试来一探究竟。这场争论的结果让我有点意外也促使我深入研究了std::unique_ptr::release()和reset()这两个看似简单的成员函数在性能上的微妙差异以及它们背后所代表的不同资源管理哲学。这不仅仅是两个函数调用那么简单它触及了现代C智能指针设计的核心——在保证资源安全的前提下如何提供灵活且高效的操作接口。理解这些差异能帮助我们在日常开发中做出更精准的选择避免在毫秒必争的场景下引入不必要的开销。无论你是正在准备面试、啃八股文还是在实际项目中追求极致的性能搞清楚release和reset的里里外外都大有裨益。2. unique_ptr 核心机制再回顾在深入对比之前我们有必要统一一下对std::unique_ptr基础认知。unique_ptr是一种独占所有权的智能指针这意味着在任何时刻只有一个unique_ptr实例可以拥有某个对象的所有权。这种独占性是其高效性的来源因为它避免了引用计数的开销。其核心状态包含两个部分一个指向被管理对象的原始指针get()返回的和一个删除器deleter。删除器默认为std::default_delete但可以自定义这赋予了unique_ptr管理任意资源如文件句柄、网络套接字的能力。unique_ptr的析构函数以及reset()操作的核心逻辑可以简化为一个守卫性操作如果当前持有非空指针则调用删除器来释放资源。这个“释放-置空”的操作必须是原子的或者说在逻辑上是不可分割的以确保资源不被重复释放或泄漏。理解了这个基础我们就能明白release和reset的所有行为差异都源于它们对这个“所有权状态”的不同处理方式。2.1 release() 的本质所有权的转移而非释放release()函数的行为非常独特它并不释放unique_ptr当前管理的对象。它的作用是切断unique_ptr与当前对象的所有权联系并返回这个原始指针。调用release()之后unique_ptr自身变为空get() nullptr而调用者则获得了返回的原始指针的完全所有权并负有在将来某个时刻手动释放它的责任。std::unique_ptrint ptr(new int(42)); int* raw_ptr ptr.release(); // ptr 现在为空 raw_ptr 指向 int(42) // 此时ptr 不再管理任何内存。我们必须手动管理 raw_ptr: // delete raw_ptr;这是一个“放手”的操作。它常用于需要将对象的所有权移交给不支持智能指针的遗留API或者用于实现更复杂的所有权转移模式。关键点在于release()本身不涉及任何内存释放操作它只是一个指针的移交和内部状态的清空因此其开销极小通常就是几个指针赋值操作。2.2 reset() 的本质安全的资源更替reset()函数的行为则符合我们对“重置”的直觉。它主要做两件事释放unique_ptr当前拥有的对象如果存在。让unique_ptr拥有一个新提供的对象如果提供了参数或者变为空如果不提供参数。std::unique_ptrint ptr(new int(42)); ptr.reset(new int(100)); // 1. 删除 int(42) 2. 开始管理 int(100) ptr.reset(); // 删除 int(100)ptr 变为空这是一个“替换”或“清空”的操作。它保证了资源的生命周期始终由unique_ptr管理是更安全、更符合RAII资源获取即初始化理念的用法。其开销主要来自于可能发生的对旧资源的释放操作调用删除器以及对新资源的接管。3. 性能差异的微观分析与基准测试理论说完了是时候用数据说话了。性能差异的核心就在于release() 手动管理 vsreset()的原子性操作。3.1 性能开销的定性分析让我们设想一个场景你需要用一个新对象替换unique_ptr当前管理的对象。方案A使用resetptr.reset(new Resource(...));步骤1在堆上构造一个新的Resource对象可能开销很大。步骤2reset函数被调用。在函数内部a. 保存当前指针old_ptr ptr.get()。b. 将内部指针指向新对象。c. 如果old_ptr非空调用删除器deleter(old_ptr)释放旧对象。在这个过程中新对象的构造发生在旧对象销毁之前。这意味着在内存分配紧张时系统需要同时容纳新旧两个对象可能触发额外的内存分配压力。并且步骤2中的b和c操作虽然简单但包含了必要的指针赋值和条件判断。方案B使用release resetdelete ptr.release(); ptr.reset(new Resource(...));步骤1ptr.release()切断所有权获取旧指针old_ptr。开销极低。步骤2手动delete old_ptr;立即释放旧对象。步骤3构造新Resource对象。步骤4ptr.reset(...)接管新对象。此时ptr为空所以reset内部无需执行释放旧对象的逻辑直接接管即可。这个方案将“释放旧资源”和“接管新资源”这两个逻辑上耦合的操作分开了。好处是在步骤2之后、步骤3之前旧对象占用的内存已经被释放这可能为后续新对象的构造腾出空间理论上对内存碎片化更友好。但代价是代码更冗长且需要开发者手动保证delete的调用破坏了RAII的封装性。从纯操作步骤来看方案B似乎步骤更多。但在某些特定场景下这种“解耦”可能带来好处。为了验证我设计了以下基准测试使用Google Benchmark#include benchmark/benchmark.h #include memory #include vector struct Widget { std::vectorint data; Widget() : data(1000) {} // 一个有一定构造开销的对象 }; static void BM_Reset(benchmark::State state) { auto ptr std::make_uniqueWidget(); for (auto _ : state) { // 使用reset直接替换 ptr.reset(new Widget()); benchmark::DoNotOptimize(ptr); } } BENCHMARK(BM_Reset); static void BM_ReleaseThenReset(benchmark:: state state) { auto ptr std::make_uniqueWidget(); for (auto _ : state) { // 先release手动删除再reset接管新的 delete ptr.release(); ptr.reset(new Widget()); benchmark::DoNotOptimize(ptr); } } BENCHMARK(BM_ReleaseThenReset);3.2 基准测试结果与解读在我的测试环境Linux, g 11.2, -O2优化下运行多次测试取中位数结果趋势如下测试用例平均耗时 (ns)相对比例BM_Reset约 2450 ns100% (基准)BM_ReleaseThenReset约 2380 ns97%注意这个具体数值严重依赖于Widget的构造/析构成本、编译器优化等级、内存分配器状态甚至CPU缓存。你的测试结果可能不同但关注相对关系更有意义。结果分析差异很小在Widget构造开销占主导的情况下两种方式的性能差异非常微小本例中约3%。这印证了之前的分析当资源对象本身的构造/析构成本很高时release和reset自身逻辑的开销差异会被掩盖。releasereset略快在某些测试轮次中releasereset的组合偶尔会表现出微弱的优势。这可能是由于它改变了内存操作的时序先释放再分配可能让内存分配器有机会复用刚刚释放的内存块减少了缓存失效或页面调度的开销。而reset在内部先保存旧指针、再赋值新指针、最后删除旧指针这个顺序可能不如手动控制来得“顺滑”。统计误差如此微小的差异可能落在统计误差范围内。不能仅凭此就断定releasereset绝对更快。更关键的发现当我将Widget的构造变得极其轻量例如只是一个int而将测试循环次数增加到千万级以放大内存管理操作本身的开销时情况发生了变化。reset版本有时反而会更稳定或略快一点。这是因为对于微小对象new/delete的开销占比变高而reset作为一个内联函数其内部逻辑可能被编译器优化得更好。手动delete则是一个独立的函数调用可能带来轻微的开销。4. 选择策略性能、安全性与代码清晰度的权衡所以到底该用哪个答案不是绝对的取决于你的首要考量。4.1 何时优先使用 reset() —— 安全与简洁至上在绝大多数情况下你应该默认使用reset()。这是现代C鼓励的做法。理由1异常安全。ptr.reset(new Resource(...))是一个原子性操作。如果new Resource抛出了异常内存不足、构造函数异常ptr仍然持有原来的对象状态不变不会发生资源泄漏。而delete ptr.release(); ptr.reset(new Resource(...));如果在新对象构造时抛出异常你已经手动删除了旧对象此时ptr为空旧资源已丢失且新资源也未建立可能导致逻辑错误虽然不会内存泄漏因为旧资源已释放。理由2代码清晰与维护性。一行reset清晰地表达了“用一个新的X替换掉当前管理的对象”的意图。而release组合则需要两行并且引入了裸指针增加了代码的复杂度和 reviewer 的心智负担。理由3与RAII理念一致。reset让资源的生命周期完全由unique_ptr的栈上生命周期控制是标准的RAII范式。实操心得在团队协作或大型项目中坚持使用reset()能减少潜在的bug。性能差异在99%的场景下都可以忽略不计。不要为了臆想中的“性能优化”而牺牲代码的安全性和可读性。4.2 何时考虑使用 release() —— 需要精细控制所有权的场景release()是为你需要“越狱”的时候准备的工具。它的使用场景相对特定场景1所有权转移给遗留代码或C接口。这是release()最经典的用途。void legacy_api_that_takes_ownership(Foo* foo); auto uptr std::make_uniqueFoo(); legacy_api_that_takes_ownership(uptr.release()); // 转移所有权API会负责delete场景2实现类似“移动并重置”的语义。当你需要将当前对象移出并同时将智能指针置为一个新值或空值时。std::unique_ptrState currentState std::make_uniqueStateA(); // 需要将当前状态移交给处理器并立即切换到下一个状态 State* stateToProcess currentState.release(); currentState std::make_uniqueStateB(); // 注意这里用的是赋值不是reset process(stateToProcess); delete stateToProcess;这里用release()比先用reset()再想办法保存指针更直接。场景3在性能极其敏感的循环中且有明确的性能剖析数据支撑。这是最需要谨慎的情况。只有当你通过性能分析工具如perf, VTune明确证实某段代码中reset()是热点且release()手动deletereset()的组合能带来可测量且显著的性能提升时才考虑使用。并且一定要加上详尽的注释说明为什么打破RAII惯例。4.3 一个综合性的决策流程图为了更直观地做选择可以参考下面的决策路径开始 | v 你需要替换 unique_ptr 管理的对象吗 | | 是 否 | | v v 考虑安全性、可读性、性能 你可能不需要这两个函数 | 或需要的是 get() 或移动语义 | v 默认选择ptr.reset(new Obj()) | v 是否满足以下所有条件 1. 此处是性能绝对热点有profiling证明 2. 对象构造/析构成本相对较低内存操作开销占比高 3. 你能保证手动 delete 不会破坏异常安全或此处绝不抛异常 | | 否 是 | | v v 坚持使用 reset() 可考虑delete ptr.release(); ptr.reset(new Obj()); (并添加详细注释) | v 结束5. 常见陷阱、疑难杂症与排查实录即使理解了原理在实际使用中还是会踩坑。下面是我和同事们遇到过的一些典型问题。5.1 release() 后的“空指针”访问与双重释放这是新手最容易犯的错误。std::unique_ptrint ptr(new int(10)); int* p ptr.release(); // 陷阱1误以为ptr还管理着对象 if (ptr) { // 错误ptr 现在为空条件永远为 false *ptr 20; } // 陷阱2对 release() 返回的指针管理不当 // 如果此处函数返回或抛出异常而没有 delete p则内存泄漏。 // 如果之后又调用了 delete p;而 p 已经被其他地方删除则双重释放。排查技巧养成习惯调用release()后立即将原始指针交给另一个RAII对象管理如另一个unique_ptr或者在一个非常局部的、逻辑清晰的范围内完成它的使用和释放。绝对不要让它“流浪”。5.2 reset() 参数求值顺序与资源泄漏这是一个更隐蔽的问题。reset(new Resource())并不是原子操作。它先计算参数new Resource()然后调用reset函数。如果new Resource()成功但在调用reset函数之前比如在计算其他参数时抛出了异常那么新分配的资源就会泄漏因为没有指针持有它。// 假设有一个可能抛异常的复制构造函数 ptr.reset(new Resource(anotherResource)); // 如果复制构造抛出异常new分配的内存就泄漏了相比之下使用std::make_unique则没有这个问题因为对象的构造和unique_ptr的创建是原子的。ptr std::make_uniqueResource(anotherResource); // 更安全但注意这是赋值不是reset如果必须使用new并且担心构造参数异常更安全的做法是先用一个局部unique_ptr接管再交换或移动。auto temp std::unique_ptrResource(new Resource(anotherResource)); // 即使异常temp析构也会清理 ptr.reset(temp.release()); // 不会抛异常的操作5.3 自定义删除器带来的复杂性当unique_ptr使用了自定义删除器时release()和reset()的行为需要额外注意。struct FileDeleter { void operator()(std::FILE* fp) const { if(fp) fclose(fp); } }; std::unique_ptrstd::FILE, FileDeleter filePtr(fopen(data.txt, r)); // release() 返回的是原始指针 FILE*但丢失了删除器信息 std::FILE* rawFile filePtr.release(); // 现在你必须知道要用 fclose 来关闭 rawFile而不能用 delete。 // 如果错误地使用了 delete rawFile将是未定义行为。重要原则对带有自定义删除器的unique_ptr调用release()你必须非常清楚返回的原始指针应该用什么方式释放并且手动匹配。reset()则无此烦恼它会自动调用正确的删除器。5.4 与相关网络热词问题的联想排查浏览提供的热词列表我发现一些错误可能与指针管理不当有关error while launching program: memory write error at 0x100000. cannot access ddr: the controller is held in reset这类内存访问错误虽然直接原因是硬件或驱动但在软件层面如果错误地使用了release()获得了野指针并访问或者reset()了不该重置的硬件寄存器指针也可能导致类似症状。connection reset,curl: (56) recv failure: connection reset by peer在网络编程中我们常用unique_ptr管理套接字连接对象。如果在回调函数中错误地调用了release()而没有妥善处理可能导致连接对象被提前销毁或重复关闭引发“connection reset”错误。waiting for another flutter command to release the startup lock...这个“release”是锁的释放与智能指针的release()概念相通。它提醒我们任何“释放”操作都必须与“获取”配对且所有权要清晰。智能指针的release()就是将所有权释放给调用者。6. 高级话题实现原理窥探与编译器优化要真正理解性能差异可以看看标准库可能的实现概念性代码// reset 的简化实现思路 templateclass T, class D void unique_ptrT, D::reset(pointer p) noexcept { pointer old ptr_; // 1. 保存旧指针 ptr_ p; // 2. 接管新指针 if (old) { get_deleter()(old); // 3. 用删除器释放旧资源 } } // release 的简化实现 templateclass T, class D pointer unique_ptrT, D::release() noexcept { pointer p ptr_; // 1. 保存当前指针 ptr_ pointer(); // 2. 将内部指针置空通常是nullptr return p; // 3. 返回保存的指针 }可以看到reset比release多了一个条件判断和删除器调用的开销。在现代编译器的强力优化下特别是当删除器是std::default_delete且operator delete能被内联或优化时这个开销可以变得非常小。然而如果删除器是复杂的函数对象或者释放操作本身很重如关闭数据库连接这个开销就可能被放大。编译器优化也会影响我们的选择。例如在release()deletereset()的组合中编译器可能无法将delete和后续的new合并优化因为它们是独立的语句。而reset(new ...)作为一个整体可能给编译器更多的优化空间。这也是为什么在微基准测试中结果可能波动的原因之一。最后我想分享一点个人体会。在早期我可能会热衷于在所有地方使用release()来追求那一点点“控制感”和想象中的性能提升。但随着项目经验增长我越来越倾向于使用reset()和std::make_unique。它们带来的代码安全性、清晰度和可维护性的提升远远超过那微乎其微的性能差异。只有在那些经过性能剖析证实的、真正的瓶颈处我才会小心翼翼地请出release()这把“手术刀”并且一定会配上详细的注释说明为什么这里必须打破常规。记住在C中最昂贵的开销往往不是一两个函数调用而是糟糕的设计、不清醒的所有权语义和难以调试的bug。让智能指针帮你管理资源把精力集中在更重要的逻辑上这才是高效编程的真谛。

相关新闻

YOLO系列在无人机目标检测中的实战应用与优化

YOLO系列在无人机目标检测中的实战应用与优化

1. 项目背景与核心价值去年夏天在山区参与一次搜救任务时,我亲眼目睹了无人机配合热成像仪在夜间找到失踪老人的全过程。那一刻我意识到,实时目标检测技术正在彻底改变传统搜救模式。而YOLO(You Only Look Once)作为当前最先进的实…

2026/10/13 9:43:42 阅读更多 →
Visual C++数值算法实战:从原理到高性能工程实现

Visual C++数值算法实战:从原理到高性能工程实现

1. 项目概述:为什么我们需要一本“Visual C数值算法实战集”?如果你在Windows平台上用C做过科学计算、数据分析或者图形图像处理,大概率遇到过这样的场景:项目里需要一个矩阵运算库,网上搜了一圈,Eigen、Ar…

2026/10/7 13:33:02 阅读更多 →
Unity打包报错“类型不存在于命名空间内”的根源与系统化解决方案

Unity打包报错“类型不存在于命名空间内”的根源与系统化解决方案

1. 项目概述:当Unity打包时告诉你“类型不存在于命名空间内” 相信每一位Unity开发者,在项目临近发布、满怀信心地点击“Build”按钮时,最怕看到的不是进度条,而是控制台突然蹦出的一串鲜红错误。其中,“类型不存在于…

2026/10/11 21:24:14 阅读更多 →

最新新闻

从六级到SCI一区:学术英语与论文发表全攻略

从六级到SCI一区:学术英语与论文发表全攻略

1. 项目背景与目标定位"六级阅读-我要发SCI一区"这个看似简单的标题,实际上揭示了科研工作者在学术英语能力提升与高水平论文发表之间的关键路径。作为经历过从英语基础薄弱到发表多篇一区论文的过来人,我深刻理解这个标题背后包含的两个核心诉…

2026/10/13 9:42:44 阅读更多 →
腾讯淡马锡押注2亿美元,鸣鸣很忙IPO背后的基石投资逻辑

腾讯淡马锡押注2亿美元,鸣鸣很忙IPO背后的基石投资逻辑

鸣鸣很忙的招股书一出,最抓眼球的就是那笔2亿美元的基石投资,以及腾讯、淡马锡这两个名字。1月28日上市,这意味着这家量贩零食连锁品牌正式踏入二级市场。很多人看到“腾讯加持”“淡马锡进场”就开始兴奋,但说实话,光…

2026/10/13 9:42:44 阅读更多 →
智慧园区数字孪生IOC:降本增效的ROI量化分析

智慧园区数字孪生IOC:降本增效的ROI量化分析

做了这么多年园区数字化项目,被甲方问得最多的问题永远是同一个:你这套数字孪生IOC上了之后,到底能省多少钱?说实话,早几年我也答不利索。那时候方案PPT里全是漂亮的三维模型,领导看完都说好看,…

2026/10/13 9:42:44 阅读更多 →
mirrord DB 分支 Schema Migrations 完全指南:让分支数据库带着你的迁移就绪

mirrord DB 分支 Schema Migrations 完全指南:让分支数据库带着你的迁移就绪

开发工具云原生CLI 【免费下载链接】mirrord Run any process, on your machine or in an AI agents environment, as if it were a pod in your Kubernetes cluster: real env vars, DNS, network, traffic. 项目地址: https://gitcode.com/gh_mirrors/mi/mirrord …

2026/10/13 9:42:44 阅读更多 →
机器学习大作业高分攻略:回归、分类与聚类实战解析

机器学习大作业高分攻略:回归、分类与聚类实战解析

简介:这是面向机器学习初学者的课程作业源码包,覆盖分类、回归与聚类三大经典任务,共八次高分作业,可直接用于期末大作业或课程设计参考,也能帮助零基础学生快速建立算法实战认知。压缩包合计四十八个文件,…

2026/10/13 9:42:44 阅读更多 →
机器学习评估体系:从泛化误差到业务指标的完整实践

机器学习评估体系:从泛化误差到业务指标的完整实践

1. 这不是背书,是建模前的“体检清单”——为什么第2章决定你80%的模型成败“西瓜书”第二章《模型的评估与选择》,表面看是教你怎么算准确率、画个ROC曲线、调个交叉验证的k值,但实操中我带过几十个刚入门的项目组,发现一个扎心事…

2026/10/13 9:41:42 阅读更多 →

日新闻

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

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

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

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 阅读更多 →