[STL] vector 模拟实现详解

news2024/11/17 9:51:55

目录

一,准备工作

二,push_back  

1, 关于引用

2. 参数const 的修饰

 补充

三,迭代器实现

四,Pop_back

五,insert

1. 补充——迭代器失效

六, erase

七,构造函数 

1. 迭代器构造 

2. 其他构造

3. 拷贝构造 

1) 传统写法

2)现代写法(提高函数复用性) 

八,赋值符号重载

九,resize


 

一,准备工作

      准备工作中,需要前面所学的,命名空间类模板知识,以及我们实现之前需要借鉴一下STL源代码如何实现。

开始实现前,我们先熟悉一下vector  的框架:

// 头文件
#include <iostream>
#include <vector>
using namespace std;

namespace my_vector  // 里面我们使用的类也叫vector,命名空间隔离,避免与STL中的vector命名重复
{
	template <class T>  // 这里缺了内存池的模板,后面再学习
	class vector
	{
		typedef T* iterator;  // vector在物理空间上是一段连续的空间,所有这里的迭代器就是指针
	public:

	private:
		iterator _start;  // 迭代器开始位置
		iterator _finish;  // 当前迭代器指向位置,相当于size
		iterator end_of_storage;  // 该段迭代器的最终位置
	};
}

// 测试文件///
#include "my_vector.h"

int main()
{
	my_vector::vector<int> p1; // 使用自己的vector需要进行标识命名空间,否则用的就是STL中vector
	return 0;
}

二,push_back  

void push_back(const T& data) // 这里有关 两个问题的探讨,1. const 修饰; 2. 引用
		{
		}

     这里有我们需要探讨的两个点: 1.  参数 const 修饰   ; 2. 关于引用        

先讲关于引用

1, 关于引用

   我们的数据是int,  char还好,我们不使用引用,值拷贝也不太差, 但我们的参数string类,vector<T>等等,这时string就是深拷贝,性能浪费太大。因此,这里选择引用。

2. 参数const 的修饰

   我们看下面场景

int main()
{
	my_vector::vector<int> p1;   // 使用自己的vector需要进行标识命名空间,否则用的就是STL中的vector

    // 场景一:参数是 变量对象
	int a = 10;
	p1.push_back(a);  // ok的

	// 场景二:参数是临时变量,或者匿名对象
	p1.push_back(10);  // 挪,没有const 修饰,无法接收临时数据
	my_vector::vector<string> p2;
	p2.push_back(string("xxxxx"));
	return 0;
}

 

结论: 1. 为了保证面对各种数据类型,vector都能有比较高的性能,引用是需要的; 2. 对数据进行const 修饰 能提高对数据的兼容。

 这里是push_back + operator[] 的实现代码:

        size_t size() const  // 针对被const修饰的对象,使其兼容
		{
			return _finish - _start;
		}

		size_t capacity() const
		{
			return end_of_storage - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* new_start = new T[n];
			  if (_start) // 如果旧空间不是空,则需要拷贝备份
			  {
				  // memcpy(new_start, _start, sizeof(T) * size());  为啥不直接选择memcopy
                  // 这里需要重点讲解
                   for (size_t i = 0; i < n; i++)
				  {
					  new_start[i] = _start[i];  // 如果是自定义类型,则会调用其operator
				  }
				  delete[] _start;
			  }
			  _finish = new_start + size();
			  _start = new_start;
			  end_of_storage = new_start + n;
		    }
		}

		T& operator[](size_t pos)
		{
			assert(pos < size());   //进行越界判断 
			return _start[pos];
		}


		void push_back(const T& data)   // 这里有关 两个问题的探讨,1. const 修饰; 2. 引用
		{
			if (_finish == end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finish = data;
			_finish++;
		}

 补充

为啥不用memcpy那段代码?
 

如果你一直使用 int 这样的内置类型作为 T ,那你会很难发现。当数据是自定义类型时,外层是深拷贝,而内部还是浅拷贝

同理,传统写法的拷贝构造我们也可以完善(拷贝构造在下面)

三,迭代器实现

       

 STL中, 我们可以瞧见,有两种版本的迭代器,不同的是对象是否是const的对象。

分析: const成员函数中,this 显示表现是:const   T&   const   this , 是不允许修改内容的缩小权限,因此需要我们实现一些函数时,需要2个版本。

诺:

        
        iterator begin()
		{
			return _start;
		}

        iterator end()
		{
			return _finish;
		}

        void func(const vector<T>& v)
		{
			const_iterator it = v.begin();
			while (it != v.end())
			{
				cout << *it << endl;
				it++;
			}
		}

没有const版本的迭代器,在接收const对象时就可能出现这种类型错误: 

 更改很简单,完整的实现如下:

        iterator begin()
		{
			return _start;
		}

		iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		iterator end() const
		{
			return _finish;
		}

不止是begine()函数,其他成员函数,在特定情况下也是需要const修饰版本

 既然完成了迭代器begine, end实现,那范围for面对const对象时,就能解解决了。(范围for底层仅仅只是替换为迭代器访问)

诺:

    my_vector::vector<int> p1;
    const my_vector::vector<int> p2;

	for (cosnt auto& i : p1)  // 调用普通迭代器  (这里的const与&,兼顾效率与兼容性)
	{
		cout << p1[i] << " ";
	}

	for (cosnt auto& i : p2)  // 调用const迭代器
	{
		cout << p1[i] << " ";
	}

四,Pop_back

        void Pop_back()
		{
			assert(_finish > _start);
			_finish--;
		}

五,insert

     关于插入,在C语言插入思想大差不差。

         void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			if (size() + 1 >= capacity())
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
	
			if (pos < _finish)
			{
				iterator end = _finish;
				while ( end != pos)
				{
					*end = *(end - 1);
					end--;
				}
				*pos = val;
				_finish++;
			}
			else
			{
				push_back(val);
			}
		}

这里埋个雷,看大家能排出来吗?迭代器失效再完善上面代码。 

1. 补充——迭代器失效

     基于前面所写的代码,我们来实验一下面代码。

void func1()
{
	my_vector::vector<int> p2;
	p2.push_back(1);
	p2.push_back(2);
	p2.push_back(3);
	p2.push_back(4);
	p2.push_back(5);

	auto pos = find(p2.begin(), p2.end(), 3);
	p2.insert(pos, 100); // insert一般搭配find算法,一般情况下是不知道数据的迭代器
	for (const auto& n : p2)
	{
		cout << n << " ";
	}
}

 现在运行没有问题,但我们将p2.push_back(5)注释掉后我们再运行。结果是:

 

???!!! 有bug??!!!  

 这里就不卖关子,错误原因:

        在该场景下,是既要插入,又要扩容。而迭代器pos本质是指针,由于扩容后,pos保留旧空间的位置,pos数据未更新,野指针错误。

修改方法也很简单,完善一下pos更新机制 :

 好了,那迭代器失效到底想告诉我们啥?   

 迭代器失效————在我们插入数据之后的pos,是有可能失效了的,尽量不要用pos访问数据。

这里我们可能会有疑问?我们不是已经更新了pos了吗? 

解答:  那是在insert函数中修改,值传递,行参不影响实参。(请原谅我会问出这样的问题【苦笑不得】)

另外,有的人会说,那我行参我传pos的引用,在此环境下,确实可以解决此问题,但我的回答是尽量跟STL保持一致,设计框架上的修改往往牵一发而动全身,我们现在还把握不住。

六, erase

   比较简单,就是向前替换数据。如:vector<int>  p1(10, 2);   初始10个数据,初始值为2.

        // STL中要求erase返回被删除位置的下一个位置的迭代器
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			while (pos + 1 != _finish)
			{
				*pos = *(pos + 1);
				pos++;
			}
			_finish--;
			return pos;
		}

 那么这里存在迭代器失效的问题吗? 结果:我们所实现的erase不会导致迭代器失效,那STL库里面的呢? 答案是看编译器。  (我们知道STL是一个标准库,旨在告诉大家要实现C++库函数的功能标准,具体实现得看编译器如何实现,比如:一些编译器,在实现erase时,可能会进行缩容,这样迭代器就可能失效)

 总结一下迭代器失效:insert / erase的   pos位置,一定要更新; 不要直接访问,可能会出现出乎意料的结果

七,构造函数 

      开头我们已经写了一个简单的构造函数,这里将补充:迭代器构造, 拷贝构造。

1. 迭代器构造 

       template <class inputIterator>  // 提供一个接收参数迭代器的新模板
 25       vector (inputIterator first, inputIterator last)
 26             :_start(nullptr)
 27             ,_finish(nullptr)
 28             ,end_of_storage(nullptr)
 29       {
 30               while (first != last)
 31               {
 32                  push_back(*first);
 33                  first++;
 34               }                                                                                                     
 35       }

这里需要注意的是,这是构造函数,需要将数据初始化,不要忘了。

2. 其他构造

         实现一下,一次性初始化多个数据。 

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

  

但这样写会有一个bug ,就是当两参数都是int时,会与迭代器构造产生歧义,由于计算机会寻找最匹配的函数,因此会调用迭代器构造函数,而后面访问把int当做迭代器将会导致报错。 

那如何调整?? 

 其实很简单解决:

法一: 改size_t  为  int

vector(int n , const T& val = T()) 

法二: 保留size_t,  我们再重载一个int 的构造函数

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

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

3. 拷贝构造 

1) 传统写法

         // 传统写法           
E> 56     vector (const my_vector::vector<T>& v)       
   57       :_start(nullptr)              
   58       ,_finish(nullptr)           
   59       ,end_of_storage(nullptr)    
   60     {                                                      
   61        reserve(v.size());                                  
   62        // memcpy(_start, v._start, sizeof(T) * v.size() ); 
   63         for (size_t i = 0; i < v.size(); i++ )
   64         {
   65           _start[i] = v[i];                                                                                       
   66         }                                                                         
   67        _finish += v.size();                                                       
   68     }                        

2)现代写法(提高函数复用性) 

     void swap(my_vector::vector<T>& order)
 38     {
 39       std::swap(_start, order._start);
 40       std::swap(_finish, order._finish);
 41       std::swap(end_of_storage, order.end_of_storage);
 42     }
 43 
 44     // 拷贝构造
 45     vector (const my_vector::vector<T>& v )                                                                         
 46       : _start(nullptr)
 47       , _finish(nullptr)
 48       , end_of_storage(nullptr)
 49     {
 50       my_vector::vector<T> tmp(v.begin(), v.end());
 51       // 借用迭代器构造,然后交换数据。
 52       swap(tmp);
 53     }

八,赋值符号重载

这里对一个易错点进行分析:       有些同学会分不清 p2 = p1  & p2(Data) 

    void t()                                                                               
 81 {                                                                                      
 82   vector<int> p1 = 10; // 这是一个p1的初始化,等价于p1(10)
 83   vector<int> p2;                        
 84   p2 = p1;          // p2已经存在,这才是调用赋值重载函数                                                                                                                                                                  
 86 }      

关于这个赋值重载比较简便的写法: 

      vector<T>& operator= (vector<T> v)  // 不添加&是故意的
195     {                                                                                          
196        swap(v); 
197        return *this;                                                                                                
198     }  

这里提一下里面的思路:

九,resize

   调整数据大小的函数, 解决已经存在的对象进行修改大小。

        // 调整数据大小       
132     void resize(size_t n , const T& val = T())                                                                      
133     {
134        if (n > capacity())
135        {
136          reserve(n);
137        }
138 
139        if (n > size())
140        {
141          // 开始填充数据
142          while ( _finish < _start + n )
143          {
144            *_finish = val;
145            _finish++;
146          }
147        }else 
148        {
149          _finish = _start + n;
150        }
151     }


结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/757047.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

合并当天Log

1.原因&#xff0c; 我们程序运行Log很多时&#xff0c;如果因为要写Log话费很多时间&#xff0c;这时我们可以把log保存按照更短的时间保存&#xff0c;比如一分钟一个Log,一个小时一个log&#xff0c;。。。。但我们查看Log时很麻烦&#xff0c;需要把分散的Log合并起来的工…

移动端深度学习部署:TFlite

1.TFlite介绍 &#xff08;1&#xff09;TFlite概念 tflite是谷歌自己的一个轻量级推理库。主要用于移动端。 tflite使用的思路主要是从预训练的模型转换为tflite模型文件&#xff0c;拿到移动端部署。 tflite的源模型可以来自tensorflow的saved model或者frozen model,也可…

ylb-定时任务task

总览&#xff1a; 在api模块service包&#xff0c;创建IncomeService类&#xff1a;&#xff08;收益计划 和 收益返还&#xff09; package com.bjpowernode.api.service;public interface IncomeService {/*收益计划*/void generateIncomePlan();/*收益返还*/void generate…

基于mysql+java+springboot的福州大学生就业求职系统(含源码+系统演示视频)

1、系统演示视频&#xff1a;基于JavaMySQLspringboot的福州大学生就业求职系统演示视频 2、系统源码&#xff1a;系统源码链接 文章目录 一、需求分析1、公司招聘2、简历管理3、交流咨询 二、福州大学就业求职服务平台简介1.福州大学就业求职服务平台主要功能1.1.个人求职功能…

小黑子—JavaWeb:第一章 - JDBC

JavaWeb入门1.0 1. javaweb介绍2. 数据库设计2.1 约束2.2 表关系2.3 多表查询2.3.1 内连接&#xff08;连接查询&#xff09;2.3.2 外连接&#xff08;连接查询&#xff09;2.3.3 子查询 2.4 事务 3. JDBC3.1 JDBC 快速入门 4 JDBC API详解4.1 DriverManager4.2 Conncetion4.3 …

13_Linux无设备树Platform设备驱动

目录 Linux驱动的分离与分层 驱动的分隔与分离 驱动的分层 platform平台驱动模型简介 platform总线 platform驱动 platform设备 platform设备程序编写 platform驱动程序编写 测试APP编写 运行测试 Linux驱动的分离与分层 像I2C、SPI、LCD 等这些复杂外设的驱动就不…

吴恩达ML2022-用于手写数字识别的神经网络

1 用到的包 导入在这个分配过程中需要的所有包。 Numpy 是使用 Python 进行科学计算的基本软件包。Matplotlib 是在 Python 中绘制图形的流行库。tensorflow是一种流行的机器学习平台。 import numpy as np import tensorflow as tf from tensorflow.keras.models import Se…

Java对象导论

对象具有状态、行为和标识。每个对象都可以拥有内部数据&#xff08;它们给出了该对象的状态&#xff09;和方法&#xff08;它们产生的行为&#xff09;&#xff0c;并且每个对象在内存中都有一个唯一的地址&#xff08;标识&#xff09;。 抽象过程就是在问题空间元素和解空…

Macbook下提升开发效率的几个小工具

最近倒腾mac笔记本&#xff0c;记录下一些高效率的工具吧。 首先就是alfred&#xff0c;内置可以自定义各种快捷命令查找&#xff0c;配合Dash来快速查找C系统API&#xff0c;其实Dash中包含了各种编程所需API文档&#xff0c;值得下载。 以前我都是直接查看cppreference.c…

【分享】Redis的五种基本数据类型和应用场景

前言&#xff1a; Redis支持五种基本数据类型&#xff1a; String&#xff08;字符串类型&#xff09;&#xff1a;可以是普通字符串&#xff0c;也可以是整数或浮点数值。可以设置过期时间&#xff1b;可以对字符串进行append、get、set、incr、decr等操作。Hash&#xff08…

【C++】位图和布隆过滤器

文章目录 位图概念难点代码 布隆过滤器概念插入查找删除优缺点代码 位图 概念 所谓位图&#xff0c;就是用每一个比特位位来存放某种状态&#xff0c;适用于海量数据&#xff0c;数据无重复的场景。通常是用来判断某个数据存不存在的。 给40亿个不重复的无符号整数&#xff…

buu_Misc总结2

目录 百里挑一 exiftool: [SUCTF2018]followme grep工具使用&#xff1a; [安洵杯 2019]Attack mimikatz工具使用&#xff1a; 百里挑一 打开文件是流量包&#xff0c;发现里面有很多图片 导出http 另存一个文件夹&#xff0c;里面很多图片&#xff0c;啥也看不出来 &…

医用影像技术

1.X光和CT原理 X光和CT&#xff08;计算机断层扫描&#xff09;都是医学成像技术&#xff0c;用于诊断和治疗。它们的原理如下&#xff1a; X光原理&#xff1a; X光是一种电磁辐射&#xff0c;与可见光类似&#xff0c;但具有更高的能量。当X光通过人体或物体时&#xff0c;…

创作一周年纪念日【道阻且长,行则将至】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; 技术之外的往事 &#x1f383;所处时段&#xff1a; 大学生涯[1/2] 文章目录 一、起点一切皆有定数 二、成果尽心、尽力 三、相遇孤举者难起&#xff0c;众行者易趋 四、未来长风破浪会有时&#xff0c;直挂云…

[MySQL]MySQL表中数据的增删查改(CRUD)

[MySQL]MySQL表中数据的增删查改(CRUD) 文章目录 [MySQL]MySQL表中数据的增删查改(CRUD)1. 新增数据1.1 单列插入1.2 多列插入1.3 插入否则更新1.4 替换 2. 基本查询数据2.1 全列查询2.2 指定列查询2.3 查询字段为表达式2.4 为查询结果指定别名2.5 结果去重2.6 where子句2.7 or…

修复漏洞(二)离线升级Tomcat版本

前言 生产环境无法联网&#xff0c;只能通过下载离线版本更新Tomcat到小版本最新注意Tomcat10和11与jdk1.8都不兼容&#xff0c;只能更新到小版本的最新前提是按照我这种方法配置Tomcat开机自启的https://blog.csdn.net/qq_44648936/article/details/130022136 步骤 备份整个…

IAR编译报错:Error[Pe065: expected a “.“ and Error[Pe007]:unrecognized token

IAR报错 Error[Pe065: expected a “.” and Error[Pe007]:unrecognized token 使用IAR编译报如下错误&#xff1a; 找到软件报错的地方&#xff0c;从肉眼看&#xff0c;并没有错误的地方&#xff0c;如下图所示&#xff1a; 这时肯定是丈二和尚摸不着头脑&#xff0c;这里…

VMware种ubuntu22.04挂载ax88179网卡不显示的问题

网上找了很多解决办法&#xff0c;都说是驱动的问题&#xff0c;其实不是。ubuntu22自带无bug的ax88179的驱动。 其实是Vmware的问题&#xff0c;在虚拟机设置种添加一个usb控制器&#xff0c;然后这样设置就好了。

HCIP第一课实验小练习

目录 题目&#xff1a;​编辑 第一步&#xff1a;地址规划&#xff08;子网划分&#xff09; 第二步&#xff1a;设计拓扑并规划地址配置 第三步&#xff1a;VLAN规划配置 LW1 LW2 第四步&#xff1a;网关配置 第五步&#xff1a;及静态路由配置 第六步防止成环 题目&…

深入理解Linux网络——TCP连接建立过程(三次握手源码详解)

文章目录 一、相关实际问题二、深入理解listen1&#xff09;listen系统调用2&#xff09;协议栈listen3&#xff09;接收队列定义4&#xff09;接收队列申请和初始化5&#xff09;半连接队列长度计算6&#xff09;小结 三、深入理解connect1&#xff09;connect调用链展开2&…