1. 项目概述:为什么我们需要一本“史上最全面”的C++容器教程?
干了十几年C++,从桌面应用到后台服务,再到嵌入式系统,我几乎每天都在和容器打交道。每次面试新人,或者带团队新人上手项目,总会发现一个现象:很多人对std::vector、std::map用得滚瓜烂熟,但一被问到“为什么这里用deque而不是list?”或者“unordered_map的哈希冲突怎么解决?负载因子多少合适?”,就有点含糊其辞了。市面上不缺C++容器的资料,但要么是STL源码剖析那种硬核到劝退的“天书”,要么是只讲几个常用API的“快餐教程”,中间缺了一环——一个能把容器“是什么、为什么、怎么选、怎么用、怎么避坑”串起来的体系化指南。
这就是我想写这篇东西的初衷。它不只是一份API手册,更是一个从基础认知到实战决策的完整思维框架。C++标准库的容器家族庞大而精密,理解它们,就像理解你工具箱里的每一把扳手和螺丝刀。用对了,代码高效优雅;用错了,可能就是性能瓶颈甚至内存泄漏的源头。我会带你从最基础的序列容器(vector,deque,list)和关联容器(map,set,unordered_map)讲起,深入到它们的底层实现、迭代器失效、内存布局、时间复杂度,并结合大量我踩过的坑和优化过的案例,让你真正掌握在不同场景下“选对容器、用好容器”的能力。无论你是刚接触C++的新手,还是想深化理解的中高级开发者,这篇“最全面”的解析,目标就是让你对C++容器的认知,从“会用”升级到“精通”。
2. 容器基础与核心概念:理解设计的基石
在深入每个容器之前,我们必须建立几个核心的、全局性的概念。这些概念是理解所有容器行为差异的钥匙。
2.1 迭代器:容器的“通用指针”
迭代器是STL设计的精髓,它抽象了访问容器元素的方式,让算法(如std::sort,std::find)可以独立于具体容器工作。你可以把它想象成一个智能的、知道容器内部结构的指针。
迭代器类别是理解其能力的关键:
- 输入迭代器:只能读,且只能单向向前移动(如
istream_iterator)。 - 输出迭代器:只能写,单向向前。
- 前向迭代器:可读写,单向向前,但支持多次通行(如
std::forward_list的迭代器)。 - 双向迭代器:可读写,能向前也能向后移动(如
std::list,std::map的迭代器)。 - 随机访问迭代器:功能最强大,除了双向移动,还能直接跳跃(
iter + n),支持下标式访问(如std::vector,std::deque的迭代器)。
一个关键实操点:迭代器失效。这是C++容器使用中最常见的坑之一。当容器结构发生改变(如插入、删除元素,或vector/string的重新分配内存),指向容器元素的迭代器、指针或引用可能会变得无效。例如:
std::vector<int> vec = {1, 2, 3, 4}; auto it = vec.begin() + 2; // 指向元素3 vec.push_back(5); // 可能导致容量不足,重新分配内存 // 此时 it 已失效!对其解引用 (*it) 是未定义行为注意:
list,map,set等基于节点的容器,插入操作通常不会使其他迭代器失效(除了被删除的那个)。但vector和deque则要小心,插入/删除点之后的迭代器都可能失效。
2.2 内存分配器:隐藏在幕后的内存管家
每个STL容器模板的第二个参数(通常被忽略)就是分配器(Allocator),例如std::vector<T, Allocator>。默认是std::allocator,它简单地调用::operator new和::operator delete。
为什么需要了解分配器?
- 定制内存管理:在嵌入式或高性能场景,你可能需要从特定的内存池(如栈、共享内存)分配。你可以实现自己的分配器类,满足
Allocator概念的要求,然后传给容器。 - 诊断与调试:可以写一个带日志的分配器,跟踪容器的每一次内存申请和释放,用于分析内存使用模式或检测内存泄漏。
一个简单的带日志的分配器示例框架:
template<typename T> class LoggingAllocator { public: using value_type = T; T* allocate(std::size_t n) { std::cout << “Allocating ” << n * sizeof(T) << “ bytes\n”; return static_cast<T*>(::operator new(n * sizeof(T))); } void deallocate(T* p, std::size_t n) { std::cout << “Deallocating ” << n * sizeof(T) << “ bytes\n”; ::operator delete(p); } // ... 其他必要的成员函数和类型定义(如 rebind) }; // 使用 std::vector<int, LoggingAllocator<int>> tracked_vec;2.3 时间复杂度:选择容器的核心依据
我们常说“vector访问快,list插入快”,这背后就是时间复杂度的衡量。大O符号(O)描述了算法性能随数据规模增长的趋势。
- O(1):常数时间,操作耗时与数据量无关。如
vector的随机访问([ ])、unordered_map的平均情况插入/查找。 - O(log n):对数时间,性能极佳。如
map/set的插入、查找、删除(基于红黑树)。 - O(n):线性时间,耗时与数据量成正比。如
list的查找(需要遍历)、vector在中间位置的插入/删除(需要移动元素)。
选择容器时,必须结合你最主要的操作(是频繁查找、随机访问,还是大量在头部插入?)来权衡时间复杂度。没有“最好”的容器,只有“最适合”当前场景的容器。
3. 序列容器深度解析:vector,deque,list,forward_list,array
序列容器按线性顺序存储元素,区别在于底层数据结构和由此带来的性能特征。
3.1std::vector:默认的首选,但并非万能
vector是一个动态数组,在连续的内存块中存储元素。这是你应该首先考虑的序列容器,因为它对缓存最友好(局部性原理),随机访问是O(1)。
核心机制与实操要点:
- 容量与大小:
size()是元素数量,capacity()是已分配内存可容纳的元素数量。当size() == capacity()时,再push_back会触发重新分配(reallocation):分配一块更大的新内存(通常是旧容量的1.5或2倍),将旧元素移动或复制到新内存,释放旧内存。这个过程会使所有迭代器、指针、引用失效。 - 预留空间:如果你提前知道大致元素数量,使用
reserve(n)可以一次性分配足够内存,避免多次重新分配带来的性能开销和迭代器失效问题。std::vector<int> vec; vec.reserve(1000); // 一次性分配至少1000个int的内存 for (int i = 0; i < 1000; ++i) { vec.push_back(i); // 这1000次push_back都不会触发重新分配 } - 元素擦除的陷阱:
erase函数返回被删除元素之后元素的有效迭代器。经典的删除特定元素循环应该这样写:std::vector<int> vec = {1, 2, 3, 4, 5, 3}; for (auto it = vec.begin(); it != vec.end(); ) { if (*it == 3) { it = vec.erase(it); // erase 返回下一个有效迭代器 } else { ++it; } } // vec 现在是 {1, 2, 4, 5} - 移动语义与
emplace:C++11后,优先使用emplace_back代替push_back,它直接在容器尾部构造元素,避免临时对象的创建和拷贝/移动。struct Widget { Widget(int a, double b) { /* ... */ } }; std::vector<Widget> widgets; widgets.emplace_back(10, 3.14); // 直接在vector内存中构造Widget // 优于 widgets.push_back(Widget(10, 3.14));
适用场景:需要频繁随机访问;元素数量相对稳定或可预测;尾部插入/删除是主要操作。不适用场景:频繁在头部或中间插入/删除(需要移动大量元素,O(n))。
3.2std::deque:双端队列,头尾操作的高手
deque(双端队列)支持在头部和尾部进行高效的插入和删除(O(1))。它的名字常让人误以为底层是链表,其实它通常由一段段固定大小的连续内存块(缓冲区)组成,并通过一个中央映射器来管理这些块。
与vector的关键区别:
- 内存非完全连续:
deque的元素在逻辑上是连续的(迭代器可以++/--),但物理内存是分段的。这意味着对缓存不如vector友好,且不能保证像&vec[0]那样获得指向所有元素的裸指针。 - 头插高效:
push_front是O(1),而vector的insert(begin(), val)是O(n)。 - 重新分配影响更小:
deque的扩容通常只需分配新的缓冲区并添加到映射中,不需要移动所有现有元素,因此插入操作使迭代器失效的概率比vector低,但使所有迭代器失效的情况依然存在(例如当映射器本身需要扩容时)。
实操心得:当你需要一个既支持高效随机访问,又需要频繁在两端插入删除的序列时,deque是比vector更好的选择。例如,实现一个任务队列(生产者从一端推入,消费者从另一端取出)。
3.3std::list与std::forward_list:基于节点的链表
list是双向链表,forward_list(C++11)是单向链表。它们的元素存储在独立的节点中,通过指针链接。
核心优势与代价:
- 优势:在任何位置插入/删除元素都是O(1)(前提是已有指向该位置的迭代器),且不会使其他迭代器失效(除了被删除的那个)。
- 代价:内存开销大(每个节点需要额外存储前后指针);内存不连续,对缓存极不友好;不支持随机访问(
[ ]运算符),查找需要O(n)。
list的特殊操作:list提供了几个高效的成员函数,这些是算法(如std::sort)无法替代的:
splice:将另一个list的部分或全部节点移动到本list的指定位置,无需拷贝或移动元素,只调整指针,O(1)或O(n)(取决于范围)。sort:成员函数list::sort进行归并排序,比通用算法std::sort(需要随机访问迭代器)更适合链表。merge,unique:也有对应的成员函数版本,效率更高。
forward_list的极简主义:forward_list只提供单向遍历,因此每个节点节省了一个指针的开销。它的API设计也更节省,例如没有size()函数(因为计算size是O(n)),删除操作需要给定前驱节点的迭代器。
std::forward_list<int> flist = {1, 2, 3, 4}; auto it = flist.begin(); // 指向1 ++it; // 指向2 // 要删除元素2,需要获取其前驱(元素1)的迭代器,或者使用 erase_after flist.erase_after(flist.before_begin()); // 删除第一个元素(1)之后的元素,即2适用场景:频繁在任意位置插入/删除大量元素(如编辑一个大型列表);需要稳定的迭代器(插入删除不影响其他迭代器);内存碎片化不是主要顾虑。不适用场景:需要频繁随机访问或查找;对缓存性能要求极高。
3.4std::array:编译期定长的静态数组
std::array<T, N>是C++11引入的,封装了C风格数组,提供了STL容器的接口(如begin(),end(),size()),且大小在编译期确定。
与普通数组和vector的比较:
- 对比C数组:更安全(知道自身大小,避免退化成指针),支持STL算法。
- 对比
vector:内存分配在栈上(如果array本身在栈上)或作为对象的一部分,无动态内存管理开销,性能极致。但大小固定,无法改变。
典型用法:
#include <array> #include <algorithm> std::array<int, 5> arr = {5, 3, 1, 4, 2}; std::sort(arr.begin(), arr.end()); // 可以安全使用STL算法 // arr.size() 编译期常量,可用于模板参数等场景适用场景:大小在编译期已知且固定的小型集合;对性能有极致要求,需要避免堆分配;作为轻量级的容器式数据结构传递。
4. 关联容器深度解析:map,set,multimap,multiset
关联容器按关键字(Key)来保存和访问元素。它们分为有序和无序两大类。
4.1 有序关联容器:基于红黑树的std::map/set
map存储键值对(pair<const Key, Value>),set只存储关键字。它们基于红黑树(一种自平衡的二叉搜索树)实现,因此元素总是按键的升序排列(默认使用std::less<Key>,也可自定义比较函数)。
核心特性:
- 排序:遍历
map或set,会得到有序序列。 - 对数复杂度:插入、删除、查找操作的平均和最坏情况时间复杂度都是O(log n)。
- 关键字不可修改:
map的键和set的元素是const的,不能直接修改,以免破坏树的结构。要修改键,通常需要先删除再插入。
插入操作的选择:
insert:插入单个元素或范围。返回一个pair<iterator, bool>,bool表示是否插入成功(键不存在则成功)。operator[](仅map):map[key]。如果key不存在,会插入一个用Value的默认构造函数创建的元素,并返回其引用。这是一个容易踩坑的地方:如果你只是想查找,不小心用了[],可能会意外插入元素。std::map<std::string, int> wordCount; // 正确计数方式 for (const auto& word : words) { ++wordCount[word]; // 如果word不存在,会插入{word, 0},然后递增到1 } // 仅查找,不应使用[] auto it = wordCount.find(“hello”); if (it != wordCount.end()) { // 找到了,使用 it->second }
自定义比较函数:当键类型没有定义<运算符,或者你想定义特殊的排序规则时。
struct 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::map<std::string, int, CaseInsensitiveCompare> caseInsensitiveMap;multimap和multiset:允许重复键。它们没有operator[],因为一个键可能对应多个值。查找一个键需要使用equal_range(key),它返回一个迭代器对[first, last),表示该键对应的所有元素的范围。
4.2 无序关联容器:基于哈希表的std::unordered_map/set
C++11引入,基于哈希表实现。元素的存储顺序与插入顺序或键值无关,取决于哈希函数和桶的布局。
核心机制:
- 哈希函数:将任意大小的键映射到固定大小的哈希值(
std::size_t)。标准库为内置类型和std::string等提供了特化。自定义类型需要提供哈希函数,通常通过特化std::hash模板或传递一个自定义函数对象给容器。struct MyKey { int id; std::string name; }; struct MyKeyHash { std::size_t operator()(const MyKey& k) const { return std::hash<int>()(k.id) ^ (std::hash<std::string>()(k.name) << 1); } }; struct MyKeyEqual { bool operator()(const MyKey& lhs, const MyKey& rhs) const { return lhs.id == rhs.id && lhs.name == rhs.name; } }; std::unordered_map<MyKey, Value, MyKeyHash, MyKeyEqual> myMap; - 桶与冲突解决:哈希表维护一个桶数组。元素根据哈希值被分配到某个桶中。多个元素哈希到同一桶时发生冲突,标准库通常采用链地址法(每个桶是一个链表)。
- 负载因子:
load_factor() = size() / bucket_count()。当负载因子超过max_load_factor()(默认1.0)时,容器会重新哈希(rehash):增加桶的数量,重新计算所有元素的哈希并分配到新桶中。这个过程开销很大,会使所有迭代器失效(但指针/引用指向的元素本身不变)。
性能调优关键点:
- 预留桶数量:如果你知道大概有多少元素,使用
reserve(n)或rehash(n)来预分配足够多的桶,可以避免插入过程中的多次重哈希。std::unordered_map<int, Data> bigMap; bigMap.reserve(100000); // 提示容器准备存储大约100000个元素,预分配足够的桶 - 选择好的哈希函数:目标是让哈希值均匀分布,减少冲突。糟糕的哈希函数会导致大量元素聚集在少数桶中,使性能退化为O(n)。
- 观察桶状态:调试时可以使用
bucket_count(),bucket_size(n),load_factor()等函数来了解哈希表的健康状况。
有序 vs 无序 如何选?
- 需要元素有序遍历,或者键的比较操作很廉价时,用
map/set。 - 需要极快的平均查找速度(O(1)),且不关心顺序,用
unordered_map/set。但要注意,其最坏情况性能(所有元素哈希到一个桶)是O(n)。 - 当键是自定义类型且没有现成的、良好的哈希函数时,实现一个分布均匀的哈希函数可能比实现一个正确的比较运算符更困难,此时用
map可能更简单。
5. 容器适配器:stack,queue,priority_queue
它们不是独立的容器,而是在某种序列容器(默认deque或vector)之上,提供特定的接口。
5.1std::stack:后进先出(LIFO)
默认基于deque实现,你也可以指定底层容器(如vector,list)。
#include <stack> #include <vector> std::stack<int> s1; // 默认使用 deque std::stack<int, std::vector<int>> s2; // 使用 vector 作为底层容器 s2.push(1); s2.push(2); int top = s2.top(); // 2 s2.pop(); // 移除2底层容器选择:vector可能更节省内存,但pop时不会释放内存(vector::pop_back只减少size,不改变capacity)。deque是默认的平衡选择。list开销最大,通常不必要。
5.2std::queue:先进先出(FIFO)
默认基于deque实现。要求底层容器支持front,back,push_back,pop_front。因此vector不能直接用作queue的底层容器(因为vector没有pop_front)。
std::queue<int> q; q.push(1); q.push(2); int front = q.front(); // 1 q.pop(); // 移除15.3std::priority_queue:优先级队列
默认基于vector实现,并使用std::less比较器来构造一个最大堆(堆顶元素最大)。你可以自定义比较器来改变优先级。
#include <queue> #include <functional> // 最大堆(默认) std::priority_queue<int> maxHeap; maxHeap.push(3); maxHeap.push(1); maxHeap.push(4); int top = maxHeap.top(); // 4 // 最小堆 std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap; minHeap.push(3); minHeap.push(1); minHeap.push(4); top = minHeap.top(); // 1底层原理:priority_queue不进行全局排序,而是维护一个堆结构。push和pop操作的时间复杂度是O(log n),top是O(1)。它适用于需要不断处理当前最高(或最低)优先级元素的场景,如任务调度、Dijkstra算法。
6. 容器实战:选择策略、性能陷阱与惯用法
懂了所有容器的原理,最终还是要落到“怎么用”上。这部分是我多年实战中总结的经验和教训。
6.1 容器选择决策树
面对一个具体问题,可以按以下思路选择:
- 是否需要按键快速查找?
- 是:进入关联容器。
- 是否需要元素有序?
- 是:用
std::map(键值对)或std::set(仅键)。 - 否:用
std::unordered_map或std::unordered_set(追求平均O(1)查找)。
- 是:用
- 是否需要元素有序?
- 否:进入序列容器。
- 是:进入关联容器。
- 元素数量是否固定且在编译期已知?
- 是:用
std::array。 - 否:继续。
- 是:用
- 主要的操作是什么?
- 频繁随机访问:首选
std::vector。 - 频繁在两端插入/删除:用
std::deque。 - 频繁在任意位置插入/删除(已知位置迭代器):用
std::list(双向)或std::forward_list(单向,更省内存)。 - 需要后进先出/先进先出/优先级管理:用容器适配器
stack/queue/priority_queue。
- 频繁随机访问:首选
6.2 性能陷阱与优化技巧
vector的“增长策略”与reserve:如前所述,未预分配的vector在多次push_back时,重新分配和元素拷贝/移动的开销巨大。经验法则:如果能预估元素数量,哪怕只是粗略估计,也请使用reserve。erase-remove惯用法:要从vector或deque中删除满足条件的所有元素,不要用循环调用erase(每次都是O(n)移动)。使用erase-remove惯用法:std::vector<int> vec = {1, 2, 3, 4, 5, 3}; vec.erase(std::remove(vec.begin(), vec.end(), 3), vec.end()); // vec 现在是 {1, 2, 4, 5}std::remove将不等于3的元素移动到前面,并返回新的逻辑结尾迭代器,erase再删除尾部多余的元素。对于list,直接使用成员函数list::remove更高效。map的operator[]vsinsertvsemplace:- 如果键可能已存在,且你想更新值,用
operator[]或insert/emplace配合返回值判断都可以。 - 如果键很可能不存在,且你想插入新值,优先用
try_emplace(C++17)或emplace,它们只在键不存在时才构造元素,避免了不必要的临时对象。std::map<std::string, std::unique_ptr<Widget>> widgetMap; // 不好:即使键存在,也会构造一个临时的 unique_ptr // widgetMap[“key”] = std::make_unique<Widget>(args); // 更好:只在键不存在时构造 widgetMap.try_emplace(“key”, std::make_unique<Widget>(args));
- 如果键可能已存在,且你想更新值,用
unordered_map的哈希质量:自定义类型的哈希函数如果质量差,会导致大量冲突。一个简单技巧是利用现有类型的哈希函数进行组合,例如使用boost::hash_combine或自己实现类似逻辑:std::size_t hash = std::hash<int>()(key.id); hash ^= std::hash<std::string>()(key.name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
6.3 容器与算法(STL Algorithms)的配合
STL算法(<algorithm>头文件)大多通过迭代器与容器协作。理解迭代器类别,就能知道算法对容器的要求。
std::sort,std::nth_element:需要随机访问迭代器,因此只能用于vector,deque,array,string。对list要用list::sort。std::stable_sort,std::partial_sort:同样需要随机访问。std::find,std::count,std::for_each:只需要输入迭代器,所有容器都适用。std::copy,std::transform:需要指定输出迭代器,常用于将结果输出到另一个容器。
一个高效拷贝到vector的惯用法:
std::set<int> sourceSet = {5, 1, 4, 2, 3}; std::vector<int> destVec; destVec.reserve(sourceSet.size()); // 预分配,避免多次扩容 std::copy(sourceSet.begin(), sourceSet.end(), std::back_inserter(destVec)); // destVec 现在是 {1, 2, 3, 4, 5},且已排序7. 高级话题与C++新标准中的容器演进
7.1 移动语义与容器
C++11的移动语义极大地提升了容器操作的性能,特别是对于存储昂贵拷贝的对象(如std::string, 大型vector)。
- 当向容器插入临时对象(右值)时,容器会调用移动构造函数,而不是拷贝构造函数。
std::vector重新分配内存时,如果元素类型有noexcept的移动构造函数,则会使用移动来转移元素,否则使用拷贝(为了保证强异常安全)。- 因此,为你自定义的、作为容器元素的类实现移动构造函数和移动赋值运算符(并标记为
noexcept)是重要的优化手段。
7.2 容器与异常安全
STL容器提供了基本的异常安全保证。最重要的两个级别是:
- 强异常安全保证:操作要么成功,要么失败,失败后容器状态与操作前完全相同。例如,
vector::push_back在因拷贝/移动构造函数抛出异常而失败时,容器会恢复到调用前的状态(这通常意味着如果重新分配失败,会保持旧内存块不变)。 - 不抛异常保证:某些操作承诺绝不抛出异常,如
pop_back,swap(对于标准容器类型)。
编写异常安全的代码时,要小心“迭代器失效”和“资源泄漏”。利用RAII(资源获取即初始化)和智能指针(如std::unique_ptr作为容器元素)可以大大简化资源管理。
7.3 C++17和C++20中的新特性
std::optional作为“可能不存在”的元素:有时你需要在容器中表示一个“可能有值,可能为空”的状态。与其使用特殊值(如-1, 空字符串)或指针(nullptr),不如使用std::optional<T>作为元素类型,语义更清晰。std::variant作为类型安全的联合体:容器需要存储多种类型的元素时,std::variant比void*或继承体系更安全。- C++20的
std::span:它不是一个容器,而是一个轻量级的、不拥有所有权的视图,可以表示一个连续序列(如数组、vector的一部分)。用于函数参数传递非常高效,可以替代(指针, 长度)对。void process(std::span<int> data) { for (auto& elem : data) { /* ... */ } } std::vector<int> vec = {1,2,3,4,5}; process(vec); // 隐式转换 process({vec.data() + 1, 3}); // 处理子范围 - 范围库(Ranges Library, C++20):提供了操作整个容器的更简洁、更可组合的语法。例如,上面的
erase-remove可以写成:std::vector<int> vec = {1,2,3,4,5,3}; std::erase(vec, 3); // C++20,直接删除所有3 // 或者使用范围视图 auto even = vec | std::views::filter([](int i){ return i % 2 == 0; });
8. 常见问题与排查技巧实录
这里记录了一些我实际调试中遇到的和常见的问题。
问题1:程序运行一段时间后变慢,内存使用持续增长。
- 排查:使用Valgrind Massif或类似工具分析内存分配。检查容器(尤其是
vector,string)是否因反复插入删除而capacity远大于size(内存未释放)。对于vector,可以使用shrink_to_fit()(C++11)来请求释放未使用的内存(注意,这是一个非强制性的请求)。对于长期存在的、容量波动大的容器,考虑在适当时候用swap技巧释放内存:std::vector<int>(vec).swap(vec); // 用一个新的临时vector(使用vec的元素构造)与vec交换,临时vector析构后释放内存
问题2:unordered_map查找性能突然下降。
- 排查:检查负载因子。如果插入了大量元素而未预分配桶,可能导致负载因子过高,冲突严重。在插入大量数据前使用
reserve。同时检查自定义哈希函数是否分布均匀。
问题3:迭代器在循环中失效导致崩溃或数据错误。
- 典型场景:在遍历容器(尤其是
vector,deque)时插入或删除元素。 - 解决方案:
- 如果要在遍历时删除元素,对于序列容器,使用
erase返回的新迭代器(见3.1节)。对于关联容器,可以先记录要删除的键或迭代器到另一个临时容器,遍历结束后再批量删除(C++11后,erase返回下一个迭代器,可以直接it = container.erase(it))。 - 如果要在遍历时插入元素,通常更复杂,需要重新设计逻辑,比如先收集要插入的数据,遍历结束后再插入。
- 如果要在遍历时删除元素,对于序列容器,使用
问题4:自定义类型作为map键或unordered_map键时,查找失败。
- 排查:
- 对于
map:确保自定义类型的operator<(或你提供的比较函数)定义了严格弱序。即满足:非自反(comp(a, a)为false)、不对称(若comp(a, b)为true则comp(b, a)为false)、可传递(若comp(a, b)和comp(b, c)为true则comp(a, c)为true),以及等价传递性。 - 对于
unordered_map:确保哈希函数对等价的对象(由相等性判断函数定义)产生相同的哈希值。同时,确保相等性判断函数(operator==或自定义)正确实现。
- 对于
问题5:在多线程环境下使用容器。
- 核心原则:STL容器本身不是线程安全的(除了
const成员函数,多个线程同时读是安全的)。如果多个线程需要读写同一个容器,必须在外层进行同步(如使用std::mutex)。 - 注意:即使像
size()这样的const成员函数,在vector等可能被其他线程修改的容器上调用,也可能因为读取内部计数器而导致数据竞争(未定义行为)。最安全的做法是任何对容器的非const访问(包括通过迭代器)都需要加锁保护。