从零了解Vector详细解析
与string的衔接
顺序表 vector ,是一个标准的模板
vector是一个双参数模板:
第一个参数:
T→ 容器里面存的数据类型(int/Channel)第二个参数:
Alloc→ 内存分配器,默认值就是allocator<T>
2.allocator<T>是什么
std::allocator是 C++ STL 默认内存管理器。
vector 需要堆内存存放元素;
allocator的工作就是:向操作系统申请内存、释放内存。
它封装了底层的:
new/deletemalloc/free
它干两件核心工作
分配原始内存:只开辟一块裸内存,不调用构造函数
释放内存:归还内存,不调用析构函数
区分两个概念
分配内存:找一块空地
构造对象:在这块空地上调用类的构造函数
emplace_back:allocator 先分配内存 → 然后原地构造对象,正好就是你之前代码_channels.emplace_back(wfd,subid)的完整流程。
而 string 是一个具体类型,在库中string本质为 basic_string <char> 的别名
vector是类模板,类模板必须显示实例化
std::vector<T>—— 这是模板(图纸),不是一个可用的类型。
显式实例化:给模板参数传值,造出一个真实的类(成品)
std::vector<int> v1; // 显式实例化,得到一个实实在在的类 std::vector<int> v2(10,1); std::vector<int> v3(v2.begin(),v2.end());下面这个写法根本不能用来定义变量
为没有显式实例化,就是只写了模板名字,没有给尖括号传类型参数
std::vector; // 报错!!没有填模板参数T遍历vector的方法:
用迭代器:
vector<int>::iterator it = v3.begin();范围for:
for(auto e;v3) { //... }vector不会缩容
而string会缩容:重新分配更小的内存 + 拷贝数据 + 释放旧的大内存
(操作系统的堆内存管理规则是:分配的内存块必须整体释放,无法拆分归还)
vector::resize
把vector的数据个数改为 n(size也会变)
<size:删除数据
>size:插入数据,空间不够就扩容
vector::operator[]
operator[]就是下标运算符重载
v3[i]编译器等价翻译成:v3.operator[](i)
1.两个重载版本
① 非 const 版本 —— 可读、可修改元素
int &a = v3[0]; a = 666; //可以修改容器里面的对象返回值:引用reference→ int&
② const 版本(容器被 const 修饰时调用),只读
const std::vector<int>& b = v3; const int& c = b[0]; // c = 999; //报错,不能修改2.operator [] 最核心特点:不做越界检查
std::vector<int> v{10,20,30}; v[100] = 999; //下标100远远超出范围编译不会报错,运行时不会抛出异常,直接访问非法内存 → 未定义行为 (UB),程序可能随机崩溃、乱改内存。
这是和
.at()的最大区别
| 方法 | 越界检查 | 越界行为 |
| vec[i]operator[] | 无检查 | 未定义行为,危险 |
| vec.at(i) | 边界检查 | 抛出 std::out_of_range 异常,安全 |
示例对比
v.at(100); //越界 →抛异常,可以try‑catch捕获 v[100]; //越界 →直接野内存访问,无法捕获3.返回引用
std::vector<int> v = {1,2,3};int x = v[0]; // 值拷贝,x是副本,改x不会影响vint &y = v[0]; // 拿到容器内部元素的引用,改y就改v内部元素 y = 99;// v[0] 变成994.迭代器 / 引用失效大坑(网络编程必踩坑)
当你调用push_back / emplace_back触发 vector 扩容
之前用
operator[]获取到的引用Channel& ch = _channels[0];直接失效!变成野引用,再使用就是 UB 崩溃。
例子:
std::vector<int> v{1,2,3};int &ref = v[0]; v.reserve(100); //扩容!内存搬家! ref = 99; //野引用!未定义行为!5.底层原理(vector 内存连续)
vector 的元素在内存一块连续数组。operator[]底层实现逻辑简化:
//伪代码 T& operator[](size_t pos) { return *( _start + pos ); }_start指向数组首地址,下标就是指针偏移。
所以
vec[2]和*(vec.begin()+2)完全等价。
6.什么时候用 [],什么时候用 at ()
你已经手动保证下标一定合法 → 用
[],速度更快,无额外检查开销(循环遍历 i 从 0 到 size ()-1)
for(int i=0;i<_channels.size();i++) { auto &ch = _channels[i]; //安全,i一定合法 }下标来自外部输入、不确定是否合法 → 用
.at(),开启越界保护
vector::insert
insert:在指定迭代器位置,插入一个 / 一批元素。
注意:vector 内存连续,插入中间位置,后面所有元素向后移位,效率低 O (n)
重载 1:插入 n 个相同的值
iterator insert(iterator pos, size_type count, const T& value);std::vector<int> v = {1,2,3}; v.insert(v.begin(), 3, 88);// 在最前面插入3个88// v: 88,88,88,1,2,3重载 2:插入一段区间 [first, last)
template<class InputIt> iterator insert(iterator pos, InputIt first, InputIt last);std::vector<int> a = {1,2,3}; std::vector<int> b = {100,200};// 在a的尾部,插入b的全部元素 a.insert(a.end(), b.begin(), b.end());// a: 1,2,3,100,200重载 3:初始化列表 C++11
v.insert(v.begin(), {5,6,7});返回值极其重要(大坑!迭代器失效)
vector 一旦在中间insert,有可能触发扩容,所有旧迭代器全部失效!
std::vector<int> v = {1,2,3};auto it = v.begin()+1; v.insert(it, 99); //插入后,it失效!!不能再用it!// 正确做法:用insert返回的新迭代器 it = v.insert(v.begin()+1, 99);不要保存旧迭代器;
insert返回新迭代器,赋值回去。
vector::erase
erase的作用:删除 vector 中一个或者一段区间的元素
注意: vector 内存连续,删除中间元素后,后面所有元素必须向前移动补齐空位,时间复杂度 O (n);并且会造成迭代器失效,这是最高频的坑
重载 1:删除单个元素(传迭代器)
iterator erase(iterator pos);重载 2:删除一段区间,左闭右开
[first, last)
iterator erase(iterator first, iterator last);返回值:返回被删除元素的下一个位置的新迭代器
最经典坑:一边遍历一边删除
错误代码(失效崩溃)
std::vector<int> v = {1, 2, 2, 3}; for(auto it = v.begin(); it != v.end(); it++) { if(*it == 2) { v.erase(it); // erase后it立刻失效!循环下一次it++直接崩溃 } }正确写法(利用 erase 返回值)
for(auto it = v.begin(); it != v.end(); ) { if(*it == 2) { it = v.erase(it); //接收返回的有效迭代器,不做it++ } else { ++it; } }原理:erase返回删除点后面合法的迭代器,不需要再it++。
vector::push_back
在 vector 的尾部追加一个元素
// 范围for for (auto e : v2) { cout << e << endl; } //其中 e 得到的是v2中所存的类型的深拷贝,而v2类型为string,拷贝效率低 ——> 用引用 for (const auto &e : v2)for循环
// 二维数组,如4*5的 vector<int> v(5, 1); vector<vector<int>> v2(4, v); for (auto e : v2) { for (auto e1 : e) { cout << e1 << " "; } cout << endl; }简易版vector的底层模型(STL 源码思路,简化掉分配器 allocator)
template <class T> class vector { private: T *_a; size_t _size; size_t _capacity; };_a
_a 是一个指针,类型:T*
_a 保存堆上那块连续数组内存的起始地址。
vector 的所有元素,并不存在栈上;而是在堆 (heap) 上面开辟一块连续的内存存放。
_a 就是指向这块堆数组第一个元素的指针。
三个成员变量一一对应含义
| 成员 | 含义 |
| T* _a | 内存起始指针,堆数组首地址 |
| size_t _size | 当前元素个数。你能访问的有效元素数量。v.size() 返回的值 |
| size_t _capacity | 已经分配的内存总容量。这块堆内存一共可以放下多少个 T 对象。v.capacity() |
内存布局示意图:
堆内存: [ T0 ][ T1 ][ T2 ][ T3 ][ 空闲 ][ 空闲 ] ↑ _a _size = 3 //有效元素0,1,2 _capacity = 6 //整块内存一共能存6个元素vector下标运算符重载
//可读可写,非const对象调用 T& operator[](int i) { return _a[i]; } //只读版本,const对象调用,不能修改元素 const T& operator[](int i) const { return _a[i]; }下标运算符operator[]底层怎么实现
依靠_a指针偏移
T& operator[](size_t pos) { return _a[pos]; //等价于 *( _a + pos ) }_a+pos:指针向后偏移 pos 个 T 类型的距离,找到对应元素。 这也就是为什么vector下标访问速度极快。
push_back 扩容时_a发生了什么
判断:
_size == _capacity→ 内存满了,必须扩容开辟一块更大的新堆内存,得到新指针
T* new_a把
_a指向的旧内存所有元素拷贝到new_a释放旧的 _a 指向的堆内存
将
_a = new_a;,让指针指向新内存_capacity更新为新容量在尾部放入新元素,
_size++
扩容后旧的
_a内存被释放! 所以之前保存的引用、迭代器(本质就是基于旧_a的指针)全部失效。
