《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

在 VSCode 中本地运行 DeepSeek,打造强大的私人 AI

在 VSCode 中本地运行 DeepSeek,打造强大的私人 AI

本文将分步向您展示如何在本地安装和运行 DeepSeek、使用 CodeGPT 对其进行配置以及开始利用 AI 来增强您的软件开发工作流程,所有这些都无需依赖基于云的服务。  步骤 1:在 VSCode 中安装 Ollama 和 CodeGPT         要在本地运行 DeepSeek,我们首先需要安装Ollama,它允许我们在我们的机器上运行 LLM,以及CodeGPT,它是集成这些模型以提供编码辅助的 VSCode 扩展。 安装 Ollama Ollama 是一个轻量级平台,可以轻松运行本地 LLM。 下载Ollama 访问官方网站:https://ollama.com * 下载适合您的操作系统(Windows、macOS 或 Linux)的安装程序。 * 验证安装 安装后,打开终端并运行: ollama --version  如果 Ollama 安装正确,

By Ne0inhk
DeepSeek-R1是真码农福音?我们问了100位开发者……

DeepSeek-R1是真码农福音?我们问了100位开发者……

从GitHub Copilot到DeepSeek-R1,AI编程工具正在引发一场"效率革命",开发者们对这些工具的期待与质疑并存。据Gartner预测,到2028年,将有75%的企业软件工程师使用AI代码助手。 眼看着今年国产选手DeepSeek-R1凭借“深度思考”能力杀入战场,它究竟是真码农福音还是需要打补丁的"潜力股"? ZEEKLOG问卷调研了社区内来自全栈开发、算法工程师、数据工程师、前端、后端等多个技术方向的100位开发者(截止到2月25日),聚焦DeepSeek-R1的代码生成效果、编写效率、语法支持、IDE集成、复杂代码处理等多个维度,一探DeepSeek-R1的开发提效能力。 代码生成效果:有成效但仍需提升 * 代码匹配比例差强人意 在代码生成与实际需求的匹配方面,大部分开发者(58人)遇到生成代码与实际需求完全匹配无需修改的比例在40%-70%区间,12人遇到代码匹配比例在70%-100%这样较高的区间。 然而,有30人代码匹配比例低于40%。这说明DeepSeek-R1在代码生成方面有一定效果,但在部分复杂或特定场景下,仍有很大的提升空间。

By Ne0inhk
AI+游戏开发:如何用 DeepSeek 打造高性能贪吃蛇游戏

AI+游戏开发:如何用 DeepSeek 打造高性能贪吃蛇游戏

文章目录 * 一、技术选型与准备 * 1.1 传统开发 vs AI生成 * 1.2 环境搭建与工具选择 * 1.3 DeepSeek API 初步体验 * 二、贪吃蛇游戏基础实现 * 2.1 游戏结构设计 * 2.2 初始化游戏 * 2.3 DeepSeek 生成核心逻辑 * 三、游戏功能扩展 * 3.1 多人联机模式 * 3.2 游戏难度动态调整 * 3.3 游戏本地保存与回放 * 3.4 跨平台移植 * 《Vue.js项目开发全程实录/软件项目开发全程实录》 * 编辑推荐 * 内容简介 * 作者简介 * 目录 一、

By Ne0inhk
[DeepSeek] 入门详细指南(上)

[DeepSeek] 入门详细指南(上)

前言 今天的是 zty 写DeepSeek的第1篇文章,这个系列我也不知道能更多久,大约是一周一更吧,然后跟C++的知识详解换着更。 来冲个100赞兄弟们 最近啊,浙江出现了一匹AI界的黑马——DeepSeek。这个名字可能对很多人来说还比较陌生,但它已经在全球范围内引发了巨大的关注,甚至让一些科技巨头感到了压力。简单来说这 DeepSeek足以改变世界格局                                                   先   赞   后   看    养   成   习   惯  众所周知,一篇文章需要一个头图                                                   先   赞   后   看    养   成   习   惯   上面那行字怎么读呢,让大家来跟我一起读一遍吧,先~赞~后~看~养~成~习~惯~ 想要 DeepSeek从入门到精通.pdf 文件的加这个企鹅群:953793685(

By Ne0inhk