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

#新星杯·14天创作挑战营·第10期#

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;                  // 空vector
    std::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个位置前插入10
    
    it = 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++学习之旅系列的第二十三篇技术文章。查看完整系列目录了解更多内容。

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

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superior tigre

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值