尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

深入STL源码:从容器原理到性能优化实战指南

深入STL源码:从容器原理到性能优化实战指南 1. 从“会用”到“懂用”为什么我们需要深入STL源码在C开发者的成长路径上STLStandard Template Library是一个绕不开的里程碑。大多数人的起点是从std::vector、std::map这些容器开始学习它们的接口然后在项目中熟练地调用push_back、find。这个阶段我们关注的是“它能做什么”。然而当项目规模扩大性能瓶颈出现或者遇到一些诡异的内存错误和迭代器失效问题时仅仅停留在“会用”的层面就显得捉襟见肘了。这时我们不得不去思考“它为什么能这样做”以及“它内部是怎么做的”。这就是侯捷老师的《STL源码剖析》一书的价值所在。它像一把手术刀精准地剖开了STL这个庞大而精密的“黑盒”让我们得以窥见其内部精巧的架构和令人惊叹的设计哲学。阅读这本书或者说整理相关的学习笔记其目的绝非为了炫耀“我读过源码”而是为了解决实际开发中的痛点为什么std::list的插入是O(1)而std::vector在某些情况下是O(n)std::sort在面对不同迭代器类型时内部采用了哪些不同的排序策略std::unordered_map的哈希冲突是如何解决的负载因子为何如此重要我自己的经历就是最好的例证。曾经在一个高并发的网络服务中我们大量使用了std::map来维护会话信息。随着在线用户数激增服务的响应延迟开始不可预测地飙升。通过性能剖析工具我们发现瓶颈竟在std::map的查找操作上。当时的第一反应是“红黑树的查找不是O(log n)吗怎么会这么慢”直到我翻开《STL源码剖析》重新审视了红黑树的实现、节点的内存布局以及频繁插入删除导致的树结构调整开销才恍然大悟。我们场景的特点是键值用户ID是连续递增的且插入后几乎不再修改但查找极其频繁。std::map的通用平衡树设计在此场景下反而带来了不必要的开销。最终我们将其替换为针对连续键值优化的std::vector结合二分查找性能提升了数十倍。这个案例深刻地告诉我不了解底层机制所谓的“优化”往往是盲人摸象。因此这份笔记的目标读者是那些已经熟悉STL基本用法渴望在技术深度上更进一步的中高级C开发者。它不适合零基础的初学者因为其中不会赘述vector::size()返回什么这类基础问题。它旨在串联起《STL源码剖析》中的核心知识点并结合我个人的实践与思考为你提供一个从“知其然”到“知其所以然”的路线图最终让你在面对复杂问题时能做出更明智、更高效的技术选型和设计决策。2. 庖丁解牛STL六大组件的协同架构与内存基石在深入任何一个具体容器或算法之前我们必须先建立起对STL整体架构的宏观认知。STL并非一堆零散工具的大杂烩而是一个由六大组件精密协作构成的生态系统。这六大组件是容器Containers、算法Algorithms、迭代器Iterators、仿函数Functors、适配器Adapters和分配器Allocators。它们之间的关系可以用一个经典的编程范式来概括算法通过迭代器操作容器过程中可以使用仿函数来改变策略这一切的内存来源由分配器管理适配器则用于修饰或组合现有组件以提供新的接口。2.1 六大组件如何各司其职容器是数据存储的主体如vector、list、deque、set、map等。它们负责数据的组织和管理。但STL设计最精妙的一点在于容器本身不提供任何遍历、查找、排序等数据操作方法。这些功能全部交给了算法。算法是STL的“大脑”如sort、find、copy、transform等。它们是全局函数模板独立于任何特定的容器。算法要操作容器内的数据需要一个通用的“访问媒介”这就是迭代器。迭代器是连接容器和算法的“桥梁”。它将不同容器的内部访问方式如数组的指针、链表的节点指针抽象成一套统一的接口如、*、-。正是由于迭代器的存在std::sort既可以排序vector也可以排序deque而无需关心它们底层是连续数组还是分段数组。仿函数或称函数对象是行为类似函数的对象。它重载了operator()使得对象可以像函数一样被调用。在算法中仿函数常用于指定比较准则如std::less、执行特定操作如std::plus。相比于普通函数指针仿函数可以拥有自己的状态并且编译器更容易对其进行内联优化。适配器是一种设计模式它修改现有组件的接口使其适应新的需求。STL中的适配器包括容器适配器如stack、queue底层默认由deque实现、迭代器适配器如back_insert_iterator和函数适配器旧式bind1st、bind2nd现多被std::bind和lambda表达式替代。分配器是最底层、也最容易被忽视但至关重要的组件。它封装了内存的分配与释放策略。默认的std::allocator简单地调用::operator new和::operator delete。理解分配器是理解STL内存管理、尤其是像vector增长策略和std::allocator实现的关键。2.2 分配器的秘密与std::allocator的演进SGI STL侯捷书中剖析的版本的分配器设计尤为经典它采用了双层配置器策略旨在提升小内存块的管理效率。第一级配置器__malloc_alloc_template直接使用malloc()和free()。它包含了一个类似于new-handler的机制当malloc失败时会尝试调用用户预设的“内存不足处理例程”并重试分配这为处理内存碎片化问题提供了一种可能。第二级配置器__default_alloc_template这是精华所在。它维护了一个自由链表数组负责管理小于128字节的小内存块。这个数组有16个槽位分别管理81624...128字节的内存块。当申请小内存时分配器从对应的自由链表中取出一块释放时则回收到链表。这极大地减少了频繁向操作系统申请/释放微小内存带来的开销和碎片。然而C标准库中的std::allocator在历史上很长一段时间内并没有采用如此复杂的设计它非常简单。直到C11之后标准分配器的能力才被增强如支持状态、支持propagate_on_container_copy_assignment等类型特征。但SGI STL的双层分配器思想在追求极致性能的底层库开发中依然具有极高的参考价值。注意在现代C项目中除非有极其特殊和确切的性能需求通常不建议自己从头实现复杂的分配器。更常见的做法是使用标准分配器或者利用std::pmr多态内存资源命名空间下的内存池工具它们是标准化的、更安全的内存管理方案。理解这套架构就像拿到了STL这座大厦的蓝图。后续我们再去看每一个具体的“房间”容器或“工具”算法时就能清楚地知道它们在整个体系中的位置和作用以及它们是如何与其他部件协同工作的。这种全局观是高效、正确使用STL的基石。3. 序列式容器深度解析vector、list、deque的设计哲学与性能抉择序列式容器维护了元素的插入顺序是我们最常打交道的伙伴。vector、list和deque是三种最核心的序列容器它们背后的数据结构选择动态数组、双向链表、分段连续数组直接决定了其性能特征和适用场景。仅仅记住“vector是数组list是链表”是远远不够的我们必须深入其源码理解每个操作背后的代价。3.1std::vector动态数组的智慧与陷阱vector的底层是一个动态分配的连续数组。它的强大在于随机访问的常数时间复杂度O(1)和优秀的缓存局部性。但它的“动态”二字也带来了最经典的问题容量管理。容量增长策略这是vector源码中最值得玩味的部分。当push_back新元素且当前容量(capacity)不足时vector必须重新分配一块更大的内存将旧元素全部移动或复制过去然后释放旧内存。这个“更大”是多大常见的策略也是SGI STL和多数现代实现采用的是按几何级数增长通常是旧容量的1.5倍或2倍。为什么是1.5或2这是一个工程上的权衡。增长因子太小如1.1会导致频繁的重新分配复制开销大。增长因子太大又会浪费内存。1.5或约1.618的黄金比例在多次扩容后之前释放的旧内存块有可能在后续分配中被复用这对某些内存分配器友好。而2倍增长计算简单且能保证在任何时候新分配的内存块大小都大于之前所有已分配内存块的总和这在某些特定场景下可以避免内存无法复用的问题。在实际项目中如果你能预先知道元素的大致数量使用reserve()函数提前分配足够容量是避免中间多次重新分配、提升性能的关键手段。迭代器失效问题这是vector使用中最常见的坑。任何可能引起vector重新分配内存的操作如insert,push_back导致扩容都会使指向容器内元素的所有迭代器、指针和引用失效。即使没有重新分配在插入点之后位置的迭代器等也会失效。例如std::vectorint vec {1, 2, 3, 4}; auto it vec.begin() 2; // it 指向 3 vec.push_back(5); // 可能导致扩容 // 此时 it 已失效对其解引用(*it)是未定义行为。而erase操作也会使被删除元素及其之后位置的迭代器失效。因此在循环中删除vector元素时必须小心处理迭代器// 错误写法 for (auto it vec.begin(); it ! vec.end(); it) { if (*it % 2 0) { vec.erase(it); // erase后it失效后续it行为未定义 } } // 正确写法 for (auto it vec.begin(); it ! vec.end(); ) { if (*it % 2 0) { it vec.erase(it); // erase返回被删除元素下一个元素的有效迭代器 } else { it; } }3.2std::list双向链表的恒定代价list是一个双向链表每个节点包含数据、指向前驱和后继的指针。它的核心优势在于在任何已知位置的插入和删除操作都是常数时间O(1)且不会使其他元素的迭代器失效。它的劣势是不支持随机访问访问第n个元素需要O(n)时间并且由于节点在内存中不连续缓存不友好遍历速度通常远慢于vector。节点的精巧设计SGI STL的list实现了一个精巧的环状双向链表。它有一个额外的“尾哨兵”节点这个节点的next指向头节点prev指向尾节点。这种设计使得list::end()返回的是这个哨兵节点的迭代器简化了begin()和end()的判断逻辑也让插入操作在头尾保持统一。何时使用list当你需要频繁在容器中间进行插入和删除并且不需要随机访问时list是理想选择。例如实现一个LRU缓存需要频繁将访问的元素移动到链表头部list的splice操作可以在常数时间内移动整个节点范围效率极高。但在绝大多数情况下由于缓存命中率的问题vector的性能表现往往更好即使它涉及元素的移动。不要因为“中间插入多”就盲目选择list先用vector做性能测试通常是更稳妥的做法。3.3std::deque双端队列的分段数组魔法deque双端队列的设计目标是在头尾两端都能进行高效的插入和删除。它不像vector那样要求所有元素严格连续也不像list那样完全离散。它的底层是一个“分段连续”的数组或者说一个“数组的数组”。中控器与缓冲区deque维护了一个名为map的指针数组中控器map中的每个指针指向一块固定大小的连续线性空间缓冲区。当在头部或尾部添加元素时deque会检查当前缓冲区是否已满如果已满则分配一个新的缓冲区并调整map中的指针。这种结构使得在头尾增删元素时大部分情况下只需要在已有的缓冲区操作只有在缓冲区边界时才需要分配新内存代价远低于vector的整体搬迁。迭代器的复杂性deque的迭代器是一个“智能”指针它需要知道当前元素位于哪个缓冲区以及在该缓冲区中的位置。因此deque的迭代器是一个包含多个指针的类其自增、自减操作可能需要跨缓冲区跳转。这也导致了deque的迭代器属于“随机访问迭代器”但其操作比vector的普通指针迭代器要稍慢。deque的适用场景deque非常适合作为先进先出FIFO或后进先出LIFO的队列基底。标准库中的stack和queue默认就是用deque作为底层容器实现的。当你需要一个既支持快速随机访问又需要在头尾高效插入的容器时deque是比vector和list更好的折中选择。下表总结了三大序列容器的核心特性对比特性std::vectorstd::liststd::deque底层结构动态连续数组双向链表分段连续数组数组的数组随机访问O(1)极快O(n)慢O(1)较快头部插入/删除O(n)O(1)O(1)尾部插入/删除平摊O(1)O(1)O(1)中间插入/删除O(n)O(1)O(n)迭代器失效容量变则全失效插入/删除点后失效仅被删除元素失效插入可能导致所有迭代器失效删除点失效内存使用紧凑少额外开销每个元素需两个指针开销有中控器开销内存局部性较好缓存友好度极好差较好4. 关联式容器与哈希表map/set与unordered_map/unordered_set的平衡之术当我们需要根据键来快速查找、插入和删除元素时关联式容器是我们的首选。它们主要分为两大类基于红黑树的有序关联容器map,set,multimap,multiset和基于哈希表的无序关联容器unordered_map,unordered_set等。4.1 红黑树std::map与std::set的秩序守护者map和set的底层实现通常是红黑树这是一种自平衡的二叉搜索树。红黑树通过一组复杂的规则节点有颜色、根黑、叶黑、红节点子必黑、任意路径黑节点数相同来保证树的大致平衡从而确保最坏情况下的查找、插入、删除时间复杂度都是O(log n)。为什么是红黑树而不是AVL树这是一个经典的面试题。AVL树是更严格的平衡二叉树其查找性能略优于红黑树。但红黑树在插入和删除节点时为了维持平衡所需的旋转操作更少。因此红黑树在需要频繁修改的动态数据集上综合性能更好。STL选择红黑树正是出于对插入、删除、查找操作整体性能的权衡。map的operator[]的“魔术”map的[]运算符行为非常特殊。m[key]会执行以下操作1. 查找键key是否存在。2. 如果存在返回其对应值的引用。3.如果不存在则插入一个键为key、值被值初始化的新元素并返回其值的引用。这意味着m[key]永远成功且可能改变map的大小。如果你只是想查找而不想插入应该使用find()成员函数。这个特性使得map可以非常方便地作为计数器使用word_count[word]。键的不可变性set中的元素和map中的键都是常量。这是由红黑树的结构决定的。树根据键值进行排序和组织如果允许修改键将会破坏树的排序性质导致后续查找等操作出错。因此set::iterator和map::iterator解引用后得到的是const类型。4.2 哈希表std::unordered_map的极速查找unordered_map的底层是哈希表它通过哈希函数将键映射到一个桶bucket中理想情况下查找、插入、删除的平均时间复杂度是O(1)。但这依赖于一个好的哈希函数和合理的冲突解决策略。哈希冲突与开链法SGI STL的hash_table实现采用“开链法”解决冲突。每个桶bucket不是一个直接存储元素的位置而是一个指针指向一个链表或单链表。所有哈希到同一位置的元素都放在这个链表中。当链表过长时查找会退化为O(n)。负载因子与重哈希负载因子 元素数量 / 桶的数量。当负载因子超过某个阈值默认通常是1.0哈希表的性能会急剧下降。此时容器会执行“重哈希”创建一个新的、桶数更多的桶数组然后根据新的桶数重新计算每个元素的哈希值并将其插入到新数组对应的桶中。这个过程开销很大类似于vector的扩容。你可以通过max_load_factor()和rehash()、reserve()成员函数来干预这一过程。自定义类型作为键如果你想将自定义类型作为unordered_map的键你必须提供两个东西哈希函数一个可以调用、返回size_t的函数对象告诉容器如何计算你的类型的哈希值。通常需要特化std::hash模板。相等比较函数当哈希冲突发生时用来判断两个键是否真正相等。通常是重载operator。struct MyKey { std::string name; int id; }; // 1. 定义相等运算符 bool operator(const MyKey lhs, const MyKey rhs) { return lhs.name rhs.name lhs.id rhs.id; } // 2. 特化 std::hash namespace std { template struct hashMyKey { size_t operator()(const MyKey k) const { // 组合 name 和 id 的哈希值 return hashstring()(k.name) ^ (hashint()(k.id) 1); } }; } // 现在可以使用 MyKey 作为 unordered_map 的键 std::unordered_mapMyKey, std::string myMap;有序与无序的选择选择map/set当你需要元素始终按键排序或者需要按顺序遍历从小到大或者对最坏情况下的性能有严格要求保证O(log n)时。选择unordered_map/unordered_set当你对查找速度有极致要求平均O(1)且不需要有序遍历并且愿意为自定义类型提供哈希函数时。实操心得在大多数情况下unordered_map的查找速度远快于map。但在键的数量很少例如少于100个时由于红黑树优秀的缓存局部性map的性能可能反超。同时如果哈希函数质量很差导致大量冲突unordered_map的性能会雪崩。因此在性能关键处最好进行基准测试。5. 迭代器与算法泛型编程的灵魂与效率引擎迭代器是STL泛型编程的基石它抽象了访问容器元素的统一方式。算法则建立在迭代器提供的抽象之上实现了“数据与操作分离”的高阶目标。5.1 迭代器五种类型与萃取机制迭代器不仅仅是指针的泛化。根据支持的操作迭代器被分为五类形成一个层次结构输入迭代器只读且只能向前移动。例如从标准输入读取数据的迭代器。输出迭代器只写且只能向前移动。前向迭代器可读写只能向前移动。std::forward_list的迭代器就是前向迭代器。双向迭代器可读写能向前和向后--移动。std::list、std::set的迭代器属于此类。随机访问迭代器功能最全支持所有指针算术运算如n、-n、[]、比较大小等。std::vector、std::deque的迭代器属于此类。迭代器萃取iterator_traits这是一个经典的C元编程技术。算法需要知道迭代器所指元素的类型value_type、差值类型difference_type、指针类型pointer、引用类型reference以及迭代器类别iterator_category。对于原生指针也是一种随机访问迭代器它并没有这些内嵌类型定义。iterator_traits模板类通过特化为原生指针和类类型的迭代器提供了一个统一的类型查询接口。这使得像std::distance、std::advance这样的算法能够根据迭代器类别选择最高效的实现例如对随机访问迭代器直接用减法对输入迭代器则用循环累加。5.2 算法的泛化与特化以std::sort和std::copy为例STL算法是泛型的但为了效率它们常常会对特定迭代器类别进行特化优化。std::sort的智慧std::sort要求随机访问迭代器因为它内部需要快速计算中间位置。它的实现通常是内省排序结合了快速排序、堆排序和插入排序的优点主体采用快速排序递归划分。当递归深度过深可能退化为O(n²)时转而使用保证O(n log n)的堆排序。当分区规模很小时例如少于16个元素改用插入排序因为插入排序对小规模几乎有序的数据效率很高。std::copy的极致优化std::copy是一个将效率发挥到极致的例子。它的实现会通过iterator_traits判断迭代器类型如果迭代器是普通指针且所指类型是平凡可复制的它会直接调用memcpy或memmove这是最快的内存块拷贝。否则它会退化为循环赋值*result *first; result; first;。这种根据类型特性选择不同实现的技术是C模板元编程和泛型编程强大威力的体现。它保证了通用性的同时又不牺牲性能。算法与容器的协作记住算法通过迭代器操作容器但算法本身对容器的内部结构一无所知。这意味着有些操作在特定容器上有更高效的成员函数版本。最经典的例子是std::find和std::map::find。std::find是线性查找O(n)。而std::map::find利用红黑树特性是O(log n)。因此对关联容器进行查找一定要用其自身的find成员函数。6. 仿函数、适配器与现代C的Lambda表达式仿函数和适配器是STL中实现策略定制和接口转换的重要工具它们赋予了算法极大的灵活性。6.1 仿函数携带状态的函数仿函数本质是一个重载了operator()的类。相比于普通函数它的优势在于可拥有状态你可以在构造函数中初始化仿函数对象使其携带信息。例如一个记录调用次数的仿函数。可作为模板参数编译器在实例化模板时就知道仿函数的类型便于内联优化。可适配可以与旧的函数适配器如bind1st配合使用尽管现代C中不推荐。STL定义了许多标准仿函数位于functional头文件中如std::plus,std::less,std::greater,std::logical_and等。它们都是模板类可以对不同类型进行操作。6.2 适配器的演进从bind1st到std::bind与Lambda早期的STL提供了bind1st,bind2nd,not1,not2等函数适配器用于将二元仿函数适配成一元仿函数或者对谓词取反。但这些适配器使用起来非常繁琐且对函数指针和成员函数支持不好。C11引入的std::bind和Lambda表达式彻底改变了游戏规则。Lambda表达式它允许你在调用算法的地方就地定义一个匿名函数对象语法简洁直观。std::vectorint vec {5, 3, 1, 4, 2}; // 使用Lambda表达式按降序排序 std::sort(vec.begin(), vec.end(), [](int a, int b) { return a b; }); // 查找第一个大于3的元素 auto it std::find_if(vec.begin(), vec.end(), [](int x) { return x 3; });Lambda可以捕获外部变量[]值捕获[]引用捕获或指定变量功能极其强大现在已成为STL算法中指定策略的首选方式。std::bind它可以部分绑定函数参数重新排列参数顺序将成员函数绑定为可调用对象等。虽然功能强大但在Lambda面前其语法相对晦涩在简单场景下已较少使用。using std::placeholders::_1; bool is_divisible(int a, int b) { return a % b 0; } // 创建一个判断是否能被5整除的新函数对象 auto is_div_by_5 std::bind(is_divisible, _1, 5); bool result is_div_by_5(10); // true6.3 容器适配器stack、queue和priority_queue它们不是独立的容器而是对底层容器默认dequepriority_queue默认用vector的接口进行封装提供特定的数据结构语义。std::stack后进先出LIFO适配了push、pop、top等操作。std::queue先进先出FIFO适配了push、pop、front、back等操作。std::priority_queue优先队列出队顺序按优先级默认大顶堆。其底层通常用vector存储并用堆算法维护。理解适配器就是理解“组合优于继承”的设计思想。它们通过限制和重新组合底层容器的接口提供了更安全、语义更明确的数据结构而无需重新实现底层存储。7. 从源码到实战避坑指南与高效使用守则阅读源码的最终目的是为了更好的实践。结合《STL源码剖析》的启示和多年的项目经验我总结了一些关键的使用准则和避坑点。7.1 迭代器失效你必须牢记的规则表迭代器失效是STL使用中最容易导致未定义行为的错误。不同容器的不同操作对迭代器的影响不同。下面这个表格可以帮你快速查阅容器操作迭代器失效情况vector/string所有插入操作 (insert,push_back等)若引起重新分配则所有迭代器、指针、引用均失效。若未重新分配则插入点之后的迭代器、指针、引用失效。所有删除操作 (erase,pop_back等)被删除元素及其之后的迭代器、指针、引用失效。deque在头尾插入 (push_front/back)所有迭代器失效但指针和引用通常仍有效除非元素被移动。在中间插入 (insert)所有迭代器、指针、引用失效。在头尾删除 (pop_front/back)所有迭代器失效但指向剩余元素的指针和引用仍有效。在中间删除 (erase)所有迭代器、指针、引用失效。list/forward_list插入不会使任何迭代器失效除了指向被插入元素的迭代器。删除仅使指向被删除元素的迭代器失效。关联容器 (map,set...)插入不会使任何迭代器失效。删除仅使指向被删除元素的迭代器失效。无序容器 (unordered_*)插入若引起重哈希则所有迭代器失效。否则不影响。删除仅使指向被删除元素的迭代器失效。黄金法则在循环中修改容器增删元素时务必使用容器操作返回的新迭代器来更新循环变量或者使用erase-remove惯用法针对vector/deque/string。7.2 效率陷阱与优化策略vector的reserve与shrink_to_fit如果事先知道vector要存储的元素数量使用reserve()预分配空间可以避免多次扩容和数据拷贝这是提升性能最有效的手段之一。相反如果vector扩容后删除了大量元素可以使用shrink_to_fit()请求释放未使用的内存注意这是一个非强制性的请求。emplace系列函数C11引入了emplace_back,emplace,emplace_hint等函数。它们直接在容器内部构造元素避免了先构造临时对象再移动或拷贝的开销。对于构造开销大的类型性能提升显著。std::vectorstd::pairint, std::string vec; vec.push_back(std::make_pair(1, hello)); // 构造临时pair再移动 vec.emplace_back(1, hello); // 直接在vector内存中构造pair算法与成员函数的选择如前所述对关联容器使用std::find是O(n)而使用其自身的find成员是O(log n)。同样std::list有自己的sort成员函数它利用链表特性进行归并排序通常比通用std::sort要求随机访问迭代器更高效。std::move与容器在C11中你可以使用std::move将左值转换为右值从而在容器操作中触发移动语义而非拷贝语义这对于管理资源如std::string,std::vector的对象能大幅提升性能。std::vectorstd::string bigStrings; std::string hugeString ...; // 一个很大的字符串 bigStrings.push_back(std::move(hugeString)); // 移动开销小 // 此后 hugeString 状态有效但未指定通常为空7.3 理解std::allocator与现代内存管理虽然我们很少需要自定义分配器但理解其工作原理有助于我们理解STL的内存行为。现代CC17/20引入了多态分配器和内存资源的概念std::pmr命名空间。它允许容器使用一个共同的分配器接口但背后可以连接不同的内存池如单调缓冲区资源、同步池资源等。这对于在特定场景如高频交易、游戏引擎下管理内存、避免碎片化提供了标准化的解决方案。例如你可以使用一个栈上的缓冲区作为vector的初始内存来源#include memory_resource std::byte buffer[1024]; // 栈上缓冲区 std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)}; std::pmr::vectorint vec{pool}; // vec 会先使用栈缓冲区用尽后再向系统申请这在不允许动态内存分配或对性能有极端要求的场景下非常有用。回过头看侯捷老师的《STL源码剖析》它不仅仅是一本讲解代码的书更是一本关于软件设计、数据结构和算法权衡的经典教材。它教会我们的是一种深入底层、理解抽象背后代价的思维方式。在日复一日的编码中这种思维方式能让你在面对“用vector还是list”、“这里会不会迭代器失效”、“为什么性能上不去”这类问题时不再凭感觉猜测而是能有理有据地分析、验证和决策。这才是阅读源码、记下这些笔记的终极意义。
返回列表