当前位置: 首页 > news >正文

C++学习:六个月从基础到就业——C++学习之旅:STL容器详解

C++学习:六个月从基础到就业——C++学习之旅:STL容器详解

本文是我C++学习之旅系列的第二十三篇技术文章,也是第二阶段"C++进阶特性"的第一篇,主要介绍C++ STL容器。查看完整系列目录了解更多内容。

引言

在前面的文章中,我们探讨了C++的内存管理机制,这为我们理解STL的底层实现打下了基础。今天,我们将深入探讨C++ STL(标准模板库)的核心组件之一——容器。STL容器是C++程序员的瑞士军刀,掌握它们能极大提高我们的编程效率和代码质量。

什么是STL容器?

STL容器是存储数据集合的类模板,它们遵循特定的设计模式,提供标准化的接口,并且拥有各自的性能特点。容器是STL三大核心组件(容器、算法、迭代器)中最为基础的部分。

容器的分类

STL容器主要分为三大类:

  1. 顺序容器:按线性顺序存储元素
  2. 关联容器:按特定顺序存储元素,便于快速查找
  3. 无序关联容器:使用哈希表实现的关联容器
  4. 容器适配器:基于其他容器实现的特殊接口容器

顺序容器详解

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

multimapmultiset允许重复键,其他特性与mapset相似。

示例代码

#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;
}

容器的选择指南

选择合适的容器对程序性能影响重大,下面是一些选择指南:

  1. 默认首选 std::vector

    • 内存连续,缓存友好
    • 随机访问高效
    • 尾部增删高效
  2. 当需要频繁在中间/头部增删时

    • 考虑 std::liststd::forward_list
  3. 当需要在两端都高效操作时

    • 使用 std::deque
  4. 需要关联查找时

    • 如果元素需要有序:std::map/std::set
    • 如果不需要有序,优先使用:std::unordered_map/std::unordered_set
  5. 固定大小数组

    • 使用 std::array 而非C风格数组
  6. 特殊数据结构需求

    • 栈:std::stack
    • 队列:std::queue
    • 优先队列:std::priority_queue

容器性能比较

容器随机访问插入/删除(中间)插入/删除(两端)查找内存开销
vectorO(1)O(n)尾部O(1)/头部O(n)O(n)
listO(n)O(1)O(1)O(n)
dequeO(1)O(n)O(1)O(n)
set/mapO(log n)O(log n)O(log n)O(log n)
unordered_set/mapN/AO(1)平均N/AO(1)平均

实际应用场景

  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);
  1. 哈希表实现快速查找
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;
}
  1. 用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 << " ";
}

容器使用的最佳实践

  1. 选择合适的容器

    • 根据需求选择适当的容器,避免为了一些小优化选择更复杂的容器
  2. 预分配内存

    std::vector<int> vec;
    vec.reserve(1000);  // 预分配1000个元素的空间
    
  3. 传递容器时使用引用

    void processVector(const std::vector<int>& vec) {// 避免拷贝整个容器
    }
    
  4. 使用emplace代替insert

    std::vector<std::pair<int, std::string>> pairs;
    pairs.emplace_back(1, "one");  // 直接构造,无需创建临时对象
    
  5. 利用容器算法

    #include <algorithm>
    std::vector<int> vec = {5, 2, 8, 1, 3};
    std::sort(vec.begin(), vec.end());  // 排序
    
  6. 注意迭代器失效

    • 增删操作可能导致迭代器失效,特别是对于vector和deque

结论

STL容器是C++程序员的强大工具,掌握它们的特性和适用场景,可以大大提高代码质量和效率。本文只是概述了各种容器的主要特性和用法,建议深入研究STL文档和相关书籍,进一步提升对容器的理解和应用能力。

在下一篇文章中,我们将探讨STL的另一个核心组件——迭代器系统。通过迭代器,我们能更灵活地操作各种容器,实现算法与容器的解耦。

参考资源

  • C++ 参考手册
  • 《Effective STL》 by Scott Meyers
  • 《C++ 标准库》by Nicolai M. Josuttis

这是我C++学习之旅系列的第二十三篇技术文章。查看完整系列目录了解更多内容。

如有任何问题或建议,欢迎在评论区留言交流!

相关文章:

  • [特殊字符]【Qt自定义控件】创意开关按钮 - 丝滑动画+自定义样式+信号交互 | 附完整源码
  • OpenLDAP 管理 ELK 用户
  • PHP通讯录网站源码无需sql数据库
  • 【中级软件设计师】程序设计语言基础成分
  • 从零开始创建MCP Server实战指南
  • STM32外部中断与外设中断区别
  • Element Plus表格组件深度解析:构建高性能企业级数据视图
  • Vue2-指令语法
  • C++静态与动态联编区别解析
  • Windows安装Hadoop(图文解说版)
  • 【华为HCIP | 华为数通工程师】821—多选解析—第十二页
  • Spring中配置 Bean 的两种方式:XML 配置 和 Java 配置类
  • NDSS 2025|侧信道与可信计算攻击技术导读(二)系统化评估新旧缓存侧信道攻击技术
  • Mininet--node.py源码解析
  • ViViT: 一种视频视觉Transformer
  • Cline 之Plan和Act模式
  • [大模型]AI Agent入门01——AI Agent概念梳理
  • Ollama 实战手册
  • 植被参数遥感反演技术革命!AI+Python支持向量机/随机森林/神经网络/CNN/LSTM/迁移学习在植被参数反演中的实战应用与优化
  • spark jar依赖顺序
  • 上海4-6月文博美展、剧目演出不断,将开设直播推出文旅优惠套餐
  • 文旅部:今年一季度国内出游人次17.94亿,同比增长26.4%
  • 2025年一季度上海市国民经济运行情况
  • 全国人大常委会启动工会法执法检查
  • 国际金价冲上3500美元,本月已涨超12%!分析人士提醒:警惕短期多头获利了结
  • 新童谣童诗征稿活动在沪开启:设三个创作主题,面向全国征集