string 字符串 · vector 动态数组 · list 双向链表 · deque 双端队列 —— STL 工具箱里最常用的四种"装数据的箱子"。
11.0
想象一下,你有一大堆乐高积木,每次想搭一个城堡都要自己从零开始做每一块砖——这太累了!如果有人提前给你准备好了各种形状的积木块:长方形的基础块、可以转动的轮子、连接用的铰链……你只需要把它们拼起来就行了。
STL 就是 C++ 给程序员准备的"积木工具箱"。它里面装好了编程时最常用的一些数据结构:可以自动变长的数组、可以两头操作的队列、可以快速查找的字典……我们不再需要从零开始写这些复杂的东西,直接用就好。
begin() 指向第一个元素,end() 指向最后一个元素之后的位置(标记"结束了")。
好消息是,几乎所有 STL 容器都共享一套相同的操作。就像一个工具箱里的所有工具都有"拿起来"和"放下去"这两个动作一样,无论你用 vector 还是 list,下面这些操作你都能用:
| 函数 / 类型 | 功能 | 返回值 |
|---|---|---|
| .size() | 返回容器中元素的个数 | size_t(无符号整数) |
| .empty() | 判断容器是否为空 | bool(空为 true) |
| .clear() | 清空所有元素 | 无 |
| .begin() | 返回指向第一个元素的迭代器 | 迭代器 |
| .end() | 返回指向尾后(最后一个元素之后) | 迭代器 |
| .front() | 返回第一个元素的引用 | 元素的引用 |
| .back() | 返回最后一个元素的引用 | 元素的引用 |
| .swap(b) | 交换两个同类容器的全部内容 | 无(void) |
| ::iterator | 容器的迭代器类型 | 类型名 |
11.1
C++ 标准库提供了 string 类型,专门用来处理文字信息。它比 C 风格的字符数组(比如 char s[100])更安全、更灵活,是处理字符串的首选工具。使用前需引入 <string> 头文件。
| 1 | #include <iostream> |
| 2 | #include <string> // 必须包含这个头文件 |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | string s1; // 空字符串 |
| 8 | string s2 = "Hello"; // 从 C 风格字符串初始化 |
| 9 | string s3(s2); // 拷贝构造 |
| 10 | string s4(5, 'a'); // "aaaaa"——5 个字符 'a' |
| 11 | // string s5 = "Hello"s; // C++14 字面量语法,需 using namespace std::string_literals; |
| 12 | |
| 13 | return 0; |
| 14 | } |
string 默认为空字符串(长度为 0),不会像字符数组那样出现乱码。string 支持通过下标 [] 访问字符,但不能给还不存在的位置赋值。
读字符串有两种常见方式:只要一个单词用 cin >>,要一整行(包括空格)用 getline()。两者混用时有一个经典的坑,一定要小心。
| 1 | string word, line; |
| 2 | |
| 3 | // 情况一:读一个单词(遇到空格就停) |
| 4 | cin >> word; // 输入 "Hello World" → word 得到 "Hello" |
| 5 | |
| 6 | // 情况二:读一整行(包括空格) |
| 7 | getline(cin, line); // 输入 "Hello World" → line 得到 "Hello World" |
| 8 | |
| 9 | // ⚠️ 混用时的大坑! |
| 10 | int n; |
| 11 | cin >> n; // 输入 "42\n",读到 42,但缓冲区还剩一个 '\n' |
| 12 | cin.ignore(); // 🔑 把残留的换行符清掉 |
| 13 | getline(cin, line); // 现在才能正常读下一行 |
cin >> n 读取完数字后,你按下的回车键(\n)会残留在输入缓冲区里。紧接着 getline 一上来就看到了这个换行符,以为"哦,这行结束了",于是直接返回一个空字符串——看起来就像程序"跳过"了输入。cin 之后、getline 之前,加一句 cin.ignore(),把残留的换行符清掉即可。
cin >> str;getline(cin, str);cin 和 getline 时,务必用 cin.ignore() 清理换行符
🔗 拼接与比较 string 支持用 + 和 += 进行拼接,也支持直接用比较运算符进行字典序比较。
| 1 | string s = "Hello"; |
| 2 | s = s + " World"; // "Hello World" |
| 3 | s += "!"; // "Hello World!" |
| 4 | |
| 5 | // 字典序比较:从左到右逐字符比较 ASCII 码 |
| 6 | string a = "abc", b = "abd"; |
| 7 | cout << (a < b); // 1 (true),因为 'c' < 'd' |
| 8 | cout << (a == b); // 0 (false) |
"ab" < "abc")。
🔍 元素访问:[ ] vs at()
| 1 | string s = "Hello"; |
| 2 | char c1 = s[0]; // 'H',不检查越界——快速,但需自行保证安全 |
| 3 | char c2 = s.at(0); // 'H',越界时抛出异常——安全,但略有开销 |
| 4 | char c3 = s.front(); // 'H' |
| 5 | char c4 = s.back(); // 'o' |
| 方式 | 建议 |
|---|---|
| [] | 更快但不安全,越界访问是未定义行为 |
| at() | 更安全但稍慢,越界时会抛出异常提醒你 |
[],调试阶段或不确定时用 at()。✏️ 修改操作
| 1 | string s = "Hello"; |
| 2 | s.append(" World"); // "Hello World"——在末尾追加 |
| 3 | s.push_back('!'); // "Hello World!"——在末尾追加一个字符 |
| 4 | s.pop_back(); // "Hello World"——删除最后一个字符 |
| 5 | s.insert(5, " C++"); // "Hello C++ World"——在下标 5 处插入 |
| 6 | s.erase(2, 3); // 删除从下标 2 开始的 3 个字符 |
| 7 | s.replace(0, 5, "Hi"); // 把下标 0 起的 5 个字符替换为 "Hi" |
| 8 | s1.swap(s2); // 交换两个字符串的全部内容(速度极快) |
🔎 查找操作
| 1 | string s = "Hello World"; |
| 2 | size_t pos = s.find("World"); // 查找 "World",返回首次出现位置(这里是 6) |
| 3 | size_t pos2 = s.rfind("l"); // 从后往前找 'l',返回最后一次出现的位置 |
| 4 | size_t pos3 = s.find_first_of("aeiou"); // 查找元音字母第一次出现的位置 |
| 5 | |
| 6 | if (s.find("x") == string::npos) { // npos 表示"没找到" |
| 7 | cout << "没找到'x'" << endl; |
| 8 | } |
string::npos 是什么?你可以把它理解为"无效位置"。当 find() 找不到你要的东西时,就返回 string::npos 来告诉你"抱歉,没找到"。使用时只需要记住:拿 find() 的返回值和 string::npos 比较,相等就说明没找到。✂️ 截取与转换
| 1 | string s = "Hello World"; |
| 2 | string sub = s.substr(0, 5); // "Hello"——从下标 0 开始,截取 5 个字符 |
| 3 | |
| 4 | // 数字 ↔ 字符串互转(C++11 起支持) |
| 5 | string numStr = to_string(42); // 数字 → 字符串:"42" |
| 6 | int n = stoi("123"); // 字符串 → 整数:123 |
| 7 | double d = stod("3.14159"); // 字符串 → 小数:3.14159 |
🔄 迭代器遍历
把 string 想象成一排连续的格子,每个格子里放一个字符。迭代器就像夹在格子之间的"书签"——它记住了你当前所在的位置。你可以用 *it 读取当前格子的字符,用 ++it 把书签往后移一格。
| 1 | string s = "Hi"; |
| 2 | // begin() 指向第一个字符,end() 指向尾后位置 |
| 3 | for (auto it = s.begin(); it != s.end(); ++it) |
| 4 | { |
| 5 | cout << *it << " "; // 输出:H i |
| 6 | } |
| 7 | |
| 8 | // C++11 范围 for 循环(底层也是用迭代器) |
| 9 | for (char c : s) |
| 10 | { |
| 11 | cout << c; // 输出:Hi |
| 12 | } |
s.begin() 返回指向第一个字符的迭代器,s.end() 返回指向最后一个字符后面的迭代器——它不指向任何字符,只表示"已经遍历完了"。11.2
vector 是 STL 中最常用的容器,可以把它理解成一个能自动伸缩的数组。当你需要一个可以随时增减元素、且能用下标快速访问任意位置的序列时,vector 是第一选择。使用前需引入 <vector> 头文件。
| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | vector<int> v1; // 空数组,里面什么都没有 |
| 8 | vector<int> v2(5); // 5 个元素,全部自动初始化为 0 |
| 9 | vector<int> v3(5, 7); // 5 个元素,全部初始化为 7 |
| 10 | vector<int> v4 = {1, 2, 3, 4, 5}; // 直接用花括号列出元素(最直观的写法) |
| 11 | vector<int> v5(v4); // 拷贝构造——复制一个一模一样的数组 |
| 12 | |
| 13 | // 二维数组:3 行 4 列,全部初始化为 0 |
| 14 | vector<vector<int>> matrix(3, vector<int>(4, 0)); |
| 15 | return 0; |
| 16 | } |
vector<int> v(n) 创建的 n 个元素会被自动初始化为 0,不会像局部数组那样留下"脏数据"。vector<int> v(n, val) 创建的 n 个元素会被初始化为指定的 val。{...} 是 C++11 引入的语法,简洁直观,优先使用。
📏 容量管理
| 1 | vector<int> v = {1, 2, 3}; |
| 2 | v.size(); // 3——当前有多少个元素 |
| 3 | v.capacity(); // 已分配的内存最多能装多少个(通常 ≥ size) |
| 4 | v.empty(); // false——判断是否为空 |
| 5 | v.clear(); // 清空所有元素,size 变成 0 |
| 6 | v.resize(5); // 把大小调整为 5,多出来的位置填 0 |
| 7 | v.resize(10, -1); // 把大小调整为 10,多出来的位置填 -1 |
| 8 | v.reserve(1000); // 提前预留 1000 个位置的空间 |
| 9 | v.shrink_to_fit(); // 把多余的内存释放掉 |
vector 扩容时,需要重新找一块更大的内存,然后把旧数据全部搬过去——这很花时间。如果你预先知道大概要存 1000 个元素,提前调用 reserve(1000) 就可以避免反复搬家,显著提升性能。这是竞赛中常见的优化手段。
clear() 只把元素清掉,让 size() 变成 0,但已经占用的内存不会释放。如果想真正释放内存,可以用 vector<int>().swap(v) 这个小技巧。
🔍 元素访问
| 1 | vector<int> v = {10, 20, 30}; |
| 2 | int x = v[0]; // 10——用下标访问,不检查越界 |
| 3 | int y = v.at(0); // 10——用 at 访问,越界时会报错 |
| 4 | int first = v.front(); // 10——第一个元素 |
| 5 | int last = v.back(); // 30——最后一个元素 |
| 6 | int* p = v.data(); // 获取底层数组的原始指针(需要调用 C 函数时有用) |
memcpy、scanf),这时可以用 v.data() 把 vector 当成普通数组来用。vector 在尾部增删元素是 O(1),非常快。但在头部或中间插入/删除是 O(n),因为需要把后面的元素全部挪位置。如果需要频繁在中间操作,应该考虑用 list。✏️ 增删修改
| 1 | vector<int> v = {1, 2, 3}; |
| 2 | v.push_back(4); // 在末尾追加:{1, 2, 3, 4} |
| 3 | v.pop_back(); // 删除最后一个:{1, 2, 3} |
| 4 | v.insert(v.begin() + 1, 99); // 在下标 1 处插入 99:{1, 99, 2, 3} |
| 5 | v.erase(v.begin() + 1); // 删除下标 1 的元素:{1, 2, 3} |
| 6 | v1.swap(v2); // 交换两个 vector 的全部内容(O(1),极快) |
| 7 | v.emplace_back(10, 'a'); // 在末尾原地构造元素(比 push_back 更高效) |
push_back 是先造好一个临时对象再拷贝进去,emplace_back 是直接在末尾原地构造,省去了拷贝这一步。对于简单的 int 差别不大,但对于复杂的类型(比如 pair、结构体),emplace_back 更高效。| 1 | // ❌ 错误写法——erase 之后迭代器已经"过期"了 |
| 2 | for (auto it = v.begin(); it != v.end(); ++it) |
| 3 | { |
| 4 | if (*it == target) v.erase(it); // 危险!it 已失效,++it 会出错 |
| 5 | } |
| 6 | |
| 7 | // ✅ 正确写法——利用 erase 返回的新迭代器 |
| 8 | for (auto it = v.begin(); it != v.end(); ) |
| 9 | { |
| 10 | if (*it == target) |
| 11 | it = v.erase(it); // erase 返回被删除元素的下一个位置 |
| 12 | else |
| 13 | ++it; |
| 14 | } |
++ 是未定义行为,轻则结果错误,重则程序崩溃。记住口诀:erase 返回新位置,用它接着往下走,不要再手动 ++it。| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | vector<int> v = {1, 2, 3, 4, 5}; |
| 8 | |
| 9 | // 遍历方式一:下标 |
| 10 | for (size_t i = 0; i < v.size(); i++) { |
| 11 | cout << v[i] << " "; |
| 12 | } |
| 13 | cout << endl; |
| 14 | |
| 15 | // 遍历方式二:范围 for(最简洁) |
| 16 | for (int x : v) { |
| 17 | cout << x << " "; |
| 18 | } |
| 19 | cout << endl; |
| 20 | |
| 21 | // 删除偶数 |
| 22 | for (auto it = v.begin(); it != v.end(); ) { |
| 23 | if (*it % 2 == 0) |
| 24 | it = v.erase(it); |
| 25 | else |
| 26 | ++it; |
| 27 | } |
| 28 | // v 变成 {1, 3, 5} |
| 29 | |
| 30 | cout << "size: " << v.size() << endl; // 输出:3 |
| 31 | |
| 32 | return 0; |
| 33 | } |
11.3
list 是一个双向链表。链表不像数组那样所有元素挤在一块连续的内存里,而是每个元素各住各的,彼此用"指针"串起来。这带来的好处是:在任意位置插入或删除元素都是 O(1),快到飞起。代价是:不能用下标直接跳到第 n 个元素,只能顺着链条一个一个找。使用前需引入 <list> 头文件。
list 不支持随机访问,不能像 vector 那样用 l[0] 或 l.at(0) 访问元素,必须通过迭代器遍历。list 支持 O(1) 的头部插入/删除(push_front / pop_front),这是 vector 不具备的。
| 1 | #include <iostream> |
| 2 | #include <list> // list 位于此头文件中 |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | list<int> l1; // 空链表 |
| 8 | list<int> l2(5); // 5 个元素,全部初始化为 0 |
| 9 | list<int> l3(5, 7); // 5 个元素,全部初始化为 7 |
| 10 | list<int> l4 = {1, 2, 3, 4, 5}; // 用花括号初始化 |
| 11 | list<int> l5(l4); // 拷贝构造 |
| 12 | return 0; |
| 13 | } |
🔍 元素访问
| 1 | list<int> l = {10, 20, 30}; |
| 2 | int first = l.front(); // 10——第一个元素 |
| 3 | int last = l.back(); // 30——最后一个元素 |
| 4 | // ❌ l[0] —— 编译错误!list 不支持下标访问 |
✏️ 增删修改
| 1 | list<int> l = {2, 3, 4}; |
| 2 | l.push_front(1); // {1, 2, 3, 4}——头部插入 |
| 3 | l.push_back(5); // {1, 2, 3, 4, 5}——尾部插入 |
| 4 | l.pop_front(); // {2, 3, 4, 5}——头部删除 |
| 5 | l.pop_back(); // {2, 3, 4}——尾部删除 |
| 6 | |
| 7 | auto it = l.begin(); |
| 8 | ++it; // it 指向第二个元素(3) |
| 9 | l.insert(it, 99); // 在 it 前面插入 99:{2, 99, 3, 4} |
| 10 | l.erase(it); // 删除 it 指向的元素:{2, 99, 4} |
| 11 | l1.swap(l2); // 交换两个链表的内容 |
🔄 迭代器遍历
| 1 | list<int> l = {1, 2, 3}; |
| 2 | // 用迭代器遍历(list 的迭代器只支持 ++ 和 --,不支持 it + 3 这种跳转) |
| 3 | for (auto it = l.begin(); it != l.end(); ++it) |
| 4 | { |
| 5 | cout << *it << " "; // 输出:1 2 3 |
| 6 | } |
| 7 | |
| 8 | // 范围 for 更简洁 |
| 9 | for (int x : l) { |
| 10 | cout << x << " "; // 输出:1 2 3 |
| 11 | } |
| 1 | #include <iostream> |
| 2 | #include <list> |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | list<int> l; |
| 8 | |
| 9 | // 在头部和尾部随意插入 |
| 10 | l.push_back(2); |
| 11 | l.push_front(1); |
| 12 | l.push_back(3); |
| 13 | // l = {1, 2, 3} |
| 14 | |
| 15 | cout << "链表内容: "; |
| 16 | for (int x : l) { |
| 17 | cout << x << " "; // 输出:1 2 3 |
| 18 | } |
| 19 | cout << endl; |
| 20 | |
| 21 | // 删除中间的偶数 |
| 22 | for (auto it = l.begin(); it != l.end(); ) { |
| 23 | if (*it % 2 == 0) |
| 24 | it = l.erase(it); // list 的 erase 也返回下一个有效迭代器 |
| 25 | else |
| 26 | ++it; |
| 27 | } |
| 28 | // l = {1, 3} |
| 29 | |
| 30 | cout << "首元素: " << l.front() << endl; // 输出:1 |
| 31 | cout << "尾元素: " << l.back() << endl; // 输出:3 |
| 32 | cout << "大小: " << l.size() << endl; // 输出:2 |
| 33 | |
| 34 | return 0; |
| 35 | } |
11.4
deque 是 C++ 标准库中的双端队列,全称 Double-Ended Queue。是 vector 和 list 的"混血儿":它既支持下标随机访问,又支持在头部和尾部快速增删。底层由多段连续空间拼接而成,所以头部操作也是 O(1)。使用前需引入 <deque> 头文件。
deque 支持 O(1) 的头部和尾部插入/删除,弥补了 vector 头部操作慢的缺点。deque 支持 operator[] 和 at() 随机访问,弥补了 list 不能随机访问的缺点。deque 的底层是分段连续空间,不是一整块连续内存,因此某些需要连续内存指针的场景(如传给 C 风格接口)不适合用 deque。
| 1 | #include <iostream> |
| 2 | #include <deque> // 必须包含这个头文件 |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | deque<int> dq1; // 空双端队列 |
| 8 | deque<int> dq2(5); // 5 个元素,全部初始化为 0 |
| 9 | deque<int> dq3(5, 7); // 5 个元素,全部初始化为 7 |
| 10 | deque<int> dq4 = {1, 2, 3, 4, 5}; // 初始化列表 |
| 11 | deque<int> dq5(dq4); // 拷贝构造 |
| 12 | return 0; |
| 13 | } |
deque 拥有 vector 的全部功能([]、at()、size()、empty() 等),同时额外支持头部操作:
| 1 | deque<int> dq = {2, 3, 4}; |
| 2 | // 头部操作(vector 没有的!) |
| 3 | dq.push_front(1); // {1, 2, 3, 4} |
| 4 | dq.pop_front(); // {2, 3, 4} |
| 5 | // 尾部操作(和 vector 一样) |
| 6 | dq.push_back(5); // {2, 3, 4, 5} |
| 7 | dq.pop_back(); // {2, 3, 4} |
| 8 | // 随机访问(list 没有的!) |
| 9 | cout << dq[1] << endl; // 输出:3——支持下标访问 |
| 10 | cout << dq.at(0) << endl; // 输出:2——支持 at 访问 |
deque 底层是分段连续空间,不是一整块连续内存,所以没有 data() 函数。如果需要把数据传给要求连续内存的 C 函数,不能用 deque,请用 vector。
| 1 | #include <iostream> |
| 2 | #include <deque> |
| 3 | using namespace std; |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | deque<int> dq; |
| 8 | |
| 9 | // 头部和尾部都能插入 |
| 10 | dq.push_back(2); // {2} |
| 11 | dq.push_front(1); // {1, 2} |
| 12 | dq.push_back(3); // {1, 2, 3} |
| 13 | dq.push_front(0); // {0, 1, 2, 3} |
| 14 | |
| 15 | // 随机访问 |
| 16 | cout << "第2个元素: " << dq[1] << endl; // 输出:1 |
| 17 | cout << "首元素: " << dq.front() << endl; // 输出:0 |
| 18 | cout << "尾元素: " << dq.back() << endl; // 输出:3 |
| 19 | |
| 20 | // 遍历 |
| 21 | cout << "遍历: "; |
| 22 | for (int x : dq) { |
| 23 | cout << x << " "; // 输出:0 1 2 3 |
| 24 | } |
| 25 | cout << endl; |
| 26 | |
| 27 | // 头部和尾部删除 |
| 28 | dq.pop_front(); // {1, 2, 3} |
| 29 | dq.pop_back(); // {1, 2} |
| 30 | |
| 31 | cout << "大小: " << dq.size() << endl; // 输出:2 |
| 32 | |
| 33 | return 0; |
| 34 | } |
| 容器 | 随机访问 | 头部增删 | 尾部增删 | 中间增删 | 典型场景 |
|---|---|---|---|---|---|
| vector | O(1) | O(n) | O(1) | O(n) | 大多数场景的默认选择 |
| list | 不支持 | O(1) | O(1) | O(1) | 频繁在中间插入/删除 |
| deque | O(1) | O(1) | O(1) | O(n) | 两端都要快速操作(如滑动窗口) |
| string | O(1) | O(n) | O(1) | O(n) | 文本处理(本质是特化的 vector<char>) |