【C++】vector的底层原理及实现

news2024/10/7 14:26:26

文章目录

  • vector的底层结构
  • 迭代器
  • 容量操作
    • size()
    • capacity()
    • reserve()
    • resize()
  • 默认成员函数
    • 构造
      • 无参构造函数
      • 带参构造函数
    • 析构
    • 拷贝构造
    • 赋值重载
  • operator[ ]
  • 插入删除操作
    • insert()任意位置插入
    • erase()任意位置删除
    • push_back()尾插
    • pop_back()尾删

vector的底层结构

我们的目的不是去模拟实现vector,而是更深入地理解vector的底层原理,更好地提升自己。本篇将简单地模拟实现vector,更好地理解它的构造和原理。参考:vector使用说明

在C++的STL中,vector是最常用的容器之一,底层是一段连续的线性内存空间(泛型的动态类型顺序表),可以支持随机访问。vector可以存储各种类型,int、char、string等,所以它是一种类模板,可以套用各种类型。

STL标准库中vector的核心是这样定义的,这里的alloc涉及到内存池的知识,我们可以先不用管。
在这里插入图片描述
vector的底层会用三个指针,利用三个指针相减来实现动态存储。

我们自己定义一个vector类

template<class T>
class vector
{
public:
	typedef T* iterator;//迭代器
	typedef const T* const_iterator;//常量迭代器
private:
	iterator _start;
	iterator _finish;
	iterator _end_of_storage;
}

迭代器

vector的迭代器是一个原生指针,他的迭代器和string相同都是操作指针来遍历数据。

迭代器返回的是存储数据的起始位置和末尾的下一个位置,区间是左开右闭的[_start, _finish)
实现了迭代器,我们在代码测试时就可以使用范围for了。

iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

容量操作

size()

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

capacity()

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

reserve()

这个函数十分重要,因为vector许多地方都会用reserve()去扩容,并且还有几个非常容易搞错的地方。

reserve只能扩容,不能增容,因此要进行判断是否需要扩容,缩容就不进行操作。

扩容的步骤是:申请一块更大的新空间,再将旧空间数据移动到新空间中,最后释放旧空间。

下面这种写法有什么问题?

void reserve(size_t n)
{
	if (n > capacity())
	{
		//申请新空间
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * old_size);
			//释放旧空间
			delete[] _start;
		}
		_start = tmp;
		_finish = tmp + size();
		_end_of_storage = _start + n;
	}
}

错误一:倒数第二行的_finish没有发生变化

看这段代码的最后三行,_start先指向新空间的起始位置,_finish再调用size()的话,此刻的size()已经不是当初的size()了。size()的返回值是_finish - _start,而原本的_start已经改变成了tmp,此时_finish的值 = _start + _finish - _start = _finish;所以_finish没有发生变化。


解决方法有两种:
1._start和_finish赋值的顺序调换一下,先改变_finish,再改变_start。
2.挪动数据前先保留原本的size();

我们这里采用第二种写法。

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();//保留之前的size
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * old_size);
			delete[] _start;
		}
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = _start + n;
	}
}

2.不能用memcpy去拷贝内容,而用赋值去拷贝内容。

对于int、char等内置类型,可以使用memcpy不会出问题。对于自定义类型一般也没有问题,但是对于动态申请资源的自定义类型,memcpy就会发生浅拷贝,导致一块空间析构两次。

比如vector<string>类型,此时的T是string类型。上一篇我们了解过string的底层原理,string底层用的是char*指针_str来存储字符串的地址,因此需要动态申请空间。

所以我们是在申请的空间(_start)上面又申请了一块空间(_str),如果我们使用memcpy去拷贝_start中的内容到tmp中;就会把申请的_str指向的地址拷贝给tmp,这样_start和tmp中的_str就会指向同一块空间,再执行delete[] _start;的时候就会执行string的析构函数把这块空间释放。
在这里插入图片描述
所以不能用memcpy浅拷贝,解决方法:
一个个遍历用=赋值,对于string这种深拷贝的类,调用的是string的赋值重载完成深拷贝。
注意:这里的string使用的是STL库中的string类。

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();//保留之前的size
		if (_start)
		{
			for (size_t i = 0; i < old_size; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = _start + n;
	}
}

resize()

resize函数用于改变vector的大小,调整vector中元素的数量。

n > size():多余空间添加元素(第二个参数)
n <= size():删除n后面的元素。

void resize(size_t n, const T& val = T())//匿名对象
{
	if (n <= size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish++ = val;
		}
	}
}

默认成员函数

构造

无参构造函数

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

使用

vector<int> v1;
vector<string> s2;

带参构造函数

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

使用

vector<int> v1(10);//开辟5个int大小空间,默认初始化为0
vector<int> v2(10, 1);//开辟10个int大小空间,并初始化为1
vector<string> s2(10,"abc");//开辟10个string大小空间,并初始化为"abc"

析构

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

拷贝构造

写法一:尾插法

vector(const vector<T>& v)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(v.size());
	for (auto& e : v)//引用防止调用拷贝构造
	{
		push_back(e);
	}
}

写法二:常规法

vector(const vector<T>& v)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	_start = new T[v.size()];
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

赋值重载

和string一样,我们直接复用标准库的swap函数。

注意:用swap的话,拷贝构造的参数要用传值传递(不能改变实参),且不能用const修饰(发生交换)

void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

operator[ ]

两个重载版本,一个普通对象调用,一个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];
}

插入删除操作

insert()任意位置插入

插入之后,如果原始空间发生扩容,则pos指针仍指向原来空间的位置,迭代器就会发生失效。所以我们需要在扩容之前保存pos的相对位置,然后重新指向正确位置。

//insert后 迭代器可能会失效,扩容就会引起失效
iterator insert(iterator pos, const T& val)
{
	assert(pos >= _start && pos <= _finish);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//保存pos相对位置,防止失效
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
		//解决迭代器失效问题
		pos = _start + len;
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}

但是,这样只保证了能正确插入数据。如果在使用时,我们insert()一个元素后,迭代器还会可能失效,因为我们使用的传值传递,形参不会影响实参。因此insert()函数返回pos这个位置的迭代器,我们在外部使用的时候更新迭代器即可。

vector<int> v(5, 1);
vector<int>::iterator p = v.begin() + 1;
p = v.insert(p, 10);//更新迭代器

erase()任意位置删除

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

删除虽然不会扩容(不用保存pos指针的相对位置),但是删除一个元素后,后面的元素都会向前移动,此时迭代器指向空间虽然不变,但内容变成了下一个元素,我们在使用的时候要注意这个问题。

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(6);
	auto it = v1.begin();
	//法一:边使用边更新
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			it = v1.erase(it);
		}
		else
		{
			it++;
		}
	}
	//法二:使用完将迭代器减一,防止++出现走两步的情况
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
			it--;
		}
		it++;
	}
}

push_back()尾插

实现insert()函数后,我们就可以直接复用。

void push_back(const T& val)
{
	/*if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	*_finish++ = val;*/
	insert(end(), val);
}

pop_back()尾删

同样直接复用erase(),让erase()自己判断合法性。

void pop_back()
{
	/*assert(size() > 0);
    _finish--;*/
	erase(_finish - 1);
}

vector模拟实现代码

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

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

相关文章

数据万象推出智能检索MetaInsight,现已开启限时公测

海量文件的分析统计一直是对象存储COS的热点需求&#xff0c;伴随AIGC飞速迭代发展&#xff0c;在众多不同模态素材的海洋中&#xff0c;用户也急需更高效地管理和利用多媒体内容&#xff0c;打破传统搜索的桎梏。 数据万象推出的智能检索 MetaInsight 服务将多模态检索与元数…

第十四章 Qt绘图

目录 一、Qt绘图基础 1、主要的类 2、paintEvent 事件 二、坐标体系 三、画笔 1、画笔的常用接口 2、画笔样式 3、画笔画线时的端点样式 4、画笔画线时,连接点的样式 5、实例 四、画刷 1、画刷的填充样式 2、实例 五、基本图形的绘制 1、画矩形 drawRect 2、画…

miniconda3 安装jupyter notebook并配置网络访问

由于服务器安装的miniconda3&#xff0c;无jupyter notebook&#xff0c;所以手工安装jupyter notebook 1 先conda 安装相关包 在base 环境下 conda install ipython conda install jupyter notebook 2 生成配置文件 jupyter notebook --generate-config Writing defaul…

Coze终于顶不住了?开始收费了

&#x1f914;各位老铁都知道&#xff0c;之前Coze以免费出圈&#xff0c;香碰碰&#xff0c;字节一个月几个亿补贴用户。现在终于顶不住了&#xff0c;开始收费了&#xff01; 我们来看看具体情况吧&#xff01; &#x1f4b8;收费情况一览 目前国内版本还没有开始收费&#x…

CesiumJS【Basic】- #054 绘制渐变填充多边形(Entity方式)-使用shader

文章目录 绘制渐变填充多边形(Entity方式)-使用shader1 目标2 代码2.1 main.ts绘制渐变填充多边形(Entity方式)-使用shader 1 目标 使用Entity方式绘制绘制渐变填充多边形 - 使用shader 2 代码 2.1 main.ts import * as Cesium from cesium;const viewer = new Cesium…

迅睿CMS 后端配置项没有正常加载,上传插件不能正常使用

首先&#xff0c;尝试迅睿CMS官方提供的【百度编辑器问题汇总】解决方案来解决你的问题。你可以访问这个链接&#xff1a;官方解决方案。 如果按照【百度编辑器问题汇总】解决方案操作后&#xff0c;依然遇到“后端配置项没有正常加载&#xff0c;上传插件不能正常使用”的问题…

windows重装系统

一、下载Ventoy工具&#xff0c;制作启动盘 官网地址&#xff1a;https://www.ventoy.net/cn/download.html 电脑插入用来制作系统盘的U盘&#xff0c;建议大小在8G以上。 双击打开刚解压出来的Ventoy2Disk.exe文件。打开界面如图&#xff1a; 确认U盘&#xff0c;如图&am…

Linux_管道通信

目录 一、匿名管道 1、介绍进程间通信 2、理解管道 3、管道通信 4、用户角度看匿名管道 5、内核角度看匿名管道 6、代码实现匿名管道 6.1 创建子进程 6.2 实现通信 7、匿名管道阻塞情况 8、匿名管道的读写原子性 二、命名管道 1、命名管道 1.1 命名管道通信 …

VoiceCraft—— 业界最高水平的自然语音合成语言模型

VoiceCraft: 实现语音编辑和合成的 SOTA 论文地址&#xff1a;https://arxiv.org/html/2403.16973v1 源码地址&#xff1a;https://github.com/jasonppy/voicecraft 本文介绍VoiceCraft 的开发情况&#xff0c;它在语音编辑和零点语音合成 (TTS) 方面都实现了 SOTA。在本文中…

如何压缩jpg图片的大小?关于缩小jpg图片的四种方法

如何压缩jpg图片的大小&#xff1f;压缩JPG图片大小是一项常见的技术&#xff0c;用来优化图片以适应不同的应用需求。无论是为了在网页上提高加载速度、减少存储空间占用&#xff0c;还是为了便于通过电子邮件或社交媒体分享&#xff0c;压缩jpg图片都是必不可少的步骤。这种技…

AIoTedge:智能边缘计算平台

随着物联网(IoT)和人工智能(AI)技术的飞速发展&#xff0c;AIoT&#xff08;人工智能物联网&#xff09;已成为推动智能化转型的关键力量。AIoT Edge作为这一领域的创新平台&#xff0c;通过边缘计算技术&#xff0c;为企业提供了一个高效、灵活且安全的解决方案。 边云协同架构…

Java 7新特性深度解析:提升效率与功能

文章目录 Java 7新特性深度解析&#xff1a;提升效率与功能一、Switch中添加对String类型的支持二、数字字面量的改进三、异常处理&#xff08;捕获多个异常&#xff09;四、增强泛型推断五、NIO2.0&#xff08;AIO&#xff09;新IO的支持六、SR292与InvokeDynamic七、Path接口…

Xilinx FPGA:vivado实现乒乓缓存

一、项目要求 1、用两个伪双端口的RAM实现缓存 2、先写buffer1&#xff0c;再写buffer2 &#xff0c;在读buffer1的同时写buffer2&#xff0c;在读buffer2的同时写buffer1。 3、写端口50M时钟&#xff0c;写入16个8bit 的数据&#xff0c;读出时钟25M&#xff0c;读出8个16…

前端进阶全栈计划:Spring扫盲

Spring扫盲 spring 和 springboot的关系? 类比前端&#xff1a;vue.js和nuxt.js的关系 Spring Boot 是基于 Spring 框架的快速开发工具&#xff0c;简化了 Spring 应用的配置和部署。 spring核心特性 1. 依赖注入 依赖注入是Spring框架的核心功能之一。它允许你通过配置将对…

Golang | Leetcode Golang题解之第206题反转链表

题目&#xff1a; 题解&#xff1a; func reverseList(head *ListNode) *ListNode {if head nil || head.Next nil {return head}newHead : reverseList(head.Next)head.Next.Next headhead.Next nilreturn newHead }

什么是Web3D交互展示?有什么优势?

在智能互联网蓬勃发展的时代&#xff0c;传统的图片、文字及视频等展示手段因缺乏互动性&#xff0c;正逐渐在吸引用户注意力和提升宣传效果上显得力不从心。而Web3D交互展示技术的横空出世&#xff0c;则为众多品牌与企业开启了一扇全新的展示之门&#xff0c;让线上产品体验从…

学校卫星电子怎么自动校准时间呢

在学校的教室里&#xff0c;卫星电子钟精准地为师生们提供着时间服务&#xff0c;而其自动校准时间的功能令人称奇。那么&#xff0c;学校卫星电子钟是如何实现自动校准时间的呢&#xff1f; 学校卫星电子钟自动校准时间的原理基于卫星导航系统。常见的如北斗卫星导航系统或 GP…

小程序-<web-view>嵌套H5页面支付功能

背景&#xff1a;小程序未发布前&#xff0c;公司使用vue框架搭建了管理系统&#xff0c;为了减少开发成本&#xff0c;微信提供了web-view来帮助已有系统能在小程序上发布&#xff0c;详见web-view | 微信开放文档。因公司一直未打通嵌套H5小程序的支付功能&#xff0c;导致用…

湖北建筑安全员A证跨省调出审核不通过?可能是这些原因

湖北建筑安全员A证跨省调出审核不通过&#xff1f;可能是这些原因 湖北建筑安全员A证跨省调出审核不通过怎么办&#xff1f; 湖北建筑安全员ABC正常情况下都是可以跨省调出的&#xff0c;现在建筑三类人员安全员ABC在全国工程质量安全监管信息平台都是可以查询的&#xff0c;在…

offer150-16:数值的整数次方

题目描述:实现函数double Power(double base,int exponent),求base 的exponent次方。不得使用库函数&#xff0c;同时不需要考虑大数问题。 分析&#xff0c;题目要求实现库函数pow(),由于不需要考虑大数问题&#xff0c;不必担心溢出&#xff0c;那么就需要对输入的各种情况进…