C++标准模板库(STL)提供了丰富的容器类型,它们是数据管理的核心工具,能够高效地存储和操作数据。本文将全面介绍C++常用容器,包括其底层原理、特性、适用场景,并通过实战代码示例展示如何使用它们。
一、容器概述与分类
C++容器主要分为三大类:
- 序列容器:按顺序存储元素,如
vector
、list
、deque
等。 - 关联容器:按关键字存储元素,如
set
、map
等。 - 无序关联容器:基于哈希表实现,如
unordered_set
、unordered_map
等。 - 容器适配器:基于其他容器实现,提供特定接口,如
stack
、queue
等。
容器通过封装底层数据结构(如动态数组、链表、红黑树、哈希表)的复杂性,提供了统一、易用的接口,让开发者能专注于业务逻辑而非数据存储细节。
二、序列容器详解
1. vector:动态数组
原理:vector
在连续的内存空间中存储元素,支持动态扩容(通常按1.5-2倍增长)。
特点:
- 随机访问效率高(O(1))
- 尾部插入/删除高效(O(1)摊销)
- 中间插入/删除效率低(需移动元素,O(n))
- 内存连续,缓存友好
适用场景:需要频繁随机访问、尾部操作密集的场景,如数值计算、动态数组。
代码示例:
#include <vector>
#include <iostream>
#include <algorithm>// for remove_if
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5};
// 尾部插入
vec.push_back(6);
// 随机访问
std::cout << "Element at index 2: " << vec[2] << std::endl;
// 遍历(C++11范围for循环)
for (const auto& num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
// 删除特定元素(擦除-移除惯用法)
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; }),
vec.end());
// 预分配空间避免频繁扩容
vec.reserve(100);
return 0;
}
2. list:双向链表
原理:list
通过双向链表实现,每个节点包含数据和前后指针。
特点:
- 任意位置插入/删除高效(O(1))
- 不支持随机访问(访问需遍历,O(n))
- 迭代器稳定性高(插入删除不影响其他迭代器)
适用场景:需要频繁在任意位置插入删除的场景,如游戏对象管理、日程安排。
代码示例:
#include <list>
#include <iostream>
int main()
{
std::list<std::string> items = {"apple", "banana", "cherry"};
// 头部插入
items.push_front("orange");
// 中间插入
auto it = items.begin();
std::advance(it, 2);
items.insert(it, "grape");
// 安全删除元素
for (auto it = items.begin(); it != items.end(); ) {
if (*it == "banana") {
it = items.erase(it);// erase返回下一个有效迭代器
}
else {
++it;
}
}
// 链表特有操作:排序和反转
items.sort();
items.reverse();
return 0;
}
3. deque:双端队列
原理:deque
由多个固定大小的数组段组成,通过中央映射表管理。
特点:
- 头尾插入/删除高效(O(1))
- 支持随机访问(效率略低于vector)
- 中间插入/删除效率低(O(n))
适用场景:需要频繁在两端操作的场景,如消息队列、实时交易系统。
代码示例:
#include <deque>
#include <iostream>
int main()
{
std::deque<int> dq = {10, 20, 30};
// 头部操作
dq.push_front(5);
dq.pop_front();
// 尾部操作
dq.push_back(40);
dq.pop_back();
// 随机访问
std::cout << "Element at index 1: " << dq[1] << std::endl;
return 0;
}
4. array:固定大小数组(C++11)
原理:封装固定大小数组,大小编译时确定。
特点:
- 大小固定,不支持动态扩容
- 随机访问高效(O(1))
- 比原生数组更安全(提供at()进行边界检查)
适用场景:元素数量固定且已知的场景。
代码示例:
#include <array>
#include <iostream>
int main()
{
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// 安全访问(边界检查)
try
{
std::cout << arr.at(10) << std::endl;// 抛出std::out_of_range异常
}
catch (const std::out_of_range& e)
{
std::cout << "Index out of range: " << e.what() << std::endl;
}
// 填充
arr.fill(0);
return 0;
}
三、关联容器详解
1. map:键值对映射(基于红黑树)
原理:基于红黑树实现,保持键有序。
特点:
- 按键自动排序
- 查找、插入、删除效率高(O(log n))
- 键唯一
适用场景:需要键值对映射且按键有序的场景,如配置管理、字典。
代码示例:
#include <map>
#include <string>
#include <iostream>
int main()
{
std::map<std::string, int> ageMap;
// 插入元素
ageMap["Alice"] = 30;
ageMap["Bob"] = 25;
ageMap.insert({"Charlie", 35});
// 查找元素
if (auto it = ageMap.find("Alice"); it != ageMap.end())
{
std::cout << "Alice's age: " << it->second << std::endl;
}
// 遍历(自动按键排序)
for (const auto& [name, age] : ageMap) {
std::cout << name << ": " << age << std::endl;
}
return 0;
}
2. set:唯一键集合(基于红黑树)
原理:基于红黑树实现,存储唯一键。
特点:
- 元素唯一,自动排序
- 查找、插入、删除效率高(O(log n))
适用场景:需要存储唯一元素且有序的场景,如权限系统、去重。
代码示例:
#include <set>
#include <iostream>
int main()
{
std::set<int> uniqueNumbers;
// 插入元素(自动去重)
uniqueNumbers.insert(5);
uniqueNumbers.insert(3);
uniqueNumbers.insert(5);// 不会被插入
// 查找元素
if (uniqueNumbers.find(3) != uniqueNumbers.end()) {
std::cout << "3 found in set" << std::endl;
}
// 遍历(自动排序)
for (const auto& num : uniqueNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
四、无序关联容器详解(C++11)
1. unordered_map:哈希映射
原理:基于哈希表实现,使用桶数组和链表/红黑树解决冲突。
特点:
- 平均查找、插入、删除效率高(O(1))
- 不保证元素顺序
适用场景:需要快速键值查找且不要求顺序的场景,如缓存系统、字典。
代码示例:
#include <unordered_map>
#include <string>
#include <iostream>
int main()
{
std::unordered_map<std::string, int> cache;
// 插入元素
cache["data1"] = 42;
cache["data2"] = 100;
// 查找元素
if (auto it = cache.find("data1"); it != cache.end()) {
std::cout << "Found: " << it->first << " => " << it->second << std::endl;
}
// 遍历(顺序不确定)
for (const auto& [key, value] : cache) {
std::cout << key << ": " << value << std::endl;
}
return 0;
}
2. unordered_set:哈希集合
原理:基于哈希表实现,存储唯一键。
特点:
- 平均查找、插入、删除效率高(O(1))
- 不保证元素顺序
适用场景:需要快速存在性检查且不要求顺序的场景,如大规模数据去重。
代码示例:
#include <unordered_set>
#include <iostream>
int main()
{
std::unordered_set<std::string> usernames;
// 插入元素
usernames.insert("user1");
usernames.insert("user2");
usernames.insert("user1");// 不会被插入
// 检查存在性
if (usernames.contains("user1")) {
// C++20
std::cout << "user1 exists" << std::endl;
}
return 0;
}
五、容器适配器
1. stack:后进先出(LIFO)
原理:默认基于deque
实现,提供栈接口。
特点:
- 只允许在顶部操作(push、pop、top)
- 无迭代器支持
适用场景:撤销/重做系统、深度优先搜索、语法解析。
代码示例:
#include <stack>
#include <iostream>
int main()
{
std::stack<int> s;
// 入栈
s.push(10);
s.push(20);
s.push(30);
// 访问栈顶
std::cout << "Top element: " << s.top() << std::endl;
// 出栈
s.pop();
// 检查是否为空
if (!s.empty()) {
std::cout << "Stack size: " << s.size() << std::endl;
}
return 0;
}
2. queue:先进先出(FIFO)
原理:默认基于deque
实现,提供队列接口。
特点:
- 允许在尾部添加、头部删除
- 无迭代器支持
适用场景:任务队列、广度优先搜索、生产者-消费者模型。
代码示例:
#include <queue>
#include <iostream>
int main()
{
std::queue<int> q;
// 入队
q.push(10);
q.push(20);
q.push(30);
// 访问队首
std::cout << "Front element: " << q.front() << std::endl;
// 出队
q.pop();
return 0;
}
六、容器选择指南
下表总结了主要容器的特性,帮助你在不同场景下做出合适的选择:
特性 | vector | deque | list | map/set | unordered_map/unordered_set |
---|---|---|---|---|---|
内存布局 | 连续 | 分段连续 | 非连续 | 非连续 | 非连续 |
随机访问 | O(1) | O(1) | O(n) | O(log n) | O(1)平均 |
头部插入 | O(n) | O(1) | O(1) | - | - |
尾部插入 | O(1) | O(1) | O(1) | - | - |
中间插入 | O(n) | O(n) | O(1) | O(log n) | O(1)平均 |
迭代器稳定性 | 低 | 中等 | 高 | 中等 | rehash时失效 |
选型建议:
- 需要随机访问且尾部操作多 →
vector
- 需要频繁在两端操作 →
deque
- 需要频繁在任意位置插入删除 →
list
- 需要键值映射且有序 →
map
- 需要快速查找且不要求顺序 →
unordered_map
- 后进先出需求 →
stack
- 先进先出需求 →
queue
七、实战应用案例
案例1:使用vector实现粒子系统
#include <vector>
#include <algorithm>
#include <execution>// C++17并行算法
struct Particle
{
float position[3];
float velocity[3];
float lifetime;
void update(float dt) {
// 更新位置和生命周期
for (int i = 0; i < 3; ++i) {
position[i] += velocity[i] * dt;
}
lifetime -= dt;
}
};
class ParticleSystem
{
private:
std::vector<Particle> particles;
public:
void update(float dt) {
// 删除生命周期结束的粒子(擦除-移除惯用法)
particles.erase(
std::remove_if(particles.begin(), particles.end(),
[](const Particle& p) { return p.lifetime <= 0; }),
particles.end()
);
// 并行更新所有粒子(C++17)
std::for_each(std::execution::par_unseq, particles.begin(), particles.end(),
[dt](Particle& p) { p.update(dt); });
}
void addParticle(const Particle& p) {
particles.push_back(p);
}
};
案例2:使用map实现配置管理系统
#include <map>
#include <string>
#include <fstream>
#include <iostream>
class ConfigManager {
private:
std::map<std::string, std::string> configs;
public:
void loadConfig(const std::string& filename) {
std::ifstream file(filename);
std::string key, value;
while (file >> key >> value) {
configs[key] = value;
}
}
std::string getValue(const std::string& key, const std::string& defaultValue = "") {
auto it = configs.find(key);
return it != configs.end() ? it->second : defaultValue;
}
void setValue(const std::string& key, const std::string& value) {
configs[key] = value;
}
void printAll() const {
for (const auto& [key, value] : configs) {
std::cout << key << " = " << value << std::endl;
}
}
};
案例3:使用unordered_map实现缓存系统
#include <unordered_map>
#include <string>
#include <iostream>
template<typename Key, typename Value>
class LRUCache {
private:
size_t capacity;
std::unordered_map<Key, std::pair<Value, typename std::list<Key>::iterator>> cache;
std::list<Key> lruList;
public:
LRUCache(size_t cap) : capacity(cap) {}
Value* get(const Key& key) {
auto it = cache.find(key);
if (it == cache.end()) return nullptr;
// 移动到最近使用位置
lruList.erase(it->second.second);
lruList.push_front(key);
it->second.second = lruList.begin();
return &(it->second.first);
}
void put(const Key& key, const Value& value) {
auto it = cache.find(key);
if (it != cache.end()) {
lruList.erase(it->second.second);
cache.erase(it);
}
if (cache.size() >= capacity) {
// 删除最久未使用的元素
auto last = lruList.back();
cache.erase(last);
lruList.pop_back();
}
// 插入新元素
lruList.push_front(key);
cache[key] = {value, lruList.begin()};
}
};
八、最佳实践与常见陷阱
-
vector扩容陷阱:频繁扩容会导致性能下降,使用
reserve()
预分配足够空间std::vector<int> vec; vec.reserve(1000);// 预分配空间,避免多次扩容
-
迭代器失效:
vector
/deque
插入删除可能使所有迭代器失效list
插入删除只影响被操作元素的迭代器map
/set
插入删除只影响被操作元素的迭代器
-
选择合适容器:根据操作特点选择容器,而不是总是使用
vector
-
使用emplace操作:直接构造对象,避免临时对象创建和拷贝
std::vector<std::string> vec; vec.emplace_back("hello");// 直接构造,优于push_back("hello")
-
利用移动语义(C++11):减少不必要的拷贝
std::vector<std::string> createStrings() { std::vector<std::string> result; // ... 填充数据 return result;// 使用移动语义而非拷贝 }
九、C++17/20容器新特性
1. 节点句柄(Node Handle)
C++17允许在不同容器间转移节点,避免拷贝开销:
std::map<int, std::string> map1, map2;
// ... 填充map1// 提取节点并插入到map2
auto node = map1.extract(1);
if (!node.empty()) {
map2.insert(std::move(node));
}
2. try_emplace和insert_or_assign
C++17为map提供更高效的操作:
std::map<std::string, std::string> m;
// 只在键不存在时插入
m.try_emplace("key", "value");
// 插入或赋值
m.insert_or_assign("key", "new_value");
3. contains方法(C++20)
更直观的存在性检查:
std::set<int> s = {1, 2, 3};
if (s.contains(2)) {
// 优于s.find(2) != s.end()
std::cout << "Set contains 2" << std::endl;
}
总结
C++容器是标准库的核心组件,提供了丰富的数据结构选择。选择合适的容器需要考虑操作特点(随机访问、插入删除位置)、内存布局和性能要求。现代C++(C++11/14/17/20)引入了许多新特性和优化,如移动语义、节点句柄、并行算法等,进一步提升了容器的性能和易用性。
掌握各种容器的特性和适用场景,能够帮助我们编写出更高效、更易维护的C++代码。在实际开发中,应根据具体需求选择合适的容器,并遵循最佳实践以避免常见陷阱。