C++学习:六个月从基础到就业——C++学习之旅:STL容器详解
C++学习:六个月从基础到就业——C++学习之旅:STL容器详解
本文是我C++学习之旅系列的第二十三篇技术文章,也是第二阶段"C++进阶特性"的第一篇,主要介绍C++ STL容器。查看完整系列目录了解更多内容。
引言
在前面的文章中,我们探讨了C++的内存管理机制,这为我们理解STL的底层实现打下了基础。今天,我们将深入探讨C++ STL(标准模板库)的核心组件之一——容器。STL容器是C++程序员的瑞士军刀,掌握它们能极大提高我们的编程效率和代码质量。
什么是STL容器?
STL容器是存储数据集合的类模板,它们遵循特定的设计模式,提供标准化的接口,并且拥有各自的性能特点。容器是STL三大核心组件(容器、算法、迭代器)中最为基础的部分。
容器的分类
STL容器主要分为三大类:
- 顺序容器:按线性顺序存储元素
- 关联容器:按特定顺序存储元素,便于快速查找
- 无序关联容器:使用哈希表实现的关联容器
- 容器适配器:基于其他容器实现的特殊接口容器
顺序容器详解
std::vector
std::vector
是最常用的容器,提供了类似动态数组的功能。
特点:
- 随机访问元素 - O(1)
- 尾部插入/删除 - 均摊O(1)
- 中间/头部插入删除 - O(n)
- 内存连续存储
示例代码:
#include <iostream>
#include <vector>int main() {// 创建和初始化std::vector<int> vec1; // 空vectorstd::vector<int> vec2(5, 10); // 包含5个值为10的元素std::vector<int> vec3 = {1, 2, 3, 4, 5}; // 使用初始化列表// 添加元素vec1.push_back(42); // 在尾部添加元素// 访问元素std::cout << "第一个元素: " << vec3[0] << std::endl; // 使用[]操作符std::cout << "第二个元素: " << vec3.at(1) << std::endl; // 使用at()方法(带边界检查)std::cout << "最后一个元素: " << vec3.back() << std::endl;// 遍历std::cout << "vec3的所有元素: ";for (const auto& element : vec3) {std::cout << element << " ";}std::cout << std::endl;// 大小和容量std::cout << "大小: " << vec3.size() << std::endl;std::cout << "容量: " << vec3.capacity() << std::endl;// 在中间插入元素vec3.insert(vec3.begin() + 2, 10); // 在第三个位置插入10// 删除元素vec3.pop_back(); // 删除最后一个元素vec3.erase(vec3.begin() + 1); // 删除第二个元素return 0;
}
性能注意事项:
- 预先调用
reserve()
可以避免频繁的内存重新分配 - 当频繁在中间或开头插入/删除元素时,考虑使用
std::list
- 避免不必要的拷贝,使用引用或移动语义
std::list
std::list
是双向链表实现的容器。
特点:
- 插入/删除任何位置 - O(1)
- 访问元素 - O(n)
- 不支持随机访问
- 内存不连续
示例代码:
#include <iostream>
#include <list>int main() {std::list<int> myList = {1, 2, 3, 4, 5};// 头尾插入myList.push_front(0); // 链表特有操作myList.push_back(6);// 遍历std::cout << "list的所有元素: ";for (const auto& element : myList) {std::cout << element << " ";}std::cout << std::endl;// 插入和删除auto it = myList.begin();std::advance(it, 3); // 迭代器移动到第4个元素myList.insert(it, 10); // 在第4个位置前插入10it = myList.begin();std::advance(it, 2);myList.erase(it); // 删除第3个元素// 元素个数std::cout << "元素个数: " << myList.size() << std::endl;return 0;
}
std::deque
std::deque
(双端队列)提供了类似vector的功能,但支持在两端高效插入和删除。
特点:
- 随机访问元素 - O(1)
- 头部和尾部插入/删除 - O(1)
- 中间插入/删除 - O(n)
- 内存不完全连续
示例代码:
#include <iostream>
#include <deque>int main() {std::deque<int> myDeque = {1, 2, 3, 4, 5};// 头尾插入myDeque.push_front(0); // 前端插入myDeque.push_back(6); // 后端插入// 随机访问std::cout << "第三个元素: " << myDeque[2] << std::endl;// 遍历std::cout << "deque的所有元素: ";for (const auto& element : myDeque) {std::cout << element << " ";}std::cout << std::endl;return 0;
}
其他顺序容器
- std::array:固定大小的数组,大小在编译期确定
- std::forward_list:单向链表,比
list
内存占用更小,但只能向前遍历
关联容器详解
关联容器将键与值关联起来,通常基于红黑树实现,保证元素有序。
std::map
std::map
是键值对容器,键唯一且有序。
特点:
- 查找、插入、删除 - O(log n)
- 自动排序(按键)
- 键唯一
示例代码:
#include <iostream>
#include <map>
#include <string>int main() {std::map<std::string, int> ages;// 插入元素ages["Alice"] = 30;ages["Bob"] = 25;ages.insert({"Charlie", 35});ages.insert(std::make_pair("David", 40));// 访问元素std::cout << "Bob的年龄: " << ages["Bob"] << std::endl;// 检查键是否存在if (ages.count("Eve") == 0) {std::cout << "Eve不在map中" << std::endl;}// at()方法访问(会进行边界检查)try {std::cout << ages.at("Charlie") << std::endl;std::cout << ages.at("Eve") << std::endl; // 抛出异常} catch (const std::out_of_range& e) {std::cout << "异常: " << e.what() << std::endl;}// 遍历std::cout << "所有人的年龄: " << std::endl;for (const auto& pair : ages) {std::cout << pair.first << ": " << pair.second << std::endl;}// 删除ages.erase("Bob");// 检查大小std::cout << "map大小: " << ages.size() << std::endl;return 0;
}
std::set
std::set
是只包含唯一键的容器,元素自动排序。
特点:
- 查找、插入、删除 - O(log n)
- 元素唯一且有序
- 元素一旦插入不可修改(只能删除再插入)
示例代码:
#include <iostream>
#include <set>int main() {std::set<int> numbers = {5, 3, 1, 4, 2};// 插入元素numbers.insert(6);auto result = numbers.insert(3); // 尝试插入已存在的元素if (!result.second) {std::cout << "3已经存在,插入失败" << std::endl;}// 遍历(会按升序输出)std::cout << "set中的元素: ";for (const auto& num : numbers) {std::cout << num << " ";}std::cout << std::endl;// 查找auto it = numbers.find(4);if (it != numbers.end()) {std::cout << "找到元素: " << *it << std::endl;}// 删除numbers.erase(3);return 0;
}
std::multimap 和 std::multiset
multimap
和multiset
允许重复键,其他特性与map
和set
相似。
示例代码:
#include <iostream>
#include <map>
#include <set>int main() {// multimap示例std::multimap<std::string, int> studentScores;studentScores.insert({"Alice", 85});studentScores.insert({"Bob", 90});studentScores.insert({"Alice", 92}); // Alice有两个分数std::cout << "学生分数:" << std::endl;for (const auto& score : studentScores) {std::cout << score.first << ": " << score.second << std::endl;}// multiset示例std::multiset<int> repeatedNumbers = {1, 3, 3, 5, 7, 7, 7};std::cout << "\nmultiset中的元素: ";for (const auto& num : repeatedNumbers) {std::cout << num << " ";}std::cout << std::endl;// 计算特定元素出现次数std::cout << "7出现的次数: " << repeatedNumbers.count(7) << std::endl;return 0;
}
无序关联容器详解
C++11引入了无序容器,它们基于哈希表实现,提供平均O(1)的查找复杂度。
std::unordered_map
特点:
- 平均查找、插入、删除 - O(1)
- 最坏情况 - O(n)
- 元素无序排列
示例代码:
#include <iostream>
#include <unordered_map>
#include <string>int main() {std::unordered_map<std::string, int> scores;// 插入元素scores["Math"] = 95;scores["English"] = 88;scores["Science"] = 92;// 访问和遍历std::cout << "所有科目分数:" << std::endl;for (const auto& subject : scores) {std::cout << subject.first << ": " << subject.second << std::endl;}// 查找if (scores.find("History") == scores.end()) {std::cout << "没有历史科目的分数" << std::endl;}// 哈希表特有操作std::cout << "桶数: " << scores.bucket_count() << std::endl;std::cout << "加载因子: " << scores.load_factor() << std::endl;return 0;
}
std::unordered_set
与std::set
类似,但基于哈希表实现,元素无序。
示例代码:
#include <iostream>
#include <unordered_set>
#include <string>int main() {std::unordered_set<std::string> animals = {"cat", "dog", "elephant"};// 插入animals.insert("tiger");// 判断存在if (animals.count("dog") > 0) {std::cout << "dog在集合中" << std::endl;}// 遍历(无序)std::cout << "所有动物: ";for (const auto& animal : animals) {std::cout << animal << " ";}std::cout << std::endl;return 0;
}
容器适配器
容器适配器提供了特殊的接口,底层使用其他容器实现。
std::stack
栈是后进先出(LIFO)容器。
示例代码:
#include <iostream>
#include <stack>int main() {std::stack<int> myStack;// 压栈myStack.push(1);myStack.push(2);myStack.push(3);// 获取顶部元素std::cout << "栈顶: " << myStack.top() << std::endl;// 弹栈myStack.pop();std::cout << "弹栈后的栈顶: " << myStack.top() << std::endl;// 检查大小和空状态std::cout << "栈大小: " << myStack.size() << std::endl;std::cout << "栈是否为空: " << (myStack.empty() ? "是" : "否") << std::endl;return 0;
}
std::queue
队列是先进先出(FIFO)容器。
示例代码:
#include <iostream>
#include <queue>int main() {std::queue<std::string> myQueue;// 入队myQueue.push("first");myQueue.push("second");myQueue.push("third");// 访问首尾元素std::cout << "队首: " << myQueue.front() << std::endl;std::cout << "队尾: " << myQueue.back() << std::endl;// 出队myQueue.pop();std::cout << "出队后的队首: " << myQueue.front() << std::endl;// 大小std::cout << "队列大小: " << myQueue.size() << std::endl;return 0;
}
std::priority_queue
优先队列是按优先级排序的队列,默认是最大堆。
示例代码:
#include <iostream>
#include <queue>
#include <vector>
#include <functional>int main() {// 默认最大堆std::priority_queue<int> maxHeap;maxHeap.push(3);maxHeap.push(1);maxHeap.push(4);maxHeap.push(2);std::cout << "最大堆弹出顺序: ";while (!maxHeap.empty()) {std::cout << maxHeap.top() << " ";maxHeap.pop();}std::cout << std::endl;// 最小堆std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;minHeap.push(3);minHeap.push(1);minHeap.push(4);minHeap.push(2);std::cout << "最小堆弹出顺序: ";while (!minHeap.empty()) {std::cout << minHeap.top() << " ";minHeap.pop();}std::cout << std::endl;return 0;
}
容器的选择指南
选择合适的容器对程序性能影响重大,下面是一些选择指南:
-
默认首选
std::vector
:- 内存连续,缓存友好
- 随机访问高效
- 尾部增删高效
-
当需要频繁在中间/头部增删时:
- 考虑
std::list
或std::forward_list
- 考虑
-
当需要在两端都高效操作时:
- 使用
std::deque
- 使用
-
需要关联查找时:
- 如果元素需要有序:
std::map
/std::set
- 如果不需要有序,优先使用:
std::unordered_map
/std::unordered_set
- 如果元素需要有序:
-
固定大小数组:
- 使用
std::array
而非C风格数组
- 使用
-
特殊数据结构需求:
- 栈:
std::stack
- 队列:
std::queue
- 优先队列:
std::priority_queue
- 栈:
容器性能比较
容器 | 随机访问 | 插入/删除(中间) | 插入/删除(两端) | 查找 | 内存开销 |
---|---|---|---|---|---|
vector | O(1) | O(n) | 尾部O(1)/头部O(n) | O(n) | 低 |
list | O(n) | O(1) | O(1) | O(n) | 高 |
deque | O(1) | O(n) | O(1) | O(n) | 中 |
set/map | O(log n) | O(log n) | O(log n) | O(log n) | 中 |
unordered_set/map | N/A | O(1)平均 | N/A | O(1)平均 | 高 |
实际应用场景
- 向量作为通用容器:
std::vector<int> data = {1, 2, 3, 4, 5};
// 快速添加元素
for (int i = 6; i <= 100; ++i) {data.push_back(i);
}
// 二分查找(要求有序)
bool found = std::binary_search(data.begin(), data.end(), 42);
- 哈希表实现快速查找:
std::unordered_map<std::string, std::string> dictionary;
// 填充词典
dictionary["apple"] = "一种水果";
dictionary["computer"] = "电子设备";// 快速查找
std::string word = "apple";
if (dictionary.find(word) != dictionary.end()) {std::cout << word << ": " << dictionary[word] << std::endl;
}
- 用set维护唯一有序元素集合:
std::set<std::string> uniqueNames;
// 添加名字,自动去重和排序
uniqueNames.insert("Zhang");
uniqueNames.insert("Wang");
uniqueNames.insert("Li");
uniqueNames.insert("Wang"); // 重复,不会插入// 按字母顺序打印所有唯一名字
for (const auto& name : uniqueNames) {std::cout << name << " ";
}
容器使用的最佳实践
-
选择合适的容器:
- 根据需求选择适当的容器,避免为了一些小优化选择更复杂的容器
-
预分配内存:
std::vector<int> vec; vec.reserve(1000); // 预分配1000个元素的空间
-
传递容器时使用引用:
void processVector(const std::vector<int>& vec) {// 避免拷贝整个容器 }
-
使用emplace代替insert:
std::vector<std::pair<int, std::string>> pairs; pairs.emplace_back(1, "one"); // 直接构造,无需创建临时对象
-
利用容器算法:
#include <algorithm> std::vector<int> vec = {5, 2, 8, 1, 3}; std::sort(vec.begin(), vec.end()); // 排序
-
注意迭代器失效:
- 增删操作可能导致迭代器失效,特别是对于vector和deque
结论
STL容器是C++程序员的强大工具,掌握它们的特性和适用场景,可以大大提高代码质量和效率。本文只是概述了各种容器的主要特性和用法,建议深入研究STL文档和相关书籍,进一步提升对容器的理解和应用能力。
在下一篇文章中,我们将探讨STL的另一个核心组件——迭代器系统。通过迭代器,我们能更灵活地操作各种容器,实现算法与容器的解耦。
参考资源
- C++ 参考手册
- 《Effective STL》 by Scott Meyers
- 《C++ 标准库》by Nicolai M. Josuttis
这是我C++学习之旅系列的第二十三篇技术文章。查看完整系列目录了解更多内容。
如有任何问题或建议,欢迎在评论区留言交流!