文章目录
- 1. STL使用
- 2. STL原理(六大组件)
- 2.1 容器
- 2.1.1 vector 和 list的区别?
- 2.1.2 vector 是如何插入数据?如何扩容的?
- 2.1.3 vector 和 list都有缺点,有什么其它方案,能解决问题?
- 2.1.4 map和set底层是什么?
- 2.1.5 map的operator[]实现是什么?功能是什么?如何实现?
- 2.1.6 一个类型做map和set的key有什么要求?
- 2.1.7 map 和 set 和 multi_set 的区别?
- 2.1.8 unordered_map 和 unordered_set是如何实现的?
- 2.1.9 unordered_map 和 map 的区别是什么?
- 2.1.10 一个类型做unordered_map 和 unordered_set的key有什么要求?
- 2.2 算法
- 2.3 迭代器
- 2.4 适配器
- 2.5 仿函数
- 2.6 空间配置器
- 2.7 STL六大组件之间的关系
- 2.8 迭代器失效是什么?
- 2.9 STL的优缺点是什么?设计得好和不好的分别是什么?
- 2.10 容器是否是线程安全的?
1. STL使用
1.1 常见的容器
1.1.1 序列式容器
std::string
- 用途:用于处理和操作字符串。
- 特点:
- 动态大小:可以根据需要自动调整大小。
- 支持操作:提供了多种成员函数和操作符来处理字符串,如拼接 (+), 查找 (find), 替换 (replace), 和比较 (compare)。
- 内存管理:自动管理内存,不需要手动分配和释放。
std::vector
- 用途:用于存储动态大小的元素序列。
- 特点:
- 动态数组:底层实现为动态数组,能够自动调整大小以适应增加的元素。
- 随机访问:支持通过下标直接访问元素,时间复杂度为 O(1)。
- 效率:插入或删除元素时,特别是在数组中间,可能需要移动元素,时间复杂度为 O(n)。
- 内存管理:支持内存的自动分配和释放,通常有较高的内存局部性。
std::list
- 用途:用于存储双向链表结构的元素。
- 特点:
- 双向链表:每个元素都包含指向前一个和下一个元素的指针。
- 插入和删除:在任意位置插入或删除元素时时间复杂度为 O(1),但需要遍历链表来找到位置,时间复杂度为 O(n)。
- 随机访问:不支持随机访问,必须通过迭代器顺序访问元素。
- 内存管理:每个元素需要额外的指针存储前后节点的地址。
std::deque
- 用途:双端队列,支持高效的两端插入和删除操作。
- 特点:
- 双端操作:可以在两端快速插入和删除元素,时间复杂度为 O(1)。
- 随机访问:支持通过下标直接访问元素,时间复杂度为 O(1),但实现上可能涉及到多个缓冲区。
- 内存管理:内部实现为多个小块的内存块(称为缓冲区),因此比 vector 更灵活,但可能导致较高的内存开销和不如 vector 的缓存局部性。
总结:
std::string
:专门用于字符串操作。
std::vector
:适合需要快速随机访问的动态数组。
std::list
:适合频繁插入和删除操作,特别是在容器中间。
std::deque
:适合需要高效两端操作和随机访问的情况。
1.1.2 关联式容器
std::map
和std::set
std::map
:
- 用途:关联容器,用于存储键值对。
- 特点:
- 有序:元素按照键的顺序自动排序。
- 键唯一:每个键只能出现一次,不能重复。
- 查找:提供基于红黑树的实现,查找、插入和删除操作的时间复杂度为 O(log n)。
- 迭代器:支持双向迭代器,可以按照键的顺序遍历元素。
- 典型操作:
- 插入 (
insert
) 键值对。- 查找 (
find
) 键是否存在,并获取对应值。- 删除 (
erase
) 键值对。
std::set
:
- 用途:集合容器,用于存储唯一元素。
- 特点:
- 有序:元素按照一定的排序规则(通常是升序)自动排序。
- 元素唯一:每个元素只能出现一次,不能重复。
- 查找:提供基于红黑树的实现,查找、插入和删除操作的时间复杂度为 O(log n)。
- 迭代器:支持双向迭代器,可以按照元素的顺序遍历。
- 典型操作:
- 插入 (
insert
) 元素。- 查找 (
find
) 元素是否存在。- 删除 (
erase
) 元素。
std::unordered_map
和std::unordered_set
std::unordered_map
:
- 用途:哈希表容器,用于存储键值对。
- 特点:
- 无序:元素不按顺序排列,使用哈希表来存储和检索。
- 键唯一:每个键只能出现一次,不能重复。
- 查找:提供基于哈希表的实现,平均情况下查找、插入和删除操作的时间复杂度为 O(1),但最坏情况下为 O(n)(例如哈希冲突严重时)。
- 迭代器:不提供排序的迭代器,遍历顺序依赖于哈希表的实现。
- 典型操作:
- 插入 (
insert
) 键值对。- 查找 (
find
) 键是否存在,并获取对应值。- 删除 (
erase
) 键值对。
std::bitset
- 用途:用于存储固定大小的位集合。
- 特点:
- 固定大小:在创建时指定大小,大小不可更改。
- 高效:以位的形式存储数据,内存使用高效。
- 操作:支持按位操作,如与 (
&
)、或 (|
)、非 (~
)、异或 (^
) 等。- 访问:可以通过下标直接访问每一位,支持查询和设置某一位的值。
- 典型操作:
- 设置 (
set
) 或重置 (reset
) 某一位的值。- 检查 (
test
) 某一位的值。- 获取总的位数 (
size
) 和已设置位的个数 (count
)。
总结
std::map
和std::set
:基于红黑树实现的有序容器,适合需要排序的场景。std::unordered_map
和std::unordered_set
:基于哈希表实现的无序容器,适合需要快速查找和插入的场景。std::bitset
:用于处理固定大小的位集合,适合需要高效位操作的场景。
1.1.3 容器适配器
在 C++ 中,stack
、queue
和 priority_queue
是三种常用的容器适配器,它们基于底层容器(如 deque
、list
或 vector
)提供特定的接口和行为。
std::stack
概述:
- 用途:后进先出(LIFO, Last In First Out)的数据结构。
- 底层实现:通常使用
deque
或vector
作为底层容器。
特点:
- 基本操作:
push
:将元素添加到栈顶。pop
:移除栈顶元素。top
:访问栈顶元素而不移除它。
- 访问限制:只能访问栈顶元素,无法直接访问其他元素。
- 典型应用:函数调用、括号匹配、反转字符串等。
示例代码:
#include <iostream>
#include <stack>
int main() {
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
std::cout << "Top element: " << s.top() << std::endl; // 输出 3
s.pop();
std::cout << "Top element after pop: " << s.top() << std::endl; // 输出 2
return 0;
}
std::queue
概述:
- 用途:先进先出(FIFO, First In First Out)的数据结构。
- 底层实现:通常使用
deque
作为底层容器。
特点:
- 基本操作:
push
(也称为enqueue
):将元素添加到队列尾部。pop
(也称为dequeue
):移除队列头部元素。front
:访问队列头部元素而不移除它。back
:访问队列尾部元素而不移除它。
示例代码:
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
std::cout << "Front element: " << q.front() << std::endl; // 输出 1
q.pop();
std::cout << "Front element after pop: " << q.front() << std::endl; // 输出 2
return 0;
}
std::priority_queue
概述
- 用途:支持按优先级排序的队列,元素按照优先级顺序进行访问。
- 底层实现:基于堆(通常是最大堆),使用
vector
作为底层容器。
特点
- 基本操作:
- push:将元素插入队列,自动调整顺序以维持优先级。
- pop:移除优先级最高的元素(通常是最大元素)。
- top:访问优先级最高的元素而不移除它。
- 优先级:默认情况下,优先级队列中的元素是根据其值进行比较,值大的元素具有更高的优先级。
- 自定义优先级:可以通过提供自定义比较函数来定义元素的优先级。
- 典型应用:任务调度、图的最短路径算法(如 Dijkstra 算法)等。
示例代码
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(2);
std::cout << "Top element (highest priority): " << pq.top() << std::endl; // 输出 3
pq.pop();
std::cout << "Top element after pop: " << pq.top() << std::endl; // 输出 2
return 0;
}
总结
std::stack
:后进先出(LIFO),仅能访问顶部元素,适合需要反向处理的场景。
std::queue
:先进先出(FIFO),可以访问队列头部和尾部,适合处理排队问题。
std::priority_queue
:基于优先级的队列,支持快速访问最高优先级元素,适合需要优先级排序的场景。
这三种容器适配器在不同情况下各有其应用场景,选择合适的容器适配器能够提高程序的效率和可读性。
1.2 迭代器
在 STL(标准模板库)中,迭代器是一个重要的概念,它们允许你以一种统一的方式访问不同类型的容器中的元素。
1.2.1 输入迭代器 (Input Iterator)
- 功能:允许读取容器中的数据,但不允许修改数据。只能进行前向遍历。
- 特性:
- 只支持解引用操作(
*iter
)和增量操作(++iter
)。- 只能从容器中读取数据,而不能修改。
- 使用场景:通常用于只需要读取数据的操作,例如读取流数据。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = vec.begin();
while (it != vec.end()) {
std::cout << *it << " ";
++it;
}
return 0;
}
1.2.2 输出迭代器 (Output Iterator)
- 功能:允许将数据写入容器,但不允许读取数据。只能进行前向遍历。
- 特性:
- 支持解引用和增量操作(
++iter
),但不能进行减量操作或随机访问。- 只能进行写操作,不能进行读操作。
- 使用场景:通常用于将数据写入容器,例如通过
std::ostream_iterator
将数据写入输出流。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = vec.begin();
while (it != vec.end()) {
std::cout << *it << " ";
++it;
}
return 0;
}
1.2.3 前向迭代器 (Forward Iterator)
- 功能:支持多次遍历,可以进行读写操作,支持前向移动。
- 特性:
- 支持解引用(
*iter
)、增量操作(++iter
),但不支持回退操作。- 可以在遍历过程中多次访问同一元素。
- 使用场景:适用于需要单向遍历的数据结构,如
std::forward_list
。
示例
#include <iostream>
#include <forward_list>
int main() {
std::forward_list<int> flist = {1, 2, 3, 4, 5};
for (auto it = flist.begin(); it != flist.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
1.2.4 双向迭代器 (Bidirectional Iterator)
- 功能:支持前向和回退操作,适用于双向遍历。
- 特性:
- 支持解引用(
*iter
)、增量操作(++iter
)和回退操作(--iter
)。- 可以在遍历过程中前进和回退。
- 使用场景:适用于需要双向遍历的数据结构,如
std::list
和std::deque
。
示例:
#include <iostream>
#include <list>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it; // Move to 2
std::cout << "Forward: ";
while (it != lst.end()) {
std::cout << *it << " ";
++it;
}
std::cout << "\nBackward: ";
while (it != lst.begin()) {
--it;
std::cout << *it << " ";
}
return 0;
}
1.2.5 随机访问迭代器 (Random Access Iterator)
- 功能:支持所有前向和双向迭代器的操作,还支持随机访问和直接跳转到特定位置。
- 特性:
- 支持解引用(
*iter
)、增量操作(++iter
)、回退操作(--iter
)、加法(iter + n
)、减法(iter - n
)以及直接访问(iter[n]
)。- 可以直接通过下标访问容器中的任意元素。
- 使用场景:适用于可以进行随机访问的数据结构,如
std::vector
和std::deque
。
示例
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
// Random access iterator
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << "\nUsing index: ";
for (std::size_t i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
return 0;
}
迭代器的主要操作
- 解引用 (
*iter)
:获取迭代器指向的元素。 - 增量 (
++iter
):移动到下一个元素。 - 回退 (
--iter
):移动到前一个元素(双向及随机访问迭代器)。 - 下标访问 (
iter[n]
):访问容器中距离当前迭代器n
个位置的元素(随机访问迭代器)。
STL 迭代器使得容器的遍历和操作变得一致且高效,为各种算法和容器提供了通用接口。通过掌握不同类型的迭代器,可以有效地利用 STL 提供的各种功能。
1.3 常见的算法
-
std::sort
:对容器中的元素进行排序,默认使用快速排序,时间复杂度为 O(n log n)。
示例:std::sort(vec.begin(), vec.end());
-
std::find
:在容器中查找第一个等于指定值的元素。
示例:auto it = std::find(vec.begin(), vec.end(), value);
-
std::binary_search
:在已排序的容器中检查是否存在指定值,时间复杂度为 O(log n)。
示例:bool found = std::binary_search(vec.begin(), vec.end(), value);
-
std::count
:统计容器中等于指定值的元素数量。
示例:size_t cnt = std::count(vec.begin(), vec.end(), value);
-
std::accumulate
:计算范围内所有元素的和(或应用其他二元操作)。
示例:int sum = std::accumulate(vec.begin(), vec.end(), 0);
-
std::transform
:对容器中的每个元素应用一个函数,并将结果存储到目标范围。
示例:std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) { return x * 2; });
-
std::remove
:移除容器中所有等于指定值的元素,实际上是将其移动到容器末尾,返回新的逻辑结尾。
示例:auto new_end = std::remove(vec.begin(), vec.end(), value);
-
std::reverse
:反转容器中的元素顺序。
示例:std::reverse(vec.begin(), vec.end());
-
std::swap
:交换两个对象的值。
示例:std::swap(a, b);
2. STL原理(六大组件)
2.1 容器
容器是用来存储和管理数据的类模板。STL中提供了多种类型的容器,适用于不同的存储和访问需求。主要的容器包括:
- 序列容器:如
vector
、list
和deque
,用于存储元素的线性序列。- 关联容器:如
set
、map
、multiset和multimap
,用于存储以某种方式排序的数据元素,并允许快速查找。- 无序容器:如
unordered_set
、unordered_map
、unordered_multiset
和unordered_multimap
,用于存储元素的哈希表实现,提供常数时间复杂度的查找操作。
2.1.1 vector 和 list的区别?
- 用法:
vector
适合需要快速随机访问和动态大小调整的场景,如动态数组。list
适合频繁插入和删除操作的场景,如双向链表。
- 优缺点:
vector
:
- 优点:支持随机访问,性能优越;内存布局连续,有利于缓存。
- 缺点:中间插入或删除开销大;扩容时可能重新分配内存并移动元素。
list
:
- 优点:插入和删除操作高效;无需移动其他元素。
- 缺点:不支持随机访问;额外的内存开销用于存储指针。
- 底层实现:
- vector使用动态数组,内存连续,支持随机访问,通过指针偏移访问元素。
- list使用双向链表,每个节点有前后指针,内存分布不连续,访问元素需要从头或尾遍历。
2.1.2 vector 是如何插入数据?如何扩容的?
vector
插入数据通常有两种方式:在末尾插入(push_back
)和在指定位置插入(insert
)。在末尾插入时,vector
直接将元素添加到数组末尾vector
扩容时分配更大内存(通常为原容量的2倍或者1.5倍),将现有元素移动到新内存,然后释放旧内存。
2.1.3 vector 和 list都有缺点,有什么其它方案,能解决问题?
deque
(双端队列):
- 优点:支持在两端高效插入和删除操作,且比vector在中间插入/删除效率更高。支持随机访问。
- 缺点:内存使用不如vector紧凑,内存分配复杂。
array
(C++11及以上提供的固定大小数组):
- 优点:内存连续,性能优秀,支持随机访问。
- 缺点:大小固定,不适用于需要动态调整大小的场景。
forward_list
(单向链表):
- 优点:比list更轻量,仅有一个前向指针,减少内存开销。
- 缺点:不支持双向遍历,不能在末尾插入/删除操作中保持效率。
2.1.4 map和set底层是什么?
在C++中,map
和set
的底层实现通常是基于红黑树(一种自平衡的二叉搜索树)。
map
:
- 底层:红黑树。
- 特点:存储键值对(key-value)。每个元素都有唯一的键(key),可以通过键访问对应的值(value)。
set
:
- 底层:红黑树。
- 特点:只存储键(key),元素唯一且自动排序。
红黑树保证了插入、删除和查找操作的时间复杂度为O(log n)。
- 红黑树是一种自平衡的二叉搜索树,具有以下主要性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(空节点)是黑色。
- 如果一个节点是红色,则它的两个子节点都是黑色(即不能有两个连续的红色节点)。
- 从任何节点到其每个叶子节点的所有路径上,黑色节点的数量相同(保证树的平衡)。
这些性质保证了树的高度不会超过2*log(n+1)
,使得插入、删除和查找操作的时间复杂度为O(log n)
。红黑树通过调整节点颜色和进行旋转操作来维护这些性质,从而保持平衡。
2.1.5 map的operator[]实现是什么?功能是什么?如何实现?
在C++中,std::map
的 operator[]
实现主要分为两个步骤:查找和插入。具体步骤如下:
- 查找:使用键查找映射中是否已有对应元素。
- 插入:如果键不存在,则插入一个具有默认值的元素,并返回其引用。
伪代码示例如下:
template<typename KeyType, typename ValueType>
class Map {
public:
ValueType& operator[](const KeyType& key) {
auto it = find(key); // 查找元素
if (it != end()) {
return it->second; // 元素已存在,返回其值的引用
} else {
// 元素不存在,插入默认值的元素
insert(std::make_pair(key, ValueType()));
return find(key)->second; // 返回新插入元素的值的引用
}
}
};
- 查找 (
find
): 搜索树中是否有对应键。- 插入 (
insert
): 添加新键及其默认值。- 返回引用: 通过键找到或插入后返回值的引用。
2.1.6 一个类型做map和set的key有什么要求?
- 可比较性:必须提供一个可以比较大小的操作,通常是
<
操作符。std::map
和std::set
使用这个操作符来保持元素的有序性。- 默认构造函数(仅对
std::map
的键有影响):如果你使用std::map
,键类型需要支持默认构造函数,因为operator[]
可能会插入新元素时需要使用默认构造函数。
简而言之,键类型必须支持 <
操作符来进行排序和比较。对于 std::map
,通常还需要支持默认构造。
2.1.7 map 和 set 和 multi_set 的区别?
std::map
: 存储键值对,键唯一,每个键关联一个值,按键排序。std::set
: 存储唯一的值,值不可重复,按值排序。std::m ultiset
: 存储可重复的值,允许重复的元素,按值排序。
2.1.8 unordered_map 和 unordered_set是如何实现的?
unordered_map
和 unordered_set
是基于哈希表实现的:
unordered_map
:使用哈希表存储键值对。每个键通过哈希函数映射到一个桶中,每个桶中可能包含多个键值对。查找、插入和删除操作的平均时间复杂度为
O(1)。unordered_set
:使用哈希表存储唯一的值。每个值通过哈希函数映射到一个桶中,每个桶中可能包含多个值。查找、插入和删除操作的平均时间复杂度为
O(1)。
哈希表是一种数据结构,用于高效地存储和检索数据。以下是哈希表的基本概述:
- 规则:
- 哈希函数:将键映射到哈希表的索引位置。
- 冲突处理:处理两个键映射到同一位置的情况,常见方法有链表法和开放地址法。
- 动态调整:哈希表大小可能会随着负载因子的变化而动态调整,以保持操作效率。
- 效率:
- 插入、删除、查找:在理想情况下,这些操作的时间复杂度为O(1),但由于冲突和动态调整,实际情况可能略差。
- 实现:
- 哈希函数:将键转换为表中的索引。例如,index = hash(key) % table_size。
- 冲突处理:
- 链表法:在每个索引位置存储一个链表,用于存储所有映射到该位置的元素。
- 开放地址法:在发生冲突时,寻找表中的其他空位置(如线性探测、二次探测)。
- 动态调整:根据负载因子(元素数量与表大小的比率),当负载因子过高时,扩展哈希表并重新哈希现有元素。
2.1.9 unordered_map 和 map 的区别是什么?
- 数据结构:
map
:红黑树(有序存储)unordered_map
:哈希表(无序存储)
- 操作复杂度:
map
:O(log n) — 查找、插入、删除unordered_map
:平均 O(1) — 查找、插入、删除(最坏情况 O(n))
- 元素顺序:
map
:按键升序排列unordered_map
:无特定顺序
- 内存使用:
map
:内存开销大(红黑树节点)unordered_map
:内存开销较小(哈希表桶和链表)
总结:map
适用于需要排序的情况,unordered_map
适用于需要快速查找且顺序无关的情况。
2.1.10 一个类型做unordered_map 和 unordered_set的key有什么要求?
unordered_map
和 unordered_set
的键类型需要满足以下要求:
- 哈希函数:键类型必须能够通过
std::hash
提供的哈希函数进行哈希计算。可以自定义哈希函数或使用标准库中已有的哈希函数。- 相等比较:键类型必须能够通过
operator==
进行相等比较,以确定两个键是否相同。
通常,这两个要求都可以通过键类型定义合适的哈希函数和相等比较操作来满足。如果键类型是自定义的,可以通过特化 std::hash
和定义 operator==
来实现。
2.2 算法
算法是对容器中数据进行操作的函数模板。STL提供了各种通用算法,例如排序、查找、删除、复制等。这些算法可以与各种容器配合使用,因为它们通过迭代器进行访问,而不是直接操作容器。这种设计使得算法可以被广泛地复用。
2.3 迭代器
迭代器是遍历容器中元素的工具。它们提供了一种统一的方式来访问容器中的数据,类似于指针。迭代器有多种类型,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器,分别支持不同的操作和访问模式。
2.4 适配器
适配器用于修改容器或迭代器的接口,以提供不同的操作方式。STL中的适配器包括:
- 容器适配器:如
stack
、queue
和priority_queue
,这些适配器基于其他容器(如deque
或vector
)实现不同的容器功能。 - 迭代器适配器:如
reverse_iterator
和insert_iterator
,用于改变迭代器的行为或接口。
2.5 仿函数
仿函数(或函数对象)是重载了operator()
的类或结构体。它们提供了灵活的方式来定义操作和行为,通常用于算法中。仿函数可以像普通函数一样调用,但它们可以拥有状态,因此能够提供比普通函数更复杂的功能。
2.6 空间配置器
空间配置器(Allocator)是用于管理内存分配的工具。STL中的默认配置器是std::allocator
,它负责在容器中分配和释放内存。空间配置器允许用户自定义内存管理策略,以适应不同的性能需求或资源限制。
2.7 STL六大组件之间的关系
STL(标准模板库)的六大组件相互协作,以实现高效且灵活的容器管理和算法操作。以下是它们之间的关系和如何互相配合:
2.7.1 容器(Containers):
- 容器是用来存储和管理数据的结构,如
vector
,list
,map
等。 - 容器负责持有元素,但并不直接操作这些元素的数据。它们通常通过迭代器与算法和其他组件进行交互。
- 容器使用 空间配置器 来分配和管理内存。
2.7.2 算法(Algorithms):
算法对容器中的数据进行操作,如排序、查找、复制等。常见的算法有 sort
, find
, copy
等。
算法通过 迭代器 访问容器中的元素,因而不需要知道容器的具体类型。
算法可以使用 仿函数 来改变算法的行为,例如指定排序的比较规则。
2.7.3 迭代器(Iterators):
- 迭代器是用于遍历容器元素的对象,类似于指针。它们提供了一种访问容器中元素的方式。
- 迭代器是 算法 操作容器的主要手段,使得算法能够与不同类型的容器一起工作。
- 迭代器的设计使得容器与算法之间的耦合性降低,从而提高了通用性和灵活性。
迭代器为什么设计的很厉害?
- 充分体现了面向对象的封装特性,容器的底层个不一样(数组、链表、树、哈希…),容器内部结构是私有的外部访问不了的。
- 迭代器封装,不需要暴露底层复杂的结构
- 迭代器封装,提供统一简单的方式访问容器
- 迭代器把算法和容器粘合起来了。算法通过迭代器,访问各种容器,但是如果不符合算法要求又可以直接报错。比如
sort
算法要求随机迭代器,传一个单向或者双向,就直接报错了。- 算法的实现,脱离具体存储结构,直接通过迭代器实现,体现了设计角度的复用
2.7.4 适配器(Adapters):
- 适配器提供了对底层容器的不同视图或接口。例如,
stack
和queue
是对底层容器的适配器,它们使用了容器适配器(如deque
或list
)来提供特定的功能。 - 适配器本质上是将已有的容器和算法组合起来,以实现新的数据结构或功能。
2.7.5 仿函数(Function Objects):
- 仿函数是可以像函数一样使用的对象。它们通常用于定义算法的行为,比如排序中的比较函数。
- 仿函数可以作为 算法 的参数,使得算法的行为可以灵活定制。
- 它们通常会提供
operator()
以便像函数那样调用。
2.7.6 空间配置器(Allocators):
- 空间配置器负责容器的内存管理,包括内存的分配和释放。
- 容器通过 空间配置器 来管理内部存储的内存,以提高内存使用的灵活性和效率。
- 默认的空间配置器提供基本的内存分配功能,但也可以自定义空间配置器以满足特定的内存管理需求。
2.7.7 组件之间的关系总结:
- 容器 通过 空间配置器 管理内存。
- 容器 使用 迭代器 来暴露其数据,使 算法 可以进行操作。
- 算法 使用 迭代器 访问容器中的数据,并且可以通过 仿函数 自定义操作行为。
- 适配器 通过组合现有的容器和 算法 提供新的数据结构和功能。
- 仿函数 提供自定义的操作逻辑,通常作为 算法 的参数。
- 空间配置器 管理容器的内存,以支持 容器 的高效存储和操作。
2.8 迭代器失效是什么?
是指迭代器失效指的是在容器修改后,迭代器不再有效。常见情况包括:
vector
:
- 增加元素:如果
vector
重新分配内存,所有旧的迭代器都会失效。- 删除元素:删除操作会使删除位置后的迭代器失效。
list
:
- 增加或删除元素:通常不会导致迭代器失效,除非删除了迭代器指向的元素。
map
和set
:
- 增加元素:不会导致现有迭代器失效。
- 删除元素:删除元素会使删除元素的迭代器失效。
deque
:
- 增加或删除元素:可能导致迭代器失效,特别是当
deque
重新分配内存时
避免迭代器失效的方法:
- 使用范围-based for 循环,减少直接操作迭代器。
- 修改容器前保存必要的迭代器位置。
- 选择适合的容器,减少失效的风险。
2.9 STL的优缺点是什么?设计得好和不好的分别是什么?
STL的优点
- 通用性:提供多种数据结构和算法,适用于多种场景。
- 高效性:实现通常经过优化,性能优秀。
- 一致性:接口统一,易于使用和维护。
- 模板编程:支持泛型编程,实现类型安全。
- 可扩展性:支持自定义比较器和分配器等扩展功能。
STL的缺点
- 复杂性:学习曲线陡峭,模板编程和迭代器概念较难。
- 编译时间:使用模板可能导致较长的编译时间和复杂的错误信息。
- 内存消耗:某些容器可能会占用更多内存。
- 灵活性:对特定需求可能需要额外的手动实现。
- 线程安全:没有对线程安全进行管理。
- 更新比较慢:没有增加什么新容器
- 冗余设计:
array
、forward_list
等
设计得好得特性
- 迭代器:提供通用元素访问得方式。
- 算法容器分离:提高了灵活性和重用性。
- 适配器:如
stack
、queue
等,增加了便利性。
设计不好得特性
- 迭代器失效:某些操作会导致迭代器失效。
- 异常安全:需要注意自定义分配器可能引发的问题。
- 非自解释性:高度抽象可能影响代码可读性。
2.10 容器是否是线程安全的?
STL 容器本身并不是线程安全的。具体来说:
标签:std,容器,复习,迭代,map,STL,元素,C++,访问 From: https://blog.csdn.net/Colorful___/article/details/141228230
- 并发访问:如果多个线程同时对同一个 STL 容器进行读写操作,可能会导致数据竞争和未定义行为。
- 解决方案:你可以使用互斥锁(
std::mutex
)来保护对容器的访问,确保在同一时间只有一个线程可以操作容器。另一种方法是使用线程安全的容器实现(例如并发队列)。