STL 容器简介

本文系统介绍 C++ STL 标准库中的各类容器,涵盖序列容器、关联容器、无序容器、容器适配器、String、文件读写及仿函数适配器等内容,配以丰富的代码示例,帮助开发者深入理解并正确选择合适的容器。


目录

  1. 序列容器
  2. 关联容器
  3. 无序容器
  4. 容器适配器
  5. String 详解
  6. 文件读写
  7. 仿函数与适配器
  8. 容器选择指南与性能对比

一、序列容器

序列容器以线性方式存储元素,元素的位置取决于插入时机和位置,而非元素的值。STL 提供了四种序列容器:vectorlistdequearray

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

二、关联容器

关联容器中的元素根据键值自动排序,查找效率高。主要包括 mapsetmultimapmultiset

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 提供了三种容器适配器:stackqueuepriority_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)

实际场景建议

  1. 存储基本类型,需要随机访问vector
  2. 频繁插入删除,不需要随机访问list
  3. 需要键值映射,按键排序map
  4. 需要快速查找,不关心顺序unordered_map
  5. 去重存储set
  6. 后进先出stack
  7. 先进先出queue
  8. 优先级处理priority_queue

总结

本文系统介绍了 C++ STL 中的各类容器,从序列容器到关联容器,从无序容器到容器适配器,涵盖了 String、文件读写以及仿函数与适配器等重要内容。选择合适的容器是编写高效 C++ 程序的关键,希望本文能帮助开发者在实际项目中做出正确的选择。


参考资料

更多推荐