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

文章目录

  • 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.mfbz.cn/a/767589.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

海外注册 | 欧盟医疗器械法规下免除临床试验的条件与要求

在欧盟医疗器械法规&#xff08;MDR&#xff09;的严格监管下&#xff0c;植入性医疗器械和III类医疗器械通常需要进行临床试验来证明其安全性和性能。 然而&#xff0c;MDR也规定了一些特定情况下免除临床试验的可能性。以下是免除临床试验的条件和要求的详细说明&#xff1a…

offer150-16:数值的整数次方

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

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

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

antd+vue——实现table组件跨页多选,已选择数据禁止第二次重复选择

需求场景&#xff1a;点击【新增】按钮可以在分页弹窗中跨页多选选择数据后添加到页面中&#xff0c;再次点击【新增】&#xff0c;已经选择过的数据则置灰不让重复选择。 选择后&#xff0c;置灰 点击【确定】数据添加到页面中&#xff0c;可再次点击【新增】进行添加数据 …

一篇文章入门主成分分析PCA

文章目录 基本概念事件随机变量独立同分布离散型随机变量伯努利分布&#xff08;两点分布&#xff09;二项分布几何分布泊松分布 连续型随机变量正态分布 期望方差标准化协方差相关系数线性组合特征值和特征向量特征值分解对称矩阵的特征值分解 齐次线性方程组单位向量基向量矩…

算法体系-25 第二十五节:窗口内最大值或最小值的更新结构

一 滑动窗口设计知识点 滑动窗口是什么&#xff1f; 滑动窗口是一种想象出来的数据结构&#xff1a; 滑动窗口有左边界L和有边界R 在数组或者字符串或者一个序列上&#xff0c;记为S&#xff0c;窗口就是S[L..R]这一部分 L往右滑意味着一个样本出了窗口&#xff0c;R往右滑意味…

Markdown+VSCODE实现最完美流畅写作体验

​下载VSCODE软件 安装插件 Markdown All in One &#xff1a;支持markdown的语言的&#xff1b; Markdown Preview Enhanced &#xff1a;观看写出来文档的效果&#xff1b; Paste IMage :添加图片的 Code Spell Checker检查英文单词错误&#xff1b; 基础语法 标题 #一个…

Batch Size 不同对evaluation performance的影响

目录 问题描述如果是bugbatch size的设置问题尝试使用GroupNorm解决batchsize不同带来的问题归一化的分类 参考文章 问题描述 深度学习网络训练时&#xff0c;使用较小的batch size训练网络后&#xff0c;如果换用较大的batch size进行evaluation&#xff0c;网络的预测能力会…

In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking

文章目录 In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking背景关键点内容预处理Long-Term Recurrent CNNsLSTM-RNN模型训练实验data启示In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking 会议:2018 IEEE International…

如何选择适合自己的巴比达内网穿透方案

选择适合自己的巴比达内网穿透方案&#xff0c;需要考虑几个关键因素&#xff0c;包括您的具体需求、安全性要求、技术水平以及预算。以下是一些选择巴比达内网穿透方案的建议步骤&#xff1a; 1. 确定需求和用途 首先&#xff0c;需要明确您希望通过内网穿透实现的具体目标和…

【Python网络通信】基于Bypy调用百度网盘api实现自动上传和下载网盘文件

网盘对于大家的生活工作可以说是息息相关&#xff0c;但是如果每天都重复去上传下载文件就会很浪费时间&#xff0c;所以有没有什么办法可以解放双手&#xff1f;那就是网盘接口&#xff0c;本文通过Bypy库实现百度网盘的自动上传和下载文件。 原创作者&#xff1a;RS迷途小书童…

ubuntu 安装并启用 samba

环境&#xff1a;ubuntu server 24.04 步骤如下&#xff1a; sudo apt update sudo apt install samba修改配置文件&#xff1a; sudo vi /etc/samba/smb.conf新增内容&#xff1a; [username]path /home/[username]available yesvalid users [username]read only nobrow…

Squid配置用户名密码的方法

环境 Centos7.9 Squid 3.5.20 步骤 1 使用htpasswd工具&#xff0c;生成用户名密码。 例如这里添加用户名peter, 密码123. yum install httpd-tools htpasswd -c /etc/squid/squid_user peter New password: 123 Re-type new password: 123 Adding password for user peter…

人工智能在软件开发中的角色:助手还是替代者?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

一键转换,高效管理:引领文件批量改后缀名与TXT转DOCX格式新潮流

在这个数字化时代&#xff0c;文件管理和格式转换成为了我们日常工作中不可或缺的一部分。然而&#xff0c;手动更改文件后缀名以及将TXT文件转换为DOCX格式&#xff0c;不仅耗时耗力&#xff0c;还容易出错。幸运的是&#xff0c;我们有了文件批量改名高手这款强大的工具&…

大模型在软件测试领域的应用场景有哪些?_大模型在测试领域应用

在数字化转型的大背景下&#xff0c;在软件定义一切的趋势下&#xff0c;软件测试人员需要接触和理解的信息越来越多&#xff0c;并呈现加速增长的态势。需求越来越大&#xff0c;交付周期越来越短&#xff0c;受制于体力和能力限制&#xff0c;测试人员的效率和质量难以同步提…

Mysql在Windows系统下安装以及配置

目录 一、下载Mysql 二、安装Mysql及环境配置 一、下载Mysql 1. 下载地址 官网:https://www.mysql.com&#xff0c;这里我选用的是Mysql8.0.37版本&#xff08;版本无所谓&#xff0c;随便下8.0.几都行&#xff09; 2.点击DOWNLOADS 然后&#xff0c;点击 MySQL Community…

【SOLID原则前端中的应用】开闭原则(Open/Closed Principle)- vue3示例

开闭原则&#xff08;Open/Closed Principle&#xff09;在Vue 3中的应用 开闭原则&#xff08;Open/Closed Principle&#xff0c;OCP&#xff09;规定&#xff0c;软件实体&#xff08;类、模块、函数等&#xff09;应该对扩展开放&#xff0c;对修改关闭。 也就是说&#xf…

中国植物志(80卷)

中国植物志&#xff0c;全书共80卷126分册&#xff0c;3700页&#xff0c;记载了我国301科3408属31142种植物学名、形态特征、生态环境、地理分布、经济用途和物候期等。是研究中国植物的重要论著&#xff08;截图仅部分&#xff09;。

VBA通过Range对象实现Excel的数据写入

前言 本节会介绍通过VBA中的Range对象&#xff0c;来实现Excel表格中的单元格写入、区域范围写入&#xff0c;当然也可以写入不同类型的数据&#xff0c;如数值、文本、公式&#xff0c;以及实现公式下拉自动填充的功能。 一、单元格输入数据 1.通过Value方法实现输入不同类型…