C++初阶(十四)--STL--vector的模拟实现
文章目录
一、vector的基本结构
二、默认成员函数的实现
1.构造函数
2.拷贝构造函数
3.赋值运算符重载
4. 析构函数
三、迭代器相关函数
begin和end
四、容量和大小相关函数
size
capacity
reserve
resize
empty
五、修改容器的函数
push_back
pop_back
insert
erase
swap
六、访问容器相关函数
operator[]
七、迭代器失效
1.迭代器失效的概念
2.列如我们的 vector容器
3.如何避免迭代器失效问题
一、vector的基本结构
首先定义一个vector模版类,其中三个成员变量均为迭代器,且此处vector的迭代器是一个原生指针,我们这里为其定义别名iterator。
namespace My_Vector
{template<class T>//此处为类模板,实现不同数据类型的存储class vector{public://vector迭代器为原生指针typedef T* iterator;//定义指针类型的别名iteratortypedef const T* const_iterator;//...函数接口的实现private:iterator _start;// 指向容器的开始iterator _finish;// 指向容器中最后一个有效数据的下一个位置iterator _endofstorage; // 指向存储容量的结尾};
}
私有成员变量:
_start: 这是一个指针,指向容器的第一个元素。
_finish: 这个指针指向容器中最后一个有效数据的下一个位置。
_endofstorage: 这个指针指向分配给vector的内存块的末尾。这不是最后一个有效元素的位置,而是整个内存块的结束位置,在这之后可能会有额外的未初始化空间,预留以实现当vector增长时无需重新分配整个数组。
二、默认成员函数的实现
1.构造函数
构造函数1
vector首先支持一个无参的构造函数,对于这个无参的构造函数,我们直接将构造对象的三个成员变量都设置为空指针即可。
vector()//初始化列表构造:_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{}
构造函数2
其次,vector还支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,这个类型也有可能是一个类,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。
//构造函数3
// 类模板的成员函数,也可以是一个函数模板template <class InputIterator>vector(InputIterator first, InputIterator last){while (first != last)//左闭右开{push_back(*first);++first;}}
构造函数3
此外,vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。
vector(size_t n, const T& val)
{reseve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}
注意:
1)该构造函数知道其需要用于存储n个数据的空间,所以最好用reserve函数一次性开辟好空间,避免调用push_back函数时需要增容多次,导致效率降低。
2)该构造函数还需要实现两个重载函数。否则调用该构造函数的时候可能会出现一些问题。
vector(int n, const T& val)
{reseve(n);for (int i = 0; i < n; i++){push_back(val);}
}
vector(long n, const T& val)
{reseve(n);for (long i = 0; i < n; i++){push_back(val);}
}
可以看到,这两个重载函数与之不同的就是其参数n的类型不同,但这却是必要的,否则当我们使用以下代码时,编译器会优先与构造函数2相匹配。
vector<int> v(5, 7); //调用构造函数3 ???
构造函数4
vecotrh还支持使用initializer_list<T>构造,initializer_list是一个类,支持{}初始化。这个是在C++11中新增的。
实现过程和构造函数3类似。
vector(initializer_list<T> il){reserve(il.size());for (auto& e : il){push_back(e);}}
2.拷贝构造函数
拷贝构造函数的写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。
vector(vector<T>& v){reserve(v.capacity);for (auto e : v){pushu_back(e);}}
3.赋值运算符重载
这里的写法较为巧妙,可以参考前面的string模拟,首先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构。
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}
4. 析构函数
对容器进行析构时,首先判断该容器是否为空容器,若为空容器,则无需进行析构操作,若不为空,则先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针即可。
~vector()
{if (_start)//避免对空指针释放{delete[] _start;_start = nullptr;_finish = nullptr;_endofstorage = nullptr;}
}
三、迭代器相关函数
vector当中的迭代器实际上就是容器当中所存储数据类型的指针。
//vector迭代器为原生指针
typedef T* iterator;//定义指针类型的别名iterator
typedef const T* const_iterator;
begin和end
begin返回容器当中的首地址,end返回容器当中有效数据的下一个数据的地址
//可修改
iterator begin()
{return _start;//返回首地址
}
iterator end()
{return _finish;//返回容器当中有效数据的下一个数据的地址
}
//不可修改
再重载一对适用于const的begin和end,使得const对象调用begin和end时只能对数据进行读而不能进行修改。
//不可修改
const_iterator begin() const
{return _start;//返回首地址
}
const_iterator end() const
{return _finish;//返回容器当中有效数据的下一个数据的地址
}
四、容量和大小相关函数
size
两个指针相减的结果,就是这两个指针之间对应类型的数据个数,所以size可以由_finish - _start得到
szie_t size()
{return _finish-_start;
}
capacity
capacity可以由_endofstorage - _start得到。
size_t capacity()
{return _endofstorage - _start;
}
reserve
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
reserve函数的实现思路也是很简单的,先判断所给n是否大于当前容器的最大容量(否则无需进行任何操作),操作时直接开辟一块可以容纳n个数据的空间,然后将原容器当中的有效数据拷贝到该空间,之后将原容器存储数据的空间释放,并将新开辟的空间交给该容器维护,最好更新容器当中各个成员变量的值即可。
void reserve(size_t n)
{if (n > capacity()){size_t OldSize = size();T* tmp = new T[n];if (_start){// memcpy(tmp, _start, sizeof(T) * oldSize);for (size_t i = 0; i < size(); i++){tmp[i] = _start[i];}delete _start;}_start = tmp;_finish = _start + OldSize;_endofstorage = _start + n;}
}
在reserve函数的实现当中有两个地方需要注意:
1)在进行操作之前需要提前记录当前容器当中有效数据的个数。
因为我们最后需要更新_finish指针的指向,而_finish指针的指向就等于_start指针加容器当中有效数据的个数,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。
2)拷贝容器当中的数据时,不能使用memcpy函数进行拷贝。
可能你会想,当vector当中存储的是string的时候,虽然使用memcpy函数reserve出来的容器与原容器当中每个对应的string成员都指向同一个字符串空间,但是原容器存储数据的空间不是已经被释放了,相当于现在只有一个容器维护这这些字符串空间,这还有什么影响。
但是不要忘了,当你释放原容器空间的时候,原容器当中存储的每个string在释放时会调用string的析构函数,将其指向的字符串也进行释放,所以使用memcpy函数reserve出来的容器当中的每一个string所指向的字符串实际上是一块已经被释放的空间,访问该容器时就是对内存空间进行非法访问。
所以说我们还是得用for循环将容器当中的string一个个赋值过来,因为这样能够间接调用string的赋值运算符重载,实现string的深拷贝。
resize
resize规则:
1、当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
2、当n小于当前的size时,将size缩小到n。
根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。
void resize(const T& n,const T& val = T())
{if(size()>=n){_finish = _start + n;}else{reserve(n);while (_finish <_start + n){*_finish = val;_finish++;}}
}
empty
empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。
bool empty() const
{return _finish == _start;
}
五、修改容器的函数
push_back
这里是老套路,插入数据先考虑要不要扩容,若已满,则扩容,然后将数据放到_finish指向的位置,_finish再往后移。
void push_back(const T& x)
{if (size() == capacity()){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish = x;_finish++;
}
pop_back
这里做一下判空处理
//尾删数据
void pop_back()
{assert(!empty()); //容器为空则断言_finish--; //_finish指针前移
}
insert
insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
注意: 若需要增容,则需要在增容前记录pos与_start之间的间隔,然后通过该间隔确定在增容后的容器当中pos的指向,否则pos还指向原来被释放的空间。
iterator insert(iterator pos, const T& val)
{assert(pos <= _finish && pos <= _start);if (_finish == _endofstorage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);//这个时候的_start已经改变,pos的对应位置也应该改变pos = _start + len;}iterator end = _finish;while (end > pos){*end = *(end - 1);end--;}*pos = val;_finish++;return pos;
}
这里我们给了返回值,涉及到迭代器失效问题,会在后面讲到,往下翻。
erase
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器是否为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
iterator erase(iterator pos)
{assert(pos >= _start);assert(pos < _finish);iterator i = pos + 1;while (i < _finish){*(i - 1) = *i;++i;}_finish--;return pos;
}
swap
swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。
void swap(vector<T> v)
{std::swap(_start, v._start);std::swap(_start, v._finish);std::swap(_endofstorage, v._endofstorage);
}
六、访问容器相关函数
operator[]
vector也是支持下标访问的。
T& operator[](size_t i)
{assert(i < size());return _start[i];
}const T& operator[](size_t i) const
{assert(i < size());return _start[i];
}
七、迭代器失效
1.迭代器失效的概念
- 在 C++ 中,迭代器是一种用于遍历容器(如
vector
、list
、map
等)中元素的对象。迭代器失效是指当容器的内部结构发生改变时,原来指向容器中元素的迭代器可能不再有效,使用这些失效的迭代器可能会导致程序出现未定义行为,如程序崩溃、错误的结果等。
2.列如我们的 vector
容器
- 插入操作导致的失效:
- 当在
vector
中插入元素时,如果插入操作导致了容器的内存重新分配,那么所有指向该容器的迭代器都会失效。这是因为vector
的存储结构是连续的内存空间,当元素数量增加到超过当前容量时,vector
会重新分配一块更大的内存空间来存储元素,并将原来的元素复制(或移动)到新的内存空间中。 - 例如,在一个
vector<int> v
中,如果v.capacity() < v.size() + n
(n
为要插入元素的数量),就会发生内存重新分配。假设vector
原来有一些迭代器指向其中的元素,在重新分配内存后,这些迭代器所指向的原来的内存地址已经不再是vector
元素的有效地址了。 - 不过,如果插入操作没有导致内存重新分配,那么只有指向插入位置及之后元素的迭代器会失效。这是因为插入元素后,插入位置之后的元素会向后移动,原来指向这些元素的迭代器就不再指向正确的元素了。
- 当在
- 删除操作导致的失效:
- 当从
vector
中删除元素时,指向被删除元素的迭代器会立即失效。而且,指向被删除元素之后元素的迭代器也可能失效,因为删除元素后,后面的元素会向前移动,这些迭代器可能不再指向正确的元素
- 当从
3.如何避免迭代器失效问题
方法一:及时更新迭代器
- 在进行可能导致迭代器失效的容器操作后,及时重新获取有效的迭代器。例如,在
vector
中插入元素后,如果担心迭代器失效,可以在插入操作后重新定位迭代器。假设it
是一个指向vector
元素的迭代器,在插入元素后,可以通过it = v.insert(it, value);
(v
是vector
容器,value
是要插入的值)来插入元素并更新迭代器it
,这样it
就会指向新插入的元素,之后可以继续安全地使用这个迭代器进行遍历等操作。
方法二:使用返回值更新迭代器
我们这里使用的是这个方法
- 很多容器的插入和删除操作会返回有用的信息,比如
vector
的erase
操作会返回一个迭代器,指向被删除元素之后的元素。可以利用这个返回值来更新迭代器,避免使用已经失效的迭代器。例如,it = v.erase(it);
,这里v
是vector
容器,it
是指向要删除元素的迭代器,执行erase
操作后,it
就被更新为指向被删除元素之后的元素,这样可以继续安全地使用it
进行后续操作。
我们只有深入了解不同容器的内部结构和操作对迭代器的影响,才能真正有效的避免这个问题,比如,当使用
vector
时,要特别注意插入和删除操作可能导致的迭代器失效情况;而在使用list
时,就可以相对放心地进行插入和删除操作,只要正确处理被操作元素的迭代器即可。对于map
和unordered_map
,在大多数情况下,迭代器失效的情况相对较少,但也要注意特殊情况。
本篇博客到此结束,欢迎各位评论区留言~
相关文章:

C++初阶(十四)--STL--vector的模拟实现
文章目录 一、vector的基本结构 二、默认成员函数的实现 1.构造函数 2.拷贝构造函数 3.赋值运算符重载 4. 析构函数 三、迭代器相关函数 begin和end 四、容量和大小相关函数 size capacity reserve resize empty 五、修改容器的函数 push_back pop_back insert…...

贴代码框架PasteForm特性介绍之query,linkquery
简介 PasteForm是贴代码推出的 “新一代CRUD” ,基于ABPvNext,目的是通过对Dto的特性的标注,从而实现管理端的统一UI,借助于配套的PasteBuilder代码生成器,你可以快速的为自己的项目构建后台管理端!目前管…...

高防IP如何构建安全高效的数字政务新生态
随着数字化转型浪潮的日渐汹涌,政务行业也在朝着智慧政务的方向高速迈进,提升了为民服务的整体效率。然而,凡事都有双面性,随着政务服务线上化的深入发展,网络安全威胁也日益严峻。黑客攻击、DDoS攻击、CC攻击等安全事…...

数据结构与算法——1122—复杂度总结检测相同元素
1、复杂度总结 1、时间复杂度计算遵循的原则 1、复杂度与其具体的常系数无关(即:常数项的系数不要) 2、多项式级复杂度相加的时候,把其高项作为结果(即:多项式只保留最大项) 3、O(1)含义为&…...

HTML通过JavaScript获取访问连接,IP和端口
<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <title>Get IP Address</title> <script> function displayURL() { var url window.location.href; // 获取当…...

自动化测试过程操作细节
一、软件与框架介绍 1. Postman 读音:[pəʊstmən](剖斯特曼) 介绍:API开发与测试的得力助手,通过直观界面发送HTTP请求,查看响应数据。支持环境变量、集合、脚本等功能。 主要特点:易于使用…...

AR智能眼镜|AR眼镜定制开发|工业AR眼镜方案
AR眼镜的设计与制造成本主要受到芯片、显示屏和光学方案的影响,因此选择合适的芯片至关重要。一款优秀的芯片平台能够有效提升设备性能,并解决多种技术挑战。例如,采用联发科八核2.0GHz处理器,结合12nm制程工艺,这种低…...

从〇开始深度学习(0)——背景知识与环境配置
从〇开始深度学习(0)——背景知识与环境配置 文章目录 从〇开始深度学习(0)——背景知识与环境配置写在前面1.背景知识1.1.Pytorch1.2.Anaconda1.3.Pycharm1.4.CPU与GPU1.5.整体关系 2.环境配置2.1.准备工作2.1.1.判断有无英伟达显卡2.1.2.清理电脑里的旧环境 2.1.安装Anaconda…...

实验室管理技术革新:Spring Boot系统
4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式,是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示: 图4-1系统工作原理…...

C语言 蓝桥杯某例题解决方案(查找完数)
蓝桥杯原题: 一个数如果恰好等于它的因子之和,这个数就称为“完数”。例如6 1 2 3.编程找出1000以内的所有完数。 这个题没有很大的难点,与我们上一个解决的问题“质因数分解”不同,它不需要判断因数是否是质数,因此…...

Prompting LLMs to Solve Complex Tasks: A Review
文章目录 题目简介任务分解未来方向结论 题目 促使 LLM 解决复杂任务: 综述 论文地址:https://www.intjit.org/cms/journal/volume/29/1/291_3.pdf 简介 大型语言模型 (LLM) 的最新趋势显而易见,这体现在大型科技公司的投资以及媒体和在线社…...

C++ 编程指南05 - 编译时检查优于运行时检查
一:概述 编译时错误检查是C编程中一条非常重要的原则,它强调了在可能的情况下,应该优先依赖编译时检查(静态检查)而不是运行时检查。这样做的主要目的是提高程序的性能、安全性和可维护性。 编译时检查,即在…...

【优先算法】专题——双指针
1.移动零 移动零 题目描述: 思路: 本题我们把数组分块,将非零元素移动到左边,为零元素移动右边。 我们使用双指针算法(利用数组下标来充当指针) 两个指针的作用: cur:从左往右…...

CSP/信奥赛C++语法基础刷题训练(23):洛谷P1217:[USACO1.5] 回文质数 Prime Palindromes
CSP/信奥赛C语法基础刷题训练(23):洛谷P1217:[USACO1.5] 回文质数 Prime Palindromes 题目描述 因为 151 151 151 既是一个质数又是一个回文数(从左到右和从右到左是看一样的),所以 151 151 …...

C语言练习.if.else语句.strstr
今天在做题之前,先介绍一下,新学到的库函数strstr 想要使用它,要先给它一个头文件<string.h> char *strstr(const char*str1,const char*str2); 首先:1.strstr的返回值是char,字符类型的。 2.两个实参ÿ…...

利用浏览器录屏
以下内容参考自网络 <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title></title> </head> <body> <div class"left"> <di…...

python中的map、split、join函数的作用 => ACM输入输出流
map(func,iter) lst_str ["1", "2", "3"] # 得到lst_num为[1, 2, 3] lst_num list(map(int, lst_str))如果想把一个列表里的所有元素批量地调用某一个函数,并映射得到一个新的列表(原列表中元素相对位置不变࿰…...

Ubuntu20.04下安装向日葵
向日葵远程控制app官方下载 - 贝锐向日葵官网 下载Ununtu版的图形版本的安装deb包SunloginClient_15.2.0.63064_amd64.deb 直接执行 sudo dpkg -i SunloginClient_15.2.0.63064_amd64.deb 的话会报错: 如果在Ubuntu20.04里直接执行sudo apt install libgconf-2-4安装libgco…...

常用并发设计模式
避免共享的设计模式 不变性(Immutability)模式,写时复制(Copy-on-Write)模式,线程本地存储(Thread-Specific Storage)模式本质上都是为了避免共享。 1、使用时需要注意不变性模式…...

Redis Search系列 - 第七讲 Windows(CygWin)编译Friso
目录 一、背景二、安装CygWin三、编译Friso四、运行Friso五、Friso分词效果测试 一、背景 最近在做RedisSearch的中文分词效果调研,底层的中文分词插件使用的就是Friso,目前手里的Linux环境上yum镜像仓库有问题导致没法安装gcc,又急于验证Fr…...

利用Docker容器技术部署发布web应用程序
Docker是什么? docker 是一个开源的应用容器引擎,可以帮助开发者打包应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何…...

[免费]SpringBoot+Vue毕业设计论文管理系统【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的SpringBootVue毕业设计论文管理系统,分享下哈。 项目视频演示 【免费】SpringBootVue毕业设计论文管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 现代经济快节奏发展以及不断完善升级的信…...

BFS 算法专题(五):BFS 解决拓扑排序
目录 1. 拓扑排序简介 1.1 有向无环图 (DAG 图) 1.2 AOV 网(顶点活动图) 1.3 拓扑排序 1.3.1 如何实现 2. 力扣实战应用 2.1 课程表 2.1.1 算法原理 2.1.2 算法代码 2.2 课程表 II 2.2.1 算法原理 2.2.2 算法代码 2.3 火星词典 (hard) (原剑指offer) 2.3.1 算法原理…...

【Mysql】开窗聚合函数----SUM,AVG, MIN,MAX
1、概念 在窗口中,每条记录动态地应用聚合函数(如:SUM(),AVG(),MAX(),MIN(),COUNT(),)可以动态计算在指定的窗口内的各种聚合函数值。 2、操作 以下操作将基于employee表进行操作。 sum() 进行sum的时候,没有order …...

java操作doc——java利用Aspose.Words操作Word文档并动态设置单元格合并
在实际工作中,如果业务线是管理类项目或者存在大量报表需要导出的业务时,可以借助第三方插件实现其对应功能。 尤其是需要对word文档的动态操作或者模板数据的定向合并,使用Aspose会相对来说容易一些,而且相关文档比较完整&#…...

探索 .NET 9 控制台应用中的 LiteDB 异步 CRUD 操作
本文主要是使用异步方式,体验 litedb 基本的 crud 操作。 LiteDB 是一款轻量级、快速且免费的 .NET NoSQL 嵌入式数据库,专为小型本地应用程序设计。它以单一数据文件的形式提供服务,支持文档存储和查询功能,适用于桌面应用、移动…...

《进程隔离机制:C++多进程编程安全的坚固堡垒》
在当今数字化时代,软件系统的安全性愈发成为人们关注的焦点。尤其是在 C多进程编程领域,如何确保进程间的安全交互与数据保护,是每一位开发者都必须面对的重要课题。而进程隔离机制,犹如一座坚固的堡垒,为 C多进程编程…...

构建无障碍的数字世界:深入探讨Web可访问性指南
文章目录 前言一、什么是Web可访问性?二、Web内容无障碍指南(WCAG)三、ARIA属性的应用四、实践中的Web可访问性结语 前言 在当今高度互联的世界里,互联网已成为人们日常生活不可或缺的一部分。然而,对于数百万残障人士…...

跨境出海安全:如何防止PayPal账户被风控?
今天咱们聊聊那些让人头疼的事儿——PayPal账户被风控。不少跨境电商商家反馈,我们只是想要安安静静地在网上做个小生意,结果不知道为什么,莫名其妙账户就被冻结了。 但其实每个封禁都是有原因的,今天就来给大家分享分享可能的原…...

学习日记_20241123_聚类方法(MeanShift)
前言 提醒: 文章内容为方便作者自己后日复习与查阅而进行的书写与发布,其中引用内容都会使用链接表明出处(如有侵权问题,请及时联系)。 其中内容多为一次书写,缺少检查与订正,如有问题或其他拓展…...