从零实现C++ unique_ptr:深入理解独占所有权与RAII机制
1. 项目概述从“智能指针”到“独占所有权”在C的世界里内存管理一直是开发者必须直面的核心挑战。手动new和delete的配对操作稍有不慎就会导致内存泄漏、悬垂指针或双重释放等顽疾。C11标准引入的“智能指针”家族正是为了解决这一痛点将资源管理的责任从程序员肩上转移到对象生命周期上。其中std::unique_ptr以其独特的所有权语义和近乎零开销的抽象成为了现代C资源管理的基石。简单来说unique_ptr是一个“独占式智能指针”。它意味着对所指资源拥有唯一且排他的所有权。一个资源在任何时刻只能由一个unique_ptr对象所“拥有”。当这个unique_ptr被销毁例如离开作用域时它所拥有的资源也会被自动、确定性地释放。这种设计完美契合了RAII资源获取即初始化思想将资源的生命周期与对象的生命周期严格绑定。为什么我们需要自己动手实现一个unique_ptr市面上成熟的实现如GCC的libstdc、Clang的libc已经非常完善。但对于深入理解C核心机制——如模板、移动语义、资源管理模型——而言亲手实现一遍是无可替代的学习路径。它能让你透彻理解所有权转移的本质如何通过移动构造函数和移动赋值运算符实现资源的“交接棒”。零开销抽象如何设计使得智能指针在运行时几乎不带来额外负担。定制删除器如何通过模板和类型擦除技术支持灵活的资源释放策略。与原始指针的互操作如何安全地提供get()、release()、reset()等接口。接下来我们将从零开始构建一个名为UniquePtr的简化版实现一步步揭开其神秘面纱。我们的目标是实现核心功能保持接口与std::unique_ptr高度相似并在此过程中深入每一个设计决策背后的“为什么”。2. 核心设计思路与类结构拆解一个unique_ptr的核心任务非常明确安全地持有一个指向动态分配对象的指针并确保在其生命周期结束时正确释放该对象。围绕这个核心我们可以拆解出几个关键的设计维度。2.1 所有权模型移动而非复制这是unique_ptr最根本的特性。它不能被拷贝只能被移动。这意味着所有权是线性的、传递的杜绝了多个指针指向同一资源可能引发的混乱。在C11中我们通过删除拷贝构造函数和拷贝赋值运算符同时提供移动构造函数和移动赋值运算符来实现这一点。template class UniquePtr { public: // 禁止拷贝 UniquePtr(const UniquePtr) delete; UniquePtr operator(const UniquePtr) delete; // 允许移动 UniquePtr(UniquePtr other) noexcept; UniquePtr operator(UniquePtr other) noexcept; // ... 其他成员 private: T* ptr_; };注意移动操作必须标记为noexcept。这对于标准库容器如std::vector在重新分配内存时优化性能至关重要。如果移动构造函数可能抛出异常容器将被迫使用拷贝操作影响效率。2.2 模板化与资源类型unique_ptr需要能够管理任意类型的对象因此它必须是一个模板类。模板参数T代表所指对象的类型。内部的存储成员是一个T*类型的原始指针ptr_。template class UniquePtr { // ... private: T* ptr_ nullptr; // 核心持有资源的原始指针 };2.3 删除器的支持默认情况下unique_ptr使用delete或delete[]来释放资源。但现实场景中资源释放方式可能多种多样可能是fclose关闭文件可能是SDL_FreeSurface释放SDL表面也可能是自定义的内存池回收操作。因此unique_ptr的第二个模板参数用于指定删除器类型。删除器是一个可调用对象接受一个T*参数并执行释放操作。我们可以通过模板参数默认值来提供默认删除器。template class UniquePtr { public: // 使用默认删除器释放资源 ~UniquePtr() { if (ptr_) { Deleter()(ptr_); // 构造一个删除器实例并调用 } } private: T* ptr_ nullptr; };这里的设计巧妙之处在于删除器的类型是模板的一部分。这意味着对于不同的删除器类型编译器会实例化出不同的UniquePtr类型。如果删除器是无状态的例如一个空类得益于空基类优化它不会增加UniquePtr对象的大小实现了“零开销抽象”。2.4 指针操作符的重载为了让UniquePtr用起来像一个真正的指针我们需要重载operator*和operator-。T operator*() const { return *ptr_; } T* operator-() const { return ptr_; }这两个操作符都应该是const成员函数因为它们不改变UniquePtr对象本身即不改变所有权只是提供了访问其所管理对象的途径。2.5 辅助接口get release resetget(): 返回内部保存的原始指针不释放所有权。用于需要向兼容C的API传递指针的场景。release(): 释放所有权返回原始指针并将内部指针置为nullptr。调用者需要负责之后释放这个资源。reset(T* new_ptr nullptr): 释放当前拥有的资源如果存在然后取得对新资源的所有权。这是主动管理资源生命周期的关键接口。基于以上分析我们可以勾勒出UniquePtr类的基本骨架。3. 核心成员函数实现详解有了清晰的设计蓝图我们现在开始填充每一个成员函数的实现细节。我们将遵循“资源管理类”的最佳实践确保异常安全并解释每一行代码的意图。3.1 构造函数与析构函数构造函数需要处理多种资源获取方式析构函数则要保证资源在任何情况下都能被正确释放。1. 默认构造函数与nullptr构造创建一个不拥有任何资源的UniquePtr。UniquePtr() noexcept : ptr_(nullptr) {} explicit UniquePtr(std::nullptr_t) noexcept : ptr_(nullptr) {}这里使用explicit关键字防止从nullptr_t隐式转换避免意外的构造。2. 从原始指针构造接管所有权这是最常用的构造函数。它接受一个由new分配得来的原始指针并接管其所有权。关键点这个构造函数必须是explicit的以防止从原始指针的隐式转换这种转换非常危险容易导致重复释放。explicit UniquePtr(T* p) noexcept : ptr_(p) {}实操心得永远不要将栈上对象的地址local_var或者数组元素的地址交给unique_ptr。unique_ptr默认使用delete而非delete[]进行释放这会导致未定义行为。对于数组应使用std::unique_ptrT[]特化版本。3. 移动构造函数这是实现所有权转移的核心。它从另一个UniquePtr右值那里“夺取”资源。UniquePtr(UniquePtr other) noexcept : ptr_(other.ptr_) { other.ptr_ nullptr; // 至关重要使源对象处于空状态 }移动后源对象other的内部指针被置为nullptr变成一个“空”的、不拥有任何资源的智能指针。这确保了资源不会被释放两次。4. 析构函数析构函数的职责很单纯如果拥有资源则使用删除器释放它。~UniquePtr() { if (ptr_) { Deleter()(ptr_); // 使用删除器对象释放资源 } }对于默认删除器std::default_deleteDeleter()(ptr_)就等价于delete ptr_。3.2 移动赋值运算符移动赋值运算符比移动构造函数复杂一些因为它需要处理目标对象*this可能已经拥有资源的情况。我们必须先安全地释放旧资源再接管新资源。UniquePtr operator(UniquePtr other) noexcept { // 1. 防止自赋值up std::move(up); if (this ! other) { // 2. 释放当前拥有的资源 if (ptr_) { Deleter()(ptr_); } // 3. 接管新资源 ptr_ other.ptr_; // 4. 置空源对象 other.ptr_ nullptr; } return *this; }为什么需要检查自赋值如果不检查在up std::move(up);这种虽然奇怪但合法的操作中我们会先释放ptr_指向的资源然后试图从other也就是自己接管一个已经被释放的指针这会导致未定义行为。检查this ! other可以避免这个问题。3.3 关键接口实现reset, release, get1.reset(T* new_ptr nullptr)这个函数是资源管理的“总开关”。它改变了UniquePtr当前拥有的资源。void reset(T* new_ptr nullptr) noexcept { // 保存旧指针 T* old_ptr ptr_; // 更新为新指针 ptr_ new_ptr; // 释放旧资源如果存在 if (old_ptr) { Deleter()(old_ptr); } }注意操作的顺序先更新内部指针再释放旧资源。这个顺序在异常安全上更优假设删除器可能抛出异常虽然不常见但更重要的是它保证了在new_ptr可能就是old_ptr的情况下即reset同一个指针资源不会被错误地提前释放。不过在我们的实现中由于先保存了old_ptr即使new_ptr与ptr_相同old_ptr也保存了正确的地址释放操作依然正确。2.release()这个函数用于放弃所有权将资源“交还”给调用者。T* release() noexcept { T* released_ptr ptr_; ptr_ nullptr; return released_ptr; }调用release()后UniquePtr对象变为空不再负责释放返回的那个指针。调用者必须确保最终以适当的方式释放该资源否则会导致内存泄漏。3.get()这是一个简单的访问器。T* get() const noexcept { return ptr_; }它不涉及任何所有权转移只是提供对内部指针的只读访问。常用于调用需要原始指针的遗留函数。3.4 删除器的集成与空基类优化我们的模板声明是template 。Deleter默认是std::default_delete。在类内部我们通过Deleter()构造一个临时删除器对象来调用。然而更高效的做法是将删除器作为UniquePtr的一个成员。但这里有一个优化空间如果删除器是一个无状态的类型比如一个没有非静态成员变量的类或结构体那么它本身不占用存储空间。我们可以利用C的空基类优化让UniquePtr私有继承自Deleter。template class UniquePtr : private Deleter { // 私有继承 private: T* ptr_ nullptr; public: // 获取删除器的引用 Deleter get_deleter() noexcept { return *this; } const Deleter get_deleter() const noexcept { return *this; } ~UniquePtr() { if (ptr_) { get_deleter()(ptr_); // 直接使用继承来的删除器 } } // ... 其他成员在需要释放资源的地方使用 get_deleter()(ptr_) };通过私有继承UniquePtr获得了删除器的调用能力并且如果Deleter是空类编译器不会为其分配单独的内存从而保证了UniquePtr对象的大小仍然等同于一个原始指针在大多数平台上为8字节。这是std::unique_ptr能够实现零开销抽象的关键技巧之一。4. 完整代码实现与测试用例将上述所有部分组合起来我们就得到了一个功能完整的UniquePtr简化实现。下面给出完整代码并附上详细的注释。#include #include // std::is_nothrow_move_constructible_v, 用于static_assert // 前向声明默认删除器通常定义在或中 namespace std { template struct default_delete { void operator()(T* p) const noexcept { delete p; } }; template struct default_delete { void operator()(T* p) const noexcept { delete[] p; } }; } template class UniquePtr : private Deleter { private: T* ptr_; public: // --- 构造函数 --- // 默认构造不拥有资源 UniquePtr() noexcept : ptr_(nullptr) {} // 从nullptr构造 explicit UniquePtr(std::nullptr_t) noexcept : ptr_(nullptr) {} // 接管原始指针所有权 explicit UniquePtr(T* p) noexcept : ptr_(p) {} // 移动构造 UniquePtr(UniquePtr other) noexcept : Deleter(std::move(other.get_deleter())), ptr_(other.ptr_) { other.ptr_ nullptr; } // --- 析构函数 --- ~UniquePtr() { reset(); } // --- 赋值运算符 --- // 删除拷贝赋值 UniquePtr operator(const UniquePtr) delete; // 移动赋值 UniquePtr operator(UniquePtr other) noexcept { if (this ! other) { // 释放当前资源 reset(); // 移动删除器和资源指针 get_deleter() std::move(other.get_deleter()); ptr_ other.ptr_; other.ptr_ nullptr; } return *this; } // --- 资源管理接口 --- // 释放当前资源如果存在并接管新资源 void reset(T* p nullptr) noexcept { T* old_ptr ptr_; ptr_ p; if (old_ptr) { get_deleter()(old_ptr); } } // 放弃所有权返回原始指针 T* release() noexcept { T* released_ptr ptr_; ptr_ nullptr; return released_ptr; } // 交换两个UniquePtr的内容 void swap(UniquePtr other) noexcept { using std::swap; swap(ptr_, other.ptr_); swap(get_deleter(), other.get_deleter()); } // --- 访问器 --- T* get() const noexcept { return ptr_; } Deleter get_deleter() noexcept { return *this; } const Deleter get_deleter() const noexcept { return *this; } // --- 指针语义重载 --- T operator*() const noexcept { return *ptr_; } T* operator-() const noexcept { return ptr_; } // --- 布尔转换用于条件判断--- explicit operator bool() const noexcept { return ptr_ ! nullptr; } // --- 禁止拷贝构造 --- UniquePtr(const UniquePtr) delete; }; // 非成员swap函数用于支持ADL template void swap(UniquePtr lhs, UniquePtr rhs) noexcept { lhs.swap(rhs); } // 测试用例 int main() { // 测试1: 基本构造和析构 std::cout Test 1: Basic lifecycle\n; { UniquePtr up(new int(42)); std::cout *up *up std::endl; // 应输出 42 } // up离开作用域自动释放内存 // 测试2: 移动语义 std::cout \nTest 2: Move semantics\n; UniquePtr up1(new int(100)); // UniquePtr up2 up1; // 错误拷贝构造被禁用 UniquePtr up2 std::move(up1); // 正确移动构造 std::cout After move, up2 owns: *up2 std::endl; std::cout up1 is now (up1 ? not null : null) std::endl; // 应输出 null // 测试3: reset 和 release std::cout \nTest 3: reset and release\n; UniquePtr up3(new int(200)); int* raw_ptr up3.release(); std::cout After release, up3 is (up3 ? not null : null) std::endl; std::cout Raw pointer value: *raw_ptr std::endl; delete raw_ptr; // 手动释放 up3.reset(new int(300)); std::cout After reset, *up3 *up3 std::endl; // 测试4: 自定义删除器 std::cout \nTest 4: Custom deleter\n; struct FileDeleter { void operator()(std::FILE* fp) const { if (fp) { std::fclose(fp); std::cout File closed by custom deleter.\n; } } }; { UniquePtr:file filedeleter filePtr(std::fopen(test.txt, w)); if (filePtr) { std::fputs(Hello, UniquePtr!, filePtr.get()); } } // 离开作用域FileDeleter会自动调用fclose // 测试5: 数组支持需使用特化的删除器 std::cout \nTest 5: Array support\n; UniquePtr:default_delete arr(new int[5]{1, 2, 3, 4, 5}); for (int i 0; i 5; i) { std::cout arr.get()[i] ; } std::cout std::endl; return 0; }5. 常见问题、陷阱与进阶思考在实际使用和实现unique_ptr的过程中会遇到一些典型的坑和需要深入理解的点。5.1 循环引用问题unique_ptr因其独占性本身不会直接导致循环引用因为无法共享所有权。循环引用问题更多出现在std::shared_ptr中。但是如果使用unique_ptr管理具有环形所有权关系的对象需要特别小心地设计所有权结构通常需要将环中的某一环改为原始指针或弱引用并明确生命周期的负责方。5.2 与标准库的兼容性我们实现的UniquePtr是一个教学模型。生产环境中应始终使用std::unique_ptr。我们的实现可能缺少一些边缘情况的处理和完善例如转换构造函数std::unique_ptr支持从派生类指针到基类指针的转换std::unique_ptr。比较运算符std::unique_ptr重载了operator,operator!,operator等用于与nullptr比较或排序。make_unique(C14): 这是一个非常重要的辅助函数它完美解决了异常安全的问题。std::make_unique(args...)在分配内存和构造对象时如果构造过程抛出异常已分配的内存会被自动清理而直接使用new则可能在异常发生时泄漏内存。5.3 性能考量与零开销原则unique_ptr被设计为“零开销抽象”。在典型的优化编译下一个std::unique_ptr在运行时产生的代码与手动正确使用new和delete的代码在效率上是完全等同的。所有资源管理逻辑都在编译期确定。这也是为什么其移动操作要声明为noexcept以及删除器类型作为模板参数的原因——一切都是为了给编译器最大的优化空间。5.4 自定义删除器的使用场景与实现技巧场景举例管理C风格资源如FILE*,SDL_Window*,pthread_mutex_t*等需要对应的fclose,SDL_DestroyWindow,pthread_mutex_destroy来释放。使用内存池释放时不是调用delete而是将内存块返回到特定的内存池中。延迟删除或记录日志在释放资源前执行一些额外操作如打印日志。实现技巧如果删除器有状态例如需要记录日志的文件句柄它将成为UniquePtr对象的一部分会增加对象大小。可以使用Lambda表达式作为删除器但需要注意其类型。每个Lambda都有唯一的类型因此两个使用不同Lambda的unique_ptr将是不同的类型。auto log_deleter [](int* p) { std::cout Deleting *p std::endl; delete p; }; std::unique_ptr p(new int(5), log_deleter); // decltype(p) 是一个独特的类型5.5 异常安全保证unique_ptr提供了强异常安全保证。其成员函数除了可能由用户提供的删除器抛出的异常都是noexcept的。这意味着在移动、reset等操作中如果发生异常比如在reset中先delete旧资源然后在new新资源时失败unique_ptr对象本身会保持一个有效状态通常是空状态不会发生资源泄漏。亲手实现一遍UniquePtr就像完成了一次对C资源管理核心思想的深度解剖。它不仅仅是一个工具类更是RAII理念、移动语义、模板编程和零开销抽象原则的集中体现。理解它对于编写安全、高效、现代的C代码至关重要。在实际项目中请毫不犹豫地使用std::unique_ptr它将是你管理独占资源最得力的助手。

相关新闻

ViGEmBus虚拟游戏控制器驱动:Windows游戏设备兼容性完整解决方案

ViGEmBus虚拟游戏控制器驱动:Windows游戏设备兼容性完整解决方案

ViGEmBus虚拟游戏控制器驱动:Windows游戏设备兼容性完整解决方案 【免费下载链接】ViGEmBus Windows kernel-mode driver emulating well-known USB game controllers. 项目地址: https://gitcode.com/gh_mirrors/vi/ViGEmBus 想要在Windows系统上完美使用各…

2026/7/29 10:12:31 阅读更多 →
深信服入选Gartner®《私有云市场指南》代表厂商

深信服入选Gartner®《私有云市场指南》代表厂商

近日,Gartner 发布《私有云市场指南》,深信服凭借云平台托管私有云的完整能力,入选代表厂商。我们从报告中总结得出,受VMware替代、AI规模落地、应用现代化与数据主权合规驱动,私有云市场迎来新一轮增长。Gartner给出若…

2026/7/29 10:12:31 阅读更多 →
从大厂离职,想回流原公司,简历被冻结了

从大厂离职,想回流原公司,简历被冻结了

从字节离职,上个月打算回流原公司,HR 直接告诉我:简历在“冷冻期”,系统过不去。 这种情况在大厂很常见。离职后想回来,不是你想回就能回,都有冷冻期卡着。 具体规则分人:正常离职、主动跳槽的&…

2026/7/29 10:12:31 阅读更多 →

最新新闻

TI AWR1642BOOST毫米波雷达评估板:从硬件解析到开发实践

TI AWR1642BOOST毫米波雷达评估板:从硬件解析到开发实践

1. 开箱与初识:AWR1642BOOST评估板的核心价值如果你对自动驾驶、工业机器人或者智能安防领域的技术实现感兴趣,那么“毫米波雷达”这个词你一定不陌生。它就像是设备的“眼睛”,能在雨雪、雾霾、黑暗等恶劣环境下,稳定地“看清”周…

2026/7/29 10:21:34 阅读更多 →
掌控板教学:从开源硬件到计算思维培养的课程设计实践

掌控板教学:从开源硬件到计算思维培养的课程设计实践

1. 从“掌控板”大赛看开源硬件教学的破局点 最近,首届“掌控板”教学应用设计大赛的课程设计示范案例公布了。作为一个在创客教育和开源硬件领域摸爬滚打了十来年的老玩家,看到这个消息,第一反应是欣慰,紧接着就是思考。欣慰的是…

2026/7/29 10:21:34 阅读更多 →
基于英特尔Edison的机器人开发:从硬件集成到SLAM算法实践

基于英特尔Edison的机器人开发:从硬件集成到SLAM算法实践

1. 项目概述:为什么选择英特尔 Edison 来造机器人?几年前,当我在实验室里捣鼓一个需要实时图像处理的小型移动机器人时,遇到了一个经典难题:树莓派性能不够,而工控机又太重太耗电。就在那时,我重…

2026/7/29 10:21:34 阅读更多 →
Triton语言where操作GPU优化全解析

Triton语言where操作GPU优化全解析

1. Triton语言中的where操作深度解析在GPU高性能计算领域,Triton语言正逐渐成为编写高效核函数的利器。其中where操作作为条件筛选的核心功能,其性能表现直接影响到许多实际应用的吞吐量。今天我们就来深入剖析triton_language.where这个看似简单却暗藏玄…

2026/7/29 10:21:34 阅读更多 →
AI产品增长策略年度复盘:SEO、内容营销与社区运营的投入产出分析

AI产品增长策略年度复盘:SEO、内容营销与社区运营的投入产出分析

AI产品增长策略年度复盘:SEO、内容营销与社区运营的投入产出分析 一、增长困局:当AI产品遭遇用户获取的成本黑洞 过去一年里,AI赛道经历了从狂热到理性的剧烈震荡。模型能力以月为单位迭代,但用户获取成本却在持续攀升。大量AI产品…

2026/7/29 10:21:34 阅读更多 →
企业网盘防勒索怎么选?权限控制、历史版本与恢复流程实战指南

企业网盘防勒索怎么选?权限控制、历史版本与恢复流程实战指南

一、勒索病毒变了:为什么传统备份不够用了 2024到2026年,勒索软件的攻击模式发生了两个关键变化。第一个是"横向移动"——勒索病毒不再只加密感染的那台电脑,而是通过企业内网的共享文件夹、同步盘挂载目录、NAS映射盘符逐级扩散。…

2026/7/29 10:20:34 阅读更多 →

日新闻

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:00:23 阅读更多 →
AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础

AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础

AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础 在上一期「AI编程系列」中,我们学习了如何构建一个基础的 AI 问答系统,通过简单的输入输出让模型回应问题。但现实世界中的 AI 应用往往需要处理更复杂的场景:…

2026/7/29 0:00:23 阅读更多 →
AI智能体开发实战:从工具调用到企业级部署

AI智能体开发实战:从工具调用到企业级部署

1. 从被动问答到主动执行:AI Agent的范式转变过去两年,大语言模型最显著的应用形态是聊天机器人——用户提问,AI回答。但真正的生产力革命发生在2023年下半年:当AI学会主动调用工具完成任务时,生产力工具的历史被彻底改…

2026/7/29 0:00:23 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/28 12:04:22 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/28 8:29:16 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/28 5:03:42 阅读更多 →

月新闻