C++ vector 的模拟实现

news2024/12/26 22:17:18

 

目录

1. vector 类的成员变量

2. 无参构造

3. 析构函数 

4.  size_t capacity()

5. size_t size()

6. void reserve(size_t n)

 7. 迭代器

8. void push_back(const T& x) 

9. T& operator[](size_t pos) 

10. iterator insert(iterator pos, const T& val)

11.  iterator erase(iterator pos)

12. void pop_back()

13. void resize(size_t n, const T& val = T()) 

14. void swap(vector& v)

15. 拷贝构造 

16. 赋值运算符重载 

17. vector(size_t n, const T& val = T()) 

18. vector(InputIterator first, InpuIterator last) 


1. vector 类的成员变量

在上一讲我们学习了如何使用vector,我们很可能会认为:vector类的成员变量是和 string 类的成员变量差不多:T* _data,size_t size,size_t _capacity。这样来实现当然没有什么问题,这不就是和顺序表的实现差不多嘛!但是我们会参考库里面 vector 的实现来模拟实现 vector。

我们可以参考 STL_30 中 vector 的源码:

 我们可以看到库里面关于 vector 的实现是维护三个迭代器变量,用这三个迭代器变量来控制成员函数的实现逻辑。根据变量名,我们可以盲猜出这三个变量的含义:

恭喜你,猜对了!库里面的三个迭代器变量就是这么一个意思。在 vector 的使用哪一节,我们已经知道了vector 的迭代器就是 T*。那我们就能够定义出 vector 的基本结构啦!但这里有个问题就是维护三个迭代器变量来实现 vector 有什么好处呢?我们在实现的过程中再来细谈!

namespace Tchey
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	}
}

2. 无参构造

无参构造不需要做什么事儿,只需要将我们的三个迭代器初始化为 nullptr 就行啦!

vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{}

3. 析构函数 

析构函数就是释放 vector 维护的空间,只有当 _start 不为 nullptr 才需要释放。当然 delete nullptr也没啥问题,但是为了严谨嘛!

~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = nullptr;
		_finish = nullptr;
		_endofstorage = nullptr;
	}
}

4.  size_t capacity()

这个函数比较简单呢!vector 的实际容量就是 _endofstorage - _finish 啊!可以结合三个变量的意义来看:

size_t capacity()
{
	return _endofstorage - _start;
}

5. size_t size()

size 的求法和 capacity 的求法是一样的哇!size = _finish - _start。请参照上图。

size_t size()
{
	return _finish - _start;
}

6. void reserve(size_t n)

1:判断是否需要扩容!只有当 n > capacity() 的时候才需要扩容!为什么要有这一步检查呢?因为这个 reserve 不仅仅是给其他成员函数使用的,还有可能直接被用户使用!因此还是需要有合法性检查。

2:开辟新空间,拷贝原数据,释放旧空间。

3:更新 _start,_finish,_endofstorage。

void reserve(size_t n)
{
	if (n > capacity())
	{
        size_t sz = size();
		T* tmp = new T[n];
		if (_start) //有数据才拷贝
		{
			memcpy(tmp, _start, sz * sizeof(T));
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_endofstorage = _start + n;
	}
}

注意:更新 _finish 的时候不能直接写:_start + size(),因为size() 的计算依赖于 _start 和 _finish,因为 _start 已经被修改了,因此不可以直接用size(),需要提前保存 size()。 

但是这样写真的没问题嘛?我们经过测试发现这样的代码会使程序崩掉的:

 

这是为啥呢?我们来看看图解:

 

string 维护的三个成员变量管理着堆区的空间,当我们需要扩容的时候,拷贝 vector 中原来的数据,因为我们用的是 memcpy 知识单纯的赋值,因此拷贝后的数据同样也是指向原先 vector 中的 string 指向的空间,在我们 delete 掉原空间之后,实际上新的 vector 中的 string 指向的空间已经被释放了!等函数调用结束,势必会出现二次析构的问题!

解决的办法很简单,直接 for 循环赋值就行了!

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start) //有数据才拷贝
		{
			for (int i = 0; i < sz; i++)
				tmp[i] = _start[i];
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_endofstorage = _start + n;
	}
}

对于内置类型,= 赋值会调用赋值运算符重载,这样就没问题啦! 

 7. 迭代器

维护三个迭代器变量 begin() 函数,end() 函数的实现就比较简单啦!

iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

8. void push_back(const T& x) 

1:检查是否需要扩容。

2:插入数据。

3:更新 _finish。

void push_back(const T& x)
{
	if (_finish == _endofstorage) //扩容逻辑
	{
		size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapaciy);
	}

	*_finish = x;
	_finish++;
}

9. T& operator[](size_t pos) 

1:检查 pos 的合法性。

2:找到 pos 位置对应的迭代器,返回其解引用的值就 OK。可以这么写:*(_start + pos),定睛一看不就等于:_start[pos] 嘛!

T& operator[](size_t pos)
{
	assert(pos < size());
	return _start[pos];
}

10. iterator insert(iterator pos, const T& val)

1:检查 pos 合法性。

2:扩容逻辑的判断。

3:挪动数据。我们可以发现维护三个迭代器变量的好处就来了,在 string 的模拟实现中,我们移动数据的时候可能会发生死循环的问题,就是当 end == 0 的时候,减一之后变成 -1,因为 pos 是 size_t 类型的,end 会被整形提升,导致陷入死循环。但是使用迭代器之后完全没有这种问题!

于是你写出来了这样的代码:

void insert(iterator pos, const T& val)
{
	assert(pos >= _start && pos <= _finish);

	if (_finish == _endofstorage)
	{
		size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapaciy);
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}

	*pos = val;
	++_finish;
}

这样做真的没有问题吗!我们来看看下面的这组测试用例:

 

为什么头插一个 0 的时候会出现随机值,并且 0 还没有插入进去呢?这里有一个严重的问题:迭代器失效的问题,我们现在书写的 insert 函数,在扩容的时候就会发生迭代器失效的问题。

我们来分析出现这种情况的原因哈:当我们的 vector 已经有 4 个元素了,在插入一个元素就会扩容,一旦扩容就会开辟新的空间并且会拷贝原空间的数据,那么实参的 begin() 指向的空间已经被释放了,这就造成了迭代器失效的问题!

 应该怎么解决这个问题呢?我们在扩容之前保存 pos 相对于 _start 的偏移量即可。

我们现在解决了 insert() 函数内部迭代器失效的情况,那么如何解决外部迭代器失效的情况呢?

什么是外部迭代器失效?来看下面的例子:

想必你也知道原因了:形参是实参的拷贝,形参的改变不影响实参,即使扩容的时候我们在函数内部修改了形参 pos 的,实参依然是不会改变的!上面的代码中让 *it-- 就发生了内存的非法访问,这是不被允许的!

你可能一下就想到了一个解决办法:把 insert() 函数的参数改为引用不就行啦?但是当我们这样调用 insert() 函数的时候编译就无法通过啦:

insert(a.begin(), 0);  // begin() 函数是值返回,是一个临时对象具有常性不可以被 iterator& 接收。

insert(a.begin() + 3, 0) // 这是一个表达式的计算,计算结果也是一个临时对象,同样不能被 iterator& 的形参接收。

那你可能会说:我用const iterator& 来做形参,那你在函数内部就无法修改形参 pos 了,怎么解决迭代器失效的问题呢?

因此正确的解决办法是参考库函数, 给 insert() 函数加上返回值。

iterator insert(iterator& pos, const T& val)
{
	assert(pos >= _start && pos <= _finish);

	if (_finish == _endofstorage)
	{
		size_t offset = pos - _start;
		size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapaciy);
		pos = _start + offset;
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}

	*pos = val;
	++_finish;
	return pos;
}

11.  iterator erase(iterator pos)

1:检查 pos 位置的合法性。

2:挪动元素。

就很简单哇!但是我们需要考虑的问题是 erase 是否有迭代器失效的问题呢?

看下面的代码,我们删除 4 这个元素之后呢,再去访问 it 迭代器不就是越界访问了嘛。

当你在VS中使用 std::vector 使用 it 迭代器会直接报错,VS 认为无论是否越界访问,使用删除的迭代器就是不正确的。

但是在 Linux 下使用 g++ 编译器均不会报错呢!我们看到不同的编译器对此的处理结果也是不相同的嘞!

为了使得C++代码兼容 g++ 编译器和 VS 的编译器,我们必须让 erase 有返回值,返回删除位置的下一个位置的迭代器,这样就能做到 VS 下访问不报错了!

补充:VS 库中 vector 迭代器的实现其实是经过封装的,并不是原生指针。可见实现 vector 的方式真的很多哇!

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

12. void pop_back()

1:注意有元素才能删除嘛。std::vector 是直接断言检查的!

2:其实也可以复用 erase 函数。

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

13. void resize(size_t n, const T& val = T()) 

实现的思路和 string 的 resize 差不多:

1:当 n < size() 直接修改 _finish 即可。

2:其余情况我们都可以调用 reserve 把空间开好,因为 reserve 的实现做了检查,不需要扩容的时候是不会扩容的!空间好了之后填充 val 就可以啦!

void resize(size_t n, const T& val = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);

		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
}

14. void swap(vector<T>& v)

这是交换两个 vector 对象,很简单只需要交换 vector 的成员变量就行了!

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

15. 拷贝构造 

类提供的默认拷贝构造会实现直接赋值的浅拷贝,导致析构的时候同一块堆区的空间会被释放两次。这就是经典的浅拷贝,因为我们的 vector 维护了堆区的数据,因此要实现类的深拷贝。

老老实实开空间拷贝数据。很简单的!

vector(const vector<T>& v)
{
	_start = new T[capacity()];
	memcpy(_start, v._start, sizeof(T) * size());
	_finish = _start + v.size();
	_endofstorage = _start + v.capacity();
}

16. 赋值运算符重载 

传统写法很简单,这里就不写了。

我们的现代写法在 string 的模拟实现哪一节提到过。就是利用自定义类型函数传值调用会调用拷贝构造的特点,然后将拷贝构造出来的形参交换给自己,同时随着形参的销毁,形参右释放了原来的空间,简直就是一举两得!

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

17. vector(size_t n, const T& val = T()) 

这个构造函数直接调用 resize() 就可以啦!

vector(size_t n, const T& val = T())
{
	resize(n, val);
}

18. vector(InputIterator first, InpuIterator last) 

这个构造函数是使用一段迭代器区间来初始化 vector ,区间:[first, lasr),InpuIterator 是模板参数。

例如:

代码实现:

		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

但是这么写的话,就会有一个问题:我们这样初始化 vector 就会报错:

vector<int> a(10, 1);

这是因为:参数 10 和 1 均会解析成 int 类型,从而构造函数走的是:迭代器初始化的版本。导致编译错误!为了解决这个问题,我们可以再提供一个构造函数:将 size_t 变成 int,这样就不会报错了。

vector(int n, const T& val = T())
{
	resize(n, val);
}

 

 

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

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

相关文章

【哈希表完整代码】模拟实现哈希表和unordered_set与unordered_map

目录 HashTable.h: Test.cpp: MyUnorderedSet.h: HashTable.h: #pragma once #include<iostream> #include<vector> #include<utility>//pair头文件 #include<assert.h> #include<string>using namespace std;namespace CLOSEHASH {enum Sta…

mk语法示例

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

【会员管理系统】篇一之项目预热

一、技术架构 vue.js vueCLI 3.x Axios babel EcmaScript6 Eslint Mock.js Easy-Mock Element UI Vuex 二、RESTful风格 三、Mock.js简单使用 1、简单使用 &#xff08;1&#xff09;新建文件夹&#xff0c;在vscode中打开&#xff0c;使用终端…

在线存储系统源码 网盘网站源码 云盘系统源码

Cloudreve云盘系统源码-支持本地储存和对象储存,界面美观 云盘系统安装教程 测试环境:PHP7.1 MYSQL5.6 Apache 上传源码到根目录 安装程序: 浏览器数据 http://localhost/CloudreveInstallerlocalhost更换成你的网址 安装完毕 记住系统默认的账号密码 温馨提示:如果默认…

【数据结构与算法篇】还不会二分查找?看这篇就够了!

​&#x1f47b;内容专栏&#xff1a; 《数据结构与算法篇》 &#x1f428;本文概括&#xff1a;整数二分算法&#xff08;朴素二分&#xff0c;查找区间左端点与区间右端点二分&#xff09;、浮点数二分 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&…

【Python数据挖掘 基础篇】Python数据挖掘是个啥?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 梦想从未散场&#xff0c;传奇永不落幕&#xff0c;博主会持续更新优质网络知识、Python知识、Linux知识以及各种小技巧&#xff0c;愿你我共同在CSDN进步 目录 一、了解数据挖掘 1. 数据挖掘是什么&#xff…

【LeetCode:1402. 做菜顺序 | 动态规划 + 贪心】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【Kotlin精简】第5章 简析DSL

1 DSL是什么&#xff1f; Kotlin 是一门对 DSL 友好的语言&#xff0c;它的许多语法特性有助于 DSL 的打造&#xff0c;提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤&#xff0c;以及如何通过 DslMarker &#xff0c; Context Receivers 等…

音乐制作软件 Studio One 6 mac中文版软件特点

Studio One mac是一款专业的音乐制作软件&#xff0c;该软件提供了全面的音频编辑和混音功能&#xff0c;包括录制、编曲、合成、采样等多种工具&#xff0c;可用于制作各种类型的音乐&#xff0c;如流行音乐、电子音乐、摇滚乐等。 Studio One mac软件特点 1. 直观易用的界面&…

Spring中静态代理设计模式

目录 一、为什么需要代理设计模式 二、代理设计模式 三、静态代理设计模式 3.1 存在的问题 一、为什么需要代理设计模式 在项目的开发过程中我们知道service层是整个项目中最重要的部分&#xff0c;在service中一般会有两个部分&#xff0c;一个是核心业务&#xff0c;一个是额…

DJYROS产品:基于DJYOS的国产自主割草机器人解决方案

基于都江堰泛计算操作系统的国产自主机器人操作系统即将发布…… 1、都江堰机器人操作系统命名&#xff1a;DJYROS 2、机器人算法&#xff1a;联合行业自主机器人厂家&#xff0c;构建机器人算法库。 3、机器人芯片&#xff1a;联合行业机器人AI芯片公司&#xff0c;构建专用…

电商API是何时?以什么姿态开启了它的时代?

说到API&#xff0c;非业内技术人士&#xff0c;大家似乎对它还是知之甚少。 但如果有关注这个领域&#xff0c;其实不难发现&#xff0c;国内一些所谓大厂已经在电商API接口方面做了不少动作&#xff0c;不论是对外宣称的API生态&#xff0c;还是相对低调的API市场&#xff0c…

冲刺学习-MySQL-基础

基础 数据类型 常见数据类型的属性 整型 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT 可选属性 M&#xff1a;表示显示宽度&#xff08;从MySQL 8.0.17开始&#xff0c;整数数据类型不推荐使用显示宽度属性&#xff09;UNSIGNED&#xff1…

hdlbits系列verilog解答(7458芯片)-10

文章目录 wire线网类型介绍一、问题描述二、verilog源码三、仿真结果 wire线网类型介绍 wire线网类型是verilog的一种数据类型&#xff0c;它是一种单向的物理连线。它可以是输入也可以是输出&#xff0c;它与reg寄存器数据类型不同&#xff0c;它不能存储数据&#xff0c;只能…

设计模式——七大原则详解

这里写目录标题 设计模式单一职责原则应用实例注意事项和细节 接口隔离原则应用实例 依赖倒转&#xff08;倒置&#xff09;原则基本介绍实例代码依赖关系传递的三种方式注意事项和细节 里氏替换原则基本介绍实例代码 开闭原则基本介绍实例代码 迪米特法则基本介绍实例代码注意…

【苍穹外卖 | 项目日记】第八天

前言&#xff1a; 昨天晚上跑完步回来宿舍都快停电了&#xff0c;就没写项目日记&#xff0c;今天补上 目录 前言&#xff1a; 今日完结任务&#xff1a; 今日收获&#xff1a; 引入百度地图接口&#xff1a; 引入spring task &#xff0c;定时处理异常订单&#xff1a; …

ssm+vue的软考系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的软考系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff…

【RNA structures】RNA-seq Part2: RNA转录的重构和前沿测序技术

文章目录 RNA转录重建1 先简单介绍一下测序相关技术2 Map to Genome Methods2.1 Step1 Mapping reads to the genome2.2 Step2 Deal with spliced reads2.3 Step 3 Resolve individual transcripts and their expression levels 3 Align-de-novo approaches3.1 Step 1: Generat…

你还不会DeBug?太low了吧

编程时调试是不可缺少的&#xff0c;Unity中用于调试的方法均在Debug类中。 浅试一下 新建一个物体和脚本&#xff0c;并把脚本挂载到物体上&#xff01; using System.Collections; using System.Collections.Generic; using UnityEngine;public class DeBugTest : MonoBeh…

JavaSE入门---掌握面向对象三大特性:封装、继承和多态

文章目录 封装什么是封装&#xff1f;如何实现封装&#xff1f; 继承什么是继承&#xff1f;继承的语法父类成员访问子类访问父类的成员变量子类访问父类的成员方法 认识super关键字认识final关键字子类的构造方法super VS this在继承关系中各代码块的执行顺序是怎样的&#xf…