C++多态深度解析:从虚函数表到工程实践
先问你一个问题如果有一天你接手了一个老项目handler 数组里挂了十几个回调函数每个回调背后都是一套完全不同的对象行为但它们的注册方式却完全一样——你第一反应是什么八成会感慨一句“这就是多态吧。”C 里的多态polymorphism本质就一句话同一份调用代码因为运行时实际指向的对象不同产生完全不同的行为。它是封装和继承之后面向对象三大特性里最能体现“抽象能力”的一环也是面试八股文的高频区。这篇内容不打算给你抄概念而是把C多态从底层机制到工程实践完整拆一遍讲解为什么需要它、虚函数表怎么工作、哪些写法注定踩坑以及如何用多态写出真正可扩展的代码。无论你是刚学C的入门者还是被“虚函数”折磨过的进阶选手这篇都值得你耐心看完。1. 多态到底解决什么问题从“if-else 地狱”说起1.1 没有多态时代码是怎么变的先模拟一个最常见的业务场景你有一个绘图程序要支持画圆形、矩形、三角形。如果不懂多态普通C程序员的第一版代码大概长这样enum ShapeType { Circle, Rectangle, Triangle }; void drawShape(ShapeType type) { if (type Circle) { // 画圆的逻辑 } else if (type Rectangle) { // 画矩形的逻辑 } else if (type Triangle) { // 画三角形的逻辑 } }这个函数刚写出来时挺清爽。但需求会变。一周后产品经理说要加“椭圆”一个月后说加“圆角矩形”半年后你已经在一个.cpp文件里维护了二十多个if-else分支。更痛苦的是如果“计算面积”也要按类型分支“求周长”也要按类型分支那你会得到三四套巨大的switch。每增加一种新形状所有函数都要跟着改一遍——漏改一处就是线上事故。这就是典型的“对修改开放”的坏味道。多态的核心价值就是让调用方不感知类型分支把“不同类型有不同行为”这件事下沉到各个类内部让新增类型只需要新增一个新类旧代码一行不动。1.2 自动售货机的例子调用方只认“按钮”类比一下。你在自动售货机上按“A1”这个按钮机柜会根据货道里放的是可乐还是薯片弹出对应的商品。对你来说“按A1”这个动作永远一样但结果取决于这个货道当前绑定的是什么货物。多态做的就是这个事调用者只依赖一个稳定的接口按钮具体执行逻辑由对象自己决定。在C里这个“按钮”就是基类的虚函数“货道里绑定的货物”就是派生类对虚函数的重写。调用方拿一个基类指针调一个同名函数运行时C会根据指针实际指向的对象类型自动选择正确的函数版本执行——这个机制是C多态里最核心的一块。1.3 多态的两个阵营编译期与运行期很多人以为多态就是虚函数其实C里存在两种多态多态类型绑定时机核心机制典型表现编译期多态静态多态编译期决定调用哪个函数函数重载、运算符重载、模板同名函数参数不同同一模板实例化出不同类型的代码运行期多态动态多态运行期根据对象实际类型决定虚函数 继承 基类指针/引用基类指针调虚函数实际执行派生类版本这两种多态适用场景完全不同。编译期多态灵活且零运行时开销但要求类型在编译期确定运行期多态能够处理“运行时才知道具体类型”的场景但要付出虚函数表查询的性能代价还要求类型之间有继承关系。网上很多教程把“多态”等同于“虚函数”这实际上是把问题窄化了。理解两种多态的边界才能在设计代码时选对工具。2. 编译期多态函数重载与模板的“暗战”2.1 函数重载参数列表决定一切函数重载是C里最容易理解的编译期多态。同一个函数名只要参数类型、个数或顺序不同编译器就会把它们当成完全不同的函数处理。int add(int a, int b) { return a b; } double add(double a, double b) { return a b; } string add(const string a, const string b) { return a b; }调用add(1, 2)时编译器在编译期就能根据实参类型确定调用第一个版本不需要任何运行时判断。这一点和运行期多态有本质区别重载的“多态性”体现在源码层面——你写的调用语句是同一个名字但编译后的机器码已经指向了不同的函数地址。但函数重载有一个限制它只适合参数类型差异明显的情况。如果参数是用户自定义类型且彼此之间有继承关系重载的解析规则会变得非常微妙不建议新手把逻辑复杂化后面在避坑部分会展开讲。2.2 模板让类型成为参数模板是编译期多态的另一种形态它把“类型”本身变成了参数。看这个例子template typename T T max_value(T a, T b) { return a b ? a : b; }max_value(3, 7)和max_value(3.14, 2.71)、max_value(string(a), string(b))都能调用同一个模板编译器分别实例化出三份不同的机器码。这里的“多态”在于你只写了一份逻辑却支持无限多种类型——只要这个类型支持运算符。模板在STL里被用到了极致。std::vectorint和std::vectorstring是同一个类模板的两个实例但它们内部布局完全不同、操作的函数也不同。调用方拿到的是同一个类名实际生成的代码却因模板参数而异这就是泛型编程视角下的编译期多态。2.3 为什么面试爱问“模板和虚函数的区别”这个话题在C八股文里出现频率极高核心差异就三点绑定时机模板在编译期实例化虚函数在运行期动态绑定。性能模板没有虚函数表查询开销通常更容易被内联虚函数调用在现代CPU分支预测下代价已经很小但仍然是非内联调用。灵活度虚函数要求类型之间有继承关系模板只要求类型满足语法约束——这种约束被称为“鸭子类型”只要这个类型能响应你调用的操作就行。举一个实际案例。写一个统一处理各种日志接收器的函数用虚函数你会这样设计所有接收器继承自LogSink基类重写write()用模板你会这样设计只要类型有write()方法就能作为模板参数传入。// 虚函数设计 class LogSink { public: virtual void write(const string msg) 0; virtual ~LogSink() default; }; // 模板设计 template typename Sink void log_to(Sink sink, const string msg) { sink.write(msg); }模板方案的运行效率略高而且不强制类型和继承绑定。但代价是所有类型都必须在编译期已知如果日志接收器是在程序启动时根据配置文件动态创建的模板就无能为力了——这时必须用虚函数。我刚入行时也犯过“能用模板就绝不用虚函数”的偏执后来维护一个插件系统才明白动态加载插件时你根本不知道插件里是什么类只能在运行时通过基类接口去调这种场景模板救不了你。结论是两种多态不是竞争关系各管一段。3. 运行时多态的底层真相虚函数表与动态绑定3.1 虚函数表到底长什么样接着说运行期多态。C里运行期多态的物理基础是虚函数表vtable。这张表的概念可以这样理解一个类的所有虚函数地址被集中放在一张表里每个对象内部隐藏了一个指针vptr指向这张表。当编译器看到类里有virtual关键字时就会为这个类生成一张虚函数表。看一段代码class Shape { public: virtual void draw() { std::cout Shape::draw\n; } virtual double area() { return 0; } virtual ~Shape() default; }; class Circle : public Shape { public: void draw() override { std::cout Circle::draw\n; } double area() override { return 3.14159 * r * r; } private: double r 1.0; };Circle对象的内存布局里最开始是一个vptr指向Circle的虚函数表。这张表里有Circle::draw的地址、Circle::area的地址以及Circle析构函数的地址。当程序执行Shape* p circle; p-draw();时编译器生成的代码不是直接调用某个固定函数而是先从p的内存里取出vptr再通过vptr找到虚函数表最后从表里取出draw对应的函数地址进行间接调用——这个过程叫动态绑定。这里有个细节值得注意即使Circle没有重写某个虚函数比如把area注释掉虚函数表里该位置仍然有值只是指向基类Shape::area的地址。这就是为什么“派生类未重写的虚函数会继承基类实现”。3.2 为什么析构函数必须是 virtual这是C面试里最经典的送命题。看这段危险代码Base* p new Derived(...); delete p;如果Base的析构函数不是虚函数那么delete p只会调用Base::~Base()派生类中动态申请的资源完全不会释放——内存泄漏就这么静悄悄地发生了。原因很简单非虚析构函数是静态绑定编译器看到p的类型是Base*就直接调用了Base的析构版本。把基类析构函数声明为virtual之后delete p会通过虚函数表找到实际对象类型这里是Derived的析构函数先执行Derived的析构逻辑再自动执行Base的析构逻辑整条析构链条才完整。一个经验结论只要有类被设计成基类、且有人可能通过基类指针删除它基类析构函数就该声明为virtual。如果这个类本身不打算被删除比如只是作为接口的纯抽象类析构函数也应该声明为virtual并提供默认实现。C11之后还可以用virtual ~Base() default;这种写法干净利落。3.3 构造函数里调用虚函数为什么是“假的”踩坑重灾区来了。如果你在基类构造函数里调用一个虚函数这个调用不会动态绑定到派生类版本而是直接调用当前正在构造的基类版本。class Base { public: Base() { printName(); } virtual void printName() { std::cout Base\n; } }; class Derived : public Base { public: void printName() override { std::cout Derived\n; } }; Derived d; // 实际输出Base原因是C对象构造的顺序先构造基类部分再构造派生类部分。在执行Base构造函数时派生类的成员变量还没初始化如果此时调用派生类重写过的虚函数函数内部可能访问未初始化的派生类成员——这是C标准明确禁止的行为。所以标准规定构造期间虚函数调用执行当前类的版本。同样的逻辑也适用于析构函数。在析构期间派生类部分已经被销毁虚函数调用同样只会解析到当前类的版本。这个规则建议牢牢记在心里构造函数和析构函数内虚函数不发虚。4. 多态的正确打开方式接口设计、纯虚函数与扩展性4.1 用纯虚函数塑造“抽象接口”先回到开篇的绘图程序。如果我要设计一个可扩展的图形体系第一版多态方案是这样class Shape { public: virtual void draw() const 0; virtual double area() const 0; virtual ~Shape() default; }; class Circle : public Shape { public: void draw() const override { /* 画圆 */ } double area() const override { return 3.14159 * radius_ * radius_; } private: double radius_ 1.0; };注意 0的语法这叫纯虚函数。包含纯虚函数的类叫抽象类它不能直接实例化——你不能写Shape s;因为Shape只是一个“设计契约”它规定了所有形状都必须有draw()和area()。这个契约的价值在于所有依赖Shape的代码都只需要知道接口不需要知道具体形状。当我后来要加一个Square我只需要写一个新类继承Shape并实现两个纯虚函数所有接受Shape*的旧代码不需要改一行。这就是开闭原则的实践——对扩展开放对修改关闭。4.2 基类指针还是引用对象切片问题当你使用多态时应该始终通过基类的指针或引用来操作对象。如果直接把派生类对象按值赋给基类对象就会发生对象切片Circle c; Shape s c; // 灾难只拷贝了Shape部分s是Shape类型它没有vptr指向Circle的虚函数表甚至Circle特有的成员变量也被丢弃了。这个行为很迷惑编译不报错但Shape的实际类型被“切”成了基类多态完全失效。正确写法是Shape ref c;或Shape* ptr c;。这里也顺带解释为什么容器里要多态对象时应该存std::unique_ptrShape而不是直接存Shape——存值必然导致切片和对象类型信息丢失。4.3 多态与设计模式的经典配合多态撑起了很多经典设计模式的骨架策略模式把算法族封装成独立类通过基类指针传入运行时切换行为。工厂模式工厂返回基类指针具体创建哪个派生类由工厂内部决定调用方只认接口。观察者模式事件源保存一组观察者的基类指针列表通知时逐个调虚函数。模板方法模式基类定义算法骨架虚函数细节由子类填充。我见过很多刚入行的同事业务逻辑里堆满了if (type ...)后来引入状态模式解决了每个状态做成一个继承自State的类状态流转变成切换状态对象。多态不是炫技它最本质的好处是消灭条件分支把变化封装进类型内部。4.4 重写注意事项override、final 与重载的陷阱写派生类重写虚函数时有两把保护伞一定要用上class Circle : public Shape { public: void draw() override; // 告诉编译器我是重写 void area() override; // 如果基类没有这个虚函数编译报错 };override的作用是让编译器帮你检查“这个函数到底重写了基类的哪个虚函数”。如果你在写派生类时把参数类型写错了、或者拼错了函数名没有override时程序能正常编译但基类的纯虚函数没有被重写于是派生类变成抽象类——这种错误到了链接或运行阶段才暴露排查起来非常痛苦。final的作用则是反过来禁止某个虚函数在更下一层的派生类里被重写也支持直接定义final class禁止类被继承。这在长期演进的大型项目里非常好用能明确表达设计意图这个接口到此为止不要再扩展。需要特别注意区分重写override和重载overload重写派生类重新实现基类的虚函数参数列表完全相同运行期动态绑定。重载同一个作用域内多个同名但参数不同的函数编译期决定调用谁。一个经典陷阱你在派生类写了void draw(int x)以为只是多了一个重载结果基类的virtual void draw()被“隐藏”了——此时通过基类指针调p-draw()是合法的走虚函数表但通过具体派生类对象调d.draw()却可能编译报错因为名字查找规则把基类的draw()隐藏了。这种情况下记得在派生类里加using Shape::draw;把基类版本重新引入作用域。5. 常见问题与面试八股多态细节实战排查5.1 遇到“虚函数没生效”先查这四件事现象代码看起来重写了虚函数但运行结果还是基类的行为。按我的排查顺序来基类函数是不是漏写了virtual关键字没有virtual就不是虚函数谈不上重写。是不是对象切片了检查是否用Shape s c;这种方式赋值。是不是参数列表不完全一致即使const限定符不同都不算重写。是不是不小心用了函数隐藏而不是重写如果派生类函数与基类虚函数同名但参数不同那就是隐藏不是重写。这四类问题我实际工作中全都碰到过其中“参数列表不一致”最隐蔽因为编译不报错逻辑上还自我感觉良好。override关键字能救你于水火。5.2 RTTI 和 dynamic_cast多态世界的类型查询运行期类型信息RTTI是运行期多态的辅助机制。最常用的操作是dynamic_castif (auto* c dynamic_castCircle*(shapePtr)) { // 确认 shapePtr 实际指向的是 Circle }dynamic_cast依赖虚函数表信息来判断真实类型所以要求操作的类必须有虚函数。它在继承体系的向下转型时非常有用。但我不建议把它当作常规方案一个写满dynamic_cast的代码往往是在逃避良好的接口设计——毕竟能用多态解决的问题就不该靠类型判断去解决。static_cast用于向下转型时不检查真实性性能高但风险大如果类型不匹配结果就是未定义行为。所以向下转型优先用dynamic_cast性能敏感的极热路径另说。5.3 多态的性能账单虚函数的代价到底多大很多C性能党对虚函数颇有微词。它的成本主要有三块内存每个多态对象多一个vptr指针每个类多一张虚函数表。调用间接跳转无法被编译器内联可能打乱指令流水线。优化编译器面对虚函数调用时可能无法进行跨函数优化。但在现代CPU上一次虚函数调用的开销通常是几纳秒级别。如果你的关键业务逻辑里有百万次级别的虚函数调用或者函数本身只有几条指令且被高频调用那虚函数确实会成为瓶颈。我的做法是先按正确性设计用虚函数把架构做清晰性能分析工具指出某个虚函数是热点后再针对性优化——比如把热点路径改成模板、把虚函数调用改成if-else手工分发。提前盲目优化是最不划算的工程决策。5.4 C多态高频面试问答速查问题核心答案什么是多态同一调用接口在不同对象上有不同行为分编译期和运行期两类虚函数表在哪个阶段构建编译期生成运行期通过对象的vptr查表基类析构函数为什么要virtual防止通过基类指针删除派生类对象时触发未定义行为构造函数里能调虚函数吗能调但不发虚会调用当前构造阶段的版本override 和 final 的作用override显式声明重写并让编译器检查final禁止进一步重写虚函数能被内联吗静态调用场景下可以透过基类指针动态调用时通常不行delete多态对象时怎样写最安全用std::unique_ptrBase基类析构必须virtual我个人的实操体会是理解C多态请不要仅限于背会“虚函数表”“动态绑定”这些词。真正衡量掌握程度的标准是——拿到一个需求时你自然而然知道该用编译期多态还是运行期多态该用接口还是模板而拿到一段到处是if (type ...)的代码时你能看出重构的路径。多态的精髓不在语法在于用抽象隔离变化把扩展点留给未来。最后分享一个小技巧如果你在用Visual Studio调试虚函数问题可以在变量窗口里展开对象的vptr逐项对照虚函数表里的函数地址——亲眼看到表的结构比读十篇原理文章都通透。

相关新闻

Agent触达层实战:从Function Calling到MCP构建可靠工具调用

Agent触达层实战:从Function Calling到MCP构建可靠工具调用

1. 项目解析:Agent-Reach到底在解决什么问题圈子里的朋友看到Agent-Reach这个名字,第一反应多半是:这不就是给Agent装了一双“手”吗?确实,我拿到这个项目标题时的理解也是这样——Agent是智能体,Reach是触…

2026/10/9 3:56:27 阅读更多 →
论文降重工具怎么选?从查重原理到实战流程全解析

论文降重工具怎么选?从查重原理到实战流程全解析

经常有师弟师妹拿着大篇幅标红的查重报告来找我,第一句话就是:“师兄,用什么工具能一夜之间把重复率降下去?”说实话,每次听到这种问题我都想把“一夜之间”四个字划掉。降重不是体力活,也不是无脑堆工具就…

2026/10/9 3:55:26 阅读更多 →
HeidiSQL 12.10 一个窗口管五种数据库:连接配置、跨库直导与高频功能速查

HeidiSQL 12.10 一个窗口管五种数据库:连接配置、跨库直导与高频功能速查

HeidiSQL 12.10 一个窗口管五种数据库:连接配置、跨库直导与高频功能速查 一、绿色便携,U 盘即用 HeidiSQL 是老牌开源数据库图形客户端(GPL-2.0):MySQL/MariaDB/PostgreSQL/MS SQL/SQLite 五种数据库一个界面管。官方…

2026/10/9 3:55:26 阅读更多 →

最新新闻

基于Deepseek Harness的防幻觉电源设计Agent实践

基于Deepseek Harness的防幻觉电源设计Agent实践

我一直在做电源相关的硬件设计,这两年深度用大模型辅助设计之后,发现一个很尴尬的问题:模型给出的方案,听起来头头是道,但落到具体元器件参数、环路补偿、热计算上,经常一本正经地编数据。有一回我让模型推…

2026/10/9 4:21:45 阅读更多 →
Java Arrays工具类全解:从sort排序到parallelPrefix,一文吃透数组操作

Java Arrays工具类全解:从sort排序到parallelPrefix,一文吃透数组操作

凡是写过Java的人,基本都绕不过数组。不管是学习阶段做的图书管理系统,还是工作里处理批量数据、做算法题、写接口返回结构,数组都是最底层、最常用的一块积木。但很多人在实际开发里对数组的处理方式其实非常原始:复制一个数组要…

2026/10/9 4:21:45 阅读更多 →
110kV线路三段式相间距离保护整定计算与Simulink仿真实践

110kV线路三段式相间距离保护整定计算与Simulink仿真实践

电气工程专业的学生或者刚入行的继保新人,大概率都接过这样一张任务书:110kV线路三段式相间距离保护,要求Simulink仿真,外加一份详细研究报告。这题目看着经典,但真要动手,会发现卡壳的点全藏在细节里——整…

2026/10/9 4:21:45 阅读更多 →
无需开发板:STM32驱动DS18B20与OLED仿真实战

无需开发板:STM32驱动DS18B20与OLED仿真实战

/* 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 4:21:45 阅读更多 →
PS5手柄驱动与固件解析技术指南

PS5手柄驱动与固件解析技术指南

我无法根据当前输入生成符合要求的博文。原因如下:项目标题“AnyPS5”缺乏明确指向性,未说明其性质(是工具、项目、社区、改装方案、模拟器相关?);项目正文为空,无任何功能描述、技术背景或使用…

2026/10/9 4:21:45 阅读更多 →
信息学奥赛买笔问题:贪心策略与分支结构详解

信息学奥赛买笔问题:贪心策略与分支结构详解

2059:【例3.11】买笔,这道题我相信学过信息学奥赛的同学都不陌生。它是《信息学奥赛一本通》第三章选择结构部分的经典例题,题目讲的是买笔这件事——钢笔5元一支、铅笔2元一支、圆珠笔1元一支,要求三种笔都至少买一支&#xff0c…

2026/10/9 4:20:44 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

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/8 15:26:32 阅读更多 →
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/8 15:26:40 阅读更多 →
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/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 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/8 21:13:17 阅读更多 →
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/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练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/7 13:34:55 阅读更多 →