当前位置: 首页 > news >正文

vector的增删改查模拟实现(简单版)【C++】

目录

前言:

1. vector的私有成员

2. 构造函数和析构函数

3. vector遍历

3.1 push_back

3.2 下标访问、迭代器、范围for 遍历

3.2.1 下标访问

3.2.2 迭代器

3.2.3 范围for 

3.2.4 print_vector

3.3 测试遍历

4. vector的增删改查

4.1 reserve

4.2 resize

4.3 insert

4.4 erase

4.5 push_back 现代写法

4.6 pop_back

5. 拷贝构造

6. swap

7. operator=

8. 迭代器区间构造

9. 迭代器失效

9.1 关于 insert 迭代器失效

9.2 关于erase迭代器失效 

10. 源码


前言:

模拟实现vector是为了更好地掌握vector的使用

std::vector是C++ 标准模板库(STL)中的动态数组容器。

准备工作:

首先在自己创建的my_vector的命名空间中创建一个头文件vector.h,再定义一个vector类,同时创建一个test.cpp文件来调用测试。

1. vector的私有成员

vector的私有成员 和 string的私有成员不一样,vector主要使用的是迭代器iterator的类型。

#include<iostream>
#include<assert.h>using namespace std;
namespace my_vector
{template <class T>class vector {public:typedef T* iterator;//定义了一个名为 iterator 的类型别名,实际是 T*private:iterator _start = nullptr;    //指向动态数组的起始地址(即第一个元素的位置)iterator _finish = nullptr;   //指向当前最后一个有效元素的下一个位置(相当于size()的终点)iterator _endofstorage = nullptr;    //指向整个存储空间的末尾(即容量 capacity() 的终点)};}
template <class T>
class vector 

这是一个类模板,允许用户指定任意类型T(例如 int、string等)来创建不同类型的vector,例如:vector<int>、vector<string>等。

2. 构造函数和析构函数

		//constructorvector(){}//destructor~vector() {delete[] _start;_start = _finish = _endofstorage = nullptr;}

3. vector遍历

3.1 push_back

在实现遍历之前,首先保证vector里面有元素,所以先实现一个尾插元素。

传统写法:

先判断是否需要库容,之后再插入元素。扩容的时候,先开辟新空间,再拷贝数据,释放旧空间,指向新空间,修改指针,最后添加数据。

在实现push_back之前要先实现 size() 和 capacity 分别来获取 数据个数和总容量。

		size_t capacity() const{return _endofstorage - _start;}size_t size() const{return _finish - _start;}

注意在修改_finish的时候不能单纯的加size(),因为size()是_finish 与 _start两个指针相减的结果,而此时_finish还没有更新仍指向旧空间。

		void push_back(const T& val) {//先判断是否要扩容if (_finish == _endofstorage) {size_t old_size = size();//扩容size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];//拷贝数据memcpy(tmp,_start,sizeof(T)*size());//释放就空间delete[] _start;//修改指针_start = tmp;//注意//_finish = tmp + size(); //error _finish = tmp + old_size;_endofstorage = tmp + newcapacity;}//填充数据//_start[size()] = val;*_finish = val;++_finish;}

解决方法:在扩容前,记录好size()

3.2 下标访问、迭代器、范围for 遍历

3.2.1 下标访问

先判断位置是否合法,再访问数据。注意要重载出 非const和const的。

		//允许修改元素T& operator[](size_t pos) {//先判断访问位置是否合法assert(pos < size());//返回数据return _start[pos];}//仅访问元素const T& operator[](size_t pos) const{//先判断访问位置是否合法assert(pos < size());//返回数据return _start[pos];}
3.2.2 迭代器

迭代器同样分为const和非const的。

		typedef T* iterator;typedef const T* const_iterator;iterator begin() {return _start;}iterator end() {return _finish;}const_iterator begin() const {return _start;}const_iterator end() const {return _finish;}
3.2.3 范围for 

范围for是iterator的替换。

		//范围forfor (auto e : v){cout << e << " ";}cout << endl;
3.2.4 print_vector

 这里实现一个函数,直接来打印vector。

范围for实现:

	template<typename T>void print_vector(const vector<T>& v) {for (auto e : v) {cout << e << " ";}cout << endl;}

注意: 

如果想用迭代器实现的时候注意:

 编译器编译的时候,语法检查,确保模板实例化,一个类里面的内嵌类型或者内部类,vector<T>没有示例化,无法去找,就区分不出是类型还是静态成员遍历。

解决方法加上typename 告诉编译器是一个类型。

 

3.3 测试遍历

	template<typename T>void print_vector(const vector<T>& v) {//vector<T>::const_iterator it = v.begin(); //errortypename vector<T>::const_iterator it = v.begin(); //truewhile (it != v.end()){cout << *it << " ";++it;}cout << endl;}//遍历void test_vector1() {vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);for (size_t i = 0; i < v.size();i++){cout << v[i] << " ";}cout << endl;vector<int>::iterator it = v.begin();//auto it = v.begin();	//这样写更方便while (it != v.end()){cout << *it << " ";++it;}cout << '\n';//范围forfor (auto e : v){cout << e << " ";}cout << endl;}

4. vector的增删改查

4.1 reserve

reserve用于预分配内存空间,避免频繁的动态内存分配。

  • 如果 n > capacity() 就进行扩容
  • 如果 n <= capacity() 什么都不做

根据标准库:

注意提前保存好size()。

		void reserve(size_t n) {if (n > capacity()) {//扩容时注意提前保存好size()size_t old_size = size();//开新空间,扩容T* tmp = new T[n];//拷贝数据memcpy(tmp,_statr,sizeof(T)*size());//释放旧空间delete[] _start;//修改指针_start = tmp;//_finish = tmp + size(); //error ,因为 _finsih仍指向旧空间_finish = tmp + old_size;_endofstorage = tmp + n;}}

但是仍有问题:

如果T是string的时候就有问题,如图

我们发现 memcpy 是按照字节拷贝

memcpy 是按字节复制数据的,它执行的是浅拷贝。对于简单的数据类型(如 intdouble),这没有问题。但对于包含动态分配内存的复杂类型(如 string),memcpy 会复制 string 对象的字节,包括其内部的指针(如 string 的 char* 指针),而不会复制指针指向的实际数据

 虽然_start指向的数据是深拷贝,但是_str指向的数据是浅拷贝

解决方法:使用拷贝构造函数来移动实际的数据。

		void reserve(size_t n) {if (n > capacity()) {//扩容时注意提前保存好size()size_t old_size = size();//开新空间,扩容T* tmp = new T[n];//拷贝数据//memcpy(tmp,_start,sizeof(T)*size());	//error,vector<string> 存在隐藏的浅拷贝问题//for (size_t i = 0; i < size();i++){tmp[i] = _start[i];}//释放旧空间delete[] _start;//修改指针_start = tmp;//_finish = tmp + size(); //error ,因为 _finsih仍指向旧空间_finish = tmp + old_size;_endofstorage = tmp + n;}}

4.2 resize

resize 可以直接调整 vector 的有效元素数量(size(),并可以指定新增元素的默认值。

参考标准库里面的resize

如果 n > size()就扩容并插入数据,否则就删除数据。

这里缺省参数使用匿名函数的原因:

无参的匿名函数,会去调对应的默认构造函数来初始化,因为这样可以给到合适缺省值。 

测试:

4.3 insert

在pos位置进行插入值value,首先判断插入位置是否合法,插入数据前要判断是否需要扩容,之后再移动数据,最后把数据插入。

		//pos位置进行插入void insert(iterator pos, const T& value) {//判断插入位置是否合法assert(pos >= _start);assert(pos <= _finish);//数据插入前,先判断是否需要库容if (_finish == _endofstorage) {reserve(capacity() == 0 ? 4 : 2*capacity());}//移动数据iterator it = _finish - 1;while (it >= pos)	//注意这个时候的pos指向的仍是就空间{*(it + 1) = *it;--it;}//插入数据*pos = value;++_finish;}

但是发现一下问题:

 插入多个数据出现了问题,在调试插入元素的时候发现

这里我们发现it的地址编号 比pos的地址编号小,这样就无法进入while循环来移动元素了。

原因迭代器失效

在判断空间是否需要进行扩容的时候,发生扩容,pos指向的旧空间被释放回收(可以理解为导致了野指针)。

解决方法:让其指向新空间的pos,提前判断是否扩容前,计算好相对的位置。

正确代码:

		//pos位置进行插入void insert(iterator pos, const T& value) {//判断插入位置是否合法assert(pos >= _start);assert(pos <= _finish);//数据插入前,先判断是否需要库容if (_finish == _endofstorage) {size_t len = pos - _start;    //提前计算好相对的位置reserve(capacity() == 0 ? 4 : 2*capacity());//更新pospos = _start + len;}//移动数据iterator it = _finish - 1;while (it >= pos)	//注意这个时候的pos指向的仍是就空间{*(it + 1) = *it;--it;}//插入数据*pos = value;++_finish;}

测试一下;

4.4 erase

参考一下标准库:

先检查删除位置是否合法,再进行移动元素来完成删除。

		void erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);//移动元素iterator it = pos+1;while(it < _finish) {*(it - 1) = *it;++it;}--_finish;}

 移动元素如图:

测试:

4.5 push_back 现代写法

 通过复用其他函数的方式来实现。

		void push_back(const T& val) {//先判断是否要扩容if (_finish == _endofstorage) {reserve(capacity() == 0 ? 4 : 2*capacity());}//填数据*_finish = val;++_finish;}

push_back 现代写法plus版:

		void push_back(const T& val) {insert(end(),val);}

4.6 pop_back

		bool empty() {return _start == _finish;}void pop_back(){//尾删//方式1//assert(!empty());//--_finish;//方式2erase(end()-1);}

测试:

5. 拷贝构造

当没有去手动实现去拷贝构造时

发现 这里对内置类型的拷贝构造仍然时浅拷贝,这里和string一样 都是浅拷贝问题,这里需要手动实现一个拷贝构造。

		//v2(v1)vector(const vector<T>& v) {reserve(v.capacity());	//避免频繁扩容for (auto& e : v)	//auto& 避免拷贝{push_back(e);}}

6. swap

 这里交换实现和string的类似,都是去调用标准库哦里面的swap去交换成员变量。

		void swap(vector<T>& v) {std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_endofstorage,v._endofstorage);}

7. operator=

赋值操作符重载

将一个对象赋值给另一个对象,这里复用swap的写法。

vector<T>& operator=(vector<T> v) {swap(v);return *this;}

8. 迭代器区间构造

		//迭代器区间构造template<class InputIterator>	//类模板里面还可以有函数模板vector(InputIterator first, InputIterator last) {//其他容器的迭代器都可以用while (first != last) {push_back(*first);++first;}}

 还需要注意:

		vector(size_t n,const T& val = T()) {reserve(n);for (size_t i = 0; i < n; i++) {push_back(val);}}

 解决方法: 将size_t 改为int 这样就可以保证参数匹配了

9. 迭代器失效

9.1 关于 insert 迭代器失效

在前面的insert函数里面已经去解决迭代器失效,但是void insert(iterator pos, const T& value),这里的pos参数是形参,形参的改变不影响实参。那给pos加上引用呢?也不可以,在函数调用传参的时候,传的是临时对象,临时对象又具有常性,那再加上const ,如果加上const ,那insert里面的就没有办法修改了。

it 使用insert以后,迭代器it就是失效了,所以就不要使用,重新更新迭代器it。

9.2 关于erase迭代器失效 

出现的问题:

在去写删除偶数的函数时,重复的偶数没有删除。

情况1:跳过了一些值

原因:erase的删除是通过元素移动来覆盖前面的元素

 情况2:错过了end()

 

解决方法: 

		auto it = v.begin();while (it != v.end()) {if (*it % 2 == 0){v.erase(it);}else {it++;}}

但是仅仅这样写是解决不了VS环境下的vector的问题。还需要参考标准库。

这里的erase会返回被删除后面的那个元素。

		iterator erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);//移动元素iterator it = pos + 1;while(it < _finish) {*(it - 1) = *it;++it;}--_finish;return pos;}

迭代器失效以后,不要直接使用,如果要使用按规则重新更新后使用。

10. 源码

vector简单实现源码请猛戳这里

http://www.dtcms.com/a/308444.html

相关文章:

  • 【ProtoBuf】ProtoBuf安装
  • 力扣面试150(45/150)
  • 【C语言】深度剖析指针(三):回调机制、通用排序与数组指针逻辑
  • esp32s3 + ov2640,给摄像头加上拍照功能,存储到sd卡
  • 109㎡中古风家装:北京业之峰在朝阳区绘就温馨画卷
  • 【实际项目1.2-西门子PLC的报警监控思路】
  • Java多线程详解(1)
  • C#反射的概念与实战
  • [2025CVPR-小样本方向]ImagineFSL:基于VLM的少样本学习的想象基集上的自监督预训练很重要
  • 三方支付详解
  • SQL 中 WHERE 与 HAVING 的用法详解:分组聚合场景下的混用指南
  • 大数据平台数仓数湖hive之拉链表高效实现
  • 深度学习入门:用pytorch跑通GitHub的UNET-ZOO项目
  • 云服务器数据库
  • Camx-查看sensor mode 和效果参数
  • (LeetCode 每日一题) 2683. 相邻值的按位异或 (位运算)
  • 网络操作系统与应用服务器-1
  • SIwave 中 SIwizard 的 500 多个标准列表
  • 代码详细注释:演示多线程如何安全操作共享变量,使用互斥锁避免数据竞争。
  • Linux 文件系统基本管理
  • minidocx: 在C++11环境下运行的解决方案(二)
  • 网络攻击新态势企业级安全防御指南
  • Git分支管理:每个分支为什么这么命名?
  • Acrobat DC 应用安全配置:沙箱防护、数字签名
  • 了解微前端和SSO单点登录
  • Linux/Ubuntu 系统中打开火狐firefox、chromium浏览器失败
  • (三)从零搭建unity3d机器人仿真:使用WheelCollider实现turtlebot轮子差速运动
  • Linux系统编程-gcc(黑马笔记)
  • 译 | 用于具有外生特征的时间序列预测模型TimeXer
  • JavaScript 大数运算!