STL 容器简介
STL 容器简介
本文系统介绍 C++ STL 标准库中的各类容器,涵盖序列容器、关联容器、无序容器、容器适配器、String、文件读写及仿函数适配器等内容,配以丰富的代码示例,帮助开发者深入理解并正确选择合适的容器。
目录
一、序列容器
序列容器以线性方式存储元素,元素的位置取决于插入时机和位置,而非元素的值。STL 提供了四种序列容器:vector、list、deque 和 array。
1.1 vector 动态数组
基本概念
vector 的数据安排和操作方式与数组非常相似,两者的唯一区别在于空间运用的灵活性:
- array 是静态空间,一旦配置就无法改变。如果需要调整大小,必须由程序员手动完成:配置新空间、拷贝数据、释放旧空间。
- vector 是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。
这种特性使得 vector 对内存的合理运用具有很大帮助,再也不必担心空间不足而一开始就申请大块内存。
数据结构
vector 采用线性连续空间,通过三个迭代器进行管理:
_Myfirst:指向配置得来的连续空间起始位置_Mylast:指向目前已使用空间的末尾_Myend:指向整块连续内存空间的尾端
为了降低空间配置时的速度成本,vector 实际配置的大小可能比客户需求大一些,这就是**容量(capacity)**的概念。一个 vector 的容量永远大于或等于其大小(size),一旦容量等于大小,下次新增元素时整个 vector 就得另觅居所。
⚠️ 重要提示:所谓动态增加大小,并不是在原空间之后接续新空间(无法保证原空间之后有可配置的空间),而是配置一块更大的内存空间,将原数据拷贝至新空间,并释放原空间。因此,对 vector 的任何操作,一旦引起空间重新配置,指向原 vector 的所有迭代器都会失效。
迭代器
vector 维护一个线性空间,普通指针就可以作为 vector 的迭代器,因为 vector 迭代器所需的操作行为(如 operator*、operator->、operator++、operator--、operator+、operator- 等)普通指针天生具备。vector 支持随机存取,所以提供的是随机访问迭代器。
| 迭代器类型 | 特性 |
|---|---|
| 随机访问迭代器 | 支持通过下标直接访问,如数组 |
构造方法
#include <vector>
using namespace std;
// 方式1:创建空 vector
vector<int> v1;
// 方式2:创建含有10个元素的 vector,值不确定
vector<int> v2(10);
// 方式3:创建含有10个元素的 vector,每个元素初值为1
vector<int> v3(10, 1);
// 方式4:拷贝构造
vector<int> v4(v3);
// 方式5:从另一个 vector 的区间构造
vector<int> v5(v3.begin(), v3.begin() + 3);
// 方式6:从数组构造
int arr[] = {1, 2, 3, 4, 5};
vector<int> v6(arr, arr + 5);
赋值操作
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2;
// 重载等号操作符
v2 = v1;
// assign 方法 - 替换原有所有元素
v2.assign(10, 0); // 用10个0替换原有内容
// 从区间赋值
int arr[] = {10, 20, 30};
v2.assign(arr, arr + 3);
// swap 交换
v1.swap(v2);
assign 重要特性:
- 原容量小于目标容量时,assign 会扩容
- 原容量大于目标容量时,assign 不会减小容量,只会改变 size
vector<int> vec = {1, 2, 3, 4, 5};
cout << "size: " << vec.size() << ", capacity: " << vec.capacity() << endl;
// 输出: size: 5, capacity: 5
vec.assign(10, 0);
cout << "size: " << vec.size() << ", capacity: " << vec.capacity() << endl;
// 输出: size: 10, capacity: 10
vec.assign(3, 1);
cout << "size: " << vec.size() << ", capacity: " << vec.capacity() << endl;
// 输出: size: 3, capacity: 10
大小操作
vector<int> v = {1, 2, 3, 4, 5};
// 返回元素个数
cout << v.size() << endl; // 5
// 判断是否为空
cout << v.empty() << endl; // false
// 返回容器容量
cout << v.capacity() << endl;
// 重新指定容器长度
v.resize(10); // 变长,以默认值填充
v.resize(15, 100); // 变长,以100填充
v.resize(3); // 变短,删除末尾元素
// 预留空间(不初始化,元素不可访问)
v.reserve(100);
数据存取
vector<int> v = {10, 20, 30, 40, 50};
// 通过下标访问(越界直接报错)
cout << v[0] << endl; // 10
// 通过 at 访问(越界抛出 out_of_range 异常)
cout << v.at(1) << endl; // 20
// 访问首元素
cout << v.front() << endl; // 10
// 访问尾元素
cout << v.back() << endl; // 50
插入与删除
vector<int> v;
// 尾部插入
v.push_back(1);
v.push_back(2);
v.push_back(3);
// 尾部删除
v.pop_back();
// 任意位置插入
v.insert(v.begin() + 1, 100); // 在位置1插入100
v.insert(v.begin(), 3, 0); // 在开头插入3个0
// 删除指定位置元素(返回下一个元素的迭代器)
v.erase(v.begin());
// 删除区间元素
v.erase(v.begin(), v.begin() + 2);
// 清空
v.clear();
emplace_back vs push_back
emplace_back 是 C++11 引入的方法,可以在向量末尾直接构造新元素,而不需要创建临时对象。
class Person {
public:
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
};
vector<Person> persons;
// push_back - 需要创建临时对象
persons.push_back(Person("张三", 25));
// emplace_back - 直接在容器内构造
persons.emplace_back("李四", 30);
使用建议:
- 对于已创建的对象,优先使用
push_back() - 对于未创建的对象,优先使用
emplace_back()
排序
#include <algorithm>
vector<int> v = {5, 2, 8, 1, 9};
// 默认升序排序
sort(v.begin(), v.end()); // 1, 2, 5, 8, 9
// 降序排序(使用仿函数)
sort(v.begin(), v.end(), greater<int>()); // 9, 8, 5, 2, 1
// 自定义排序规则
bool compare(int a, int b) {
return a > b; // 降序
}
sort(v.begin(), v.end(), compare);
遍历删除元素
在遍历 vector 时删除元素需要特别注意迭代器失效问题:
vector<int> v = {1, 2, 3, 4, 5, 3, 6};
// ❌ 错误写法
for (auto it = v.begin(); it != v.end(); it++) {
if (*it == 3) {
v.erase(it); // 迭代器失效!
}
}
// ✅ 正确写法
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) {
it = v.erase(it); // erase 返回下一个元素的迭代器
} else {
it++;
}
}
1.2 list 双向链表
基本概念
链表是一种物理存储单元非连续、非顺序的存储结构,数据元素的逻辑顺序通过链表中的指针链接实现。链表由一系列节点组成,每个节点包括两部分:
- 数据域:存储数据元素
- 指针域:存储下一个节点的地址
相较于 vector 的连续线性空间,list 的好处是每次插入或删除一个元素,只需配置或释放一个元素的空间,对空间的运用有绝对的精准。对于任何位置的插入或删除,list 永远是常数时间 O(1)。
迭代器
list 容器不能像 vector 一样以普通指针作为迭代器,因为其节点不能保证在同一块连续内存空间上。list 迭代器必须:
- 指向 list 节点
- 进行正确的递增、递减、取值、成员存取操作
- 递增时指向下一个节点,递减时指向上一个节点
💡 重要特性:list 的插入和删除操作不会造成原有迭代器失效。
数据结构
list 底层是一个带头节点的双向循环链表,任意位置插入和删除时间复杂度为 O(1)。
[head] <-> [node1] <-> [node2] <-> [node3] <-> [head]
构造函数
#include <list>
using namespace std;
// 空列表
list<int> l1;
// 10个值为1的元素
list<int> l2(10, 1);
// 从数组构造
int arr[] = {1, 2, 3, 4, 5};
list<int> l3(arr, arr + 5);
// 拷贝构造
list<int> l4(l3);
插入与删除
list<int> l;
// 尾部插入
l.push_back(1);
l.push_back(2);
// 头部插入
l.push_front(0);
// 尾部删除
l.pop_back();
// 头部删除
l.pop_front();
// 任意位置插入
auto it = l.begin();
l.insert(it, 100); // 在 it 位置插入
l.insert(it, 3, 50); // 在 it 位置插入3个50
// 删除指定位置
l.erase(it);
// 删除指定值的所有元素
l.remove(3); // 删除所有值为3的元素
// 条件删除
l.remove_if([](int n){ return n % 2 == 0; }); // 删除所有偶数
// 清空
l.clear();
大小操作
list<int> l = {1, 2, 3, 4, 5};
cout << l.size() << endl; // 5
cout << l.empty() << endl; // 0 (false)
l.resize(10); // 扩展到10个元素,默认值填充
l.resize(3); // 缩减到3个元素
数据存取
list<int> l = {1, 2, 3, 4, 5};
// 只能访问首尾元素
cout << l.front() << endl; // 1
cout << l.back() << endl; // 5
// 不支持随机访问
// cout << l[0] << endl; // ❌ 错误!
// cout << l.at(0) << endl; // ❌ 错误!
反转与排序
list<int> l = {3, 1, 4, 1, 5, 9, 2, 6};
// 反转链表
l.reverse(); // 6, 2, 9, 5, 1, 4, 1, 3
// 排序(成员函数)
l.sort(); // 1, 1, 2, 3, 4, 5, 6, 9
// 降序排序
l.sort(greater<int>());
// 去重(需先排序)
l.sort();
l.unique(); // 删除相邻重复元素
合并链表
list<int> l1 = {1, 3, 5};
list<int> l2 = {2, 4, 6};
// 合并两个有序链表
l1.merge(l2); // l1: 1, 2, 3, 4, 5, 6; l2 变为空
1.3 deque 双端队列
基本概念
deque(Double-Ended QUEue)是一种双端队列容器,支持在头部和尾部高效地插入和删除元素。
底层结构
deque 由一段一段的定量连续空间构成,采用中控器(map)管理:
中控器 (map)
|
v
[指针] -> [缓冲区1]
[指针] -> [缓冲区2]
[指针] -> [缓冲区3]
特点
- 双端开口,可以在头尾两端进行插入和删除
- 支持随机访问,速度略慢于 vector
- 在内部插入时,可以任意选择一端进行扩展,不需要移动所有元素
常用操作
#include <deque>
using namespace std;
deque<int> d;
// 尾部操作
d.push_back(1);
d.push_back(2);
d.pop_back();
// 头部操作
d.push_front(0);
d.pop_front();
// 随机访问
cout << d[0] << endl;
cout << d.at(1) << endl;
// 大小操作
d.size();
d.empty();
d.resize(10);
1.4 array 固定大小数组
基本概念
array 是 C++11 引入的固定大小数组容器,是对原生数组的封装。
特点
- 大小固定,无法动态扩展
- 支持随机访问
- 不会发生内存重新分配,迭代器不会失效
#include <array>
using namespace std;
array<int, 5> arr = {1, 2, 3, 4, 5};
// 访问元素
cout << arr[0] << endl;
cout << arr.at(1) << endl;
cout << arr.front() << endl;
cout << arr.back() << endl;
// 获取大小
cout << arr.size() << endl; // 固定为5
// 填充
arr.fill(0); // 所有元素置为0
二、关联容器
关联容器中的元素根据键值自动排序,查找效率高。主要包括 map、set、multimap、multiset。
2.1 map 键值对映射
基本概念
map 的所有元素都是 pair,同时拥有键值(key)和实值(value):
- pair 的第一元素为键值,起索引作用
- pair 的第二元素为实值
特性:
- 所有元素根据键值自动排序
- 不允许两个元素有相同的键值
- 底层实现为红黑树
构造函数
#include <map>
using namespace std;
// 空 map
map<int, string> m1;
// 初始化列表
map<int, string> m2 = {
{1, "张三"},
{2, "李四"},
{3, "王五"}
};
插入操作
map<int, string> m;
// 方式1:使用 pair
m.insert(pair<int, string>(1, "张三"));
// 方式2:使用 make_pair(推荐)
m.insert(make_pair(2, "李四"));
// 方式3:使用 value_type
m.insert(map<int, string>::value_type(3, "王五"));
// 方式4:使用数组方式
m[4] = "赵六";
// 方式5:使用 emplace(C++11,效率更高)
m.emplace(5, "钱七");
emplace 的优势:直接在容器中构造键值对,避免临时对象的创建,提高性能。
查找操作
map<int, string> m = {{1, "张三"}, {2, "李四"}, {3, "王五"}};
// find - 返回迭代器
auto it = m.find(2);
if (it != m.end()) {
cout << it->first << ": " << it->second << endl;
}
// count - 返回个数(map 中只能是 0 或 1)
cout << m.count(2) << endl; // 1
cout << m.count(10) << endl; // 0
// lower_bound - 返回第一个 key >= 参数的迭代器
auto it1 = m.lower_bound(2);
// upper_bound - 返回第一个 key > 参数的迭代器
auto it2 = m.upper_bound(2);
// equal_range - 返回 pair,包含 lower_bound 和 upper_bound
auto range = m.equal_range(2);
删除操作
map<int, string> m = {{1, "a"}, {2, "b"}, {3, "c"}};
// 通过迭代器删除
m.erase(m.find(1));
// 通过键值删除
m.erase(2);
// 删除区间
m.erase(m.begin(), m.end());
// 清空
m.clear();
遍历
map<int, char> m = {{1, 'a'}, {2, 'b'}, {3, 'c'}};
// 使用迭代器遍历
for (auto it = m.begin(); it != m.end(); it++) {
cout << it->first << ": " << it->second << endl;
}
// 使用范围 for 循环(C++11)
for (auto& p : m) {
cout << p.first << ": " << p.second << endl;
}
通过 key 访问 value
map<int, string> m = {{1, "张三"}, {2, "李四"}};
// 使用 []
cout << m[1] << endl; // 张三
// 注意:如果 key 不存在,会自动创建一个默认值
m[3]; // 会创建 {3, ""}
// 使用 at(不存在会抛异常)
cout << m.at(2) << endl; // 李四
2.2 set 集合
基本概念
set 是一种关联容器,用于存储同一类型的数据,具有以下特性:
- 每个元素的值都是唯一的
- 插入数据时,根据元素的值自动排序
- 查找效率高
- 元素的值不能直接修改
- 底层实现为红黑树
构造函数
#include <set>
using namespace std;
// 空 set
set<int> s1;
// 初始化列表
set<int> s2 = {3, 1, 4, 1, 5}; // 自动排序并去重:1, 3, 4, 5
// 拷贝构造
set<int> s3(s2);
// 使用自定义排序规则
set<int, greater<int>> s4; // 降序
常用操作
set<int> s;
// 插入
s.insert(5);
s.insert(3);
s.insert(1);
// 结果:1, 3, 5
// 查找
auto it = s.find(3);
if (it != s.end()) {
cout << "found: " << *it << endl;
}
// 统计个数
cout << s.count(3) << endl; // 1
// 删除
s.erase(3);
// 大小
cout << s.size() << endl;
cout << s.empty() << endl;
在 set 中插入自定义对象
在 set 中插入自定义对象,需要提供比较规则:
class Person {
public:
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
};
// 自定义排序规则(仿函数)
class PersonCompare {
public:
bool operator()(const Person& p1, const Person& p2) const {
if (p1.name < p2.name) return true;
if (p1.name == p2.name && p1.age < p2.age) return true;
return false;
}
};
void test() {
set<Person, PersonCompare> s;
s.insert(Person("张三", 25));
s.insert(Person("李四", 30));
s.insert(Person("张三", 20)); // 可以插入,年龄不同
// 查找
auto it = s.find(Person("张三", 25));
if (it != s.end()) {
cout << it->name << ": " << it->age << endl;
}
}
查找边界
set<int> s = {1, 3, 5, 7, 9};
// lower_bound - 第一个 >= key 的元素
auto it1 = s.lower_bound(5); // 指向 5
// upper_bound - 第一个 > key 的元素
auto it2 = s.upper_bound(5); // 指向 7
// equal_range - 返回 pair
auto range = s.equal_range(5);
// range.first 指向 5
// range.second 指向 7
2.3 multimap 与 multiset
区别
| 容器 | 键值唯一性 | 插入返回值 |
|---|---|---|
| set | 唯一 | pair<iterator, bool> |
| multiset | 可重复 | iterator |
| map | 唯一 | pair<iterator, bool> |
| multimap | 可重复 | iterator |
// multiset 允许重复值
multiset<int> ms;
ms.insert(1);
ms.insert(1);
ms.insert(1);
cout << ms.count(1) << endl; // 3
// multimap 允许重复键
multimap<int, string> mm;
mm.insert({1, "张三"});
mm.insert({1, "李四"});
mm.insert({1, "王五"});
// 查找所有键为1的元素
auto range = mm.equal_range(1);
for (auto it = range.first; it != range.second; it++) {
cout << it->second << endl;
}
三、无序容器
无序容器使用哈希表实现,不保证元素顺序,但查找效率更高。
3.1 unordered_map
基本概念
unordered_map 使用哈希表实现,键值对的存储位置取决于键的哈希值。
与 map 的对比
| 特性 | map | unordered_map |
|---|---|---|
| 底层实现 | 红黑树 | 哈希表 |
| 查找/插入/删除 | O(log n) | 平均 O(1),最坏 O(n) |
| 元素顺序 | 有序(按键排序) | 无序 |
| 内存占用 | 较小 | 较大 |
#include <unordered_map>
using namespace std;
unordered_map<int, string> um;
// 插入
um[1] = "张三";
um.insert({2, "李四"});
um.emplace(3, "王五");
// 查找
cout << um[1] << endl;
auto it = um.find(2);
// 遍历(无序)
for (auto& p : um) {
cout << p.first << ": " << p.second << endl;
}
3.2 unordered_set
#include <unordered_set>
using namespace std;
unordered_set<int> us = {5, 3, 1, 4, 2};
// 插入
us.insert(6);
// 查找
cout << us.count(3) << endl;
// 遍历(无序)
for (int n : us) {
cout << n << " ";
}
四、容器适配器
容器适配器是对现有容器的封装,提供特定的接口。STL 提供了三种容器适配器:stack、queue、priority_queue。
什么是适配器
适配器就像电源适配器一样,能将不适用的东西转化为适用的东西。在 STL 中:
- 容器适配器:改变容器的接口(stack、queue、priority_queue)
- 迭代器适配器:改变迭代器的接口
- 仿函数适配器:改变仿函数的接口
4.1 stack 栈
基本概念
stack 是一种**后进先出(LIFO)**的数据结构,只有一个出口。只有栈顶元素可以被访问。
#include <stack>
using namespace std;
stack<int> s;
// 入栈
s.push(1);
s.push(2);
s.push(3);
// 访问栈顶
cout << s.top() << endl; // 3
// 出栈
s.pop(); // 移除栈顶元素
// 大小
cout << s.size() << endl; // 2
cout << s.empty() << endl; // false
4.2 queue 队列
基本概念
queue 是一种**先进先出(FIFO)**的数据结构,有两个出口。只有队头和队尾元素可以被访问。
#include <queue>
using namespace std;
queue<int> q;
// 入队
q.push(1);
q.push(2);
q.push(3);
// 访问队头和队尾
cout << q.front() << endl; // 1
cout << q.back() << endl; // 3
// 出队
q.pop(); // 移除队头元素
// 大小
cout << q.size() << endl;
cout << q.empty() << endl;
4.3 priority_queue 优先队列
基本概念
priority_queue 是一种优先级队列,元素按优先级排列,默认是大顶堆(最大的元素在队头)。
#include <queue>
using namespace std;
// 默认大顶堆
priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(4);
pq.push(1);
pq.push(5);
// 访问队头(最大元素)
cout << pq.top() << endl; // 5
// 出队
pq.pop();
// 小顶堆
priority_queue<int, vector<int>, greater<int>> min_pq;
min_pq.push(3);
min_pq.push(1);
min_pq.push(4);
cout << min_pq.top() << endl; // 1
五、String 详解
基本操作
#include <string>
using namespace std;
// 构造
string s1; // 空字符串
string s2("hello"); // 从 C 字符串构造
string s3(5, 'a'); // 5个 'a'
string s4(s2); // 拷贝构造
string s5(s2, 1, 3); // 从位置1开始取3个字符
// 判断是否为空
if (s1.empty()) {
cout << "空字符串" << endl;
}
// 获取长度
cout << s2.size() << endl;
cout << s2.length() << endl;
查找与子串
string s = "hello world";
// 查找
size_t pos = s.find("world"); // 返回位置 6
pos = s.find('o', 5); // 从位置5开始找 'o'
// 从右边查找
pos = s.rfind('o'); // 返回 7
// 获取子串
string sub = s.substr(0, 5); // "hello"
sub = s.substr(6); // "world"
提取中间字符串
// 根据左右字符串提取中间的字符串
string GetMidStrByLAndR(const string& src, const string& left, const string& right) {
size_t begin = src.find(left);
if (begin != string::npos) {
begin += left.length();
size_t end = src.find(right, begin);
if (end != string::npos) {
return src.substr(begin, end - begin);
}
}
return "";
}
// 使用示例
string html = "<div>Hello World</div>";
string content = GetMidStrByLAndR(html, "<div>", "</div>");
cout << content << endl; // Hello World
提取目录和文件名
// 提取目录(不要文件名)
string getDir(const string& path) {
size_t pos = path.rfind('/');
if (pos != string::npos) {
return path.substr(0, pos + 1);
}
return path;
}
// 提取文件名(去掉路径和后缀)
string getFileName(const string& fileDir) {
size_t pos = fileDir.rfind('/');
string fileName = (pos != string::npos) ? fileDir.substr(pos + 1) : fileDir;
pos = fileName.find('.');
return fileName.substr(0, pos);
}
assign vs 构造函数
对于末尾没有 \0 的字符数组,使用 assign 更安全:
char arr[] = {'h', 'e', 'l', 'l', 'o'}; // 没有 '\0'
// ❌ 可能包含未知数据
string s1(arr);
// ✅ 只复制指定长度的数据
string s2;
s2.assign(arr, 5);
类型转换
#include <string>
// 数字转字符串
string s1 = to_string(123);
string s2 = to_string(3.14);
// 字符串转数字
int n = stoi("123");
long l = stol("1234567890");
double d = stod("3.14");
替换子串
string s = "hello world";
// 从位置0开始,替换5个字符为 "hi"
s.replace(0, 5, "hi"); // "hi world"
// 删除子串(用空串替换)
s.replace(0, 3, ""); // "world"
六、文件读写
文件类型
- 文本文件:每个字节存放一个 ASCII 码,代表一个字符
- 二进制文件:将内存中的数据按其存储形式原样存放
文件路径
// 绝对路径
"D:/wamp/img/a.txt"
"D:\\wamp\\img\\a.txt"
// 相对路径
"./a.txt" // 当前目录
"../a.txt" // 上级目录
"../../a.txt" // 上两级目录
文件打开方式
| 模式 | 说明 |
|---|---|
ios::in |
以输入方式打开(读) |
ios::out |
以输出方式打开(写),文件不存在则创建,存在则清空 |
ios::app |
以追加方式打开 |
ios::ate |
打开并定位到文件末尾 |
ios::trunc |
打开时清空文件 |
ios::binary |
以二进制方式打开 |
// 组合使用
fstream f("a.txt", ios::in | ios::out | ios::binary);
文本文件读写
#include <fstream>
using namespace std;
// 写文件
ofstream outFile("output.txt");
if (outFile.is_open()) {
outFile << "Hello" << endl;
outFile << "World" << endl;
outFile.close();
}
// 读文件
ifstream inFile("input.txt");
string line;
while (getline(inFile, line)) {
cout << line << endl;
}
inFile.close();
二进制文件读写
#include <fstream>
using namespace std;
// 写二进制文件
struct Person {
char name[20];
int age;
};
Person p = {"张三", 25};
ofstream outFile("person.dat", ios::binary);
outFile.write(reinterpret_cast<char*>(&p), sizeof(p));
outFile.close();
// 读二进制文件
Person p2;
ifstream inFile("person.dat", ios::binary);
inFile.read(reinterpret_cast<char*>(&p2), sizeof(p2));
inFile.close();
获取文件大小
ifstream file("large.bin", ios::binary | ios::ate);
streamsize size = file.tellg();
file.seekg(0, ios::beg);
char* buffer = new char[size];
file.read(buffer, size);
// 使用 buffer...
delete[] buffer;
file.close();
流指针操作
fstream f("data.txt", ios::in | ios::out);
// 获取当前位置
streampos pos = f.tellg(); // 读指针位置
streampos pos = f.tellp(); // 写指针位置
// 设置位置
f.seekg(0, ios::beg); // 定位到文件开头
f.seekg(0, ios::end); // 定位到文件末尾
f.seekg(-10, ios::cur); // 从当前位置向前移动10字节
状态检查
ifstream f("test.txt");
// 检查文件是否打开
if (f.is_open()) { }
// 检查是否到达末尾
if (f.eof()) { }
// 检查读写是否出错
if (f.bad()) { }
// 检查是否失败(包括格式错误)
if (f.fail()) { }
// 检查是否正常
if (f.good()) { }
// 清除状态标志
f.clear();
七、仿函数与适配器
仿函数(函数对象)
仿函数是重载了 operator() 的类,可以像函数一样调用。
// 定义仿函数
class PersonCompare {
public:
bool operator()(const Person& p1, const Person& p2) const {
return p1.age < p2.age;
}
};
// 使用仿函数
set<Person, PersonCompare> s;
使用仿函数和模板实现自定义类比较
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string m_name;
int m_age;
Person(string name, int age) : m_name(name), m_age(age) {}
};
// 比较仿函数
class PersonCompare {
public:
bool operator()(const Person& p1, const Person& p2) {
return p1.m_age < p2.m_age;
}
};
// 模板函数
template<class T, class Compare>
const T& myMax(const T& a, const T& b, Compare comp) {
return comp(a, b) ? b : a;
}
int main() {
Person p1("张三", 21);
Person p2("李四", 25);
Person older = myMax(p1, p2, PersonCompare());
cout << older.m_name << ": " << older.m_age << endl;
// 输出: 李四: 25
return 0;
}
仿函数适配器
仿函数适配器用于特化和扩展一元或二元函数对象。
绑定器(binder)
给二元函数对象绑定一个常量,转为一元函数。
#include <functional>
#include <algorithm>
#include <vector>
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 bind2nd(已废弃,C++11 后建议使用 bind)
// 统计大于5的元素个数
int count = count_if(v.begin(), v.end(), bind2nd(greater<int>(), 5));
// C++11 使用 bind
using namespace std::placeholders;
int count2 = count_if(v.begin(), v.end(), bind(greater<int>(), _1, 5));
取反器
将谓词函数对象结果取反。
#include <functional>
vector<int> v = {1, 2, 3, 4, 5};
// not1 - 一元取反
// 统计不大于3的元素个数
int count = count_if(v.begin(), v.end(), not1(bind2nd(greater<int>(), 3)));
// not2 - 二元取反
八、容器选择指南与性能对比
容器特性对比表
| 容器 | 底层实现 | 特点 |
|---|---|---|
| array | 静态数组 | 固定大小,支持随机访问 |
| vector | 动态数组 | 动态扩展,尾部插入删除高效,中间插入需要移动元素 |
| deque | 分段连续空间 | 双端开口,头尾插入删除高效,支持随机访问 |
| list | 双向循环链表 | 不支持随机访问,任意位置插入删除高效 |
| stack | 默认 deque | LIFO,只能操作栈顶 |
| queue | 默认 deque | FIFO,只能操作队头队尾 |
| set | 红黑树 | 元素唯一,自动排序,查找 O(log n) |
| multiset | 红黑树 | 允许重复元素 |
| map | 红黑树 | 键值对,键唯一,自动排序,查找 O(log n) |
| multimap | 红黑树 | 允许重复键 |
| unordered_map | 哈希表 | 键值对,无序,查找平均 O(1) |
选择指南
序列容器选择
// 需要随机访问,主要在尾部操作 -> vector
vector<int> v;
// 需要频繁在头部和尾部操作 -> deque
deque<int> d;
// 需要频繁在中间插入删除 -> list
list<int> l;
// 大小固定 -> array
array<int, 10> a;
关联容器选择
// 需要键值对,按键有序 -> map
map<int, string> m;
// 需要键值对,快速查找,不需要顺序 -> unordered_map
unordered_map<int, string> um;
// 只需要存储值,去重 -> set
set<int> s;
// 允许重复值 -> multiset
multiset<int> ms;
性能对比
| 操作 | vector | list | deque | map/set |
|---|---|---|---|---|
| 随机访问 | O(1) | ❌ | O(1) | ❌ |
| 尾部插入/删除 | O(1) 均摊 | O(1) | O(1) | - |
| 头部插入/删除 | O(n) | O(1) | O(1) | - |
| 中间插入/删除 | O(n) | O(1) | O(n) | - |
| 查找(按值/键) | O(n) | O(n) | O(n) | O(log n) |
实际场景建议
- 存储基本类型,需要随机访问:
vector - 频繁插入删除,不需要随机访问:
list - 需要键值映射,按键排序:
map - 需要快速查找,不关心顺序:
unordered_map - 去重存储:
set - 后进先出:
stack - 先进先出:
queue - 优先级处理:
priority_queue
总结
本文系统介绍了 C++ STL 中的各类容器,从序列容器到关联容器,从无序容器到容器适配器,涵盖了 String、文件读写以及仿函数与适配器等重要内容。选择合适的容器是编写高效 C++ 程序的关键,希望本文能帮助开发者在实际项目中做出正确的选择。
参考资料
- 《STL 源码剖析》侯捷
- C++ Reference
- 菜鸟教程 C++ STL
更多推荐
所有评论(0)