STL详解
一、问题背景------解决什么问题
1.1 C++早期编程的困境
在STL出现之前,C++程序员面临着重重的编程挑战。想象一下,你需要实现一个动态数组,你可能需要手动管理内存分配与释放,考虑边界情况,处理各种异常。每当需要使用另一种数据结构,比如链表或者红黑树,你又需要从头开始编写一套全新的代码。这种低效的编程方式严重阻碍了C++的普及与发展。
具体来说,早期C++程序员面临以下几个核心问题:
代码重复问题:每实现一种数据结构,都需要重新编写内存管理、遍历、搜索等基础功能。比如你实现了一个int类型的动态数组,当需要存储double类型时,你发现大部分代码可以复用,但数据类型不同。于是你可能需要复制粘贴整个实现,然后逐个修改类型。这不仅容易出错,而且维护成本极高。
数据结构与算法耦合:传统的实现中,数据结构与算法紧密耦合在一起。例如,你实现了一个排序函数,它只能对特定的数组类型进行排序。如果你想对链表进行排序,就需要重新实现一个完全不同的排序算法。这导致了大量相似但又不完全相同的代码存在。
内存管理混乱:每个数据结构都自己管理内存,没有统一的标准。指针操作、内存泄漏、重复释放等问题层出不穷。一个复杂的程序可能有十几种不同的内存管理方式,这给调试和维护带来了极大的困难。
缺乏统一接口:不同的数据结构提供不同的操作接口。数组使用下标访问,链表使用指针遍历,树的访问方式又各不相同。这使得编写通用算法变得极为困难,你需要为每种数据结构单独编写算法。
1.2 泛型编程的需求
为了解决上述问题,泛型编程的概念应运而生。泛型编程的核心思想是:编写与类型无关的代码,使得同一段代码可以作用于不同的数据类型。
泛型编程追求的目标是:
- 代码复用:一次编写,多处使用
- 类型安全:在编译期进行类型检查
- 性能优化:在编译期进行代码生成,避免运行时开销
STL正是泛型编程在C++中的完美实践。它提供了一套完整的、通用的、高效的标准库,让程序员能够专注于业务逻辑,而不是底层实现。
1.3 STL的诞生
STL(Standard Template Library,标准模板库)最初由HP的Alexander Stepanov和Meng Lee于1994年开发,后来被纳入C++标准库(STL成为C++标准库的一部分)。
STL的诞生标志着C++进入了一个新的时代。它不仅解决了代码复用的问题,更重要的是,它建立了一套完整的泛型编程范式,影响了后续众多编程语言和库的设计。
二、设计思路------设计师如何思考的
2.1 泛型编程思想
STL的设计核心是泛型编程。设计师思考的第一个问题是:如何让代码与类型解耦?
答案就是模板。通过模板,程序员可以编写通用的代码,编译器会根据具体的类型参数生成对应的代码。这就像是一个模具,可以生产出各种尺寸的产品。
cpp
// 泛型函数:可以对任何支持比较的类型进行排序
template<typename T>
void sort(std::vector<T>& v) {
// 排序算法实现
}
设计师的第二个思考是:如何让数据结构与算法解耦?
答案是迭代器。迭代器提供了一种统一的方式来遍历各种数据结构。算法只需要与迭代器交互,而不需要关心底层数据结构的实现细节。
cpp
// 算法只与迭代器交互
template<typename Iterator>
void sort(Iterator first, Iterator last) {
// 排序算法实现
}
2.2 STL六大组件
STL的设计采用了组件化的思想,整个库由六大核心组件构成:
容器(Containers):各种数据结构的实现,包括序列式容器(vector、list、deque)和关联式容器(map、set、multimap、multiset)。
算法(Algorithms):各种通用算法的实现,包括排序、搜索、复制、变换等。STL提供了超过100个算法函数。
迭代器(Iterators):遍历容器元素的对象,是算法与容器之间的桥梁。迭代器相当于智能指针,封装了对容器内部结构的访问。
空间配置器(Allocators):负责内存的分配与释放。空间配置器是STL最底层的基础设施,所有的容器都通过它来管理内存。
适配器(Adapters):对现有组件进行包装,提供不同的接口。包括容器适配器(stack、queue、priority_queue)和函数适配器(bind、mem_fn等)。
仿函数(Functors):可以像函数一样调用的对象。仿函数用于算法的自定义行为,比如自定义比较函数。
这六大组件之间的关系可以这样理解:容器 提供数据存储,迭代器 提供数据访问,算法 对数据进行操作,空间配置器 管理内存,适配器 改变接口,仿函数提供自定义行为。
2.3 迭代器模式
设计师思考的第三个问题是:如何统一不同容器的访问方式?
答案是迭代器模式 。迭代器模式的核心思想是:提供一种方法顺序访问集合中的元素,而不暴露集合的内部表示。
迭代器相当于一个智能指针,它封装了对容器内部结构的访问。不同的容器提供不同类型的迭代器,但它们都有统一的接口:
cpp
// 迭代器的基本操作
iterator++; // 移动到下一个元素
*iterator; // 获取当前元素
iterator1 == iterator2; // 比较两个迭代器
iterator1 != iterator2;
通过迭代器,算法可以实现完全的通用性:
cpp
// 这个函数可以对任何容器的任意区间进行排序
template<typename RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
// 使用迭代器进行排序
}
2.4 空间配置器
设计师思考的第四个问题是:如何统一内存管理?
答案是空间配置器。空间配置器负责内存的分配与释放,所有的容器都通过它来管理内存。这使得内存管理变得可控,也便于优化。
STL的空间配置器采用两级配置策略:
- 当请求的内存大于128字节时,使用一级配置器,直接调用malloc/free
- 当请求的内存小于等于128字节时,使用二级配置器,使用内存池进行管理
这种设计的好处是:对于小块内存,避免了频繁调用malloc带来的开销,同时也减少了内存碎片。
2.5 算法与数据结构的分离
STL最重要的设计思想是算法与数据结构的彻底分离。
在传统的编程中,数据结构与算法是紧密耦合的。比如你实现了一个排序算法,它与特定的数组结构绑定在一起。在STL中,算法完全独立于数据结构,它只与迭代器交互。
这种设计带来了极大的灵活性:
- 同一个算法可以作用于不同的容器
- 同一个容器可以使用不同的算法
- 可以轻松地组合出复杂的功能
cpp
// 排序算法可以作用于vector、deque、数组等
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(v.begin(), v.end()); // 对vector排序
std::deque<int> d = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(d.begin(), d.end()); // 对deque排序
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(arr, arr + 8); // 对数组排序
三、具体实现------从原理到代码实现
3.1 容器实现原理
3.1.1 vector的实现
vector是最常用的序列容器,它提供了动态数组的功能。
底层结构:vector的底层是一个连续的内存空间,类似于原生数组,但可以动态增长。当当前容量不足时,vector会分配更大的内存空间(通常是当前容量的1.5倍或2倍),然后将原有元素复制到新空间,并释放旧空间。
内存布局:
scss
┌─────────────────────────────────────────────────────────────┐
│ 分配的内存空间 │
├─────────────┬─────────────────────────────────┬────────────┤
│ start_ │ 已存储的元素 │ end_of_ │
│ (起始指针)│ [0] [1] [2] ... [size-1] │ storage_ │
├─────────────┴─────────────────────────────────┴────────────┤
│ ↑ finish_ (指向最后一个元素的下一个位置) │
└─────────────────────────────────────────────────────────────┘
核心实现:
cpp
template<typename T, typename Alloc = std::allocator<T>>
class vector {
private:
T* start_; // 指向起始位置
T* finish_; // 指向当前最后一个元素的下一个位置
T* end_of_storage_; // 指向存储空间的末尾
Alloc alloc_; // 空间配置器
public:
// 迭代器类型
typedef T* iterator;
typedef const T* const_iterator;
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
// 大小与容量
size_t size() const { return finish_ - start_; }
size_t capacity() const { return end_of_storage_ - start_; }
bool empty() const { return start_ == finish_; }
// 访问元素
T& operator[](size_t n) { return *(start_ + n); }
const T& operator[](size_t n) const { return *(start_ + n); }
// 带边界检查的访问
T& at(size_t n) {
if (n >= size()) throw std::out_of_range("vector::at");
return *(start_ + n);
}
// 首尾元素访问
T& front() { return *start_; }
T& back() { return *(finish_ - 1); }
// 添加元素
void push_back(const T& value) {
if (finish_ == end_of_storage_) {
// 容量不足,需要扩容
size_t old_capacity = capacity();
size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
reserve(new_capacity);
}
// 在末尾构造元素(使用placement new)
alloc_.construct(finish_, value);
++finish_;
}
// 移动版本的push_back,避免拷贝
void push_back(T&& value) {
if (finish_ == end_of_storage_) {
size_t old_capacity = capacity();
size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
reserve(new_capacity);
}
alloc_.construct(finish_, std::move(value));
++finish_;
}
// emplace_back:直接在末尾构造元素
template<typename... Args>
void emplace_back(Args&&... args) {
if (finish_ == end_of_storage_) {
size_t old_capacity = capacity();
size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
reserve(new_capacity);
}
alloc_.construct(finish_, std::forward<Args>(args)...);
++finish_;
}
// 扩容函数
void reserve(size_t new_cap) {
if (new_cap > capacity()) {
size_t old_size = size();
// 分配新内存
T* new_start = alloc_.allocate(new_cap);
// 将旧元素移动到新空间(使用uninitialized_move)
T* new_finish = std::uninitialized_move(start_, finish_, new_start);
// 销毁旧元素并释放旧内存
destroy(start_, finish_);
alloc_.deallocate(start_, capacity());
// 更新指针
start_ = new_start;
finish_ = new_finish;
end_of_storage_ = start_ + new_cap;
}
}
// 销毁元素
void destroy(T* first, T* last) {
for (; first != last; ++first) {
alloc_.destroy(first);
}
}
// 删除最后一个元素
void pop_back() {
--finish_;
alloc_.destroy(finish_);
}
// 清空容器
void clear() {
destroy(start_, finish_);
finish_ = start_;
}
// 迭代器
iterator begin() { return start_; }
iterator end() { return finish_; }
const_iterator begin() const { return start_; }
const_iterator end() const { return finish_; }
};
迭代器类型:vector的迭代器就是原生指针,因为vector的内存是连续的。
cpp
typedef T* iterator;
typedef const T* const_iterator;
这意味着vector的迭代器支持随机访问,可以在常数时间内跳转到任意位置。
扩容策略详解:
vector的扩容策略是影响性能的关键因素。常见的扩容倍数有1.5倍和2倍两种:
- 2倍扩容:简单直观,但可能导致较多的内存浪费
- 1.5倍扩容:更节省内存,但实现稍复杂(需要处理内存对齐)
GCC使用的是2倍扩容策略,MSVC也使用2倍,而某些实现使用1.5倍。
cpp
// 扩容时的内存分配过程
void expand() {
size_t old_cap = capacity();
size_t new_cap = old_cap == 0 ? 1 : old_cap * 2; // 2倍扩容
// 1. 分配新内存
T* new_data = alloc_.allocate(new_cap);
// 2. 移动元素(C++11使用move,效率更高)
for (size_t i = 0; i < size(); ++i) {
new (new_data + i) T(std::move(old_data[i]));
old_data[i].~T(); // 销毁旧对象
}
// 3. 释放旧内存
alloc_.deallocate(old_data, old_cap);
// 4. 更新指针
data_ = new_data;
capacity_ = new_cap;
}
emplace_back vs push_back:
cpp
// push_back:先构造对象,然后拷贝/移动到容器中
std::vector<std::string> v;
std::string s = "hello";
v.push_back(s); // 拷贝
v.push_back(std::string("world")); // 移动
// emplace_back:直接在容器内存中构造对象
v.emplace_back("hello"); // 直接构造
v.emplace_back(5, 'a'); // 相当于 string(5, 'a')
v.emplace_back(s); // 拷贝
v.emplace_back(std::move(s)); // 移动
emplace_back避免了临时对象的创建,对于复杂类型(如std::string、std::vector)可以显著提升性能。
3.1.2 list的实现
list是一个双向链表,提供了常数时间的插入和删除操作。
底层结构:list的每个节点都是独立的,通过指针连接在一起。每个节点包含数据域和两个指针域(指向前一个节点和后一个节点)。
内存布局:
kotlin
┌──────────────────────────────────────────────────────────────────┐
│ list结构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ node │◄───►│ node │◄───►│ node │◄───►│ node │ │
│ │(head)│ │ data │ │ data │ │ data │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ ▲ │
│ │ │
│ ┌──────────────────────────────────────────┐ │
│ │ 哨兵节点(不存储数据,仅作为链表的边界) │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
节点结构:
cpp
template<typename T>
struct __list_node {
__list_node* prev; // 指向前一个节点
__list_node* next; // 指向下一个节点
T data; // 存储的数据
};
核心实现:
cpp
template<typename T, typename Alloc = std::allocator<T>>
class list {
private:
typedef __list_node<T> list_node;
list_node* node_; // 哨兵节点(头节点)
// 分配节点
list_node* create_node(const T& value) {
list_node* p = allocator_.allocate(1);
allocator_.construct(p, value);
return p;
}
// 销毁节点
void destroy_node(list_node* p) {
allocator_.destroy(p);
allocator_.deallocate(p, 1);
}
public:
// 迭代器实现
template<typename T>
struct list_iterator {
list_node* node;
// 解引用操作
T& operator*() { return node->data; }
T* operator->() { return &(node->data); }
// 前向遍历
list_iterator& operator++() {
node = node->next;
return *this;
}
list_iterator operator++(int) {
list_iterator tmp = *this;
node = node->next;
return tmp;
}
// 后向遍历
list_iterator& operator--() {
node = node->prev;
return *this;
}
list_iterator operator--(int) {
list_iterator tmp = *this;
node = node->prev;
return tmp;
}
// 比较操作
bool operator==(const list_iterator& other) const {
return node == other.node;
}
bool operator!=(const list_iterator& other) const {
return node != other.node;
}
};
// 构造函数:创建哨兵节点
list() {
node_ = create_node(T()); // 创建哨兵节点
node_->next = node_;
node_->prev = node_;
}
// 在指定位置插入节点
iterator insert(iterator position, const T& value) {
list_node* new_node = create_node(value);
list_node* cur = position.node;
list_node* prev = cur->prev;
// 链接新节点
new_node->next = cur;
new_node->prev = prev;
prev->next = new_node;
cur->prev = new_node;
return iterator(new_node);
}
// 删除指定位置的节点
iterator erase(iterator position) {
list_node* cur = position.node;
list_node* prev = cur->prev;
list_node* next = cur->next;
prev->next = next;
next->prev = prev;
destroy_node(cur);
return iterator(next);
}
// 在链表末尾添加元素
void push_back(const T& value) {
insert(end(), value);
}
// 在链表头部添加元素
void push_front(const T& value) {
insert(begin(), value);
}
// 删除最后一个元素
void pop_back() {
erase(--end());
}
// 删除第一个元素
void pop_front() {
erase(begin());
}
// 链表排序(归并排序)
void sort() {
if (node_->next == node_->next->next) return; // 0或1个元素
list<T> carry;
list<T> counter[64];
int fill = 0;
while (!empty()) {
carry.splice(carry.begin(), *this, begin());
int i = 0;
while (i < fill && !counter[i].empty()) {
counter[i].merge(carry);
carry.swap(counter[i++]);
}
carry.swap(counter[i]);
if (i == fill) ++fill;
}
for (int i = 1; i < fill; ++i) {
counter[i].merge(counter[i-1]);
}
swap(counter[fill-1]);
}
// 归并操作(用于排序)
void merge(list& other) {
iterator first1 = begin(), last1 = end();
iterator first2 = other.begin(), last2 = other.end();
while (first1 != last1 && first2 != last2) {
if (*first2 < *first1) {
iterator next = first2;
transfer(first1, first2, ++next);
first2 = next;
} else {
++first1;
}
}
if (first2 != last2) {
transfer(last1, first2, last2);
}
}
// 将[first, last)区间的内容移动到position之前
void transfer(iterator position, iterator first, iterator last) {
if (position == last) return;
list_node* p = position.node;
list_node* f = first.node;
list_node* l = last.node->prev;
// 断开原链表
f->prev->next = last.node;
first.node->prev = last.node->prev;
// 链接到新位置
l->next = p;
f->prev = p->prev;
p->prev->next = f;
p->prev = l;
}
};
迭代器类型:list的迭代器是双向迭代器,不支持随机访问。++和--操作是常数时间的,但+和-操作不支持。
特点:
- 插入和删除操作是常数时间
- 不支持随机访问,遍历效率较低
- 不会导致迭代器失效(除了被删除的节点)
- 占用额外的内存存储指针(每个节点需要额外的prev和next指针)
list vs vector:
| 特性 | list | vector |
|---|---|---|
| 内存布局 | 分散的节点,通过指针连接 | 连续的内存块 |
| 随机访问 | O(n) | O(1) |
| 头部插入/删除 | O(1) | O(n) |
| 尾部插入/删除 | O(1) | O(1) amortized |
| 中间插入/删除 | O(1) | O(n) |
| 缓存命中率 | 低 | 高 |
| 内存开销 | 每节点额外16字节(64位) | 无额外开销 |
3.1.3 deque的实现
deque(双端队列)是一个可以在两端进行插入和删除的序列容器。
底层结构:deque采用多段连续的内存块来存储数据,每段内存块称为一个"缓冲区"。所有缓冲区连在一起,形成一个逻辑上的连续空间。
内存布局:
arduino
┌─────────────────────────────────────────────────────────────────────┐
│ deque结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ map(指针数组): │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ * │ * │ * │ * │ * │ * │ * │ * │ │
│ └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘ │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ buf │ buf │ buf │ buf │ buf │ buf │ buf │ (多个缓冲区) │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ │
│ start_cur ─────────────────────────────────────► 第一个元素 │
│ finish_cur ───────────────────────────────────► 最后一个元素后 │
│ │
└─────────────────────────────────────────────────────────────────────┘
核心实现:
cpp
template<typename T, typename Alloc = std::allocator<T>>
class deque {
private:
// 指向缓冲区指针数组的指针
T** map_;
size_t map_size_; // map数组的大小
size_t buffer_size_; // 每个缓冲区的大小
// 迭代器位置
T* start_cur_; // 指向第一个元素
T* finish_cur_; // 指向最后一个元素的下一个位置
// 计算元素所在的缓冲区
size_t map_index(T* p) const {
size_t n = (p - *map_) / buffer_size_;
return n;
}
// 重新分配map
void reallocate_map(size_t new_size, bool add_at_front);
public:
// 迭代器实现
template<typename T>
struct deque_iterator {
T* cur; // 当前元素
T* first; // 当前缓冲区的起始
T* last; // 当前缓冲区的结束
T** node; // 指向map中对应指针的指针
// 跳转到下一个缓冲区
void set_node(T** new_node) {
node = new_node;
first = *new_node;
last = first + buffer_size();
}
// 前向移动
deque_iterator& operator++() {
++cur;
if (cur == last) {
set_node(node + 1);
cur = first;
}
return *this;
}
// 后向移动
deque_iterator& operator--() {
if (cur == first) {
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
// 随机访问
deque_iterator& operator+=(difference_type n) {
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < buffer_size()) {
cur += n;
} else {
// 需要跨越缓冲区
difference_type node_offset = offset / buffer_size();
set_node(node + node_offset);
cur = first + (offset % buffer_size());
}
return *this;
}
difference_type operator-(const deque_iterator& other) const {
return buffer_size() * (node - other.node - 1) +
(cur - first) + (other.last - other.cur);
}
};
// 头部插入
void push_front(const T& value) {
if (start_cur_ == *map_) {
// 第一个缓冲区已满,需要扩展
reallocate_map(map_size_, true);
}
--start_cur_;
new (start_cur_) T(value);
}
// 尾部插入
void push_back(const T& value) {
if (finish_cur_ == *map_ + map_size_ - 1) {
// 最后一个缓冲区已满,需要扩展
reallocate_map(map_size_, false);
}
new (finish_cur_) T(value);
++finish_cur_;
}
// 随机访问
T& operator[](size_t n) {
return *(start_cur_ + n);
}
// 迭代器
iterator begin() { return start_cur_; }
iterator end() { return finish_cur_; }
};
迭代器实现详解:
deque的迭代器是最复杂的,因为它需要处理跨缓冲区的情况:
cpp
template<typename T>
struct deque_iterator {
T* cur; // 当前元素指针
T* first; // 当前缓冲区的起始位置
T* last; // 当前缓冲区的结束位置(不包含)
T** node; // 指向map数组中对应元素的指针
// 随机访问操作符+
deque_iterator operator+(difference_type n) const {
deque_iterator tmp = *this;
return tmp += n;
}
// += 操作符的实现
deque_iterator& operator+=(difference_type n) {
// 计算相对于当前缓冲区起始位置的偏移
difference_type offset = n + (cur - first);
// 情况1:在当前缓冲区内
if (offset >= 0 && offset < buffer_size()) {
cur += n;
}
// 情况2:向前跨越缓冲区
else if (offset < 0) {
difference_type node_offset = (offset + 1) / buffer_size() - 1;
set_node(node + node_offset);
cur = last + (offset % buffer_size());
}
// 情况3:向后跨越缓冲区
else {
difference_type node_offset = offset / buffer_size();
set_node(node + node_offset);
cur = first + (offset % buffer_size());
}
return *this;
}
// 跳转到另一个缓冲区
void set_node(T** new_node) {
node = new_node;
first = *new_node;
last = first + buffer_size();
}
};
特点:
- 两端插入删除都是常数时间
- 支持随机访问(虽然比vector稍慢)
- 迭代器失效问题比vector更复杂(插入可能只影响部分迭代器)
3.1.4 map和set的实现
map和set是关联容器,底层使用红黑树实现。
红黑树特性(五大性质):
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 每个叶子节点(NIL)是黑色
- 如果一个节点是红色,则它的两个子节点都是黑色
- 从任一节点到其每个叶子的路径上,黑色节点的数量相同
红黑树的优势:
- 最坏情况下的时间复杂度也是O(log n)
- 插入、删除、查找的效率稳定
- 自动保持平衡,不需要像AVL树那样频繁旋转
红黑树结构:
scss
┌─────────────────────────────────────────────────────────────────────┐
│ 红黑树结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 30 (黑) │
│ / \ │
│ (红) (红) │
│ 20 40 │
│ / \ / \ │
│ (黑) (黑)(黑) (黑) │
│ 15 25 35 50 │
│ │
│ 黑色高度:从任一节点到其每个叶子节点的路径上, │
│ 黑色节点的数量相同 │
│ │
└─────────────────────────────────────────────────────────────────────┘
节点结构:
cpp
enum Color { RED, BLACK };
template<typename T>
struct rb_tree_node {
rb_tree_node* parent; // 父节点
rb_tree_node* left; // 左子节点
rb_tree_node* right; // 右子节点
Color color; // 节点颜色
T value; // 存储的值(对于map是pair<Key, Value>)
};
核心实现:
cpp
template<typename Key, typename Value, typename KeyOfValue,
typename Compare, typename Alloc = std::allocator<T>>
class rb_tree {
private:
typedef rb_tree_node<Value>* node_ptr;
typedef Value* value_ptr;
node_ptr header_; // 头节点(哨兵节点)
size_t node_count_; // 节点数量
Compare key_compare_; // 键比较函数
// 获取节点颜色
Color color(node_ptr node) {
return node ? node->color : BLACK;
}
// 左旋
void rotate_left(node_ptr x) {
node_ptr y = x->right;
x->right = y->left;
if (y->left) y->left->parent = x;
y->parent = x->parent;
if (x->parent == header_) {
header_->parent = y;
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
// 右旋
void rotate_right(node_ptr x) {
node_ptr y = x->left;
x->left = y->right;
if (y->right) y->right->parent = x;
y->parent = x->parent;
if (x->parent == header_) {
header_->parent = y;
} else if (x == x->parent->right) {
x->parent->right = y;
} else {
x->parent->left = y;
}
y->right = x;
x->parent = y;
}
// 插入后平衡操作
void insert_fixup(node_ptr x) {
while (x->parent->color == RED) {
if (x->parent == x->parent->parent->left) {
node_ptr uncle = x->parent->parent->right;
// 情况1:叔叔节点是红色
if (uncle && uncle->color == RED) {
x->parent->color = BLACK;
uncle->color = BLACK;
x->parent->parent->color = RED;
x = x->parent->parent;
} else {
// 情况2:叔叔节点是黑色,当前节点是右子节点
if (x == x->parent->right) {
x = x->parent;
rotate_left(x);
}
// 情况3:叔叔节点是黑色,当前节点是左子节点
x->parent->color = BLACK;
x->parent->parent->color = RED;
rotate_right(x->parent->parent);
}
} else {
// 对称情况
node_ptr uncle = x->parent->parent->left;
if (uncle && uncle->color == RED) {
x->parent->color = BLACK;
uncle->color = BLACK;
x->parent->parent->color = RED;
x = x->parent->parent;
} else {
if (x == x->parent->left) {
x = x->parent;
rotate_right(x);
}
x->parent->color = BLACK;
x->parent->parent->color = RED;
rotate_left(x->parent->parent);
}
}
}
header_->parent->color = BLACK; // 确保根节点是黑色
}
// 删除后平衡操作
void erase_fixup(node_ptr x, node_ptr parent) {
while (x != header_->parent && x->color == BLACK) {
if (x == parent->left) {
node_ptr sibling = parent->right;
// 情况1:兄弟节点是红色
if (sibling->color == RED) {
sibling->color = BLACK;
parent->color = RED;
rotate_left(parent);
sibling = parent->right;
}
// 情况2:兄弟节点是黑色,两个侄子都是黑色
if ((!sibling->left || sibling->left->color == BLACK) &&
(!sibling->right || sibling->right->color == BLACK)) {
sibling->color = RED;
x = parent;
parent = x->parent;
} else {
// 情况3:兄弟节点是黑色,右侄子黑色,左侄子红色
if (!sibling->right || sibling->right->color == BLACK) {
sibling->left->color = BLACK;
sibling->color = RED;
rotate_right(sibling);
sibling = parent->right;
}
// 情况4:兄弟节点是黑色,右侄子红色
sibling->color = parent->color;
parent->color = BLACK;
sibling->right->color = BLACK;
rotate_left(parent);
x = header_->parent;
}
} else {
// 对称情况
node_ptr sibling = parent->left;
if (sibling->color == RED) {
sibling->color = BLACK;
parent->color = RED;
rotate_right(parent);
sibling = parent->left;
}
if ((!sibling->right || sibling->right->color == BLACK) &&
(!sibling->left || sibling->left->color == BLACK)) {
sibling->color = RED;
x = parent;
parent = x->parent;
} else {
if (!sibling->left || sibling->left->color == BLACK) {
sibling->right->color = BLACK;
sibling->color = RED;
rotate_left(sibling);
sibling = parent->left;
}
sibling->color = parent->color;
parent->color = BLACK;
sibling->left->color = BLACK;
rotate_right(parent);
x = header_->parent;
}
}
}
x->color = BLACK;
}
public:
// 插入操作
std::pair<iterator, bool> insert(const Value& value) {
node_ptr y = header_;
node_ptr x = header_->parent;
while (x != nullptr) {
y = x;
if (key_compare_(KeyOfValue()(value), KeyOfValue()(x->value))) {
x = x->left;
} else if (key_compare_(KeyOfValue()(x->value), KeyOfValue()(value))) {
x = x->right;
} else {
// 键已存在
return std::pair<iterator, bool>(iterator(x), false);
}
}
// 创建新节点
node_ptr z = create_node(value);
z->left = nullptr;
z->right = nullptr;
z->color = RED;
// 插入节点
if (y == header_ || key_compare_(KeyOfValue()(value), KeyOfValue()(y->value))) {
y->left = z;
} else {
y->right = z;
}
z->parent = y;
// 平衡操作
insert_fixup(z);
++node_count_;
return std::pair<iterator, bool>(iterator(z), true);
}
// 查找操作
iterator find(const Key& key) {
node_ptr x = header_->parent;
while (x != nullptr) {
if (key_compare_(key, KeyOfValue()(x->value))) {
x = x->left;
} else if (key_compare_(KeyOfValue()(x->value), key)) {
x = x->right;
} else {
return iterator(x);
}
}
return end();
}
// 删除操作
void erase(iterator position) {
node_ptr z = position.node;
node_ptr y = z;
node_ptr x;
node_ptr parent;
Color original_color = y->color;
if (z->left == nullptr) {
x = z->right;
parent = z->parent;
transplant(z, z->right);
} else if (z->right == nullptr) {
x = z->left;
parent = z->parent;
transplant(z, z->left);
} else {
y = minimum(z->right);
original_color = y->color;
x = y->right;
if (y->parent == z) {
parent = y;
} else {
transplant(y, y->right);
y->right = z->right;
y->right->parent = y;
parent = y->parent;
transplant(z, y);
y->left = z->left;
y->left->parent = y;
y->color = z->color;
}
}
if (original_color == BLACK) {
erase_fixup(x, parent);
}
destroy_node(z);
--node_count_;
}
// 替换节点(用于删除)
void transplant(node_ptr u, node_ptr v) {
if (u->parent == header_) {
header_->parent = v;
} else if (u == u->parent->left) {
u->parent->left = v;
} else {
u->parent->right = v;
}
if (v) v->parent = u->parent;
}
// 找到最小节点
node_ptr minimum(node_ptr node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
};
特点:
- 元素自动排序(根据key或value)
- 查找、插入、删除都是对数时间复杂度
- 迭代器是有序的(中序遍历)
- map支持键值对,set只存储键
3.1.5 unordered_map和unordered_set的实现
unordered_map和unordered_set是哈希表实现的关联容器。
哈希表基本原理:
哈希表通过哈希函数将键映射到数组的索引位置,实现常数时间复杂度的查找。
bash
┌─────────────────────────────────────────────────────────────────────┐
│ 哈希表结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 哈希函数: hash("apple") = 3 → 映射到索引3 │
│ │
│ 桶数组: │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ │
│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │
│ │ │ │ │apple│ │ │ │ │ ← 链地址法处理冲突 │
│ │ │ │ │ ↓ │ │ │ │ │ │
│ │ │ │ │cat │ │ │ │ │ │
│ └───┴───┴───┴─────┴───┴───┴───┴───┘ │
│ │
│ 负载因子 = 元素数量 / 桶数量 │
│ 当负载因子超过阈值(通常为1.0)时,进行重哈希 │
│ │
└─────────────────────────────────────────────────────────────────────┘
核心实现:
cpp
template<typename Key, typename Value, typename Hash = std::hash<Key>,
typename KeyEqual = std::equal_to<Key>,
typename Alloc = std::allocator<std::pair<const Key, Value>>>
class unordered_map {
private:
// 桶:存储链表或红黑树
std::vector<std::list<std::pair<const Key, Value>>> buckets_;
size_t bucket_count_; // 桶的数量
size_t size_; // 元素数量
float max_load_factor_; // 最大负载因子
Hash hash_function_;
KeyEqual key_equal_;
// 哈希函数
size_t bucket_index(const Key& key) const {
return hash_function_(key) % bucket_count_;
}
// 重哈希
void rehash(size_t new_bucket_count) {
unordered_map new_map(new_bucket_count);
for (auto& bucket : buckets_) {
for (auto& kv : bucket) {
new_map.insert(kv);
}
}
swap(new_map);
}
public:
// 构造函数
unordered_map(size_t bucket_count = 16,
float max_load = 1.0f,
const Hash& hash = Hash(),
const KeyEqual& equal = KeyEqual())
: bucket_count_(bucket_count),
size_(0),
max_load_factor_(max_load),
hash_function_(hash),
key_equal_(equal) {
buckets_.resize(bucket_count_);
}
// 查找
iterator find(const Key& key) {
size_t n = bucket_index(key);
for (auto& kv : buckets_[n]) {
if (key_equal_(kv.first, key)) {
return iterator(&kv, &buckets_[n]);
}
}
return end();
}
// 插入
std::pair<iterator, bool> insert(const std::pair<Key, Value>& value) {
// 检查是否需要重哈希
if (size_ > bucket_count_ * max_load_factor_) {
rehash(bucket_count_ * 2);
}
size_t n = bucket_index(value.first);
// 检查键是否已存在
for (auto& kv : buckets_[n]) {
if (key_equal_(kv.first, value.first)) {
return std::pair<iterator, bool>(iterator(&kv, &buckets_[n]), false);
}
}
// 插入新元素
buckets_[n].push_back(value);
++size_;
return std::pair<iterator, bool>(
iterator(&buckets_[n].back(), &buckets_[n]), true
);
}
// emplace
template<typename... Args>
std::pair<iterator, bool> emplace(Args&&... args) {
// 先构造对象
std::pair<Key, Value> value(std::forward<Args>(args)...);
return insert(value);
}
// 删除
size_t erase(const Key& key) {
size_t n = bucket_index(key);
for (auto it = buckets_[n].begin(); it != buckets_[n].end(); ++it) {
if (key_equal_(it->first, key)) {
buckets_[n].erase(it);
--size_;
return 1;
}
}
return 0;
}
// 访问元素
Value& operator[](const Key& key) {
auto it = find(key);
if (it == end()) {
auto result = insert(std::make_pair(key, Value()));
return result.first->second;
}
return it->second;
}
// 桶数量
size_t bucket_count() const { return bucket_count_; }
size_t size() const { return size_; }
float load_factor() const { return (float)size_ / bucket_count_; }
};
哈希冲突处理:
STL中的unordered_map通常使用链地址法(separate chaining)处理冲突:
- 链表:简单实现,插入O(1),但查找需要遍历链表
- 红黑树:C++11以后的主流实现,查找效率更高(O(log n))
重哈希(Rehashing):
当负载因子超过最大负载因子时,需要扩展桶的数量并重新计算所有元素的位置:
cpp
void rehash(size_t new_bucket_count) {
// 1. 创建新的桶数组
std::vector<std::list<std::pair<const Key, Value>>> new_buckets(new_bucket_count);
// 2. 重新分配所有元素
for (auto& bucket : buckets_) {
for (auto& kv : bucket) {
size_t new_index = hash_function_(kv.first) % new_bucket_count;
new_buckets[new_index].push_back(std::move(kv));
}
}
// 3. 替换旧桶
buckets_.swap(new_buckets);
bucket_count_ = new_bucket_count;
}
特点:
- 查找、插入、删除平均是常数时间
- 元素无序(不保证迭代顺序)
- 需要良好的哈希函数以避免冲突
- 空间开销较大(需要额外的桶数组)
unordered_map vs map:
| 特性 | unordered_map | map |
|---|---|---|
| 底层结构 | 哈希表 | 红黑树 |
| 元素顺序 | 无序 | 有序 |
| 查找复杂度 | O(1) 平均 | O(log n) |
| 插入复杂度 | O(1) 平均 | O(log n) |
| 空间复杂度 | O(n) | O(n) |
| 哈希函数开销 | 有 | 无 |
| 适用场景 | 高性能查找 | 需要有序遍历 |
3.2 迭代器底层机制
3.2.1 迭代器类型
STL定义了五种迭代器类型,根据它们支持的操作:
输入迭代器(Input Iterator):只能读取元素,只能递增。可以用于单遍扫描算法。
输出迭代器(Output Iterator):只能写入元素,只能递增。可以用于单遍输出算法。
前向迭代器(Forward Iterator):可以读写元素,只能递增。可以多次遍历同一个序列。
双向迭代器(Bidirectional Iterator):可以读写元素,可以递增和递减。可以向前和向后遍历。
随机访问迭代器(Random Access Iterator):可以读写元素,支持所有指针运算(+、-、[]、<、>等)。是功能最强大的迭代器。
各容器提供的迭代器类型:
| 容器 | 迭代器类型 |
|---|---|
| vector | 随机访问迭代器 |
| deque | 随机访问迭代器 |
| list | 双向迭代器 |
| map/set | 双向迭代器 |
| unordered_map/set | 前向迭代器 |
3.2.2 迭代器特性
每个迭代器类型都有一组特性(traits),用于获取迭代器的相关信息:
cpp
template<typename Iterator>
struct iterator_traits {
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
};
这些特性使得算法可以根据迭代器的类型进行优化。例如,std::sort需要随机访问迭代器,因为它使用快速排序等需要随机访问的算法。
3.2.3 迭代器适配器
STL提供了多种迭代器适配器,用于改变迭代器的行为:
反向迭代器(reverse_iterator):将正向迭代器转换为反向遍历。
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << " "; // 输出: 5 4 3 2 1
}
插入迭代器(insert_iterator):将迭代赋值操作转换为插入操作。
cpp
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2;
std::copy(v1.begin(), v1.end(), std::back_inserter(v2)); // v2 = {1, 2, 3}
流迭代器(istream_iterator/ostream_iterator):将流对象包装为迭代器。
cpp
// 从标准输入读取整数
std::istream_iterator<int> in(std::cin);
std::istream_iterator<int> end;
std::vector<int> v(in, end);
// 输出到标准输出
std::ostream_iterator<int> out(std::cout, " ");
std::copy(v.begin(), v.end(), out);
3.3 空间配置器实现
3.3.1 一级配置器
一级配置器直接使用malloc和free进行内存管理:
cpp
template<int inst>
class malloc_alloc_template {
private:
static void* oom_malloc(size_t);
static void* oom_realloc(void*, size_t);
public:
static void* allocate(size_t n) {
void* result = malloc(n);
if (result == nullptr)
result = oom_malloc(n);
return result;
}
static void deallocate(void* p, size_t) {
free(p);
}
static void* reallocate(void* p, size_t old_sz, size_t new_sz) {
void* result = realloc(p, new_sz);
if (result == nullptr)
result = oom_realloc(p, new_sz);
return result;
}
};
3.3.2 二级配置器
二级配置器使用内存池来管理小块内存,减少内存碎片:
cpp
template<bool threads, int inst>
class default_alloc_template {
private:
// 内存块大小对齐到8字节
static size_t ROUND_UP(size_t bytes) {
return (bytes + 7) & ~7;
}
// 自由链表数组,每个索引对应一种大小
static void* free_list[16];
static size_t FREELIST_INDEX(size_t bytes) {
return (bytes + 7) / 8 - 1;
}
// 从内存池获取内存
static void* refill(size_t n);
// 从堆获取内存池
static char* chunk_alloc(size_t size, int& nobjs);
public:
static void* allocate(size_t n) {
if (n > 128)
return malloc_alloc::allocate(n);
void** my_free_list = free_list + FREELIST_INDEX(n);
void* result = *my_free_list;
if (result == nullptr) {
// 自由链表为空,从内存池获取
return refill(ROUND_UP(n));
}
*my_free_list = (*(void**)result);
return result;
}
static void deallocate(void* p, size_t n) {
if (n > 128) {
malloc_alloc::deallocate(p, n);
return;
}
void** my_free_list = free_list + FREELIST_INDEX(n);
*(void**)p = *my_free_list;
*my_free_list = p;
}
};
3.4 算法实现
3.4.1 std::sort的实现
std::sort是STL中最常用的算法之一,它的实现非常巧妙:
cpp
template<typename RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
if (first >= last) return;
// 对于小区间,使用插入排序
if (last - first < 16) {
for (RandomAccessIterator i = first + 1; i < last; ++i) {
auto key = *i;
RandomAccessIterator j = i - 1;
while (j >= first && *j > key) {
*(j + 1) = *j;
--j;
}
*(j + 1) = key;
}
return;
}
// 使用快速排序
RandomAccessIterator pivot = partition(first, last);
sort(first, pivot);
sort(pivot + 1, last);
}
实际的std::sort实现更加复杂,通常使用introsort算法(快速排序+堆排序+插入排序的混合算法),以确保最坏情况下的时间复杂度也是O(n log n)。
3.4.2 std::find的实现
std::find是最简单的搜索算法之一:
cpp
template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, const T& value) {
while (first != last) {
if (*first == value)
return first;
++first;
}
return last;
}
这个实现是线性的,时间复杂度是O(n)。
3.4.3 std::lower_bound和std::upper_bound
这两个算法用于在有序区间中查找:
cpp
// 查找第一个不小于value的位置
template<typename ForwardIterator, typename T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last,
const T& value) {
while (first != last) {
auto mid = first;
std::advance(mid, std::distance(first, last) / 2);
if (*mid < value) {
first = ++mid;
} else {
last = mid;
}
}
return first;
}
// 查找第一个大于value的位置
template<typename ForwardIterator, typename T>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last,
const T& value) {
while (first != last) {
auto mid = first;
std::advance(mid, std::distance(first, last) / 2);
if (*mid <= value) {
first = ++mid;
} else {
last = mid;
}
}
return first;
}
这两个算法使用二分查找,时间复杂度是O(log n)。
四、常见陷阱与面试题
4.1 迭代器失效问题
迭代器失效是STL使用中最常见的问题之一。当容器发生修改操作时,迭代器可能会变得无效,继续使用会导致未定义行为。
4.1.1 vector的迭代器失效
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
// 危险:push_back可能导致迭代器失效
auto it = v.begin();
v.push_back(6); // 可能导致内存重新分配,it失效
// 正确做法:每次push_back后重新获取迭代器
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效的迭代器
} else {
++it;
}
}
vector迭代器失效的情况:
- 插入元素导致重新分配:所有迭代器失效
- 插入元素未重新分配:插入点之后的迭代器失效
- 删除元素:删除点之后的迭代器失效
4.1.2 list的迭代器失效
list的迭代器失效问题相对简单:
cpp
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
lst.erase(it); // 只有被删除的节点迭代器失效,其他迭代器仍然有效
++it; // 正确:it现在指向第二个元素
list迭代器失效的情况:
- 删除元素:只有被删除的节点迭代器失效
- 插入元素:不会导致任何迭代器失效
4.1.3 map/set的迭代器失效
cpp
std::map<int, std::string> m = {{1, "a"}, {2, "b"}, {3, "c"}};
auto it = m.begin();
m.erase(it); // 只有被删除的节点迭代器失效
// 注意:不能在使用迭代器遍历时插入相同键的元素
// 这会导致迭代器失效(因为树会重新平衡)
4.1.4 面试题:迭代器失效的场景
面试题1:下面代码有什么问题?
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) {
v.erase(it); // 错误:erase后it失效
}
}
答案:erase后it失效,++it的行为未定义。正确做法是使用erase的返回值更新迭代器。
面试题2:在遍历vector时删除偶数,如何实现?
cpp
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 方法1:使用erase返回值
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 方法2:使用erase-remove惯用法
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }),
v.end());
4.2 容器选择问题
选择合适的容器是提高程序性能的关键。
4.2.1 各容器的特点对比
| 容器 | 随机访问 | 插入/删除(头) | 插入/删除(尾) | 插入/删除(中间) | 有序 |
|---|---|---|---|---|---|
| vector | O(1) | - | O(1) amortized | O(n) | 否 |
| list | - | O(1) | O(1) | O(1) | 否 |
| deque | O(1) | O(1) | O(1) amortized | O(n) | 否 |
| map | - | - | - | O(log n) | 是 |
| unordered_map | - | - | - | O(1) avg | 否 |
4.2.2 面试题:如何选择容器?
面试题:根据以下场景,应该选择哪种容器?
- 存储10万个整数,需要频繁在末尾添加和删除
- 存储10万个整数,需要频繁在中间插入和删除
- 存储10万个整数,需要频繁查找
- 存储10万个字符串,需要按字典序遍历
答案:
- vector:末尾操作是摊销常数时间,缓存命中率高
- list:中间插入删除是常数时间
- unordered_map:平均常数时间查找
- map或set:自动维护有序性
4.3 内存管理问题
4.3.1 vector的内存管理
cpp
// 面试题:vector的capacity和size有什么区别?
std::vector<int> v;
v.reserve(100); // capacity = 100, size = 0
v.resize(50); // capacity = 100, size = 50
// 面试题:如何避免vector频繁扩容?
// 答案:使用reserve预分配足够的空间
std::vector<int> v;
v.reserve(1000); // 预先分配1000个元素的空间
for (int i = 0; i < 1000; ++i) {
v.push_back(i); // 不会触发扩容
}
4.3.2 内存泄漏问题
cpp
// 面试题:以下代码有什么问题?
void func() {
std::vector<int*> v;
for (int i = 0; i < 10; ++i) {
v.push_back(new int(i)); // 内存泄漏
}
// 函数结束时,v被销毁,但指针指向的内存未被释放
}
// 正确做法:使用智能指针
void func() {
std::vector<std::shared_ptr<int>> v;
for (int i = 0; i < 10; ++i) {
v.push_back(std::make_shared<int>(i));
}
}
4.4 性能问题
4.4.1 迭代器类型选择
cpp
// 面试题:以下代码的性能如何?
std::list<int> lst = {1, 2, 3, 4, 5};
// 错误:list的迭代器不支持随机访问
std::sort(lst.begin(), lst.end());
// 正确做法1:先复制到vector
std::vector<int> v(lst.begin(), lst.end());
std::sort(v.begin(), v.end());
// 正确做法2:使用list的成员函数
lst.sort(); // list自带的排序
4.4.2 算法复杂度选择
cpp
// 面试题:以下代码有什么性能问题?
std::vector<int> v = {1, 2, 3, 4, 5};
// 错误:在vector中查找元素使用O(n)的find
auto it = std::find(v.begin(), v.end(), 100);
// 正确做法:对于有序数据使用二分查找
auto it = std::lower_bound(v.begin(), v.end(), 100); // O(log n)
// 或者使用unordered_set进行常数时间查找
std::unordered_set<int> s(v.begin(), v.end());
auto it = s.find(100); // O(1) avg
4.5 线程安全问题
STL容器本身不是线程安全的:
cpp
// 面试题:以下代码有什么问题?
std::vector<int> v = {1, 2, 3};
// 线程1
std::thread t1([&]() {
for (int i = 0; i < 1000; ++i) {
v.push_back(i); // 不安全
}
});
// 线程2
std::thread t2([&]() {
for (int i = 0; i < 1000; ++i) {
v.push_back(i); // 不安全
}
});
解决方案:
- 使用互斥锁保护共享容器
- 每个线程使用独立的容器,最后合并
- 使用无锁数据结构(需要自行实现或使用第三方库)
4.6 常见面试题汇总
4.6.1 vector相关面试题
Q1:vector的扩容机制是什么? A:通常当size等于capacity时,vector会进行扩容。扩容比例通常是1.5倍或2倍(不同实现可能不同)。扩容时分配新内存,复制元素,释放旧内存。
Q2:vector的emplace_back和push_back有什么区别? A:push_back接受已构造的对象,会进行拷贝或移动操作。emplace_back接受构造参数,直接在容器末尾构造对象,避免了额外的拷贝或移动。
cpp
std::vector<std::string> v;
v.push_back("hello"); // 创建一个临时string,然后拷贝/移动
v.emplace_back("hello"); // 直接在容器中构造string
Q3:为什么vector的迭代器是原生指针? A:因为vector的内存是连续的,可以通过指针算术运算实现随机访问。
4.6.2 map相关面试题
Q4:map和unordered_map的区别是什么? A:map基于红黑树实现,元素按键排序,查找、插入、删除都是O(log n)。unordered_map基于哈希表实现,元素无序,查找、插入、删除平均是O(1)。
Q5:map的operator[]和find有什么区别? A:operator[]如果键不存在,会插入一个默认构造的值,可能导致副作用。find如果键不存在,只返回end(),不会修改容器。
cpp
std::map<int, std::string> m;
// 风险:如果键不存在,会插入一个空string
std::string s = m[100];
// 安全:如果键不存在,不会插入
auto it = m.find(100);
if (it != m.end()) {
std::string s = it->second;
}
Q6:为什么map的键需要支持严格弱序? A:map底层使用红黑树,需要通过比较函数对元素进行排序。严格弱序要求:非自反、非对称、传递性、等价性。
4.6.3 迭代器相关面试题
Q7:迭代器失效是什么意思? A:当容器发生修改操作(如插入、删除)时,某些迭代器可能变得无效,继续使用会导致未定义行为。
Q8:如何安全地遍历容器并删除元素? A:使用erase的返回值更新迭代器,或者使用erase-remove惯用法。
cpp
// 方法1:使用返回值
for (auto it = v.begin(); it != v.end(); ) {
if (should_delete(*it)) {
it = v.erase(it);
} else {
++it;
}
}
// 方法2:erase-remove惯用法
v.erase(std::remove_if(v.begin(), v.end(), should_delete), v.end());
4.6.4 算法相关面试题
Q9:std::sort的时间复杂度是多少? A:平均O(n log n),最坏情况也是O(n log n)(使用introsort算法)。
Q10:stable_sort和sort的区别是什么? A:stable_sort保持相等元素的相对顺序,时间复杂度是O(n log n)但需要额外内存;sort不保证稳定性,通常更快。
总结
STL是C++标准库中最重要的一部分,它体现了泛型编程的核心思想。通过模板、迭代器、算法与数据结构的分离,STL提供了一套高效、灵活、可复用的工具。
理解STL的设计思路和实现原理,不仅能够帮助我们更好地使用它,还能提升我们的编程思维。掌握STL的常见陷阱和面试要点,则是C++工程师的必备技能。
在实际开发中,我们应该:
- 根据场景选择合适的容器
- 注意迭代器失效问题
- 避免不必要的拷贝,使用emplace系列函数
- 对于性能敏感的场景,注意容器特性
- 多线程环境下注意线程安全问题
只有深入理解STL的原理,才能在工作中写出高效、正确的C++代码。