一、STL六大组件:一个老工程师的视角
STL,说白了就是C++标准库里的"乐高积木盒"。我刚开始接触时也觉得眼花缭乱,但用久了你会发现,核心就六样东西。
1.1 容器(Container)
容器就是装数据的地方。vector、list、map这些,都是容器。我个人习惯把容器分成两类:序列式容器(像数组一样按顺序存)和关联式容器(按关键字快速查找)。
1.2 算法(Algorithm)
算法就是操作容器里数据的函数。sort、find、copy这些。你想想看,如果没有STL,每次排序都要自己写快排,多累啊。
1.3 迭代器(Iterator)
迭代器是容器和算法之间的"胶水"。它像指针,但比指针更安全。我在项目中遇到过新手直接用指针遍历vector,结果越界崩溃——用迭代器就不会有这问题。
1.4 函数对象(Functor)
函数对象就是重载了operator()的类。说白了,它可以像函数一样调用,但还能保存状态。嗯,这个后面讲算法时会经常用到。
1.5 适配器(Adapter)
适配器是"包装器"。比如stack底层可以用deque实现,queue也可以用list实现。它不存数据,只是换个接口。
1.6 配置器(Allocator)
配置器管理内存分配。99%的情况下你不需要碰它,但知道它的存在很重要——vector的动态扩容就是靠它。
核心要点:STL六大组件不是孤立的。容器提供数据,迭代器连接容器和算法,函数对象定制算法行为,适配器转换接口,配置器管理内存。这套体系配合得天衣无缝。
二、vector底层实现:动态数组的秘密
vector的底层就是动态数组。但为什么它比普通数组好用?因为它会自动扩容。
2.1 内存布局
vector内部维护三个指针:start(指向数据开头)、finish(指向最后一个元素的下一个位置)、end_of_storage(指向分配内存的末尾)。
当finish == end_of_storage时,vector就会扩容。扩容策略通常是翻倍(不同实现略有差异)。
我的经验:如果你能预估数据量,尽量用reserve提前分配内存。我在项目中见过有人往vector里push_back 100万个元素,结果扩容了20多次,性能惨不忍睹。
2.2 扩容过程
- 申请新内存(通常是当前容量的2倍)
- 把旧数据拷贝/移动到新内存
- 销毁旧对象,释放旧内存
- 更新三个指针
这个过程很耗时。你想想看,如果vector里有10万个对象,每次扩容都要拷贝一遍,能不慢吗?
三、vector的构造与析构
3.1 构造方式
vector<int> v1; // 空vector
vector<int> v2(10); // 10个默认值0
vector<int> v3(10, 5); // 10个5
vector<int> v4(v3.begin(), v3.end()); // 迭代器范围构造
vector<int> v5 = {1,2,3,4,5}; // 初始化列表(C++11)
我个人最常用的是初始化列表和迭代器范围构造。特别是迭代器范围,可以从数组或其他容器切片。
3.2 析构
vector的析构函数会:先调用每个元素的析构函数,再释放整块内存。嗯,这里要注意:如果元素是指针,vector不会帮你delete指向的对象——那是你的责任。
避坑指南:我曾经在项目中看到有人用vector<int*>存动态分配的int,结果vector析构后内存全泄漏了。记住:vector只管理它自己的内存,不管理元素指向的内存。
四、常用成员函数实战
4.1 push_back vs emplace_back
这两个函数都是尾部插入。区别在于:push_back接收对象,emplace_back接收构造参数,直接在vector内部构造对象。
struct Student {
string name;
int age;
Student(string n, int a) : name(n), age(a) {}
};
vector<Student> students;
students.push_back(Student("张三", 18)); // 先构造临时对象,再拷贝
students.emplace_back("李四", 19); // 直接在vector内部构造
emplace_back少了一次拷贝,性能更好。我建议:能用emplace_back就别用push_back。
4.2 reserve vs resize
这两个函数经常被搞混。我简单说下区别:
| 函数 | 作用 | 改变size | 改变capacity |
|---|---|---|---|
| reserve(n) | 预留至少n个元素的内存 | 否 | 是(只增不减) |
| resize(n) | 把元素个数改为n | 是 | 可能 |
reserve只分配内存,不创建对象。resize会创建或删除对象。我习惯在知道最终大小时用reserve,避免多次扩容。
五、迭代器失效问题
这是vector最容易踩的坑。迭代器失效,说白了就是迭代器指向的内存地址变了,你再用它就崩了。
5.1 哪些操作会导致失效?
- 插入操作(push_back、insert等):如果导致扩容,所有迭代器都失效。没扩容的话,插入点之后的迭代器失效。
- 删除操作(pop_back、erase等):删除点之后的迭代器失效。
- reserve/resize:如果导致重新分配,所有迭代器失效。
核心原则:每次修改vector后,之前获取的迭代器都可能失效。别存着迭代器不放,用完就扔。
5.2 实战中的坑
// 错误示例
vector<int> v = {1,2,3,4,5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) {
v.erase(it); // 擦除后it失效,再++it就崩了
}
}
// 正确做法
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
我曾经在线上环境遇到过这个问题,排查了半天才发现是迭代器失效导致的。从那以后,我写循环删除时都会特别小心。
六、实战:学生成绩管理系统
来,我们写个简单的学生成绩管理系统。这个例子会用到vector的大部分特性。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
struct Student {
string name;
int score;
};
class ScoreManager {
private:
vector<Student> students;
public:
// 添加学生
void addStudent(const string& name, int score) {
students.emplace_back(name, score);
}
// 按成绩排序
void sortByScore() {
sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score; // 降序
});
}
// 查找学生
Student* findStudent(const string& name) {
for (auto& s : students) {
if (s.name == name) return &s;
}
return nullptr;
}
// 删除不及格学生
void removeFailed() {
students.erase(
remove_if(students.begin(), students.end(),
[](const Student& s) { return s.score < 60; }),
students.end()
);
}
// 打印所有学生
void printAll() {
for (const auto& s : students) {
cout << s.name << ": " << s.score << endl;
}
}
};
我的建议:注意看remove_if + erase的搭配。remove_if把"要删除的元素"移到末尾,返回新的逻辑结尾,然后erase真正删除。这是STL的"删除-擦除"惯用法,比手动循环删除高效得多。
这个系统虽然简单,但涵盖了vector的常用操作:emplace_back添加、sort排序、find查找、remove_if + erase删除。你想想看,如果用C风格数组实现这些功能,代码量至少翻一倍。
嗯,vector就讲到这里。记住它的核心:动态数组、自动扩容、迭代器失效要小心。下一章我们会聊list和deque,到时候你会看到不同容器的设计哲学差异有多大。
公众号:蓝海资料掘金营,微信deep3321