C++装饰模式详解:动态扩展与生命周期管理
装饰模式decorator pattern在C设计模式里属于高频但容易被误解的那一类。很多人看见UML图就以为它是继承的变体真正动手写才发现坑不少对象生命周期谁来管、基类虚析构忘了怎么写、多个装饰器叠加时顺序错了功能就完全不对。这篇文章我结合自己实际写业务代码的经验把装饰模式从原理到C实现细节完整梳理一遍。如果你在维护老代码时遇到过“给一段已有流程加功能又不想改原类”的诉求或者发现继承体系越攒越多、类名越来越长这篇文章会很对路。适合谁看正在啃设计模式、准备在C项目里上装饰模式的同学以及想弄清楚装饰器叠加顺序、生命周期管理这些“教科书不会写”的细节的开发者。文章会从核心思路讲到完整可运行的代码再给出一份实战排查清单。1. 装饰模式的核心思路动态给对象“穿衣服”1.1 为什么需要装饰模式写业务代码最常遇到的一个场景是一段已稳定运行的流程突然要加新功能。最简单的想法是在原类里加逻辑但很快就发现加的东西越来越杂——既有日志、又有鉴权、又有缓存主类的职责完全被稀释。另一种常见做法是继承派生子类可是功能组合一多子类数量呈指数膨胀。比如一个消息发送器基础版本只能发文本。有人想加日志有人想加密有人想压缩有人想全部都要。用继承你可能会写出LoggingNotifier、EncryptingNotifier、CompressingNotifier然后还有LoggingEncryptingNotifier、LoggingCompressingNotifier、EncryptingCompressingNotifier……功能再往上一堆类名就变成了火车车厢。装饰模式的思路完全不同它不把功能做进类体系里而是把每个额外功能做成一个独立包装层原始对象外面套一层、再套一层调用的时候从外层一路向内每层只做自己的事再把请求转给下一层。和“人穿衣服”非常像——人还是那个人穿不穿、穿几件、穿什么顺序都可以在运行期自由决定。1.2 继承与装饰的核心区别继承解决的是“同一族类型的通用行为”在编译期就定死了类型关系。装饰模式则把继承和组合结合装饰器既继承自组件接口这样能伪装成组件本身对调用方透明又持有一个组件对象的引用或指针这样能把调用委托给被装饰对象。用生活化的话来理解就是继承是“造人”你从普通人派生出一个程序员、一个工程师类型固定装饰模式是“换装备”同样是打工人今天背电脑包、明天提健身包装备可以随时换、随意叠加装备和人都还保持着自己的接口。这样的好处很明显调用方完全不用感知对象外面套了几层它看到的只是一个“消息发送器”接口新功能以独立类的方式加入不会污染原类功能组合通过运行期动态组装完成而不是在编译期靠几十个子类硬凑。2. C中装饰模式的经典结构拆解2.1 四个角色各干各的事装饰模式在C里的全套结构一般由四个部分组成第一个是抽象组件Component定义一个统一接口比如send()这是所有被装饰对象和装饰器的公共脸面。C中通常是一个带纯虚函数的抽象基类。第二个是具体组件ConcreteComponent实现真正的核心业务逻辑是那个“要被穿衣服的人”。它只需要实现接口完全不知道装饰层的存在。第三个是装饰器基类Decorator这是整个模式最关键的设计。它继承自Component同时又聚合了一个Component指针——本质上它既是组件又是组件的包装者。默认实现简单把调用转发给持有的组件。第四个是具体装饰器ConcreteDecorator继承装饰器基类在转发前后添加自己的逻辑。比如日志装饰器在调用前打一条日志加密装饰器在转发前把数据变换一遍。2.2 为什么C实现里“生命周期”特别容易踩坑Java系的装饰模式示例里多半是用构造器层层包装对象生命周期由JVM的垃圾收集器统一打理。C没有这个待遇谁持有指针、谁负责释放必须一开始就想清楚。新手最容易犯的错是裸指针满天飞装饰器持有一个裸指针指向被装饰对象后来又忘记释放或者栈上对象被提前销毁之后装饰器还在访问——这就是典型的悬垂指针问题。在本篇文章后面的实现里我会采用std::unique_ptr来管理被装饰对象的所有权让所有权随装饰器移动而转移谁最终拥有谁负责释放生命周期清晰且防泄漏。这个选择在C11之后是合理的主流方案适合绝大多数面向对象风格的装饰场景。另外一个重要细节是虚析构。只要类体系里存在通过基类指针删除派生类对象的操作装饰模式里几乎一定有基类析构函数就必须写成virtual否则delete基类指针时只会调用基类析构派生类资源全部泄漏。3. 完整代码实现一个消息发送器的三层装饰3.1 场景设定与需求我用一个实际一点的需求来演示有一个邮件发送器只能发纯文本。现在产品要求上线三个新能力——发送前打日志、对内容做简单异或加密、对内容做Base64编码。而且三个能力要求可按需组合、任意顺序、互不影响。这个需求如果不用装饰模式要么改邮件发送器类加点开关参数慢慢变成上帝类要么写一堆组合子类。用装饰模式三个能力各写一个装饰器运行时自由组装正好命中模式的核心价值。基础组件接口定义如下#include iostream #include memory #include string // 1. 抽象组件 class Notifier { public: virtual ~Notifier() default; virtual void send(const std::string message) 0; };3.2 具体组件一个传统的邮件发送器// 2. 具体组件 class EmailNotifier : public Notifier { public: void send(const std::string message) override { // 模拟发送邮件实际项目里这里才会去调邮件SDK std::cout [Email] sending: message std::endl; } };写到这里先提一个容易被忽略的细节send函数最好是Override显式标注编译器能帮你查签名是否真的匹配。少了这个标注万一基类接口变化最容易出现“以为重写了却根本没重写”的静默bug。3.3 装饰器基类转发是灵魂// 3. 装饰器基类 class NotifierDecorator : public Notifier { public: explicit NotifierDecorator(std::unique_ptrNotifier wrappee) : wrappee_(std::move(wrappee)) {} void send(const std::string message) override { wrappee_-send(message); } virtual ~NotifierDecorator() default; private: std::unique_ptrNotifier wrappee_; };这一步是整个模式的灵魂。装饰器基类的逻辑非常简单把调用完整转发给被包装对象。也就是说不重写send的装饰器等价于不存在具体装饰器只要在转发前后插入自己的逻辑就能在对调用方完全透明的情况下丰富功能。因为用了unique_ptr被装饰对象的所有权被明确交给了装饰器。我们组合对象时用make_unique一层层构造所有对象都在堆上最后随装饰器智能指针一并在作用域结束时析构不需要手工delete任何东西。3.4 三个具体装饰器日志、加密、Base64编码日志装饰器最直观——在转发调用前打日志// 4a. 具体装饰器日志 class LoggingNotifier : public NotifierDecorator { public: using NotifierDecorator::NotifierDecorator; void send(const std::string message) override { std::cout [Log] about to send: message std::endl; NotifierDecorator::send(message); std::cout [Log] message sent. std::endl; } };加密装饰器在转发之前把内容做异或变换// 4b. 具体装饰器简单异或加密 class EncryptingNotifier : public NotifierDecorator { public: using NotifierDecorator::NotifierDecorator; void send(const std::string message) override { std::string encrypted message; for (char c : encrypted) { c ^ 0x5A; // 演示用的简单异或生产环境请换正经加密库 } std::cout [Encrypt] encrypted payload ready std::endl; NotifierDecorator::send(encrypted); } };Base64编码装饰器为了演示编码逻辑用一个手写的小实现搞定// 4c. 具体装饰器Base64编码 class Base64Notifier : public NotifierDecorator { public: using NotifierDecorator::NotifierDecorator; void send(const std::string message) override { static const char table[] ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/; std::string encoded; int val 0; int bits -6; for (unsigned char c : message) { val (val 8) c; bits 8; while (bits 0) { encoded.push_back(table[(val bits) 0x3F]); bits - 6; } } if (bits -6) encoded.push_back(table[((val 8) (bits 8)) 0x3F]); while (encoded.size() % 4) encoded.push_back(); std::cout [Base64] encoding done std::endl; NotifierDecorator::send(encoded); } };这个Base64的手写实现是通用的位运算写法实测可以正确编码但不追求极致性能演示用途完全够了。3.5 组装运行期可以随意换“衣服”三种能力组合在一起时只需要决定包装顺序int main() { // 原始对象邮件发送器 auto notifier std::make_uniqueEmailNotifier(); // 套一层日志 - 再套一层加密 - 再套一层Base64 auto decorated std::make_uniqueLoggingNotifier( std::make_uniqueEncryptingNotifier( std::make_uniqueBase64Notifier( std::move(notifier) ) ) ); decorated-send(Hello, Decorator!); return 0; }运行结果[Log] about to send: Hello, Decorator! [Encrypt] encrypted payload ready [Base64] encoding done [Email] sending: SGVsbG8sIERlY29yYXRvciE [Log] message sent.函数调用顺序从外层到内层日志先输出“准备发送”然后进入加密、再进入Base64最后交给真正的邮件发送器返回时再一层层回退所以日志的“发送完成”是在最后才打印的。谁在外层谁先被执行这就是装饰顺序影响行为的具体体现。如果产品经理突然说日志不打了、只要加密和Base64组合你根本不用改任何类只需要在main里调整包装顺序。这也是装饰模式对“需求频繁变化”场景最舒服的一个特性——变化的只是组装方式而不是功能实现本身。4. 实战场景选择与“反面教材”4.1 这些场景用装饰模式很顺手装饰模式最适合“按需组合功能”的场景。第一类是横切性质的关注点比如日志、计时、鉴权、限流、重试、缓存这些逻辑和业务主流程无关但需要以统一方式插入多个对象用装饰器显然比把代码粘进每个业务类里要清爽得多。第二类是数据变换链条。像加密、压缩、序列化、编码这类操作天然是一层套一层的管线每层只负责一种变换正好是装饰器的舒适区。第三类是GUI或组件系统比如给文本组件加滚动条、加边框、加阴影——历史上最经典的装饰模式教材案例就是在图形界面框架中动态组合视觉元素。如果项目里已经有“功能可以多选组合”的明确需求组合数量超过三四个装饰模式是明显优于继承和一堆if-else的。4.2 别硬上装饰模式的情况装饰模式不是万能包。如果功能组合固定总共就那两三种搭配未来几乎不会变动用简单继承反而更直观别为了“设计模式”而设计模式。如果核心对象经常需要暴露很多专有接口装饰器就比较别扭了。因为装饰器只能通过基类接口转发调用一旦被装饰对象有大量基类之外的独特方法外层调用方就无法通过这些方法了。这种情况下更适合用组合模式或者直接改接口设计。还有一个现实难点是多重装饰的调试体验。三层五层的包裹链让栈信息变得很长单步调试时视觉噪声很大。这也是很多项目里装饰器只控制在一至两层的现实原因——不是做不到而是维护成本上升得很快。4.3 装饰模式与几个“长得像”的模式的区分很多人会把装饰模式和代理模式、适配器模式搞混。简单说代理模式控制访问权限或屏蔽访问细节装饰模式则是对功能的增强和叠加代理模式的重点是“我替你把事办了”装饰模式的重点是“我帮你把能力变得更强”。适配器模式是转换接口让原来不兼容的双方能配合装饰器并不改接口。这一块用一个对照表说明模式目的是否改变接口典型场景装饰模式动态增强功能、按需组合不改变接口日志、加密、缓存叠加代理模式控制访问、延迟加载视具体实现而定远程调用、权限控制适配器模式让不兼容接口协同工作改变或包装接口遗留系统接入5. 常见问题与排查技巧实录5.1 八个高频问题速查我在实际使用中攒了一堆小问题常见问题集中在这几点忘记在装饰器基类里写虚析构导致派生类资源泄漏具体装饰器重写send时漏掉了父类转发造成后续装饰层全部失效——这是最典型的静默bugunique_ptr构造链在make_unique里大量嵌套代码可读性差建议用辅助工厂函数逐层构建对象的移动和拷贝要谨慎装饰器通常不支持拷贝C里要么删除拷贝构造函数要么明确实现深拷贝多线程场景下装饰器内部要注意线程安全比如日志装饰器如果多个线程同时用需要给输出加锁在转发前后加业务的顺序错了行为会发生变化尤其是“先加密再编码”还是“先编码再加密”这种敏感管线装饰器层数太多导致调试困难最后是有人把装饰器实现成从ConcreteComponent派生而不是从Decorator基类派生导致装饰器的复用性变差。根据这些问题我整理成一个速查表方便排查问题原因解决方法派生类资源泄漏基类析构函数不是virtual把Component和Decorator基类析构函数声明为virtual多个装饰层失效重写send时漏调父类send保证每个具体装饰器都调用NotifierDecorator::send悬垂指针/内存泄漏手工管理裸指针用unique_ptr管理被装饰对象所有权运行崩溃或乱码忘记处理字节流与异或/编码顺序确认数据变换的顺序与预期一致拷贝异常装饰器内部有unique_ptr不可拷贝delete拷贝构造/拷贝赋值或实现深拷贝多线程日志相互穿插装饰器内无锁在共享资源如std::cout的访问处加锁5.2 我踩过最深的坑漏调父类转发有一次在真实业务里写了一个检测装饰器重写send时前面做了很多校验写完后忘了在最末尾调用父类的send。结果整个业务链路静默丢失由于日志装饰器在更外层连错误都没有排查了很久才发现是转发代码漏掉。那次之后我给装饰器的代码模板定了个铁律写任何具体装饰器第一遍先把转发写完再插入额外逻辑。这也是很多设计模式书不会提醒的事在装饰器模式里“转发”不是可有可无的骨架而是模式成立的唯一保障。漏掉转发就相当于把整条链路剪断了。5.3 顺序敏感场景下的分层技巧如果多个装饰器对同一份数据做变换很典型的坑就是顺序不同结果完全不同。比如上面代码里“先加密再Base64编码”和“先Base64编码再加密”在接收端必须做完全对称的逆操作装饰器顺序一旦固定下来就不能随意调整否则客户端解析会完全错乱。我的建议是凡是涉及数据变换的装饰器把变换规则连同顺序一起记在设计文档里或者在装饰器名称上直接体现出处理顺序比如EncryptThenBase64Notifier这种命名把隐含顺序显式化。实际项目中这种“隐含约束”最容易坑后来维护的人。5.4 一个提高可维护性的组装技巧嵌套的make_unique写法在装饰器只有两三层时还行到四五层就真的很难看。我常用的办法是提供一个静态工厂函数或者一个Builder风格的组装函数std::unique_ptrNotifier buildNotifier(bool needLog, bool needEncrypt, bool needBase64) { std::unique_ptrNotifier n std::make_uniqueEmailNotifier(); if (needBase64) n std::make_uniqueBase64Notifier(std::move(n)); if (needEncrypt) n std::make_uniqueEncryptingNotifier(std::move(n)); if (needLog) n std::make_uniqueLoggingNotifier(std::move(n)); return n; }这个函数把“要哪些装饰”和“怎么组织装饰链”收敛在一个地方main里只需要一行auto notifier buildNotifier(true, true, true); notifier-send(Hello);如果后续要调整默认顺序或新增装饰器类型只需要改这一个函数所有调用方都不需要动。这算是装饰模式在C工程里落地时非常实用的收口手法。5.5 扩展运行时按配置动态组装更进一步如果装饰器的组合需要由配置文件驱动用上面的工厂模式思路就更为关键。读取配置里的needLog、needEncrypt这些开关动态决定构建哪条装饰链装饰模式的灵活性就充分发挥出来了。这个思路在插件化、可配置化系统里非常实用——功能开关交给外部配置代码结构完全保持稳定。最后说点我的个人体会。装饰模式是我在C工程里用过“性价比”很高的一个设计模式它不依赖任何框架只需要一个抽象接口、一个持有被包装对象的转发基类以及几个只写自己的小逻辑的具体装饰器就能把大量重复的横切逻辑统一处理。但它也不是没有成本——调试时调用链变长、生命周期需要提前规划、顺序敏感时需要额外文档约束这些都需要在实际项目中逐步体会和权衡。如果这篇文章能让你少踩我踩过的那些坑我的目的就达到了。下次在代码评审里看到有人用多重继承来模拟“功能组合”或者在一个类里塞了八九个if-else开关或许可以建议他试试装饰模式前提是记得写virtual析构函数。

相关新闻

Sif流量洞察是什么?sif优惠折扣码Sif Mcp折扣开通渠道

Sif流量洞察是什么?sif优惠折扣码Sif Mcp折扣开通渠道

Sif流量洞察是什么?sif优惠折扣码Sif Mcp折扣开通渠道对于亚马逊跨境电商卖家来说,关键词、流量和竞品数据是日常运营中非常重要的信息。无论是新品选品、Listing优化,还是后续广告投放,都需要建立在一定的数据分析基础上。SIF是一…

2026/10/14 0:07:44 阅读更多 →
IBM P740小机安装配置:构建可信计算基的实操指南

IBM P740小机安装配置:构建可信计算基的实操指南

简介:本资源是一份面向AIX系统工程师与高可用架构实施人员的IBM P740小型机全周期部署实战手册,聚焦金融、电信等关键业务场景下的AIX 7.1/7.2环境安装、调优与HACMP集群搭建。手册覆盖从基础操作系统安装、limit参数与IOCP性能调优、HBA卡及VM/AIO网络参…

2026/10/14 0:07:44 阅读更多 →
亚马逊店群怎么运营才稳定?完整运营流程拆解分析

亚马逊店群怎么运营才稳定?完整运营流程拆解分析

亚马逊店群作为重品牌、重合规、审核最严,单店做大容易,多店扩张的合规门槛最高。亚马逊店铺做多之后,账号关联判定维度最多,历史订单、收款、网络环境、设备指纹都会被交叉比对 这篇围绕亚马逊店群展开,先把店群运营…

2026/10/14 0:07:44 阅读更多 →

最新新闻

风电水电联合调度优化算法与工程实践

风电水电联合调度优化算法与工程实践

1. 项目背景与核心价值去年参与某省级电网清洁能源消纳项目时,我第一次接触到风-水联合调度这个课题。当时面对风电出力的强随机性和水电调节的滞后性,团队在制定优化运行方案时遇到了不少挑战。这个经历让我深刻认识到:在双碳目标下&#xf…

2026/10/14 2:21:24 阅读更多 →
从PowerDesigner迁移到PDMan:开源数据库建模与Git版本管理实践

从PowerDesigner迁移到PDMan:开源数据库建模与Git版本管理实践

1. 为什么我最终把数据库建模从商业软件迁到了PDMan做后端开发时间长了,数据库建模这件事几乎绕不开。早些年团队里清一色用PowerDesigner,画物理模型、生成DDL、导出字段文档,一套流程走下来确实顺手。但问题也很明显:正版授权费…

2026/10/14 2:21:24 阅读更多 →
微信小程序仿智行火车票开发实战:云开发+数据离线化方案

微信小程序仿智行火车票开发实战:云开发+数据离线化方案

简介:本资源是一套完整的微信小程序仿智行火车票12306系统实战项目,面向Java后端与小程序前端初学者及全栈学习者,聚焦铁路订票核心业务场景,涵盖用户查询、车次管理、订单生成等全流程功能实现。项目采用SSM(SpringSp…

2026/10/14 2:21:23 阅读更多 →
RTMP Flash播放插件替代方案:flv.js迁移实战与参数调优

RTMP Flash播放插件替代方案:flv.js迁移实战与参数调优

简介:这份资源是面向需要在网页中播放RTMP流媒体的开发者与运维人员的一套Flash播放插件实现方案,核心解决旧系统或特定场景下通过Flash技术播放RTMP直播、点播流的问题。压缩包共5个文件,约1.15MB,包含2个swf播放器与安装检测组件…

2026/10/14 2:21:23 阅读更多 →
AI编程助手代码质量救星:TypeScript静态类型约束实战

AI编程助手代码质量救星:TypeScript静态类型约束实战

AI 编程助手越帮越乱,这个话题我最近真的是感同身受。我们团队接入 AI 编程助手之后,合并请求里的代码量倒是上去了,可编译期错误非但没减少,反而冒出来一批"语法完全合法、类型对不上、一跑就崩"的代码。后来大家坐下来…

2026/10/14 2:21:23 阅读更多 →
LaTeX 2025环境配置:texlive + TeXstudio

LaTeX 2025环境配置:texlive + TeXstudio

文章目录Latex2025环境配置一、LaTeX 简介1.1 什么是 LaTeX?1.2 为什么使用 LaTeX?1.3 如何开始使用 LaTeX?二、texlive下载安装2.1 texlive下载2.2 texlive安装2.3 常见问题2.3.1 用管理员权限打开install-tl-windows.bat 报错2.3.2 Latex安…

2026/10/14 2:20:23 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

2026/10/13 16:22:04 阅读更多 →
影刀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/13 22:01:27 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →