深入理解C++ STL list:从双向链表原理到手写实现
1. 项目概述为什么我们需要深入理解STL list在C的日常开发中尤其是处理需要频繁在序列中间进行插入和删除操作的场景时std::vector的线性内存布局带来的数据搬移成本会让人头疼。这时std::list这个基于双向链表的序列容器就成了我们的救星。很多朋友对list的认知可能停留在“它是一个链表插入删除快但访问慢”的层面这没错但远远不够。当你真正需要定制容器行为、进行内存优化或者单纯想搞懂STL的设计哲学时仅仅会调用push_back和pop_front是远远不够的。我见过不少项目因为对list的迭代器失效规则理解不透彻导致了难以追踪的“幽灵bug”也见过试图用list替代vector来“优化”遍历性能结果适得其反的案例。究其原因是对这个容器的内部机理和接口设计缺乏深度的、具象化的理解。今天我们就抛开那些泛泛而谈的八股文直接钻进std::list的“引擎盖”下面不仅详细拆解它的核心特性更重要的是我会带着你一起动手实现几个最关键的函数。这个过程远比读十篇源码分析文章来得深刻。无论你是正在准备技术面试还是希望提升自己的底层编码能力这篇内容都将为你提供扎实的、可直接复现的参考。2. list容器的核心设计思想与数据结构剖析2.1 双向循环链表一切设计的基石std::list的底层数据结构是一个带哨兵节点dummy node或sentinel node的双向循环链表。这是理解其所有行为的关键。让我们把它拆开看双向每个节点list_node除了存储数据value_type还包含两个指针一个指向前驱节点prev一个指向后继节点next。这赋予了它双向遍历的能力也是实现reverse_iterator和insert/erase等操作的基础。循环链表的“头”和“尾”在逻辑上是相连的。这意味着尾节点的next指向头节点头节点的prev指向尾节点。这个设计带来了一个巨大的好处它统一并简化了边界条件的处理。无论你在头部、尾部还是中间插入删除操作节点的逻辑几乎是一致的无需为头部或尾部编写特殊判断代码。哨兵节点这是循环设计的具体实现。list对象内部持有一个不存储有效数据的节点这个节点就是哨兵。begin()迭代器指向哨兵节点的下一个节点即第一个有效数据节点end()迭代器则指向哨兵节点本身。因此list永远不为“空”——至少有一个哨兵节点存在。判断list是否为空就是判断begin() end()。注意哨兵节点的存在使得end()迭代器成为一个“逾尾”位置这与vector和deque的设计理念一致保证了STL算法的一致性。但这也意味着对end()迭代器解引用是未定义行为。这种数据结构的选择直接决定了list的性能特征插入/删除在已知位置通过迭代器指定的插入和删除操作是常数时间O(1)因为只需要修改相邻节点的指针。随机访问不支持operator[]访问第N个元素需要从头部或尾部开始遍历是线性时间O(n)。内存每个元素独立分配节点内存不连续。这带来了额外的指针开销每个节点多两个指针并且对CPU缓存不友好缓存局部性差这是其遍历性能通常低于vector的主要原因。2.2 迭代器设计与容器的深度绑定list的迭代器不是一个简单的指针像vector那样而是一个封装了节点指针的类类型迭代器。它必须足够“智能”知道如何从一个节点移动到下一个节点通过next指针以及如何解引用获取数据访问节点的value成员。一个简易的迭代器类骨架可能长这样templateclass T struct __list_iterator { typedef __list_nodeT node_type; node_type* _node; // 核心持有一个指向链表节点的指针 // 前置 __list_iterator operator() { _node _node-_next; // 移动到下一个节点 return *this; } // 后置 __list_iterator operator(int) { /*...*/ } // 解引用 T operator*() const { return _node-_data; // 返回节点数据的引用 } // 箭头操作符 T* operator-() const { /*...*/ } // 比较操作 bool operator!(const __list_iterator other) const { /*...*/ } };关键点在于迭代器失效规则由于list的节点是独立分配的插入操作insert,push_front,push_back不会使任何已存在的迭代器失效。删除操作erase,pop_front,pop_back只会使指向被删除元素的迭代器失效其他迭代器依然有效。这与vector插入删除可能导致所有后续迭代器失效的规则截然不同是list在特定场景下安全性的重要保障。3. 核心成员函数实现详解理解了底层结构我们来实现几个最核心的函数。我们将围绕一个简化的MyList类模板展开。为了聚焦逻辑我们省略了异常安全、分配器等高级话题但会指出关键点。3.1 节点结构与类框架首先定义内部节点和迭代器。templatetypename T class MyList { private: // 链表节点 struct ListNode { T data; ListNode* prev; ListNode* next; ListNode(const T val T(), ListNode* p nullptr, ListNode* n nullptr) : data(val), prev(p), next(n) {} }; // 迭代器 (简化为双向迭代器) class iterator { public: using self iterator; ListNode* _node; iterator(ListNode* node nullptr) : _node(node) {} T operator*() { return _node-data; } T* operator-() { return (_node-data); } self operator() { _node _node-next; return *this; } // 前置 self operator(int) { self tmp *this; (*this); return tmp; } // 后置 self operator--() { _node _node-prev; return *this; } self operator--(int) { /* 类似后置 */ } bool operator(const self rhs) const { return _node rhs._node; } bool operator!(const self rhs) const { return _node ! rhs._node; } }; // 核心数据成员 ListNode* _sentinel; // 哨兵节点 size_t _size; // 元素个数用于O(1)返回size() public: // 构造函数、析构函数、接口声明... MyList(); ~MyList(); iterator begin(); iterator end(); void push_back(const T value); void push_front(const T value); iterator insert(iterator pos, const T value); iterator erase(iterator pos); size_t size() const; bool empty() const; };3.2 构造函数与析构函数资源的生命期管理默认构造函数的任务是创建那个至关重要的哨兵节点并将其初始化为一个自环。templatetypename T MyListT::MyList() : _size(0) { _sentinel new ListNode(); // 分配哨兵节点data使用T的默认构造 _sentinel-prev _sentinel; // prev指向自己 _sentinel-next _sentinel; // next指向自己 // 此时begin() end()链表为空。 }析构函数必须安全地释放所有节点包括哨兵节点。一个常见的错误是只释放数据节点而忘了哨兵或者释放顺序不当导致访问已释放内存。templatetypename T MyListT::~MyList() { clear(); // 先清空所有数据节点 delete _sentinel; // 再释放哨兵节点 _sentinel nullptr; } templatetypename T void MyListT::clear() { ListNode* cur _sentinel-next; while (cur ! _sentinel) { // 遍历所有数据节点 ListNode* to_delete cur; cur cur-next; delete to_delete; // 释放节点内存 } // 清空后恢复哨兵的自环状态 _sentinel-prev _sentinel; _sentinel-next _sentinel; _size 0; }实操心得在析构函数中调用clear()是一个清晰且安全的模式。确保clear()的实现不会破坏哨兵节点的完整性因为析构函数最后还要删除它。将_sentinel置为nullptr是一个好习惯可以防止后续误用。3.3 push_back 与 push_front头尾插入的奥秘这两个函数是insert的特例但在链表头尾操作逻辑可以更直观。templatetypename T void MyListT::push_back(const T value) { // 在哨兵节点即end()位置之前插入就是在尾部插入 ListNode* tail_node _sentinel-prev; // 当前最后一个数据节点 ListNode* new_node new ListNode(value, tail_node, _sentinel); // 调整指针 tail_node-next new_node; _sentinel-prev new_node; _size; } templatetypename T void MyListT::push_front(const T value) { // 在第一个数据节点即begin()位置之前插入 ListNode* first_node _sentinel-next; ListNode* new_node new ListNode(value, _sentinel, first_node); _sentinel-next new_node; first_node-prev new_node; _size; }可以看到得益于循环链表和哨兵节点push_back和push_front的代码对称且简洁完全不需要判断链表是否为空因为_sentinel-prev和_sentinel-next在空链表时都指向_sentinel自己代码逻辑统一。3.4 insert 函数在任意位置插入这是链表的核心操作也是理解迭代器操作的关键。insert(pos, value)表示在pos迭代器所指向的元素之前插入新元素。templatetypename T typename MyListT::iterator MyListT::insert(iterator pos, const T value) { // pos._node 是即将被“挤”到后面的那个节点 ListNode* cur_node pos._node; ListNode* prev_node cur_node-prev; // 插入位置的前一个节点 // 创建新节点其前驱是prev_node后继是cur_node ListNode* new_node new ListNode(value, prev_node, cur_node); // 更新前后节点的指针 prev_node-next new_node; cur_node-prev new_node; _size; return iterator(new_node); // 返回指向新插入元素的迭代器 }为什么返回迭代器这是STL的标准设计返回指向新插入元素的迭代器。这个迭代器是有效的并且插入操作不会使其他迭代器失效这非常有用。例如你可以用一个循环配合insert的返回值来连续插入。3.5 erase 函数安全地删除元素删除操作需要格外小心因为处理不当会导致迭代器失效和内存泄漏。templatetypename T typename MyListT::iterator MyListT::erase(iterator pos) { if (pos end() || empty()) { // 标准规定删除end()是未定义行为。这里我们简单返回end()或抛出异常。 // 为简化我们假设pos总是有效的。 return end(); } ListNode* to_delete pos._node; ListNode* prev_node to_delete-prev; ListNode* next_node to_delete-next; // 桥接前后节点 prev_node-next next_node; next_node-prev prev_node; // 保存返回值指向被删除元素下一个位置的迭代器 iterator ret(next_node); // 释放节点内存 delete to_delete; --_size; return ret; // 符合STL标准返回被删元素之后的位置 }关键注意事项迭代器失效erase调用后pos迭代器立即失效绝对不能再使用。这也是为什么函数需要返回一个新的、有效的迭代器指向下一个元素以便在循环中安全地连续删除。经典的从list中删除满足条件元素的循环写法是for (auto it lst.begin(); it ! lst.end(); ) { if (condition(*it)) it lst.erase(it); else it; }。边界检查虽然我们的简化版没做严格检查但在生产代码中必须确保pos不是end()并且链表非空。删除哨兵节点将是灾难性的。内存管理别忘了delete。对于存储指针的list可能需要先手动释放节点数据指向的内存如果数据是指针且拥有所有权这涉及到更复杂的所有权语义。3.6 begin, end, size 与 empty这些函数实现相对简单但意义重大。templatetypename T typename MyListT::iterator MyListT::begin() { return iterator(_sentinel-next); // 第一个数据节点 } templatetypename T typename MyListT::iterator MyListT::end() { return iterator(_sentinel); // 哨兵节点 } templatetypename T size_t MyListT::size() const { return _size; // O(1)时间复杂度我们维护了_size成员 // 注意早期某些STL实现list::size()可能是O(n)需要遍历计数。 } templatetypename T bool MyListT::empty() const { return _size 0; // 或者判断 begin() end() }维护一个_size成员变量用空间换时间使size()操作是常数复杂度这是现代STL实现的常见做法。4. 高级功能与性能考量4.1 splice 函数链表的神来之笔splice是list独有的、最能体现链表优势的操作。它可以在常数时间内将另一个链表或其中一部分接合到当前链表的指定位置无需拷贝元素只修改指针。// 将另一个链表other的全部内容移动到pos之前 void splice(iterator pos, MyList other) { if (other.empty()) return; ListNode* other_first other._sentinel-next; ListNode* other_last other._sentinel-prev; ListNode* pos_node pos._node; ListNode* pos_prev pos_node-prev; // 1. 从other中摘除子链 other._sentinel-next other._sentinel; other._sentinel-prev other._sentinel; // 2. 将子链接入当前链表 pos_prev-next other_first; other_first-prev pos_prev; other_last-next pos_node; pos_node-prev other_last; // 3. 更新size _size other._size; other._size 0; }应用场景合并两个有序链表、将某个元素移动到链表头部LRU Cache实现常用、批量重组链表结构等。由于只操作指针其效率极高。4.2 sort 成员函数为什么list有自己的sortstd::list提供了自己的sort成员函数而不是依赖std::sort算法。这是因为std::sort要求随机访问迭代器RandomAccessIterator而list的迭代器是双向迭代器BidirectionalIterator。list::sort通常实现为归并排序因为它非常适合链表结构可以在原链表上通过指针操作完成空间复杂度为O(1)。其基本思路是将链表递归地拆分成两半通过快慢指针找到中点。分别对两半递归排序。合并两个已排序的子链表。性能对比对于list调用成员函数lst.sort()远比std::sort(lst.begin(), lst.end())高效因为后者无法编译迭代器类型不匹配。即使能编译如将list拷贝到vector排序再拷回其性能也远差于直接操作指针的归并排序。4.3 与vector和deque的对比选型选择容器就是选择数据结构。这里有一个简单的决策表特性std::vectorstd::dequestd::list内存结构单块连续内存多段连续内存块离散节点双向链接随机访问O(1)极快O(1)但比vector慢O(n)慢头部插入/删除O(n)O(1)(摊销)O(1)尾部插入/删除O(1)(摊销)O(1)(摊销)O(1)中间插入/删除O(n)O(n)O(1)(已知位置)迭代器失效插入/删除可能使所有后续迭代器失效插入/删除可能使所有迭代器失效只有被删除元素的迭代器失效缓存友好性极好较好差额外内存开销小 (容量-大小)中 (管理多个块)大 (每个元素两个指针)选型建议默认首选vector除非有明确理由否则用vector。它的遍历速度、内存紧凑性在大多数情况下优势巨大。需要频繁在序列任意位置插入/删除选择list。例如实现一个播放列表经常需要拖动歌曲顺序。需要频繁在头部和尾部操作考虑deque。它提供了接近vector的随机访问性能和接近list的头尾操作性能是一个不错的折中。迭代器稳定性要求高如果插入删除后其他位置的迭代器必须保持有效用list。元素很大拷贝成本高list的插入删除只操作指针不涉及元素拷贝可能有优势。但也要权衡内存碎片和缓存不友好的代价。5. 常见问题、调试技巧与实战心得5.1 典型问题排查清单段错误Segmentation Fault原因最常见的是解引用了无效的迭代器如end()迭代器或已被erase的迭代器。排查检查所有迭代器操作确保在erase后不再使用原迭代器。使用valgrind或AddressSanitizer等内存调试工具。内存泄漏原因erase或pop操作时只修改了指针没有delete节点。或者析构函数没有正确释放所有节点。排查确保每个new都有对应的delete。对于存储原始指针的list可能需要自定义析构函数或使用智能指针。逻辑错误遍历时删除元素错误代码for (auto it lst.begin(); it ! lst.end(); it) { if (cond) lst.erase(it); }// 错误erase后it失效it行为未定义。正确代码for (auto it lst.begin(); it ! lst.end(); ) { if (cond) it lst.erase(it); else it; }性能未达预期原因误用list进行大量随机访问或遍历。链表遍历的常数因子很大缓存缺失。优化如果主要是遍历操作换成vector或deque性能可能提升一个数量级。使用性能分析工具如perf,gprof定位热点。5.2 调试与验证技巧可视化辅助在实现过程中可以编写一个简单的打印函数输出链表的指针关系这对于调试插入删除逻辑非常有帮助。void debug_print() { ListNode* cur _sentinel-next; std::cout Sentinel _sentinel ; while (cur ! _sentinel) { std::cout [ cur-data ]( cur-prev - cur - cur-next ) ; cur cur-next; } std::cout std::endl; }单元测试为每个核心函数push_back,insert,erase,splice等编写测试用例覆盖边界情况空链表、头尾操作、单个元素链表等。与STL std::list对比用相同的操作序列分别运行你的MyList和std::list比较最终的size()、遍历结果是否一致。这是最直接的验证方法。5.3 从实现中获得的深刻理解亲手实现一遍哪怕是一个简化版你也会对以下几点有刻骨铭心的认识哨兵节点的优雅它消除了几乎所有边界判断让代码变得干净、统一。这是算法设计中“增加冗余数据简化操作”的经典案例。迭代器的抽象代价与收益迭代器不是一个指针而是一个“智能”对象。这带来了抽象成本但换来了统一的容器访问接口和强大的泛型编程能力。数据结构决定算法list的O(1)插入删除和O(n)访问直接源于其链式存储。没有完美的数据结构只有适合场景的选择。资源管理是核心C中内存的分配与释放必须精确匹配。list的实现迫使你仔细思考每一个节点在何时创建、何时销毁这是理解RAII和智能指针重要性的绝佳练习。最后我个人的体会是学习STL容器绝不能停留在调用API的层面。像今天这样选择一个容器去深入剖析并动手实现关键部分是突破“会用”到“懂原理”这道坎的最有效方法。它不仅能让你在面试中游刃有余更能让你在真正面对复杂性能问题或需要定制数据结构时心中有图手下不慌。list的实现就像一把钥匙帮你打开了理解更复杂容器如map,set的底层红黑树的大门。

相关新闻

智能问卷设计:NLP与自适应逻辑在教育科研中的应用

智能问卷设计:NLP与自适应逻辑在教育科研中的应用

1. 教育科研问卷设计的现状与痛点在教育科研领域,问卷设计一直是数据收集的核心环节。传统问卷设计过程中,研究者常常陷入"问卷迷宫"的困境——设计者需要反复推敲问题表述、调整选项设置、优化流程逻辑,这个过程往往耗时耗力且效果…

2026/10/9 14:55:54 阅读更多 →
双端面磨床主流厂商技术特点盘点

双端面磨床主流厂商技术特点盘点

精密机械加工领域中,双端面磨削工序直接决定零部件两端面平行度、厚度尺寸一致性与表面加工质量,是精加工流程里不可或缺的关键环节。当前制造业生产模式持续迭代,传统单一品类不间断量产模式逐步减少,多材质混合加工、工件规格频…

2026/10/11 10:27:06 阅读更多 →
YOLOv8-seg改进:ContextGuidedDown模块提升白头海雕检测精度

YOLOv8-seg改进:ContextGuidedDown模块提升白头海雕检测精度

1. 项目背景与核心价值 白头海雕作为北美地区的重要保护物种,其种群监测一直面临野外环境复杂、目标尺寸多变等挑战。传统人工巡查方式效率低下,而基于无人机航拍结合计算机视觉的自动化监测方案正成为研究热点。我们团队在实测中发现,标准YO…

2026/10/7 12:16:33 阅读更多 →

最新新闻

Qt JSON实战指南:解析、序列化与避坑要点

Qt JSON实战指南:解析、序列化与避坑要点

很多刚接触Qt的开发者会在Json选型上纠结:到底用自带的QJsonDocument,还是引入第三方Json库。我的建议很简单——如果项目本身就是Qt技术栈,自带的Json支持完全够用,而且从Qt 5到Qt 6,这套API一直稳定保留。这篇文章不…

2026/10/12 17:52:21 阅读更多 →
Numpy、OpenCV、PIL.Image 图像格式转换全攻略

Numpy、OpenCV、PIL.Image 图像格式转换全攻略

只要在 Python 里折腾过图像处理,早晚都会碰见这三种格式打架的情况:直接读进 Numpy 的数组、OpenCV 读出来的 BGR 图、PIL 打开的对象。尤其是当你既想用 OpenCV 做形态学处理,又想用 PIL 去生成缩略图,或者想拿 Matplotlib 显示…

2026/10/12 17:52:21 阅读更多 →
麒麟V10 ARM离线部署K8S 1.26.15:依赖链与避坑指南

麒麟V10 ARM离线部署K8S 1.26.15:依赖链与避坑指南

简介:本资源面向需要在国产化信创环境中落地容器编排的运维与云原生工程师,提供基于银河麒麟Kylin V10、ARM架构并采用外部etcd与containerd运行时部署Kubernetes 1.26.15一主多从集群的完整离线材料,可解决ARM平台镜像与依赖包获取困难、离线…

2026/10/12 17:52:21 阅读更多 →
ObjectARX插件上云全指南:从远程桌面到容器化的落地实践

ObjectARX插件上云全指南:从远程桌面到容器化的落地实践

简介:面向AutoCAD二次开发工程师与点云技术研究者,一套ObjectARX应用源码包围绕ObjectARX与AutoCAD云平台结合处理点云数据的完整技术方案展开,提供了可直接编译的C工程,重点实现点云快速加载、渲染、过滤、测量等功能&#xff0c…

2026/10/12 17:52:21 阅读更多 →
Navicat Premium 12免安装版:便携配置与多数据库连接实战

Navicat Premium 12免安装版:便携配置与多数据库连接实战

简介:Navicat Premium 12免安装版面向数据库开发、运维及数据分析人员,尤其适合需要频繁切换多种数据库环境、又不希望在本地执行繁琐安装流程的用户。它是一款集成化数据库管理工具,可在单一客户端中同时连接MySQL、MariaDB、MongoDB、SQL S…

2026/10/12 17:52:21 阅读更多 →
BERTopic模型教程:从零把长文档变成可解释主题簇

BERTopic模型教程:从零把长文档变成可解释主题簇

简介:这份资源是面向自然语言处理初学者与主题建模实践者的BERTopic入门教程配套代码包,围绕BERT嵌入与传统主题模型结合的核心思路,帮助读者理解从文本嵌入、降维、聚类到主题表示的完整流程,并解决传统LDA语义连贯性不足的问题。…

2026/10/12 17:51:20 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:54 阅读更多 →