C++ —— 模拟实现vector和迭代器失效

news2025/8/3 7:27:25

目录

1.成员变量的设计

 2.迭代器以及接口设计

3.容量接口

4.reserve的设计

5.resize的设计

6.尾插尾删的设计

7.构造、析构函数

8.运算符重载

9.insert接口设计与迭代器失效

10.erase的设计和迭代器失效

11.双层深拷贝问题

12.完整代码

1.成员变量的设计

成员变量设计为三个迭代器,分别是:_start、_finish、_endofstorage。此设计原理参考STL源码。_start与_finish之间是存储有效元素的;_start与_endofstorage之间表容器的容量。

iterator _start;
iterator _finish;
iterator _endofstorage;

 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.容量接口

size_t size()
{
	return _finish - _start;
}
size_t capacity()
{
	return _endofstorage - _start;
}
void clear()
{
	_finish = _start;
}
bool empty()
{
	return _finish == _start;
}

4.reserve的设计

reserve如果涉及到扩容,统一采用异地扩容的方式。当参数小于当前容量时不作处理。

void reserve(const size_t& n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		assert(tmp);		//检查是否成功开辟
		int oldsize = _finish - _start;

		if (_start)		//如果_start不为空需要拷贝数据
		{
			memcpy(tmp, _start, sizeof(T) * oldsize);        
			delete[] _start;		//释放旧空间
		}

		//异地扩容,更新迭代器
		_start = tmp;
		_finish = _start + oldsize;
		_endofstorage = _start + n;
	}
}

5.resize的设计

resize需要分情况讨论,如果接收的参数小于当前容量时,空间不进行变动;如果小于当前有效元素个数时,需要更新_finish迭代器达到删除的效果。同时需要注意resize有第二个参数(官方手册)。

void resize(const size_t n, T val = T())
{
	if (n > capacity) reserve(n);		//大于当前容量,扩容
	if (n > size())
	{
		while (_finish < _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	else _finish = _start + n;
}

6.尾插尾删的设计

void push_back(const T& val)
{
	if (_finish == _endofstorage)		//是否需要扩容
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	*_finish = val;
	++_finish;
}
void pop_back()
{
	assert(!empty());		//尾删容器不能为空
	--_finish;
}

7.构造、析构函数

这里模拟实现:默认构造函数、范围拷贝、指定容器大小、拷贝构造函数(现代写法)、析构函数。

vector()		//默认构造函数
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{}

template <class it>
vector(it first, it last)		//范围拷贝构造
	: _start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

//拷贝构造现代写法
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);

}
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);
}

vector(size_t n, const T& val = T())		//指定容器大小构造
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(n);
	for (size_t i = 0; i < n; ++i)
	{
		push_back(val);
	}

}

~vector()		//析构函数
{
	delete[] _start;
	_start = _finish = _endofstorage = nullptr;
}

上面的代码会出一个错误:

其原因在于:10 在编译器的视角中默认是一个 int 类型的数,而我们的接口设计是一个 size_t 类型的数,所以会存在一些类型提升。而我们又设计了一个模板,在编译器的视角看来,调用模板函数是开销更小的选择。

解决方案是添加一个重载:

vector(int n, const T& val = T())		//指定容器大小构造
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(n);
	for (int i = 0; i < n; ++i)
	{
		push_back(val);
	}
}

8.运算符重载

需不需要使用 const 函数是看我们需求来的,这里给出设计原则:

  • 只读接口函数,设计为const函数
  • 只写接口函数,设计为非const函数
  • 读、写接口函数,const函数、非const函数都要设计

上面的代码可能考虑的不够周全,读者可自行改进。下标运算符对于我们来说是可读可写的。

T& operator[](const size_t n)
{
	assert(n >= 0 && n < size());		//防止越界
	return _start[n];
}

const T& operator[](const size_t n) const
{
	assert(n >= 0 && n < size());		//防止越界
	return _start[n];
}

vector<T>& operator=(vector<T> v)		//赋值运算符重载
{
	swap(v);
	return *this;
}

9.insert接口设计与迭代器失效

void insert(iterator pos,const T& val)
{
	assert(pos >= _start && pos < _finish);
	if (_finish == _endofstorage)		//检查是否需要扩容
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	auto end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = val;
	++_finish;
}

这样设计的接口发生扩容时会产生迭代器失效,因为扩容是异地扩容,就会导致pos指向的空间已经被释放了,需要更新pos。因为是传值调用,我们还必须保证外部的迭代器能够及时更新。

iterator insert(iterator pos,const T& val)
{
	assert(pos >= _start && pos < _finish);
	if (_finish == _endofstorage)		//检查是否需要扩容
	{
		size_t oldsize = _finish - _start;
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
		pos = _start + oldsize;		//扩容后,_start的指向已经更改,需要更新pos
	}
	auto end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = val;
	++_finish;
	return pos;		//添加一个返回值供外部迭代器更新
}

外部需要继续使用迭代器的正确方法为:

在VS环境中使用标准库的insert接口,不按上面的方式使用迭代器,就会发生迭代器失效问题。

10.erase的设计和迭代器失效

void erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);
			
	auto del = pos + 1;
	while (del < _finish)
	{
		*(del - 1) = *del;
		++del;
	}
	--_finish;
}

这样做会导致迭代器失效。其原因在于:删除最后一个元素,del的位置会指向一个不合法的位置,如果后续要对这个指针指向的内容进行读、写是非常危险的。所以与insert一样,VS的编译环境是使用insert和erase后迭代器失效。解决方案是返回指向删除元素位置的迭代器。

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);
			
	auto del = pos + 1;
	while (del < _finish)
	{
		*(del - 1) = *del;
		++del;
	}
	--_finish;
	return pos;
}

外部调用时正确的使用方法:

使用标准库中的vector容器,不按上述方法使用会发生错误:

11.双层深拷贝问题

留意刚才的reserve接口:

void reserve(const size_t& n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		assert(tmp);		//检查是否成功开辟
		int oldsize = _finish - _start;

		if (_start)		//如果_start不为空需要拷贝数据
		{
			memcpy(tmp, _start, sizeof(T) * oldsize);
			delete[] _start;		//释放旧空间
		}

		//异地扩容,更新迭代器
		_start = tmp;
		_finish = _start + oldsize;
		_endofstorage = _start + n;
	}
}

这么做会导致一个非常严重的野指针问题:

其原因在于:异地扩容之后,使用memcpy进行拷贝,而这个拷贝是一次浅拷贝,新空间和旧空间的对象都指向原来的_start指向的空间,当_start指向的空间释放后,新空间的对象_start就是野指针了,所以打印的就是乱码。当函数栈帧退出时又析构一次,就会造成一次严重的野指针问题。 

所以我们的解决方案是不使用memcpy进行浅拷贝,而是使用赋值运算符重载进行深拷贝。

void reserve(const size_t n)
{
	if (n > capacity())
	{
		int oldsize = _finish - _start;
		T* tmp = new T[n];
		assert(tmp);		//检查是否成功开辟

		if (_start)		//如果_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;
	}
}

12.完整代码

#include <assert.h>
#include <string.h>
#include <algorithm>

namespace ly
{
	template <class T>
	class vector
	{
	public:
		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;
		}

		vector()		//默认构造函数
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{}

		~vector()		//析构函数
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}


		template <class it>
		vector(it first, it last)		//范围拷贝构造
			: _start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//拷贝构造现代写法
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);

		}
		vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}

		vector(size_t n, const T& val = T())		//指定容器大小构造
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);
			}

		}
		vector(int n, const T& val = T())		//指定容器大小构造
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}

		}
		
		T& operator[](const size_t n)
		{
			assert(n < size());		//防止越界
			return _start[n];
		}
		const T& operator[](const size_t n) const
		{
			assert( n < size());		//防止越界
			return _start[n];
		}
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

void reserve(const size_t n)
{
	if (n > capacity())
	{
		int oldsize = _finish - _start;
		T* tmp = new T[n];
		assert(tmp);		//检查是否成功开辟

		if (_start)		//如果_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;
	}
}


		void resize(const size_t n, T val = T())
		{
			if (n > capacity) reserve(n);		//大于当前容量,扩容
			if (n > size())
			{
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
			else _finish = _start + n;
		}

		size_t size()
		{
			return _finish - _start;
		}
		size_t capacity()
		{
			return _endofstorage - _start;
		}
		void clear()
		{
			_finish = _start;
		}
		bool empty()
		{
			return _finish == _start;
		}

		void push_back(const T& val)
		{
			if (_finish == _endofstorage)		//是否需要扩容
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = val;
			++_finish;
		}
		void pop_back()
		{
			assert(!empty());		//尾删容器不能为空
			--_finish;
		}

		iterator insert(iterator pos,const T& val)
		{
			assert(pos >= _start && pos < _finish);
			if (_finish == _endofstorage)		//检查是否需要扩容
			{
				size_t oldsize = _finish - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				pos = _start + oldsize;		//扩容后,_start的指向已经更改,需要更新pos
			}
			auto end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = val;
			++_finish;
			return pos;		//添加一个返回值供外部迭代器更新
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			
			auto del = pos + 1;
			while (del < _finish)
			{
				*(del - 1) = *del;
				++del;
			}
			--_finish;
			return pos;
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;

	};
}

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

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

相关文章

Stream流、FiLe和IO流、

package com.streamdemo; import java.util.ArrayList; import java.util.List; /*** 体验Stream流** 创建一个集合&#xff0c;存储多个字符串元素* "张三丰","张无忌","张翠山","王二麻子","张良","谢广坤"** 把…

【Java八股文总结】之面试题(一)

文章目录面试题1、说一下ArrayList和LinkedList区别2、说一下HashMap的Put方法3、ThreadLocal4、说一下JVM中&#xff0c;哪些是共享区&#xff0c;哪些可以作为gc root?5、如何排查项目中遇到的JVM问题?6、如何查看线程死锁?7、线程之间如何进行通讯的?8、介绍一下Spring&…

分布式全局唯一 ID生成器(百度UidGenerator)

文章目录为什么要使用全局ID生成器&#xff1f;使用UUID作为主键&#xff1f;使用数据库主键自增&#xff1f;UidGenerator简介雪花算法snowflakeSpringBoot整合百度UidGenerator为什么要使用全局ID生成器&#xff1f; 在分库分表中必定会面临着一个问题&#xff0c; 就是如何…

steam搬砖项目怎么样

大家好&#xff0c;我是阿阳 Steam搬砖就是利用一些技巧和经验去Steam购买一些低价格商品&#xff0c;我们低价拿到道具&#xff0c;再以低于国内市场价的价格销售&#xff0c;保持了发货的稳定性和速度&#xff0c;赚取了利润。 如果是以前有人给我安利这种看着就不靠谱的赚…

scrapy 使用FilesPipeline和ImagesPipeline

除了爬取文本&#xff0c;我们可能还需要下载文件、视频、图片、压缩包等&#xff0c;这也是一些常见的需求。scrapy提供了FilesPipeline和ImagesPipeline&#xff0c;专门用于下载普通文件及图片。两者的使用方法也十分简单&#xff0c;首先看下FilesPipeline的使用方式。 Fi…

基于Netty的高性能RPC框架(分布式缓存、雪花算法、幂等性)

文章目录前言介绍1. 服务提供2. 安全策略3. 设计模式亮点1. 信息摘要算法的应用2. 心跳机制3. SPI 机制4. IO 异步非阻塞5. RNF 协议快速开始1.依赖1.1 直接引入1.2 maven引入2. 启动 Nacos3. 提供接口4. 启动服务5. 启动客户端5. 额外配置5.1 配置文件5.2 日志配置6. 场景应用…

AxGlyph矢量绘图软件 | 绘图软件

文章目录AxGlyph矢量绘图软件安装教程所见即所得滚动式符号面板&#xff0c;多底色、面板符号定制和分页顺序调整格式化图形&#xff0c;通过节点控制可获取丰富的变形支持自由矢量画笔、混合矢量路径和矢量漫水填充整合精简版的AxMath*&#xff0c;可方便的在图形中嵌入数学公…

Python的PyQt框架的使用-构建环境篇

Python的PyQt框架的使用-构建环境篇一、前言二、安装PyQt三、使用第三方开发工具一、前言 个人主页: ζ小菜鸡大家好我是ζ小菜鸡&#xff0c;小伙伴们&#xff0c;让我们一起来学习Python的PyQt框架的使用。如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连) Python起初是一…

SpringBoot-Eureka-xstream-rce漏洞复现

SpringBoot-Eureka-xstream-rce actuator 是 springboot 提供的用来对应用系统进行自省和监控的功能模块。其提供的执行器端点分为两类&#xff1a;原生端点和用户自定义扩展端点&#xff0c;原生端点主要有&#xff1a; 漏洞利用 1.利用trace&#xff0c;获取认证信息&#…

虚拟内存初探CSAPP

VM as a tool for caching CMU213-CSAPP-Virtual-Memory-Concepts | GreenHatHGのBlog 理解这个概念&#xff0c;就是说&#xff0c;VM是看作是独立与CPU和主存之外的disk&#xff0c;然后主存看成是这个虚拟地址数列的cache (DRAM就是物理的主存&#xff09; 重新用自己的…

大规模异构图召回在美团到店推荐广告的应用

总第530篇2022年 第047篇美团到店推荐广告团队在图神经网络的长期落地实践中&#xff0c;思考分析了场景的特点与挑战&#xff0c;针对性地进行了模型设计&#xff0c;并通过大规模训练工具及线上部署优化多次成功落地&#xff0c;带来了线上收入提升。本文主要介绍了大规模图召…

nginx--源码分析 array(实现动态数组)

1.基本数据结构 typedef struct {void *elts;ngx_uint_t nelts;size_t size;ngx_uint_t nalloc;ngx_pool_t *pool; } ngx_array_t;结构成员定义 1.void* elts &#xff1a;数组的内存位置&#xff0c; 即数组首地址 采用void* 近似使用模板技术,可以通过类…

npm配置taobao镜像及nrm快速换源工具介绍

文章目录npm配置淘宝镜像1 为什么默认源下载很慢&#xff1f;2 淘宝npm镜像服务器3 切换npm的下包镜像源4 nrmnpm配置淘宝镜像 1 为什么默认源下载很慢&#xff1f; 在使用npm下包的时候&#xff0c;默认从国外的https://registry.npmjs.orgl服务器进行下载&#xff0c;此时&…

linux的重定向与xshell原理

文章目录一、重定向1.输出重定向&#xff1a;>1.写入指定文件2. 覆盖写2.追加重定向 &#xff1a;>>3.输出重定向&#xff1a;<1.键盘显示2.文件显示4.重定向的一些认知误区1. test.c只显示错误的2. msg.c只显示正确的3.分析4.显示出正确的二 、xshell命令及原理1.…

【Python】环境搭建详细过程

前言 要想能够进行 Python 开发, 就需要搭建好 Python 的环境。 需要安装的环境主要是两个部分: 运行环境: Python开发环境: PyCharm 安装Python 1、找到官方网站 2、找到下载页面 点击后开始下载&#xff0c;下载完成后会出现exe的安装程序 3、双击安装包 注意&#xff1a…

scala语法(一)(有java基础速学)

在拥有java基础上学习scala&#xff0c;注意以下几点 1. 变量声明 var | val 变量名 [: 变量类型] 变量值 val name: String "nico" 声明变量时&#xff0c;类型可以省略&#xff08;就是叫 类型推断&#xff09; val name "nico"类型确定后&#xff…

下载MySQL驱动包,并导进idea

操作MySQL就需要下载Mysq 驱动包 Mysql驱动包可以去“中央仓库”下载&#xff1b; 打开“中央仓库”&#xff0c;可能会等待验证是否为人机&#xff1a; 进行搜索&#xff0c;找到【MySQL Connector Java】 进行下载 往下话&#xff0c;找到跟自己MySQL对应的版本号&#xf…

Linux基本指令(二)

这章我们将继续介绍一些Linux常见的指令. 目录 more指令 less指令&#xff08;重要&#xff09; head指令 tail指令 时间相关的指令 cal指令 find指令 &#xff08;很重要&#xff09; which指令 alias指令 grep指令 xargs指令 zip/unzip指令 tar指令&#xff08;重要&#xff…

使用json、yaml、toml作为配置文件,你知道他们的区别吗

前言 配置文件&#xff0c;不言而喻&#xff0c;主要是我们进行项目和工程配置的文件。 如果是站在前端角度说的话&#xff0c;我们最常接触的就是 json以及 js类型的文件&#xff0c;这种形式的配置写法对前端非常友好&#xff0c;因为都是我们熟悉的 JS 对象结构&#xff0…

计算机专业毕业设计项目,如何去做?一位大龄过期“初级”程序员来讲讲

首先自我介绍一下&#xff0c;作为一名超过35岁以上初级程序员&#xff0c;请看仔细&#xff0c;初级程序员。从业5年&#xff0c;涉及的领域web站全栈&#xff0c;后端使用语言Java。 一、主题和程序语言的选择 我记得上个小伙伴跟我说过&#xff0c;他们的大学是每年的4月份…