C++标准库提供了一系列的容器,这些容器是用于存储和组织数据的类模板。标准容器分为序列容器、关联容器、无序关联容器和容器适配器。每种容器都有其特定的用途和性能特性,开发者可以根据需求选择合适的容器。
使用标准容器的好处包括:
- 高效性:标准容器经过优化,能够提供高效的插入、删除和查找操作。
- 易用性:标准容器提供了丰富的接口,使用方便。
- 可移植性:标准容器是C++标准库的一部分,具有良好的可移植性。
- 序列容器:
std::vector
,std::deque
,std::list
,std::forward_list
- 关联容器:
std::set
,std::map
,std::multiset
,std::multimap
- 无序关联容器:
std::unordered_set
,std::unordered_map
,std::unordered_multiset
,std::unordered_multimap
- 容器适配器:
std::stack
,std::queue
,std::priority_queue
std::vector
是动态数组,支持随机访问。它的内存是连续的,因此在访问元素时非常高效。std::vector
会自动管理内存,当元素数量超过当前容量时,会自动重新分配内存。
push_back()
:在末尾插入元素。pop_back()
:删除末尾元素。size()
:返回元素数量。capacity()
:返回当前容量。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec; // 创建一个空的vector
// 插入元素
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
// 访问元素
std::cout << "First element: " << vec[0] << std::endl;
// 删除元素
vec.pop_back();
// 输出所有元素
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::vector<int> vec;
:创建一个空的std::vector
。vec.push_back(10);
:在vec
的末尾插入元素10
。vec.pop_back();
:删除vec
的最后一个元素。vec[0]
:通过下标访问vec
的第一个元素。
std::deque
(双端队列)支持在两端高效地插入和删除元素。它的内存不是连续的,而是由多个块组成的。
push_front()
:在队列前端插入元素。push_back()
:在队列后端插入元素。pop_front()
:删除队列前端元素。pop_back()
:删除队列后端元素。
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq; // 创建一个空的deque
// 插入元素
dq.push_front(10);
dq.push_back(20);
dq.push_front(5);
// 访问元素
std::cout << "First element: " << dq.front() << std::endl;
// 删除元素
dq.pop_front();
// 输出所有元素
for (int i : dq) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::deque<int> dq;
:创建一个空的std::deque
。dq.push_front(10);
:在dq
的前端插入元素10
。dq.pop_front();
:删除dq
的前端元素。dq.front()
:访问dq
的前端元素。
std::list
是双向链表,支持在任意位置高效地插入和删除元素。它的内存不是连续的,因此不支持随机访问。
push_back()
:在末尾插入元素。push_front()
:在开头插入元素。erase()
:删除指定位置的元素。
#include <iostream>
#include <list>
int main() {
std::list<int> lst; // 创建一个空的list
// 插入元素
lst.push_back(10);
lst.push_front(5);
lst.push_back(20);
// 删除元素
lst.erase(lst.begin());
// 输出所有元素
for (int i : lst) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::list<int> lst;
:创建一个空的std::list
。lst.push_back(10);
:在lst
的末尾插入元素10
。lst.erase(lst.begin());
:删除lst
的第一个元素。
std::forward_list
是单向链表,只支持单向遍历。它的内存不是连续的,因此不支持随机访问。
push_front()
:在开头插入元素。erase_after()
:删除指定位置后的元素。
#include <iostream>
#include <forward_list>
int main() {
std::forward_list<int> flst; // 创建一个空的forward_list
// 插入元素
flst.push_front(10);
flst.push_front(5);
flst.push_front(20);
// 删除元素
flst.erase_after(flst.begin());
// 输出所有元素
for (int i : flst) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::forward_list<int> flst;
:创建一个空的std::forward_list
。flst.push_front(10);
:在flst
的开头插入元素10
。flst.erase_after(flst.begin());
:删除flst
的第二个元素。
std::set
是基于红黑树的集合,存储唯一的元素,且元素自动排序。
insert()
:插入元素。find()
:查找元素。erase()
:删除元素。
#include <iostream>
#include <set>
int main() {
std::set<int> s; // 创建一个空的set
// 插入元素
s.insert(10);
s.insert(5);
s.insert(20);
// 查找元素
if (s.find(5) != s.end()) {
std::cout << "Element found!" << std::endl;
}
// 删除元素
s.erase(10);
// 输出所有元素
for (int i : s) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::set<int> s;
:创建一个空的std::set
。s.insert(10);
:插入元素10
。s.find(5)
:查找元素5
。s.erase(10);
:删除元素10
。
std::unordered_set
是基于哈希表的集合,存储唯一的元素,但不保证顺序。
insert()
:插入元素。find()
:查找元素。erase()
:删除元素。
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> us; // 创建一个空的unordered_set
// 插入元素
us.insert(10);
us.insert(5);
us.insert(20);
// 查找元素
if (us.find(5) != us.end()) {
std::cout << "Element found!" << std::endl;
}
// 删除元素
us.erase(10);
// 输出所有元素
for (int i : us) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::unordered_set<int> us;
:创建一个空的std::unordered_set
。us.insert(10);
:插入元素10
。us.find(5)
:查找元素5
。us.erase(10);
:删除元素10
。
std::stack
是基于 std::deque
的栈,遵循 LIFO(后进先出)原则。
push()
:插入元素。pop()
:删除栈顶元素。top()
:访问栈顶元素。
#include <iostream>
#include <stack>
int main() {
std::stack<int> st; // 创建一个空的stack
// 插入元素
st.push(10);
st.push(20);
st.push(30);
// 访问栈顶元素
std::cout << "Top element: " << st.top() << std::endl;
// 删除栈顶元素
st.pop();
// 输出栈顶元素
std::cout << "Top element after pop: " << st.top() << std::endl;
return 0;
}
std::stack<int> st;
:创建一个空的std::stack
。st.push(10);
:插入元素10
。st.top()
:访问栈顶元素。st.pop();
:删除栈顶元素。
迭代器是用于遍历容器中元素的对象。它类似于指针,但提供了更多的功能。
- 输入迭代器:只读,单向移动。
- 输出迭代器:只写,单向移动。
- 前向迭代器:可读写,单向移动。
- 双向迭代器:可读写,双向移动。
- 随机访问迭代器:可读写,随机访问。
begin()
:返回指向容器第一个元素的迭代器。end()
:返回指向容器末尾的迭代器。++
:移动到下一个元素。--
:移动到上一个元素。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {10, 20, 30};
// 使用迭代器遍历vector
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
vec.begin()
:返回指向vec
第一个元素的迭代器。vec.end()
:返回指向vec
末尾的迭代器。*it
:解引用迭代器,获取元素值。
std::vector
:随机访问 O(1),插入/删除 O(n)。std::list
:插入/删除 O(1),随机访问 O(n)。std::set
:插入/删除/查找 O(log n)。std::unordered_set
:插入/删除/查找 O(1)(平均情况)。
- 如果需要频繁随机访问,选择
std::vector
。 - 如果需要频繁插入/删除,选择
std::list
。 - 如果需要自动排序,选择
std::set
。 - 如果需要快速查找,选择
std::unordered_set
。
根据操作的频率和性能需求选择合适的容器。
自定义比较函数用于指定容器的排序规则。
#include <iostream>
#include <set>
struct Compare {
bool operator()(int a, int b) const {
return a > b; // 降序排列
}
};
int main() {
std::set<int, Compare> s;
s.insert(10);
s.insert(5);
s.insert(20);
for (int i : s) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
struct Compare
:定义一个比较函数对象。std::set<int, Compare> s;
:使用自定义比较函数创建std::set
。
自定义哈希函数用于指定 std::unordered_set
的哈希规则。
#include <iostream>
#include <unordered_set>
struct Hash {
std::size_t operator()(int x) const {
return x % 10; // 简单哈希函数
}
};
int main() {
std::unordered_set<int, Hash> us;
us.insert(10);
us.insert(5);
us.insert(20);
for (int i : us) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
struct Hash
:定义一个哈希函数对象。std::unordered_set<int, Hash> us;
:使用自定义哈希函数创建std::unordered_set
。
线程安全是指在多线程环境下,程序能够正确地处理共享资源,避免数据竞争(Data Race)和不一致的状态。在多线程编程中,多个线程可能同时访问和修改同一个数据结构,如果没有适当的同步机制,可能会导致不可预期的结果。
C++标准库中的容器(如 std::vector
, std::map
, std::unordered_set
等)不是线程安全的。这意味着在多线程环境中,如果多个线程同时对同一个容器进行读写操作,可能会导致以下问题:
- 数据竞争(Data Race):多个线程同时修改同一个容器,导致数据不一致。
- 未定义行为(Undefined Behavior):C++标准未定义多线程对非线程安全容器的并发操作,可能导致程序崩溃或产生不可预期的结果。
例如,以下代码在多线程环境中是不安全的:
#include <iostream>
#include <vector>
#include <thread>
std::vector<int> vec;
void add_to_vector(int value) {
vec.push_back(value); // 多线程同时写入,可能导致数据竞争
}
int main() {
std::thread t1(add_to_vector, 10);
std::thread t2(add_to_vector, 20);
t1.join();
t2.join();
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
问题分析:
vec.push_back(value);
是线程不安全的操作,多个线程可能同时修改vec
,导致数据竞争。- 运行结果可能是
10 20
,也可能是20 10
,甚至可能导致程序崩溃。
为了在多线程环境中安全地使用容器,可以采用以下方法:
- 使用互斥锁(Mutex):通过加锁来保护临界区,确保同一时间只有一个线程可以访问容器。
- 使用原子操作(Atomic Operations):对于简单的操作,可以使用
std::atomic
来避免加锁。 - 使用并发容器:C++标准库提供了一些线程安全的容器(如
std::atomic
和std::shared_mutex
),但标准容器本身不是线程安全的。
互斥锁(Mutex) 是一种同步机制,用于保护共享资源。通过在访问容器之前加锁,可以确保同一时间只有一个线程可以修改容器。
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
std::vector<int> vec;
std::mutex mtx; // 创建一个互斥锁
void add_to_vector(int value) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
vec.push_back(value); // 线程安全的操作
}
int main() {
std::thread t1(add_to_vector, 10);
std::thread t2(add_to_vector, 20);
t1.join();
t2.join();
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
std::mutex mtx;
:创建一个互斥锁。std::lock_guard<std::mutex> lock(mtx);
:在进入临界区之前加锁,离开临界区时自动解锁。vec.push_back(value);
:在加锁的保护下,线程安全地向容器中插入元素。
运行结果:
- 无论运行多少次,结果都是
10 20
,因为加锁确保了线程安全。
对于某些简单的操作,可以使用 std::atomic
来避免加锁。std::atomic
提供了线程安全的原子操作,适用于单个变量的读写。
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0); // 创建一个原子变量
void increment_counter() {
for (int i = 0; i < 1000; ++i) {
counter++; // 线程安全的原子操作
}
}
int main() {
std::thread t1(increment_counter);
std::thread t2(increment_counter);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
std::atomic<int> counter(0);
:创建一个原子变量,初始值为0
。counter++;
:线程安全的原子操作,确保多个线程同时修改counter
时不会发生数据竞争。
运行结果:
- 无论运行多少次,结果都是
2000
,因为std::atomic
确保了线程安全。
C++标准库提供了一些并发容器,适用于多线程环境。这些容器在内部实现了线程安全的操作,开发者无需手动加锁。
std::atomic
提供了对单个变量的线程安全操作,适用于简单的计数器或标志位。
std::shared_mutex
是一种读写锁,允许多个线程同时读取共享资源,但只允许一个线程写入。
#include <iostream>
#include <map>
#include <shared_mutex>
#include <thread>
std::map<int, int> data;
std::shared_mutex mtx; // 创建一个读写锁
void write_to_map(int key, int value) {
std::unique_lock<std::shared_mutex> lock(mtx); // 写锁
data[key] = value;
}
void read_from_map(int key) {
std::shared_lock<std::shared_mutex> lock(mtx); // 读锁
if (data.find(key) != data.end()) {
std::cout << "Value: " << data[key] << std::endl;
} else {
std::cout << "Key not found!" << std::endl;
}
}
int main() {
std::thread t1(write_to_map, 1, 10);
std::thread t2(read_from_map, 1);
t1.join();
t2.join();
return 0;
}
std::shared_mutex mtx;
:创建一个读写锁。std::unique_lock<std::shared_mutex> lock(mtx);
:写锁,确保只有一个线程可以写入。std::shared_lock<std::shared_mutex> lock(mtx);
:读锁,允许多个线程同时读取。
运行结果:
- 写线程和读线程可以并发执行,写锁确保写操作的线程安全,读锁允许多个线程同时读取。
在多线程环境中使用容器时,必须注意线程安全问题。可以通过以下方式实现线程安全:
- 使用互斥锁(Mutex):通过加锁保护临界区。
- 使用原子操作(Atomic):对于简单的操作,使用
std::atomic
。 - 使用并发容器:C++标准库提供了一些并发容器,适用于多线程环境。
通过合理选择同步机制,可以确保多线程程序的正确性和性能。