C++ STL set与map深度解析:从红黑树原理到现代C++高效实践
1. 项目概述为什么2024年还要深挖STL的set和map如果你是一名C开发者无论你是刚入门的新手还是像我这样在工业级项目里摸爬滚打了十多年的老手有一个工具箱你几乎每天都会打开那就是STL。而std::set和std::map绝对是这个工具箱里最趁手、也最容易被用“糙”的两把利器。网上关于它们的教程汗牛充栋但很多都停留在“怎么用”的层面对于“为什么这么用”、“什么时候用”、“坑在哪里”讲得不够透。尤其是在C标准不断演进新特性如C11/14/17/20层出不穷的今天一些“老经验”可能已经过时而一些“新特性”又没有被充分挖掘。所以这篇内容不是一份简单的API手册复读。我想结合我这些年在大规模数据处理、高并发服务和游戏引擎开发中踩过的坑、总结的经验来一次对set和map的深度“爆赞”式剖析。我们会从最基础的特性聊起一直深入到它们在C17、C20下的新玩法、性能调优的魔鬼细节以及如何避免那些教科书里不会写的典型错误。目标很简单让你看完之后不仅会用更能用好、用精在面试和实战中都能游刃有余。2. 核心基石理解set与map的底层逻辑与本质区别在急着写代码之前我们必须把地基打牢。set和map在STL中被称为“关联容器”它们的核心能力不是通过数字下标像vector那样来访问元素而是通过一个“键”来快速查找、插入和删除对应的“值”。这个“键”就是它们高效运作的灵魂。2.1 数据结构本质红黑树与有序性首先要破除一个常见的误解std::set和std::map的底层实现通常是基于红黑树。注意标准只规定了复杂度对数时间并没有规定必须用红黑树但所有主流实现GCC的libstdc、Clang的libc、MSVC的STL无一例外都使用了红黑树。这是一种自平衡的二叉搜索树。这意味着什么意味着容器中的元素始终是有序的。对于setT里面的T类型对象是按升序排列的对于mapK, V则是按键K升序排列。这个“有序”特性是双刃剑优点你可以很方便地进行范围查询比如“找出所有分数在80到90之间的学生”或者按顺序遍历。其查找、插入、删除操作的时间复杂度都是O(log n)在数据量较大时比线性查找的vector或list高效得多且性能稳定。缺点为了维持有序每次插入和删除都可能触发树的旋转和重新平衡这会带来一定的开销。并且元素的内存地址不是连续的对CPU缓存不友好。注意正因为基于红黑树set和map的迭代器在插入或删除操作后除了被删除的元素对应的迭代器通常不会失效。这是它们相对于vector和deque的一个巨大优势。2.2 set vs. map单元素与键值对这是最根本的区别但新手容易混淆std::setKey你可以把它想象成一个唯一种类的集合。它只存储“键”本身。它的主要任务是快速判断一个元素是否存在于集合中并保证集合内没有重复元素。例如存储一个系统的所有在线用户ID。std::mapKey, Value这是一个键值对字典。每个键Key都唯一地映射到一个值Value。它的核心任务是通过键快速检索到关联的值。例如通过学生学号Key快速找到他的成绩单Value。一个简单的记忆方法set是“有没有”map是“是什么”。2.3 选择的关键你需要“键”还是“键值对”在实际编程中选择哪一个往往取决于你的数据模型场景一去重与存在性检查// 使用 set std::setint uniqueUserIds; for (int id : incomingIds) { if (uniqueUserIds.find(id) uniqueUserIds.end()) { // 新ID进行处理 processNewUser(id); uniqueUserIds.insert(id); } // 否则是重复ID忽略或做其他处理 }这里我们只关心ID是否出现过不需要关联其他信息set是最佳选择。场景二建立映射关系// 使用 map std::mapstd::string, StudentInfo studentRegistry; // 注册学生 studentRegistry[S1001] {Alice, 20, Computer Science}; studentRegistry[S1002] {Bob, 21, Mathematics}; // 通过学号查询 auto it studentRegistry.find(S1001); if (it ! studentRegistry.end()) { std::cout Found: it-second.name std::endl; }这里我们需要通过学号Key获取完整的学生信息Valuemap是不二之选。踩坑心得我曾经见过有同事为了图省事用std::mapKey, bool来模拟set的功能比如onlineMap[userId] true。这非常浪费因为map需要为每个键存储一个额外的bool值通常至少1字节并且管理更复杂的节点结构。而set只存储键本身内存更紧凑。除非你需要存储的“值”本身就有意义比如用户状态不止在线/离线否则永远优先使用set。3. 现代C中的高效用法与核心API精讲了解了本质我们来看看怎么用。C11之后set和map的用法变得更加简洁和安全。我会按照“插入、访问、查找、删除、遍历”这个逻辑链条来梳理。3.1 初始化与插入告别繁琐拥抱现代传统方式先声明再一个个insert。std::mapint, std::string oldMap; oldMap.insert(std::make_pair(1, one)); oldMap.insert(std::pairint, std::string(2, two));现代方式C11起统一初始化在声明时直接赋值代码更清晰。std::setint numSet {1, 3, 5, 7, 9}; std::mapint, std::string numMap { {1, one}, {2, two}, {3, three} };emplace插入这是最重要的优化之一。emplace直接在容器内部构造元素避免了临时对象的创建和拷贝/移动。// 传统insert会先构造一个临时的pair someMap.insert(std::make_pair(complexKey, ComplexValue(arg1, arg2))); // 现代emplace直接传递构造参数给容器 someMap.emplace(complexKey, arg1, arg2); // 更高效set同理someSet.emplace(arg1, arg2, arg3);实操要点对于自定义类型特别是构造开销大的优先使用emplace而非insert。性能提升在热点路径上可能非常显著。try_emplace(C17) 和insert_or_assign(C17)这两个是解决历史痛点的神器。try_emplace(key, args...)如果键key不存在则用args构造值并插入如果键已存在什么也不做且不会覆盖已有的值。它返回一个pairiterator, bool。这避免了不必要的值类型默认构造更安全高效。std::mapstd::string, std::unique_ptrResource resourceMap; // 安全地尝试插入如果已存在不会发生任何资源释放或转移 auto [it, inserted] resourceMap.try_emplace(texture1, std::make_uniqueTexture(path.png)); if (inserted) { std::cout Inserted new resource.\n; }insert_or_assign(key, value)如果键不存在插入键值对如果键已存在则用新的value覆盖旧值。它同样返回一个pairiterator, bool其中bool表示是插入true还是赋值false。std::mapint, Config configMap; // 更新或设置配置项 configMap.insert_or_assign(1001, Config{...});这比老式的map[key] value模式更清晰因为operator[]在键不存在时会插入一个值初始化的元素对于没有默认构造函数的类型会编译失败。3.2 访问与查找安全第一性能至上访问operator[]仅适用于map。map[key]。如果key不存在它会插入一个具有该key、值被值初始化的键值对然后返回其值的引用。这是一个非常危险的操作因为它会默默地改变容器。在只读场景下绝对不要用。std::mapint, int countMap; int count countMap[42]; // 危险如果42不存在会插入{42, 0}count变为0这可能不是你的本意。at(key)同样仅适用于map。如果key存在返回其值的引用如果不存在抛出std::out_of_range异常。更安全但需要处理异常。查找find(key)核心查找函数。返回指向找到元素的迭代器如果没找到则返回end()。这是最推荐的做法。auto it myMap.find(targetKey); if (it ! myMap.end()) { // 安全地使用 it-second process(it-second); } else { // 处理未找到的情况 handleNotFound(); }count(key)对于set和map返回具有该键的元素个数。由于键是唯一的返回值只能是0或1。因此if (mySet.count(key))等价于if (mySet.find(key) ! mySet.end())。有些人觉得count的意图“是否存在”比find更直观。contains(key)(C20)这是语法糖直接返回bool表示键是否存在。代码最简洁直观。if (myMap.contains(targetKey)) { // C20 清晰 // ... }性能对比在只读场景下find、count、contains的性能几乎是一样的因为它们都基于红黑树的查找操作O(log n)。operator[]和at在键存在时也是O(log n)但operator[]在键不存在时有插入开销。3.3 删除与遍历迭代器的正确姿势删除erase(key)删除指定键的元素返回删除的元素个数0或1。erase(iterator)或erase(first, last)通过迭代器删除。这是更高效的方式尤其是当你已经通过find找到了迭代器时。auto it myMap.find(keyToDelete); if (it ! myMap.end()) { myMap.erase(it); // 直接使用迭代器删除避免二次查找 }C11后erase返回被删除元素之后元素的迭代器这方便了在遍历中删除。for (auto it mySet.begin(); it ! mySet.end(); /* 不在这里递增 */) { if (shouldRemove(*it)) { it mySet.erase(it); // erase返回下一个有效迭代器 } else { it; } }遍历基于范围的for循环 (C11)首选最简洁。for (const auto kv : myMap) { // kv 是 std::pairconst Key, Value std::cout kv.first : kv.second std::endl; } for (const auto elem : mySet) { std::cout elem std::endl; }重要在map中kv.first的类型是const Key你不能修改它因为键是排序的依据修改它会破坏红黑树的不变性。使用迭代器当需要更复杂的控制时如条件删除、同时访问多个容器。for (auto it myMap.cbegin(); it ! myMap.cend(); it) { // it-first, it-second }4. 进阶技巧与性能调优实战会用基础API只是及格线。要在实际项目中发挥最大威力必须了解下面这些进阶知识。4.1 自定义比较函数与透明比较器默认情况下set和map使用std::lessKey进行排序这意味着你的Key类型必须支持操作。但很多时候我们需要自定义排序规则。传统方式仿函数或Lambdastruct CaseInsensitiveCompare { bool operator()(const std::string a, const std::string b) const { return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char ca, char cb) { return std::tolower(ca) std::tolower(cb); }); } }; std::setstd::string, CaseInsensitiveCompare caseInsensitiveSet;现代利器透明比较器 (C14)这是为了提升性能而生的特性。看一个场景你有一个std::setstd::string你想用字符串字面量const char*去查找。传统做法会先构造一个临时的std::string对象产生不必要的内存分配。std::setstd::string names {Alice, Bob}; // 传统查找会构造一个临时的std::string(Alice) auto it names.find(std::string(Alice));透明比较器允许你直接使用不同类型的键进行比较只要它们之间可以比较。你需要做两件事比较器需要有一个is_transparent类型通常是void。比较器的operator()需要有多个重载能处理不同类型。struct StringCompare { using is_transparent void; // 关键声明为透明比较器 bool operator()(const std::string a, const std::string b) const { return a b; } bool operator()(const std::string a, const char* b) const { return a b; } bool operator()(const char* a, const std::string b) const { return a b; } }; std::setstd::string, StringCompare transparentSet {Alice, Bob}; // 现在可以直接用字符串字面量查找无需构造临时string auto it transparentSet.find(Alice); // 高效标准库提供了std::lessvoidC14起作为通用的透明比较器对于支持操作的类型可以直接使用std::setstd::string, std::less transparentSet; // 注意这里的 auto it transparentSet.find(Alice); // 可以工作强烈建议在C14及以后如果你不需要特殊排序规则声明set或map时使用std::less作为比较器可以带来潜在的查找性能提升。4.2 内存与性能考量当心“隐式”开销红黑树节点的内存开销是显著的。一个典型的std::mapint, int节点除了存储int键和int值还需要存储左右子节点指针、父节点指针以及颜色标记。在64位系统上这可能意味着每个节点额外有至少3个指针24字节的开销。如果你的键值对本身很小比如两个int8字节那么管理开销可能远大于数据本身。优化策略使用扁平容器对于小型、生命周期短、且需要频繁查找的集合考虑使用排序后的std::vector并使用std::binary_search或std::lower_bound。虽然插入删除是O(n)但数据局部性好缓存命中率高在小数据量比如几百个元素时实际性能可能远超set/map。使用std::unordered_set/unordered_map如果你不需要元素有序哈希表无序容器在平均O(1)时间复杂度的查找、插入、删除上通常更快。但它的最坏情况可能退化到O(n)且迭代顺序不确定。选择合适的键类型键的类型应该尽可能小且拷贝成本低。对于大对象作为键考虑使用指针如std::unique_ptr或std::string_viewC17作为键但要小心管理生命周期。4.3 提取与合并节点 (C17)C17引入了“拼接”功能允许你在两个同类型容器之间移动节点而无需拷贝或移动节点所包含的元素。这可以避免昂贵的拷贝构造或析构。extract(key)从容器中移除指定键的节点并返回一个node_type节点句柄。这个节点现在不属于任何容器但持有其元素。insert(node_handle)将节点句柄插入到容器中。如果目标容器中已存在相同键则插入失败节点句柄不会被消耗。std::mapint, std::string mapA, mapB; mapA[1] Alice; // 将键为1的节点从mapA移动到mapB不发生字符串拷贝 auto node mapA.extract(1); if (!node.empty()) { mapB.insert(std::move(node)); } // 现在 mapA 为空 mapB[1] Alice这在需要重组容器、或者元素类型移动成本高时非常有用。5. 常见“坑点”排查与最佳实践清单即使经验丰富有些坑还是容易踩。下面是我总结的“避坑指南”。5.1 迭代器失效陷阱相对安全但需注意如前所述set和map的迭代器在插入操作后通常保持有效在删除操作后只有指向被删除元素的迭代器会失效其他迭代器仍然有效。这比vector和deque安全得多。但遍历时删除仍需使用erase返回的新迭代器如前文所示。5.2operator[]的副作用与at()的选择这是最常见的错误来源之一。std::mapint, int counter; // 意图如果存在则加1不存在则初始化为1 if (/* 某个条件 */) { counter[key]; // 看起来没问题 } // 问题无论条件如何counter[key]都会执行。如果key不存在会插入{key, 0}然后自增为1。 // 这完全绕过了if条件判断正确做法在需要判断是否存在并访问的场景永远使用find。auto it counter.find(key); if (it ! counter.end()) { it-second; // 安全修改 } else { // 明确地插入初始值 counter[key] 1; }对于只读访问如果确定键必须存在使用at()可以暴露程序逻辑错误通过异常比operator[]的静默插入更安全。5.3 自定义类型的比较与const正确性如果你的Key是自定义类型必须确保比较函数是严格弱序的并且与运算符语义一致如果a不小于b且b不小于a则认为a等价于b。否则会导致未定义行为容器可能无法正确排序或查找。另外比较函数的operator()必须声明为const成员函数因为它不应该修改比较器对象的状态。5.4 多线程访问标准库容器包括set和map不是线程安全的。如果多个线程同时读写同一个容器必须使用互斥锁如std::mutex或其他同步机制来保护。一个常见的模式是使用读写锁如std::shared_mutexC17因为读操作find,count可以并发而写操作insert,erase需要独占。5.5 最佳实践速查表实践推荐做法理由插入优先使用emplace,try_emplace(C17),insert_or_assign(C17)避免临时对象语义更清晰安全查找只读访问用find或 C20的contains避免用operator[]查找operator[]会修改容器遍历优先使用基于范围的for循环代码简洁不易出错遍历中删除使用it container.erase(it)模式安全处理迭代器失效自定义比较考虑使用std::less(C14) 作为透明比较器提升异构查找性能性能敏感小数据集考虑排序vector无序需求用unordered_set/map缓存友好或平均O(1)复杂度键类型尽量小、拷贝成本低大对象用指针或string_view减少内存和拷贝开销线程安全自行加锁如std::shared_mutexSTL容器非线程安全最后再分享一个我调试时常用的小技巧当你怀疑set或map的顺序或查找有问题时写一个简单的循环打印出所有元素看看它们的顺序是否符合你的比较函数预期。很多时候问题就出在自定义比较函数的实现细节上。STL的关联容器是C的基石花时间深入理解它们绝对是一笔回报率极高的投资。

相关新闻

AI智慧分诊系统架构设计与医疗数字化转型实践

AI智慧分诊系统架构设计与医疗数字化转型实践

1. AI智慧分诊小程序的核心功能架构在互联网医疗领域,AI智慧分诊系统正逐渐成为医院数字化转型的关键基础设施。作为一名参与过多个三甲医院互联网平台建设的开发者,我认为一个完整的AI分诊系统需要包含以下核心模块:1.1 智能症状识别引擎这个…

2026/10/17 1:16:40 阅读更多 →
图像超分、去模糊轻量化算法,端侧实时推理实现

图像超分、去模糊轻量化算法,端侧实时推理实现

一、引言 在移动终端、嵌入式设备、物联网终端等端侧场景中,图像采集普遍存在分辨率不足、运动模糊、对焦失准、压缩失真等问题,直接影响视觉体验与机器视觉检测、识别、测温等下游任务精度。传统图像超分与去模糊算法多基于深度学习大型模型&#xff0…

2026/10/17 0:12:34 阅读更多 →
同事问我:你还算会写代码吗

同事问我:你还算会写代码吗

这句话出现的时候,桌上正开着 AI 对话窗口。 我让它生成了一段驱动骨架,自己在旁边改时序和错误处理。 同事笑着问:「你还算会写代码吗?」 像玩笑,又像刺。这个问题背后,是身份焦虑 工程师的自我认同&#…

2026/10/15 10:10:34 阅读更多 →

最新新闻

fnm 安装配置指南:让 OpenCode 在 PowerShell 里顺畅跑起来

fnm 安装配置指南:让 OpenCode 在 PowerShell 里顺畅跑起来

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 20:23:30 阅读更多 →
三种分布式时钟同步算法原理与MATLAB仿真实践

三种分布式时钟同步算法原理与MATLAB仿真实践

简介:本资源是一套面向通信工程、信号处理方向初学者与进阶学习者的定时同步算法实践材料,聚焦数据辅助型定时同步这一核心问题,适用于课程设计、毕业设计及分布式系统时序对齐场景。压缩包内含3个MATLAB源文件(.m格式&#xff09…

2026/10/17 20:23:30 阅读更多 →
动作条件世界模型评测:用真实机械臂执行结果做基准

动作条件世界模型评测:用真实机械臂执行结果做基准

有一段时间我特别烦一个词,叫“世界模型”。不是这个概念本身有问题,而是圈子里拿它讲故事的方式越来越离谱:今天这个团队说自己的模型在连续视频上预测出了下一帧,明天那个项目说自己的模型能规划动作,但你去翻评测方…

2026/10/17 20:23:30 阅读更多 →
MNN不支持5D张量?端侧3D视频推理的三种工程化绕过方案

MNN不支持5D张量?端侧3D视频推理的三种工程化绕过方案

1. 项目概述:为什么这个“不支持”反而值得深挖MNN 是一个轻量级、高性能的端侧推理框架,由某知名互联网公司开源,在移动端、嵌入式设备和边缘计算场景中被大量采用。它以模型小、启动快、内存占用低、跨平台兼容性好著称,尤其在图…

2026/10/17 20:23:30 阅读更多 →
遥感影像匀色镶嵌分幅处理全流程详解与常见问题排查

遥感影像匀色镶嵌分幅处理全流程详解与常见问题排查

简介:面向遥感影像处理与GIS领域从业者的实操手册,系统讲解影像匀色、镶嵌与分幅的完整处理流程,重点结合MCM智拼图V8软件演示各环节操作,适合需要解决多源影像色彩不一致、大范围拼接与图幅拆分等问题的技术人员使用。资源以单个…

2026/10/17 20:23:30 阅读更多 →
OpenClaw 3.13实测:浏览器操作从“能点”到“流畅”的质变

OpenClaw 3.13实测:浏览器操作从“能点”到“流畅”的质变

OpenClaw 3.13的更新公告放出来那天,我所在的讨论群里直接就炸了。不是因为发布节奏,而是因为公告里那句话:"3.13版本将流畅操作浏览器。"过去大半年,这个被大家称作"小龙虾"的开源AI助手项目,一直…

2026/10/17 20:22:29 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →