STL总览与vector:从六大组件到实战细节

STL,说白了就是C++标准库中最值钱的那部分。我刚开始学C++时,总觉得STL是个黑盒子——用起来爽,但心里没底。后来做项目多了才发现,理解它的底层逻辑,能帮你避开无数坑。

今天咱们就从STL的骨架讲起,再深入到vector这个最常用的容器。嗯,vector就像C++世界的“万能工具箱”,你几乎每个项目都会用到它。

STL六大组件:一个都不能少

STL不是一堆零散工具的堆砌,它有一套清晰的设计哲学。我个人习惯把这六大组件记成“三核心三辅助”:

  • 容器(Container):存数据的地方。vector、list、map这些都属于容器。
  • 算法(Algorithm):处理数据的方法。sort、find、copy等。
  • 迭代器(Iterator):连接容器和算法的桥梁。说白了就是“智能指针”。
  • 仿函数(Functor):行为像函数的对象。常用于算法的自定义操作。
  • 适配器(Adapter):给已有组件“换皮”。比如stack底层可以用deque实现。
  • 分配器(Allocator):管理内存分配。一般不用碰,但出问题时得懂。

核心关系:容器通过迭代器暴露数据,算法通过迭代器操作数据,仿函数和适配器提供定制能力,分配器在幕后管内存。这六者配合,构成了STL的完整生态。

你想想看,如果没有迭代器这个中间层,每个容器都得自己实现一套算法,那代码得多臃肿?

STL六大组件关系图 容器 Container 算法 Algorithm 迭代器 Iterator 暴露 操作 仿函数 Functor 适配器 Adapter 分配器 Allocator 内存支持 包装 定制 核心流程:容器 → 迭代器 → 算法 适配器、仿函数、分配器提供扩展与定制能力

vector容器原理:动态数组的智慧

vector本质上就是个动态数组。它维护一块连续的内存空间,当空间不够时自动扩容。我记得第一次看vector源码时,被它的扩容策略惊到了——不是每次加元素都扩容,而是按倍数增长。

扩容机制:当size() == capacity()时,vector会申请一块新内存(通常是原大小的1.5~2倍),然后把旧元素拷贝过去,再释放旧内存。这个操作的时间复杂度是O(n),所以频繁扩容会影响性能。

为什么会这样设计?你想想看,如果每次push_back都只多分配一个元素的空间,那插入n个元素就是O(n²)了。而按倍数扩容,均摊下来每个插入操作就是O(1)。

vector的构造与赋值

vector的构造方式有好几种,我项目中常用的是这些:

#include <vector>
using namespace std;

// 默认构造
vector<int> v1;                // 空vector

// 指定初始大小和值
vector<int> v2(5, 10);         // 5个元素,每个都是10

// 列表初始化(C++11起)
vector<int> v3 = {1, 2, 3, 4, 5};

// 拷贝构造
vector<int> v4(v3);            // 复制v3

// 迭代器范围构造
vector<int> v5(v3.begin(), v3.begin() + 3);  // 取前3个

// 赋值操作
vector<int> v6;
v6 = v3;                       // operator= 赋值
v6.assign(3, 100);             // 赋值为3个100
v6.assign(v3.begin(), v3.end()); // 用迭代器范围赋值

注意:assign会清空原有内容再填充。我曾经在循环里误用assign,结果每次迭代都清空了数据,排查了半天才发现问题。

容量与大小:别搞混了

很多新手分不清size和capacity。我简单解释一下:

  • size():当前实际存储的元素个数。
  • capacity():当前分配的内存能容纳的元素个数。
  • empty():判断是否为空(等价于size() == 0)。
  • reserve(n):预留至少能容纳n个元素的空间,但不改变size。
  • resize(n):改变size为n,多出来的元素用默认值填充。
vector<int> v;
cout << v.size() << " " << v.capacity();  // 0 0

v.reserve(100);
cout << v.size() << " " << v.capacity();  // 0 100

v.push_back(1);
cout << v.size() << " " << v.capacity();  // 1 100

v.resize(50);
cout << v.size() << " " << v.capacity();  // 50 100

实战建议:如果你预先知道要存多少元素,用reserve提前分配好空间。这样可以避免多次扩容带来的性能损耗。我在处理百万级数据时,光这一步就能省下好几秒。

插入与删除:小心迭代器失效

vector的插入和删除操作,最让人头疼的就是迭代器失效问题。我踩过这个坑,所以特别提醒你:

vector<int> v = {1, 2, 3, 4, 5};

// 尾部插入
v.push_back(6);                // v: 1 2 3 4 5 6

// 指定位置插入
v.insert(v.begin() + 2, 99);  // v: 1 2 99 3 4 5 6

// 尾部删除
v.pop_back();                  // v: 1 2 99 3 4 5

// 指定位置删除
v.erase(v.begin() + 1);       // v: 1 99 3 4 5

// 删除区间
v.erase(v.begin() + 1, v.begin() + 3);  // v: 1 4 5

// 清空
v.clear();                     // v: empty

迭代器失效:插入或删除元素后,所有指向被操作位置之后元素的迭代器、指针、引用都会失效。我曾经在循环里用失效的迭代器继续操作,结果程序直接崩溃。正确的做法是:每次插入/删除后,重新获取迭代器。

数据存取:随机访问的快乐

vector最大的优势就是随机访问——O(1)时间复杂度。因为底层是连续内存,通过偏移量直接计算地址。

vector<int> v = {10, 20, 30, 40, 50};

// 使用下标操作符
cout << v[0];                  // 10
cout << v[3];                  // 40

// 使用at()方法(带边界检查)
cout << v.at(2);               // 30
// cout << v.at(10);           // 越界,抛出out_of_range异常

// 获取首尾元素
cout << v.front();             // 10
cout << v.back();              // 50

// 获取底层数组指针
int* p = v.data();
cout << p[1];                  // 20

个人习惯:在确定下标不会越界时用[],不确定时用at()。at()虽然慢一点点,但能帮你快速定位越界问题。调试阶段我一般用at(),发布版本再改成[]。

互换容器与预留空间:两个实用技巧

这两个操作看似简单,但用好了能解决不少实际问题。

swap:交换两个vector的内容

vector<int> v1 = {1, 2, 3};
vector<int> v2 = {10, 20, 30, 40};

v1.swap(v2);  // 现在v1有4个元素,v2有3个元素
// 或者用非成员函数版本
swap(v1, v2);

swap还有一个妙用——收缩内存。当vector扩容后,即使删除了元素,capacity也不会自动缩小。这时候可以用swap技巧:

vector<int> v(1000, 1);  // capacity = 1000
v.clear();                // size = 0, capacity = 1000

// 收缩到合适大小
vector<int>(v).swap(v);  // 用临时vector交换,capacity = 0
// 或者用shrink_to_fit(C++11)
v.shrink_to_fit();        // 请求缩小capacity到size

注意:shrink_to_fit()只是一个请求,标准库不保证一定会缩小。而swap技巧是100%有效的。我在做内存敏感的项目时,一直用swap方式。

reserve:预留空间

前面已经提过,reserve用于提前分配内存。这里再强调一个细节:

vector<int> v;
v.reserve(1000);  // 分配1000个int的空间

// 此时v.size()还是0,但capacity()是1000
// 接下来push_back 1000次,不会触发任何扩容

for (int i = 0; i < 1000; ++i) {
    v.push_back(i);  // 全部O(1),没有扩容开销
}

reserve和resize的区别要搞清楚:reserve只改capacity,resize改size。resize(100)会创建100个默认构造的元素,而reserve(100)只是预留空间。

避坑指南:我曾经在项目中用reserve(100)后,直接通过下标v[i]赋值,结果程序崩溃了。因为reserve不改变size,下标访问的是未初始化的内存。记住:reserve后只能用push_back或emplace_back,不能直接用下标。

好了,vector的核心操作就这些。从构造到扩容,从插入到删除,从随机访问到内存管理,每个细节都值得你亲手敲一遍代码。STL的设计哲学就是“高效且通用”,vector正是这一哲学的完美体现。


公众号:蓝海资料掘金营,微信deep3321