《C++:从代码到机器》:vector 的坑只有 C++ 党懂?介绍使用 + 深度剖析 + 模拟实现,帮你全避开

《C++:从代码到机器》:vector 的坑只有 C++ 党懂?介绍使用 + 深度剖析 + 模拟实现,帮你全避开

个人头像

✨ 孤廖:个人主页

🎯 个人专栏:《C++:从代码到机器》

🎯 个人专栏:《Linux系统探幽:从入门到内核》

🎯 个人专栏:《算法磨剑:用C++思考的艺术》

折而不挠,中不为下



在这里插入图片描述

文章目录

  • 正文
    • 1.vector的介绍及使用
      • 1.1 vector的介绍
      • 1.2 vector的使用
        • 1.2.1 vector的定义
        • 1.2.2 vector iterator 的使用
        • 1.2.3 vector 空间增长问题
        • 1.2.4 vector 增删查改
        • 1.2.5 vector 迭代器失效问题。(重点)
        • 1.2.6 vector 在OJ中的使用。
    • 2.vector深度剖析及模拟实现
      • 2.1 std::vector的核心框架接口的模拟实现bit::vector
      • 2.2 使用memcpy拷贝问题
      • 2.3 动态二维数组理解
  • 结语:

正文

1.vector的介绍及使用

1.1 vector的介绍

vector的文档介绍
使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习

1.2 vector的使用

vector学习时一定要学会查看文档:vector的文档介绍,vector在实际中非常的重要,在实际中我们熟悉常见的接口就可以,下面列出了***哪些接口是要重点掌握的***。
1.2.1 vector的定义
构造函数声明接口说明
vector() (重点)无参构造
vector (size_type n, const value_type& val = value_type())构造并初始化 n 个 val
vector (const vector& x); (重点)拷贝构造
vector (InputIterator first, InputIterator last);使用迭代器进行初始化构造
这些构造函数在下面代码实现中均会呈现
1.2.2 vector iterator 的使用
iterator的使用接口说明
begin + end (重点)获取第一个数据位置的iterator/const_iterator,获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator
在这里插入图片描述


在这里插入图片描述
当然迭代器不是简单的指针,它的实现包装还是挺复杂的,但是在存储数据的一片连续空间中 我们可以用指针先简单模拟着
1.2.3 vector 空间增长问题
容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize (重点)改变vector的size
reserve (重点)改变vector的capacity
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STLreserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题resize 在开空间的同时还会进行初始化,影响size.
// 测试vector的默认扩容机制voidTestVectorExpand(){ size_t sz; vector<int> v; sz = v.capacity(); cout <<"making v grow:\n";for(int i =0; i <100;++i){ v.push_back(i);if(sz != v.capacity()){ sz = v.capacity(); cout <<"capacity changed: "<< sz <<'\n';}}} vs:运行结果:vs下使用的STL基本是按照1.5倍方式扩容 making foo grow : capacity changed :1 capacity changed :2 capacity changed :3 capacity changed :4 capacity changed :6 capacity changed :9 capacity changed :13 capacity changed :19 capacity changed :28 capacity changed :42 capacity changed :63 capacity changed :94 capacity changed :141 g++运行结果:linux下使用的STL基本是按照2倍方式扩容 making foo grow : capacity changed :1 capacity changed :2 capacity changed :4 capacity changed :8 capacity changed :16 capacity changed :32 capacity changed :64 capacity changed :128
// 如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够// 就可以避免边插入边扩容导致效率低下的问题了voidTestVectorExpandOP(){ vector<int> v; size_t sz = v.capacity(); v.reserve(100);// 提前将容量设置好,可以避免一遍插入一遍扩容 cout <<"making bar grow:\n";for(int i =0; i <100;++i){ v.push_back(i);if(sz != v.capacity()){ sz = v.capacity(); cout <<"capacity changed: "<< sz <<'\n';}}}
1.2.4 vector 增删查改
vector增删查改接口说明
push_back (重点)尾插
pop_back (重点)尾删
find查找。(注意这个是算法模块实现,不是vector的成员接口)
insert在position之前插入val
erase删除position位置的数据
swap交换两个vector的数据空间
operator[] (重点)像数组一样访问
1.2.5 vector 迭代器失效问题。(重点)
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器
底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
对于vector可能会导致其迭代器失效的操作有:
会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。
1.可以理解为野指针导致的迭代器失效
#include<iostream>usingnamespace std;#include<vector>intmain(){ vector<int> v{1,2,3,4,5,6};auto it = v.begin();// 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容// v.resize(100, 8);// reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容 量改变 // v.reserve(100);// 插入元素期间,可能会引起扩容,而导致原空间被释放// v.insert(v.begin(), 0);// v.push_back(8);// 给vector重新赋值,可能会引起底层容量改变 v.assign(100,8);/* 出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释 放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块 已经被释放的空间,而引起代码运行时崩溃。 解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给 it重新赋值即可。 */while(it != v.end()){ cout <<*it <<" ";++it;} cout << endl;return0;}
指定位置元素的删除操作–erase

代码解释:

#include<iostream>usingnamespace std;#include<vector>intmain(){int a[]={1,2,3,4}; vector<int>v(a, a +sizeof(a)/sizeof(int));// 使用find查找3所在位置的iterator vector<int>::iterator pos =find(v.begin(), v.end(),3);// 删除pos位置的数据,导致pos迭代器失效。 v.erase(pos); cout <<*pos << endl;// 此处会导致非法访问return0;}
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end
的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。但是Linux下不会挂掉 因此可以看出不同平台的编译器的设计在文档不做要求的情况下设计可能是不同的
以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?
#include<iostream>usingnamespace std;#include<vector>intmain(){ vector<int> v{1,2,3,4};auto it = v.begin();while(it != v.end()){if(*it %2==0) v.erase(it);++it;}return0;}intmain(){ vector<int> v{1,2,3,4};auto it = v.begin();while(it != v.end()){if(*it %2==0) it = v.erase(it);//erase 返回的删除元素的下一个元素的迭代器else++it;}return0;}

第二个正确

注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。
// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了intmain(){ vector<int> v{1,2,3,4,5};for(size_t i =0; i < v.size();++i) cout << v[i]<<" "; cout << endl;auto it = v.begin(); cout <<"扩容之前,vector的容量为: "<< v.capacity()<< endl;// 通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效 v.reserve(100); cout <<"扩容之后,vector的容量为: "<< v.capacity()<< endl;// 经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux 下不会 // 虽然可能运行,但是输出的结果是不对的while(it != v.end()){ cout <<*it <<" ";++it;} cout << endl;return0;} 程序输出: 12345 扩容之前,vector的容量为:5 扩容之后,vector的容量为 :1000234540912345
这个也就是迭代器失效的第一种案列 即野指针导致的迭代器失效
// 2. erase删除任意位置代码后,linux下迭代器并没有失效// 因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的#include<vector>#include<algorithm>intmain(){ vector<int> v{1,2,3,4,5}; vector<int>::iterator it =find(v.begin(), v.end(),3); v.erase(it); cout <<*it << endl;while(it != v.end()){ cout <<*it <<" ";++it;} cout << endl;return0;} 程序可以正常运行,并打印: 445
// 3: erase删除的迭代器如果是最后一个元素,删除之后it已经超过end// 此时迭代器是无效的,++it导致程序崩溃intmain(){ vector<int> v{1,2,3,4,5};// vector<int> v{1,2,3,4,5,6};auto it = v.begin();while(it != v.end()){if(*it %2==0) v.erase(it);++it;}for(auto e : v) cout << e <<" "; cout << endl;return0;}========================================================// 使用第一组数据时,程序可以运行[sly@VM -0-3- centos 20220114]$ g++ testVector.cpp - std = c++11[sly@VM -0-3- centos 20220114]$ ./ a.out 135=========================================================// 使用第二组数据时,程序最终会崩溃[sly@VM -0-3- centos 20220114]$ vim testVector.cpp [sly@VM -0-3- centos 20220114]$ g++ testVector.cpp - std = c++11[sly@VM -0-3- centos 20220114]$ ./ a.out Segmentation fault 
从上述三个例子中可以看到:SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的。
怎么说呢 ?在设计个人觉得还是vs的挂掉比较好,因为vs直接规避了 删除最后一个元素导致的越界问题。
//4. 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效#include<string>voidTestString(){ string s("hello");auto it = s.begin();// 放开之后代码会崩溃,因为resize到20会string会进行扩容// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了// 后序打印时,再访问it指向的空间程序就会崩溃//s.resize(20, '!');while(it != s.end()){ cout <<*it;++it;} cout << endl; it = s.begin();while(it != s.end(){ it = s.erase(it);// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后// it位置的迭代器就失效了// s.erase(it);++it;}}
所以为了使得咱们的代码可以在各个平台都能跑,在使用前,对迭代器重新赋值即可。
1.2.6 vector 在OJ中的使用。
只出现一次的数字
classSolution{public:intsingleNumber(vector<int>& nums){int value =0;for(auto e : nums){ value ^= e;}return value;}};
补充位运算小知识:0^任何数字 都是这个数字本身
2.相同的两个数按位^ 等于 0
杨辉三角
// 涉及resize / operator[]// 核心思想:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+[j]classSolution{public: vector<vector<int>>generate(int numRows){ vector<vector<int>>vv(numRows);for(int i =0; i < numRows;++i){ vv[i].resize(i +1,1);}for(int i =2; i < numRows;++i){for(int j =1; j < i;++j){ vv[i][j]= vv[i -1][j]+ vv[i -1][j -1];}}return vv;}};
总结:通过上面的练习我们发现vector常用的接口更多是***插入和遍历***。遍历更喜欢用数组operator[i]的形式访问,因为这样便捷。希望大家自己实现一遍上面讲解的OJ练习,然后请自行完成下面题目的OJ练习。以此增强学习vector的使用。

3. 删除排序数组中的重复项 OJ
4. 只出现一次的数ii OJ
5. 只出现一次的数iii OJ
6. 数组中出现次数超过一半的数字 OJ
7. 电话号码字母组合OJ

2.vector深度剖析及模拟实现

2.1 std::vector的核心框架接口的模拟实现bit::vector

【代码实现】:

#pragmaonce//vector 的底层模拟实现#include<iostream>#include<assert.h>namespace twg {template<classT>classvector{typedef T* iterator;typedefconst T* const_iterator;public://默认构造vector(){};//拷贝构造vector(const vector<T>& s){ T* tmp =new T[s.capacity()];//拷贝数据for(int i =0; i < s.size(); i++){ tmp[i]= s[i];} _start = tmp; _finish = tmp + s.size(); _end_of_storage = tmp + s.capacity();}//迭代器区间拷贝构造template<classInputIterator>vector(InputIterator first, InputIterator last):_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){while(first != last){push_back(*first);++first;}}//析构~vector(){//释放资源if(_start !=nullptr){delete[] _start; _start = _finish = _end_of_storage =nullptr;}}//赋值重载 vector<T>&operator=( vector<T> s){swap(s);return*this;}//其他常用接口//访问符重载 T&operator[](size_t n){assert(n<size());assert(n >=0);return*(_start + n);}const T&operator[](size_t n)const{assert(n <size());assert(n >=0);return _start[n];}voidswap(vector<T>& s){ std::swap(_start, s._start); std::swap(_finish, s._finish); std::swap(_end_of_storage, s._end_of_storage);}//reservevoidreserve(size_t n){if(n <=capacity())return;//不同扩容else{int oldsize =size(); T* tmp =new T[n];//拷贝数据for(int i =0; i < oldsize; i++){ tmp[i]=*(_start + i);}delete[] _start;//释放资源 _start = tmp; _finish = tmp + oldsize; _end_of_storage = tmp + n;;}}voidpush_back(const T& x){//检查空间是否够用if(_finish == _end_of_storage){//扩容 size_t newsize = _finish ==nullptr?4:capacity()*2;reserve(newsize);}//尾插元素*_finish = x;++_finish;} size_t size()const{return _finish - _start;} size_t capacity()const{return _end_of_storage - _start;}const iterator begin()const{return _start;}const iterator end()const{return _finish;}voidprint(){auto it =begin();while(it !=end()){*it;++it;} std::cout << std::endl;for(int i =0; i <size(); i++){ std::cout <<*(_start + i)<<" ";} std::cout << std::endl;}voidresize(size_t n,T x){if(n <=size()){ _finish = _start + n;}elseif(n >size()&& n <capacity()){int num = n -size();while(num--){push_back(x);}}else{reserve(n);int num = n -size();while(num--){push_back(x);}}}voidinsert(iterator pos,const T& x)// 使用const引用{// 检查pos有效性assert(pos >= _start && pos <= _finish);if(_finish == _end_of_storage){// 扩容前计算pos的相对位置 size_t pos_index = pos - _start;reserve(capacity()==0?4:capacity()*2); pos = _start + pos_index;// 更新pos位置} iterator end = _finish -1;while(end >= pos){*(end +1)=*end;--end;}*pos = x;// 在pos位置插入++_finish;}voiderase(const iterator& pos){auto it = pos+1;while(it != _finish){*(it -1)=*it;++it;} _finish--;}voidpop_back(){assert(_finish > _start);--_finish;}private://相较string //其数据是三个迭代器  iterator _start =nullptr; iterator _finish =nullptr; iterator _end_of_storage =nullptr;};}
vector 的常用接口的模拟实现

2.2 使用memcpy拷贝问题

假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
intmain(){ twg::vector<bite::string> v; v.push_back("1111"); v.push_back("2222"); v.push_back("3333");return0;}
问题分析:memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
2.2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为
memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃

2.3 动态二维数组理解

// 以杨慧三角的前n行为例:假设n为5voidtest2vector(size_t n){// 使用vector定义二维数组vv,vv中的每个元素都是vector<int> twg::vector<bit::vector<int>>vv(n);// 将二维数组每一行中的vecotr<int>中的元素全部设置为1for(size_t i =0; i < n;++i) vv[i].resize(i +1,1);// 给杨慧三角出第一列和对角线的所有元素赋值for(int i =2; i < n;++i){for(int j =1; j < i;++j){ vv[i][j]= vv[i -1][j]+ vv[i -1][j -1];}}}
bit::vector<bit::vector> vv(n); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素
都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:
在这里插入图片描述
在这里插入图片描述
使用标准库中vector构建动态二维数组时与上图实际是一致的

结语:

从 vector 的基础定义、迭代器使用,到空间增长的奥秘,再到增删查改的实战,甚至亲手模拟实现它的核心逻辑…… 我们一步步拆解了这个 C++ 里 “动态数组” 的精髓,也见识了迭代器失效这类 “坑” 与解决思路,还在 OJ 题目里感受了它的实战威力~

vector 作为 STL 容器的 “入门级核心选手”,学好它不仅能搞定不少算法题,更能帮你触摸到 “代码如何映射到底层内存” 的门道(这也是咱们《C++:从代码到机器》专栏想传递的核心 —— 看透语法背后的运行逻辑)

现在,不妨自己敲一遍 vector 的模拟实现代码,再去 OJ 里刷几道题练练手~相信你会对 “容器”“迭代器” 这些概念有更鲜活的理解,也为后续探索 list、map 等其他容器打下扎实基础~咱们下一篇,继续从代码出发,向 “机器视角” 更进一步~

在这里插入图片描述

Read more

猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 过去几年里,科技公司几乎都在同一件事上加速:让 AI 参与写代码。 从自动补全、自动生成函数,到直接修改系统配置,生成式 AI 已经逐渐走进真实生产环境。但最近发生在亚马逊的一连串事故,却给整个行业泼了一盆冷水——当 AI 开始真正参与生产环境开发时,事情可能远比想象复杂。 最近,多家媒体披露,本周二亚马逊内部紧急召开了一场工程“深度复盘(deep dive)”会议,专门讨论最近频繁出现的系统故障——其中,一个被反复提及的关键词是:AI 辅助代码。 一周 4 次严重事故,亚马逊内部紧急复盘 事情的起点,是最近一段时间亚马逊系统稳定性明显下降。 负责亚马逊网站技术架构的高级副总裁 Dave Treadwell 在一封内部邮件中坦言:“各位,正如大家可能已经知道的,最近网站及相关基础设施的可用性确实不太理想。” 为此,公司决定把原本每周例行举行的技术会议

By Ne0inhk
这回真的“装”到了!来OpenClaw全国纵深行,你只需要带一台电脑……

这回真的“装”到了!来OpenClaw全国纵深行,你只需要带一台电脑……

AI Agent 的风,已经从 GitHub 吹到了线下。 过去几个月,越来越多开发者开始讨论一个问题: 当 AI 不再只是聊天,而是可以执行任务,软件会变成什么样? 在这股浪潮中,一个开源项目迅速进入开发者视野——OpenClaw,在 GitHub 上获得大量关注,相关教程、实践案例不断出现。有人用它自动整理资料,有人用它管理开发流程,还有人尝试让它执行复杂的工作流。 很多开发者第一次意识到: AI 不只是工具,它可能成为“执行者”。 不过,在技术社区之外,大多数人对 Agent 的理解仍停留在概念层面。 * AI Agent 到底是什么? * 如何在自己的电脑上运行? * 普通开发者能否真正用起来? 带着这些问题,一场围绕 OpenClaw 的开发者城市行动正在展开。 ZEEKLOG 发起的OpenClaw 全国纵深行将走进 20 个城市,用最直接的方式回答一个问题——如果

By Ne0inhk
黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐

黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐

简历上展示黑马点评 完整代码地址 微服务学成在线项目 前言 当初就是当作一个学习笔记和个人面试记录发的,没想到这么多人收藏浏览,还是感慨学Java的人确实多啊。 适合什么人看呢,我仅仅说说我个人的理解,因为我现在也是个经历秋招的双非学生。 1.初学者学习完Redis基础,想来个实战,黑马点评还是特别好的一个项目,基本包含了所有数据类型的运用和redis其他功能的扩展,这篇文章可以带你提炼重点,很好的走下流程。 2.但大部分人是冲着找实习和秋招去的,像我这种学历不高的秋招就不要写黑马点评了,即使包装,也会很容易看出来,我找实习的时候就被面试官问到这是不是黑马点评过,我们可以把其中的闪光点迁移到你找的其他项目中,比如缓存穿透雪崩击穿的解决方法,redisson分布式锁解决一人一单,这种在大多项目中都可以添加,自圆其说就行。 3.对于找实习的像大二,大三上的,想找个小厂试试手垂直向上升的,可以吃透它,面试官问你遇到的困难或者是你觉得难点,就可以重点讲一人一单这个解决方法和流程,越详细越好。 4.前提是大家不用直接用这套模板,太多人用了,这也是我从网上找的别人的,巧用AI让它改改项

By Ne0inhk
最新Spring Security实战教程(十一)CSRF攻防实战 - 从原理到防护的最佳实践

最新Spring Security实战教程(十一)CSRF攻防实战 - 从原理到防护的最佳实践

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战 🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解 🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用 ✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧 💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程 🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整 🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术 如果文章能够给大家带来一定的帮助!欢迎关注、评

By Ne0inhk