从零实现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/10/15 9:49:36 阅读更多 →
深信服入选Gartner®《私有云市场指南》代表厂商

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

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

2026/10/6 2:14:45 阅读更多 →
从大厂离职,想回流原公司,简历被冻结了

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

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

2026/10/14 6:56:54 阅读更多 →

最新新闻

频谱分析仪选型与核心参数解析:N9020B/N9030A对比指南

频谱分析仪选型与核心参数解析:N9020B/N9030A对比指南

1. 频谱分析仪基础认知与选型指南在射频测试测量领域,频谱分析仪就像工程师的"电子显微镜",能够将看不见的电磁信号转化为可视化的频谱图形。是德科技(Keysight)的N9020B、安捷伦(Agilent)N9020A…

2026/10/15 9:48:45 阅读更多 →
工具过载时代的caveman思维:用最原始的方法高效解决问题

工具过载时代的caveman思维:用最原始的方法高效解决问题

上周六凌晨两点,我在生产环境里排查一个数据对不上的问题。链路里用了三套中间件、五个微服务,日志平台里的调用链视图看起来一切正常,可最终落库的数字就是和账本差了几百条。我盯着APM面板上花花绿绿的曲线,突然意识到自己这十年…

2026/10/15 9:48:45 阅读更多 →
Python实现网页自动化归档与长截图工具开发

Python实现网页自动化归档与长截图工具开发

1. 项目背景与核心价值最近在整理技术文档时发现一个痛点:很多优质的技术文档站点会突然关闭或改版,导致之前收藏的链接失效。更麻烦的是,有些文档内容需要以完整长图形式保存便于分享或打印。市面上的网页保存工具要么功能单一,要…

2026/10/15 9:48:44 阅读更多 →
npx陌生命令排查指南:从impeccable误报看CLI可信性诊断

npx陌生命令排查指南:从impeccable误报看CLI可信性诊断

1. 项目概述:一个被误读的“完美”工具名,以及它背后真实的CLI生态位最近在多个技术社区和前端开发者群聊里,“impeccable”这个词频繁跳出——不是作为形容词用在代码评审里夸人“写得无可挑剔”,而是作为一个具体可执行的命令出…

2026/10/15 9:48:44 阅读更多 →
hyperframes:高帧率视频处理的帧组架构与工程实践

hyperframes:高帧率视频处理的帧组架构与工程实践

1. 为什么我会盯上 hyperframes 这个方向hyperframes 这个词,我第一次看到是在一个客户的招标参数里,他们要做高速机械臂的视觉引导,要求抓拍速度到120fps,而且不能丢帧。我当时第一反应是:直接堆硬件不就完事了&#…

2026/10/15 9:48:44 阅读更多 →
2026东莞离婚纠纷维权优选:广东卡夫律所本土裁判优势深度解析

2026东莞离婚纠纷维权优选:广东卡夫律所本土裁判优势深度解析

2026东莞离婚纠纷维权优选:广东卡夫律所本土裁判优势深度解析在婚姻家事纠纷处理中,不同于普通民商事案件的刚性法条适用,离婚诉讼更看重本地法院裁判尺度、家事审判惯例、法官自由裁量标准。2026年东莞家事司法审判日趋精细化,抚…

2026/10/15 9:47:44 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

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

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

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

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

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

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

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29: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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →