C++学习记录(1):常用 STL 汇总

2026-07-20 / 约 10076 字 / 预计阅读 21 分钟 { 教程, 笔记 } [ C++, STL ]

前言

最近因为刷题和实际业务的开发,在重新整理 C++ 的笔记。

发现 STL 这东西平时一直在用,但很多时候只是依赖 AI 或者自动补全,没有认真了解过他们的特性。

比如 vector 为什么默认这么常用,mapunordered_map 到底该怎么选,list 又为什么看起来很强但实际出场率不高,这些东西如果只是记住几个接口,过一阵子还是会混。

所以这篇文章干脆把我目前最常碰到的一批 STL 类型先汇总一下。本文不追求把每个类的所有接口都列完,而是重点整理:

顺序上我没有按字母排,而是按“通常上手时比较容易理解的,和我觉得更常先学到的”来排。

string

介绍与使用场景

std::string 用来保存和处理字符串。严格来说它是 std::basic_string<char> 的常用别名,但平时基本直接把它当作字符串类型来用就行。

初始化方式

#include <string>
using namespace std;

string text;
string text1 = "hello";
string text2("hello");
string text3(5, 'a');
string text4(text1);
string text5(text1, 1, 3);
string text6(text1.begin(), text1.begin() + 3);

字符和字符串别写混:

char ch = 'a';
string text = "abc";

常用方法

设原字符串长度为 n,参与操作的新内容长度为 m

示例

拼接一个简单的问候语:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string name = "小明";
    string message = "你好,";

    message += name;
    message.push_back('!');

    //  输出 "你好,小明"
    cout << message << '\n';
}

截取文件扩展名:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string filename = "notes.md";
    string::size_type pos = filename.rfind('.');

    if (pos != string::npos) {
        string ext = filename.substr(pos + 1);
        cout << "扩展名:" << ext << '\n';
    }
}

substr() 很方便,但它会生成一个新字符串。

如果只是临时看一段内容,而且特别在意性能,后面可以再去了解 string_view

pair

介绍与使用场景

pair 用来把两个值组合成一个整体。它不是完整容器,更像一个很轻量的“二元打包工具”。

初始化方式

#include <string>
#include <utility>

std::pair<int, std::string> p1;
std::pair<int, std::string> p2(1, "apple");
std::pair<int, std::string> p3{2, "banana"};
auto p4 = std::make_pair(3, std::string("orange"));

C++17 开始还可以让编译器推导模板参数:

std::pair p5{4, std::string("pear")};

常用方法

pair 就两个成员,所以很多操作都可以直接按常数时间理解:

示例

pair 保存商品名和数量:

#include <iostream>
#include <string>
#include <utility>
using namespace std;

int main() {
    pair<string, int> item{"apple", 5};

    cout << "商品:" << item.first << '\n';
    cout << "数量:" << item.second << '\n';

    item.second += 2;
    cout << "更新后的数量:" << item.second << '\n';
}

如果数据语义已经比较复杂,pair 就会开始别扭:

struct Student {
    string name;
    int age;
};

这种时候通常不如直接写结构体。

array

介绍与使用场景

array 是对固定长度数组的封装,长度必须在编译期确定。

它和 C 风格的数组一样使用连续内存,但接口更现代一些。

初始化方式

#include <array>
using namespace std;

array<int, 5> nums{};
array<int, 5> nums1{1, 2, 3, 4, 5};
array<int, 5> nums2{1, 2};
array nums3{1, 2, 3};  // C++17 起可推导

要注意,长度是类型的一部分:

array<int, 3> a{};
array<int, 5> b{};

这两个不是同一种类型。

常用方法

设数组长度为 N

示例

统计小写字母出现次数:

#include <array>
#include <iostream>
#include <string>
using namespace std;

int main() {
    string text = "banana";
    array<int, 26> counts{};

    for (char ch : text) {
        ++counts[ch - 'a'];
    }

    cout << "a 出现了 " << counts['a' - 'a'] << " 次\n";
    cout << "b 出现了 " << counts['b' - 'a'] << " 次\n";
    cout << "n 出现了 " << counts['n' - 'a'] << " 次\n";
}

这个场景里,字符种类固定就是 26 个,用 array<int, 26> 会比 vector<int>(26) 更能体现“长度不会变”。

vector

介绍与使用场景

vector 是动态数组,我觉得它应该是用得最多的 STL 容器之一。

它的元素放在连续内存里,支持下标访问,也能在运行时动态扩容。

终于不用纠结怎么实现动态数组了!你说是吧, C 语言?

初始化方式

#include <vector>
using namespace std;

vector<int> nums;
vector<int> nums1(5);
vector<int> nums2(5, 10);
vector<int> nums3{1, 2, 3, 4, 5};
vector<int> nums4(nums3);
vector<int> nums5(nums3.begin(), nums3.begin() + 3);

下面两种写法一定要分清:

vector<int> a(5);  // 5 个元素:0 0 0 0 0
vector<int> b{5};  // 1 个元素:5

常用方法

设当前元素数量为 n

示例

保存一组成绩并求总分:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> scores{80, 92, 75};

    scores.push_back(88);
    scores.push_back(95);

    int total = 0;
    for (int score : scores) {
        total += score;
    }

    cout << "人数:" << scores.size() << '\n';
    cout << "总分:" << total << '\n';
}

如果已知大概要塞很多元素,先 reserve() 往往更舒服:

vector<int> nums;
nums.reserve(1000);

for (int i = 0; i < 1000; ++i) {
    nums.push_back(i);
}

反过来,如果一直从头删元素,vector 就不太合适:

while (!nums.empty()) {
    nums.erase(nums.begin());
}

这种写法每次都可能移动大量元素,需要频繁操作两端时通常更适合 deque

deque

介绍与使用场景

deque 是双端队列,可以在头部和尾部都高效地插入、删除,同时也支持下标访问。

初始化方式

#include <deque>
using namespace std;

deque<int> nums;
deque<int> nums1(5);
deque<int> nums2(5, 10);
deque<int> nums3{1, 2, 3, 4, 5};
deque<int> nums4(nums3);
deque<int> nums5(nums3.begin(), nums3.begin() + 3);

常用方法

设当前元素数量为 n

示例

模拟一个两端都可能来人的队伍:

#include <deque>
#include <iostream>
#include <string>
using namespace std;

int main() {
    deque<string> passengers;

    passengers.push_back("小明");
    passengers.push_back("小红");
    passengers.push_front("工作人员");

    cout << "队首:" << passengers.front() << '\n';
    cout << "队尾:" << passengers.back() << '\n';

    passengers.pop_front();
    cout << "工作人员离开后,队首是:" << passengers.front() << '\n';
}

在 BFS 这类场景里,pop_front() 的优势也很明显:

deque<int> nodes;
nodes.push_back(0);

while (!nodes.empty()) {
    int current = nodes.front();
    nodes.pop_front();

    // 处理 current,并把下一批节点塞到队尾
}

如果你只是想从头删元素,又顺手用了 vector::erase(begin()),那大概率就是该换 deque 了。

queue

介绍与使用场景

queue 是队列容器适配器,遵循先进先出,也就是 FIFO。

初始化方式

#include <deque>
#include <list>
#include <queue>

std::queue<int> q1;
std::queue<int, std::list<int>> q2;

std::deque<int> data{10, 20, 30};
std::queue<int> q3(data);

它不能直接像顺序容器那样写成初始化列表形式。

常用方法

以下复杂度按默认底层容器 deque 来理解:

如果你改了底层容器,复杂度要跟着底层容器的对应操作一起看。

示例

按进入顺序处理任务:

#include <iostream>
#include <queue>
#include <string>
using namespace std;

int main() {
    queue<string> tasks;

    tasks.push("读取配置");
    tasks.push("连接服务器");
    tasks.push("加载数据");

    while (!tasks.empty()) {
        cout << "正在处理:" << tasks.front() << '\n';
        tasks.pop();
    }
}

queue 的优势是语义非常直接,但限制也明显:它不能随机访问中间元素。要是你还想按下标看内容,那就该考虑 dequevector

stack

介绍与使用场景

stack 是栈容器适配器,遵循后进先出,也就是 LIFO。

初始化方式

#include <deque>
#include <stack>
#include <vector>

std::stack<int> s1;
std::stack<int, std::vector<int>> s2;

std::deque<int> data{1, 2, 3};
std::stack<int> s3(data);

queue 一样,它也不支持直接写初始化列表。

常用方法

以下复杂度按默认底层容器 deque 来理解:

如果换了底层容器,复杂度还是要跟着底层容器走。

示例

用栈判断括号是否匹配:

#include <iostream>
#include <stack>
#include <string>
using namespace std;

bool isValid(const string& str) {
    stack<char> brackets;

    for (char ch : str) {
        if (ch == '(' || ch == '[' || ch == '{') {
            brackets.push(ch);
            continue;
        }

        if (ch != ')' && ch != ']' && ch != '}') {
            continue;
        }

        if (brackets.empty()) {
            return false;
        }

        char left = brackets.top();
        brackets.pop();

        if ((ch == ')' && left != '(') ||
            (ch == ']' && left != '[') ||
            (ch == '}' && left != '{')) {
            return false;
        }
    }

    return brackets.empty();
}

int main() {
    cout << boolalpha;
    cout << isValid("{[()]}") << '\n';
    cout << isValid("{[(])}") << '\n';
}

这里最关键的地方就是:遇到右括号时,要优先处理最近那个还没配对的左括号,这正好就是栈擅长的顺序。

map

介绍与使用场景

map 用来保存键值对,键不能重复,并且会自动按键排序。它通常可以理解成一棵平衡搜索树。

初始化方式

#include <map>
#include <string>
#include <utility>
#include <vector>

std::map<std::string, int> m1;
std::map<std::string, int> m2{
    {"apple", 3},
    {"banana", 5}
};

std::vector<std::pair<std::string, int>> items{
    {"pen", 2},
    {"book", 4}
};
std::map<std::string, int> m3(items.begin(), items.end());

也可以指定比较规则:

#include <functional>
#include <map>

std::map<int, int, std::greater<int>> m{
    {1, 10},
    {3, 30},
    {2, 20}
};

常用方法

设容器里有 n 组键值对:

map 里负责排序的是键,所以键本身不能直接改;值是可以改的。

示例

统计单词出现次数:

#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;

int main() {
    vector<string> words{
        "apple", "banana", "apple", "orange", "banana", "apple"
    };

    map<string, int> counts;
    for (const string& word : words) {
        ++counts[word];
    }

    for (const auto& [word, count] : counts) {
        cout << word << ": " << count << '\n';
    }
}

这个写法很顺手,但也要记得:operator[] 会在键不存在时自动插入。如果你只是想查一下有没有,不想顺手把键塞进去,更适合用 find()contains()at()

set

介绍与使用场景

set 用来保存不重复元素,并且会自动排序。可以把它看成“只有键、没有值的有序集合”。

初始化方式

#include <set>
#include <vector>

std::set<int> s1;
std::set<int> s2{3, 1, 2, 2};

std::vector<int> nums{4, 2, 5, 2};
std::set<int> s3(nums.begin(), nums.end());

指定降序比较:

#include <functional>
#include <set>

std::set<int, std::greater<int>> s{1, 3, 2};

常用方法

设集合里有 n 个元素:

需要注意,set 里的元素本身就是排序关键字,所以不能直接改值。真要改,只能删了再插。

示例

先去重,再按从小到大输出:

#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main() {
    vector<int> nums{4, 2, 4, 1, 3, 2};
    set<int> uniqueNums(nums.begin(), nums.end());

    for (int num : uniqueNums) {
        cout << num << ' ';
    }
}

查找第一个不小于目标值的元素:

#include <iostream>
#include <set>
using namespace std;

int main() {
    set<int> scores{60, 70, 85, 90};

    auto it = scores.lower_bound(80);
    if (it != scores.end()) {
        cout << *it << '\n';
    }
}

unordered_map

介绍与使用场景

unordered_map 用哈希表保存键值对,不按键排序,遍历顺序也不该依赖。

初始化方式

#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

std::unordered_map<std::string, int> m1;
std::unordered_map<std::string, int> m2{
    {"apple", 3},
    {"banana", 5}
};

std::vector<std::pair<std::string, int>> items{
    {"pen", 2},
    {"book", 4}
};
std::unordered_map<std::string, int> m3(items.begin(), items.end());

如果键是自定义类型,还得自己提供相等比较和哈希函数。

常用方法

设容器里有 n 组键值对:

哈希容器最要命的一点就是:平均很快,但别把“平均 O(1)”误记成“永远 O(1)”。

示例

经典的“两数之和”:

#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;

int main() {
    vector<int> nums{2, 7, 11, 15};
    int target = 9;
    unordered_map<int, int> positions;

    for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
        int needed = target - nums[i];
        auto it = positions.find(needed);

        if (it != positions.end()) {
            cout << "下标:" << it->second << " 和 " << i << '\n';
            break;
        }

        positions[nums[i]] = i;
    }
}

这个例子不需要有序性,只需要快速查有没有,因此 unordered_map 就很顺手。

unordered_set

介绍与使用场景

unordered_set 是哈希版的集合,用来保存不重复元素,但不保证有序。

初始化方式

#include <unordered_set>
#include <vector>

std::unordered_set<int> s1;
std::unordered_set<int> s2{3, 1, 2, 2};

std::vector<int> nums{4, 2, 5, 2};
std::unordered_set<int> s3(nums.begin(), nums.end());

自定义类型做键时,也一样需要自己准备比较和哈希逻辑。

常用方法

设集合里有 n 个元素:

unordered_map 一样,它的“快”也是平均意义上的快,不是最坏情况保证。

示例

判断数组里有没有重复元素:

#include <iostream>
#include <unordered_set>
#include <vector>
using namespace std;

int main() {
    vector<int> nums{1, 4, 2, 3, 4};
    unordered_set<int> seen;

    for (int num : nums) {
        if (seen.find(num) != seen.end()) {
            cout << "发现重复元素:" << num << '\n';
            break;
        }

        seen.insert(num);
    }
}

这个问题只关心“有没有出现过”,完全不关心顺序,所以 unordered_set 很合适。

tuple

介绍与使用场景

tuple 可以把多个不同类型的值打包在一起。你可以把它理解成 pair 的“多元素版本”。

初始化方式

#include <string>
#include <tuple>
using namespace std;

tuple<int, string, double> a{1, "Alice", 95.5};
tuple<int, string, double> b = make_tuple(2, "Bob", 88.0);

int id = 3;
string name = "Carol";
auto c = tie(id, name);

结构化绑定也很常用:

auto [userId, userName, score] = a;

如果想直接引用原对象,可以写引用绑定:

auto& [refId, refName, refScore] = a;

常用方法

示例

函数一次返回多个结果:

#include <iostream>
#include <string>
#include <tuple>
using namespace std;

tuple<int, string> findUser() {
    return {1001, "Alice"};
}

int main() {
    auto [id, name] = findUser();
    cout << id << ' ' << name << '\n';
}

保存一个复合状态:

#include <iostream>
#include <tuple>
using namespace std;

int main() {
    tuple<int, int, int> state{5, 2, 3};
    auto [distance, row, column] = state;

    cout << "距离:" << distance
         << ",位置:(" << row << ", " << column << ")\n";
}

list

介绍与使用场景

list 是双向链表。它最大的特点不是“快”,而是“已知位置时,插删节点很稳”,并且不会像 vector 那样因为搬移元素把整段数据挪来挪去。

初始化方式

#include <list>
using namespace std;

list<int> nums;
list<int> nums1(5);
list<int> nums2(5, 10);
list<int> nums3{1, 2, 3, 4, 5};
list<int> nums4(nums3);
list<int> nums5(nums3.begin(), nums3.end());

常用方法

设当前元素数量为 n

这里最容易记错的一点就是:list 的插删是 O(1),前提是你已经拿到了正确位置的迭代器。如果你还得先遍历半天找到那个位置,整体就不便宜了。

示例

已知位置时在中间插入:

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> nums{10, 20, 30};

    auto pos = nums.begin();
    ++pos;

    nums.insert(pos, 15);

    for (int value : nums) {
        cout << value << ' ';
    }
}

输出会是:

10 15 20 30

但如果你每次都得先一路走过去找到位置:

auto pos = nums.begin();

for (int i = 0; i < 100; ++i) {
    ++pos;
}

那前面的查找本身就已经是 O(n) 了。所以大多数普通场景下,我反而会先考虑 vectordeque,而不是急着上 list

总结

这一篇博客我记录了一下我刷 hot 100 中经常遇到的一些 STL 类。

我感觉其实最核心的不是把接口一个个硬背下来,而是先分清楚几个问题:

如果只是给自己一个很粗的经验版结论,我目前会先这样记:

后续的话我大概会补充一下关于 priority_queuemultisetmultimap 之类的。

至于迭代器、算法库之类的我应该会单独再开一篇博客编写。那部分和这些容器放在一起看,感觉会更完整一些。


文章作者:成元
上次更新:2026-07-22