多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零了解Vector详细解析

从零了解Vector详细解析 与string的衔接顺序表 vector 是一个标准的模板vector是一个双参数模板第一个参数T→ 容器里面存的数据类型int/Channel第二个参数Alloc→ 内存分配器默认值就是allocatorT2.allocatorT是什么std::allocator是 C STL 默认内存管理器。vector 需要堆内存存放元素allocator的工作就是向操作系统申请内存、释放内存。它封装了底层的new/deletemalloc/free它干两件核心工作分配原始内存只开辟一块裸内存不调用构造函数释放内存归还内存不调用析构函数区分两个概念分配内存找一块空地构造对象在这块空地上调用类的构造函数emplace_backallocator 先分配内存 → 然后原地构造对象正好就是你之前代码_channels.emplace_back(wfd,subid)的完整流程。而 string 是一个具体类型在库中string本质为 basic_string char 的别名vector是类模板类模板必须显示实例化std::vectorT—— 这是模板图纸不是一个可用的类型。显式实例化给模板参数传值造出一个真实的类成品std::vectorint v1; // 显式实例化得到一个实实在在的类 std::vectorint v2(10,1); std::vectorint v3(v2.begin(),v2.end());下面这个写法根本不能用来定义变量为没有显式实例化就是只写了模板名字没有给尖括号传类型参数std::vector; // 报错没有填模板参数T遍历vector的方法用迭代器vectorint::iterator it v3.begin();范围forfor(auto e;v3) { //... }vector不会缩容而string会缩容重新分配更小的内存 拷贝数据 释放旧的大内存操作系统的堆内存管理规则是分配的内存块必须整体释放无法拆分归还vector::resize把vector的数据个数改为 nsize也会变size删除数据size插入数据空间不够就扩容vector::operator[]operator[]就是下标运算符重载v3[i]编译器等价翻译成v3.operator[](i)1.两个重载版本① 非 const 版本 —— 可读、可修改元素int a v3[0]; a 666; //可以修改容器里面的对象返回值引用reference→ int② const 版本容器被 const 修饰时调用只读const std::vectorint b v3; const int c b[0]; // c 999; //报错不能修改2.operator [] 最核心特点不做越界检查std::vectorint 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::vectorint 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::vectorint 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 ()-1for(int i0;i_channels.size();i) { auto ch _channels[i]; //安全i一定合法 }下标来自外部输入、不确定是否合法 → 用.at()开启越界保护vector::insertinsert在指定迭代器位置插入一个 / 一批元素。注意vector 内存连续插入中间位置后面所有元素向后移位效率低 O (n)重载 1插入 n 个相同的值iterator insert(iterator pos, size_type count, const T value);std::vectorint v {1,2,3}; v.insert(v.begin(), 3, 88);// 在最前面插入3个88// v: 88,88,88,1,2,3重载 2插入一段区间 [first, last)templateclass InputIt iterator insert(iterator pos, InputIt first, InputIt last);std::vectorint a {1,2,3}; std::vectorint b {100,200};// 在a的尾部插入b的全部元素 a.insert(a.end(), b.begin(), b.end());// a: 1,2,3,100,200重载 3初始化列表 C11v.insert(v.begin(), {5,6,7});返回值极其重要大坑迭代器失效vector 一旦在中间insert有可能触发扩容所有旧迭代器全部失效std::vectorint 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::eraseerase的作用删除 vector 中一个或者一段区间的元素注意 vector 内存连续删除中间元素后后面所有元素必须向前移动补齐空位时间复杂度 O (n)并且会造成迭代器失效这是最高频的坑重载 1删除单个元素传迭代器iterator erase(iterator pos);重载 2删除一段区间左闭右开[first, last)iterator erase(iterator first, iterator last);返回值返回被删除元素的下一个位置的新迭代器最经典坑一边遍历一边删除错误代码失效崩溃std::vectorint 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的 vectorint v(5, 1); vectorvectorint v2(4, v); for (auto e : v2) { for (auto e1 : e) { cout e1 ; } cout endl; }简易版vector的底层模型STL 源码思路简化掉分配器 allocatortemplate 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 ) }_apos指针向后偏移 pos 个 T 类型的距离找到对应元素。 这也就是为什么vector下标访问速度极快。push_back 扩容时_a发生了什么判断_size _capacity→ 内存满了必须扩容开辟一块更大的新堆内存得到新指针T* new_a把_a指向的旧内存所有元素拷贝到new_a释放旧的 _a 指向的堆内存将_a new_a;让指针指向新内存_capacity更新为新容量在尾部放入新元素_size扩容后旧的_a内存被释放 所以之前保存的引用、迭代器本质就是基于旧_a的指针全部失效。
返回列表