【C++笔记】C++STL vector类模拟实现

news2025/1/16 13:58:02

【C++笔记】C++STL vector类模拟实现

  • 一、实现模型和基本接口
    • 1.1、各种构造和析构
    • 1.2、迭代器
  • 二、各种插入和删除接口
    • 2.1、插入接口
    • 2.1、删除接口
    • 2.3、resize接口
  • 三、运算符重载
    • 3.1、方括号运算符重载
    • 3.2、赋值运算符重载

一、实现模型和基本接口

实现模型我们选择模拟库中的模型——使用三个指针来管理数据:

template<calss T>
class Vector {
public :
	// Vector迭代器
	typedef T* iterator;
private :
	iterator _start; // 指向数据块起始位置
	iterator _finish; // 指向有效数据结束位置
	iterator _end; // 指向存储容量结尾位置
};

只不过这里是把指针重定义成了迭代器,因为vector本质就是一个数组,所以vector的迭代器也是使用原生指针即可。
这三个指针的指向如下:
在这里插入图片描述
_start就是指向的数据段开始位置,但是_finish却是只指向的最后一个有效数据的下一个位置,这也是为了之后我们插入数据的时候更方便,也符合了迭代器的要求。因为现在大多的迭代器都是左闭右开的,所以当我们要返回迭代器的结束位置end时我们就可以直接返回_finsh,然后当迭代器变量==end时我们的遍历就完成了。
同理,_end也类似这样。

1.1、各种构造和析构

无参构造
当我们需要创建一个vector对象而不清楚它具体要求存储多少个数据的时候,就可以先使用无参构造。
无参构造其实很简单啦,就是将三个指针却都初始化成空指针即可:

Vector() 
	:_start(nullptr)
	,_finish(nullptr)
	,_end(nullptr)
{
}

以n相同值初构造

Vector(size_t n, const T& val = T()) {
	// 申请空间
	T* temp = new T[n];
	// 插入数据
	for (size_t i = 0; i < n; i++) {
		temp[i] = val;
	}
	_start = temp;
	_finish = temp + n;
	_end = _finish;
}

我们直接申请一段长度为n的新空间,然后再将要初始化的值依次插入进去即可。
由于这里使用到了模板,也就是说构造函数并不知道我们要存储的值具体是什么类型,所以我们这里val的值就可以使用一个匿名对象。

还有一点需要注意的是,我们这里的插入数据并不能直接使用memcpy
memcpy对内置类型是没什么影响的,但是对于一些自定义类型特别是有指向额外空间的自定义类型,直接使用memcpy就会导致浅拷贝的问题。
就拿string类来举例子:
在这里插入图片描述
如上图,每个string对象中的_str都会指向一块额外申请的char的空间。如果我们直接使用memcpy进行拷贝的话,它就会将_str的值原封不动而拷贝过去,这样就是的新的空间和旧的空间都指向了相同的char空间,这样当程序结束调用析构函数的时候就会对同一段char*空间释放两次,这样程序就崩溃了。

所以我们这里使用的是依次使用赋值的方式就行拷贝,因为库中的自定义类型是一定重载了赋值运算符的,也一定是进行深拷贝的。

以一段迭代器区间构造
有时候我们可能想要用一个同类型对象的一段区间来初始化另一个同类型对象,但对象中的成员又都是私有的,我们不能直接访问,但我们可以使用迭代器啊。
所以也就有了迭代器区间构造的方式。
这个其实听起来很复杂,但实现起来就很简单了,因为迭代器肯定是支持++操作的,所以我们就直接让起始的迭代器一直++然后对迭代器解引用,像上面的构造一样依次将数据插入新空间即可:

template <class InputIterator>
Vector(InputIterator first, InputIterator last) {
	// 开空间
	T* temp = new T[last - first];
	// 插入数据
	int i = 0;
	while (first < last) {
		temp[i] = *first;
		first++;
		i++;
	}
	_start = temp;
	_finish = _end = temp + i;
}

拷贝构造
拷贝构造的逻辑其实也和上面那些构造的逻辑差不多:

Vector(const Vector<T>& v) {
	// 申请新空间
	T* temp = new T[v.capacity()];
	// 拷贝数据
	int i = 0;
	for (auto it : v) {
		temp[i] = it;
		i++;
	}
	_start = temp;
	_finish = _start + v.size();
	_end = _start + v.capacity();
}

这里用到的范围for需要我们实现了迭代器起始和结束才行,并且是const迭代器。
析构函数
析构函数我们只需要释放_start即可:

~Vector() {
	delete[] _start;
	_start = _finish = _end = nullptr;
}

1.2、迭代器

迭代器我们需要实现非const和const版本的:

// Vector迭代器
typedef T* iterator;
// const版本
typedef const T* const_iterator;

然后起始位置我们就直接返回_start即可,结束位置我们返回_finish即可:

// 迭代器起始位置
	iterator begin() {
		return _start;
	}
	// 迭代器结束位置
	iterator end() {
		return _finish;
	}

	// cosnt版本迭代器起始位置
	const_iterator begin() const {
		return _start;
	}
	// const版本迭代器结束位置
	const_iterator end() const {
		return _finish;
	}

返回长度
计算长度我们直接使用指针相减即可,即_finish - _start:

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

返回容量
容量即_end - _start:

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

二、各种插入和删除接口

2.1、插入接口

扩容
一样的,正式插入之前我们都需要检查扩容,所以我们先来看看扩容的实现。
这里的扩容并不是直接申请新空间在拷贝数据这么简单了,因为使用的是三个指针来管理数据,所以我们需要时刻关注指针的变化。

如果你觉得这里的扩容只是简单的申请一个新空间然后拷贝数据,再让_start指向新的空间,那你就犯大错了:
在这里插入图片描述
不要忘了,这里是三个指针啊,如果只是像上图这样处理那_finsh和_end的指向都没变化:
在这里插入图片描述

之后在遍历的时候插入数据的时候是一定会出问题的。

所以我们需要先保存原来数据段中,_finish相对_start的偏移量,在_start指向新空间之后就可以加上这个偏移量,这样就更新了_finish的正确位置了。

void reverse(size_t newCapacity) {
	if (newCapacity > capacity()) {
		// 保存原来的偏移量
		size_t offset = size();
		// 申请新空间
		T* temp = new T[newCapacity];
		// 拷贝数据
		int i = 0;
		for (auto it : *this) {
			temp[i] = it;
			i++;
		}
		// 释放原来的空间
		delete[] _start;
		_start = temp;
		_finish = _start + offset;
		_end = _start + newCapacity;
	}
}

尾插
因为_finish指向的就是最后一个有效数据的下一个位置,所以尾插要做的就是在_finish位置放上要插入的数据然后再让_finish++即可:

void push_back(const T& val) {
	// 检查扩容
	if (size() == capacity()) {
		reverse(capacity() == 0 ? 4 : 2 * capacity());
	}
	*_finish = val;
	_finish++;
}

随机插入
在pos位置插入一个数据,并返回新插入的值的位置。
随机插入除了要检查扩容之外,还需要检查pos位置是否合法,也就是pos位置不能大于end()(等于end()就变成了尾插)。
那为什么要返回新插入的值的位置呢?
因为如果发生了扩容,那pos位置就失效了。

iterator insert(iterator pos, const T& val) {
	assert(pos <= end());
	size_t offset = pos - _start;
	// 检查扩容
	if (size() == capacity()) {
		reverse(capacity() == 0 ? 4 : 2 * capacity());
	}
	// 更新pos
	pos = _start + offset;
	iterator end = _finish - 1;
	// 挪动数据
	while (end >= pos) {
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}

2.1、删除接口

尾删
尾删就更简单了,直接让_finish–即可,但是还要注意当vector为空时候就不能再删了,所以我们先要进行判空:

bool empty() {
	return _start == _finish;
}
void pop_back() {
	assert(!empty());
	_finish--;
}

随机删除
删除pos位置的数据,并返回删除位置的下一个元素在删除后的位置。
随机删除判断pos位置是否合法就和insert不一样了,这里的判断是pos不能大于等于end(),因为end()即是_finish,而_finish指向的是最后一个有效数据的下一个位置,这个位置并不是有效数据的位置。
然后然后我们就直接挪动后面的数据进行覆盖即可:

iterator erase(iterator pos) {
	assert(!empty());
	assert(pos < end());
	iterator begin = pos;
	// 挪动数据
	while (begin < end() - 1) {
		*begin = *(begin + 1);
		begin++;
	}
	_finish--;
	return pos + 1;
}

2.3、resize接口

resize接口其实和string的resize逻辑是一样的:

void resize(size_t newLen, const T& val = T()) {
	if (newLen > size()) {
		if (newLen > capacity()) {
			// 扩容
			reverse(newLen);
		}
		// 填充数据
		while (_finish < (_start + newLen)) {
			*_finish = val;
			_finish++;
		}
	}
	_finish = _start + newLen;
}

三、运算符重载

3.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、赋值运算符重载

赋值运算符重载其实和拷贝构造差不多,只不过我们这里是赋值,所以我们需要释放掉原来的空间:

Vector<T>& operator=(const Vector<T>& v) {
	// 申请新空间
	T* temp = new T[v.capacity()];
	// 拷贝数据
	int i = 0;
	for (auto it : v) {
		temp[i] = it;
		i++;
	}
	if (_start) {
		// 释放原来的空间
		delete[] _start;
	}
	_start = temp;
	// 这里并没有释放掉v的空间,所以偏移量我们直接加上v的size即可
	_finish = _start + v.size(); 
	_end = _start + v.capacity();
	return *this;
}

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

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

相关文章

企业架构LNMP学习笔记35

学习目标和内容&#xff1a; 1、能够通过HAproxy实现负载均衡。 2、安装&#xff1a; yum install -y haproxy 3、配置文件修改点&#xff1a; 修改为80&#xff0c;并将后面几个用不到的&#xff0c;都进行删除。 代理转发到后端的app端。 4、后端app端的定义&#xff1b; …

第10章_瑞萨MCU零基础入门系列教程之中断控制单元简介

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

【图论】SPFA求负环

算法提高课笔记 文章目录 基础知识例题虫洞题意思路代码 观光奶牛题意思路代码 单词环题意思路代码 基础知识 负环&#xff1a;环上权值之和是负数 求负环的常用方法 基于SPFA 统计每个点入队次数&#xff0c;如果某个点入队n次&#xff0c;则说明存在负环&#xff08;完全…

OSPF路由计算

1、Router LSA LSA 链路状态通告&#xff0c;是OSPF进行路由计算的主要依据&#xff0c;在OSPF的LSU报文中携带&#xff0c;其头重要字段及解释&#xff1a; LS Type&#xff08;链路状态类型&#xff09;&#xff1a;指示本LSA的类型。 在域内、域间、域外…

upload-labs/Pass-07 未知后缀名解析漏洞复现

upload-labs/Pass-07 漏洞复现 页面&#xff1a; 我们看到有一个图片上传功能。 我们上传一个png文件发现能够成功上传&#xff0c;那其他文件呢&#xff0c;如php文件。 我们看一下是否能上传一个php文件&#xff1a; php文件内容&#xff1a; <?phpeval($_REQUEST[]…

计算机系统的基本概念

计算机系统的基本概念 本文主要以hello.c这个程序的整个生命周期来简单了解一下计算机系统结构的基本概念。 #include <stdio.h>int main() {printf("hello, world\n");return 0; }gcc hello.c -o hello ./hello hello, world此刻&#xff0c;hello.c源程序…

运算符,switch

目录 算术运算符 逻辑运算符 强制类型转换 自增自减运算符 ​编辑 三目运算符 A&#xff1f;B:C 逗号表达式 switch 算术运算符 除法的运算结果和运算对象的数据类型有关&#xff0c;两个都是int商就是int&#xff0c;被除数或者除数只要有一个是浮点型数据&#xff0c;…

ARM DIY(十一)板子名称、开机 logo、LCD 控制台、console 免登录、命令提示符、文件系统大小

文章目录 前言板子名称uboot Modelkernel 欢迎词、主机名 开机 logoLCD 控制台console 免登录命令提示符文件系统大小 前言 经过前面十篇文章的介绍&#xff0c;硬件部分调试基本完毕&#xff0c;接下来的文章开始介绍软件的个性化开发。 板子名称 uboot Model 既然是自己的…

最新2米分辨率北极开源DEM数据集(矢量文件)

一、项目背景 美国明尼苏达大学(University of Minnesota)的极地地理空间中心(Polar Geospatial Center, PGC)于2023年8月发布了北极数字高程模型4.1版本(ArcticDEM Mosaic 4.1)。该DEM数据集是革命性的&#xff0c;分辨率达到了2米&#xff0c;而一般的开源DEM数据集分辨率是3…

代码随想录算法训练营第十八天|513. 找树左下角的值|112. 路径总和|106. 从中序与后序遍历序列构造二叉树

513. 找树左下角的值 题目&#xff1a;给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1 思路一&#xff1a;层序遍历&#xff0c;最后一层的第一个元素&#xff0c;即…

【GAMES202】Real-Time Ray Tracing 1—实时光线追踪1

一、前言 这篇我们开始新的话题—Real-Time Ray Tracing简称RTRT&#xff0c;也就是实时光线追踪&#xff0c;关于光线追踪&#xff0c;我们已经不止一次提到过它的优点&#xff0c;无论是软阴影还是全局光照&#xff0c;光线追踪都很容易做&#xff0c;唯一的缺点就是速度太慢…

时空预测 | 线性时空预测模型、图时空预测

目录 线性时空预测图时空预测 线性时空预测 这篇文章在时空预测领域&#xff0c;搭建了一个简单高效的线性模型&#xff0c;且使用了channel-independence的方式进行建模。 模型的整体结构如下图所示&#xff0c;是一个级联的结构。输入分为三个部分&#xff1a;temporal embed…

java的动态代理如何实现

一. JdkProxy jdkproxy动态代理必须基于接口(interface)实现 接口UserInterface.java public interface UserService {String getUserName(String userCde); }原始实现类&#xff1a;UseServiceImpl.java public class UserServiceImpl implements UserSerice {Overridepub…

布局过程的完全解析

前言 那么为什么要分为两个流程呢 因为测量流程是一个复杂的流程&#xff0c;有时候不一定一遍就能得出测量结果&#xff0c;可能需要 2 - 3 次甚至更多 自定义布局的几种类型&#xff0c;也是自定义布局的两个方法 实战&#xff0c;第一种类型&#xff1a;改写已有View 的步骤…

day34 Map Properties

Map<String,Integer> map new HashMap<>(); map.put("a",1);map.put("b",2);map.put("c",3);map.put("d",4);Integer a map.put("a", 2);System.out.println(a);Integer chinese map.put("语文",1…

新一代G7系列浪潮云海超融合EC纠删功能设计

浪潮云海在2023年5月正式发布新一代InCloud Rail G7系列超融合一体机&#xff0c;其内置的InCloud dSAN超融合存储组件&#xff0c;基于新一代的硬件平台设计&#xff0c;支持全栈RDMA协议&#xff0c;同时在EC纠删功能上也带来全新体验&#xff0c;为新时代用户提供更丰富的产…

PYTHON(一)——认识python、基础知识

一、为什么要学习python&#xff1f; Python 被认为是人工智能、机器学习的首选语言&#xff0c;可以说是全世界最流行通用范围最广的语言&#xff0c;几乎可以完成所有的任务&#xff0c;像设计游戏、建网站、造机器人甚至人工智能等都广泛使用Python。 二、输出&#xff08;…

注解-宋红康

目录 一、注解&#xff08;Annotation&#xff09;概述二、常见的注解实例三、如何自定义注解四、JDK中的四个元注解五、Java8注解的新特性1、可重复注解2、类型注解 一、注解&#xff08;Annotation&#xff09;概述 二、常见的注解实例 三、如何自定义注解 自定义注解必须配…

查询硬盘序列号、物理地址及对应批处理命令

首先说明&#xff1a; 通过winR -> cmd -> diskpart -> list disk -> select disk 0 -> detail disk -> 然后显示磁盘ID等&#xff0c;这不是序列号&#xff0c;只是磁盘ID而已。 查询序列号命令很简单&#xff1a; wmic diskdrive get serialnumber或者 w…

权限、认证与授权

权限、认证与授权 1、权限概述 &#xff08;1&#xff09;什么是权限 权限管理&#xff0c;一般指根据系统设置的安全策略或者安全规则&#xff0c;用户可以访问而且只能访问自己被授权的资源&#xff0c;不多不少。权限管理几乎出现在任何系统里面&#xff0c;只要有用户和…