【c++】vector实现(源码剖析+手画图解)

news2024/9/24 15:28:42

        vector是我接触的第一个容器,好好对待,好好珍惜!

目录

文章目录

前言

二、vector如何实现

二、vector的迭代器(原生指针)

三、vector的数据结构

图解:

四、vector的构造及内存管理

1.push_back()

代码如下:

2.pop_back()

 代码如下:

3.insert()

图解:

代码如下:

需要注意的是insert()迭代器失效问题: 

4.erase()

图解:

代码如下:

需要注意的是erase()的迭代器失效问题:

给出三种验证的例子:

图解:

五、vector的reserve与resize

1.reserve()

代码如下:

需要注意的是memcpy()的浅拷贝问题:

改进版:

代码如下:

2.resize()

代码如下:

六、vector类的基本成员函数

1.default构造

代码如下:

2.copy构造

1)传统写法

代码如下:

需要注意的仍然是memcpy()的浅拷贝:

改进版:

2)现代写法

代码如下:

3.用迭代器区间的copy构造函数

4.析构

代码如下:

七、其他的一些函数

1.获取size

代码如下:

2.获取capacity

代码如下:

3.访问元素

代码如下:

4.迭代器 -> 使用范围for

代码如下:

5.赋值重载

1)传统写法

需要注意的仍然是memcpy()的浅拷贝:

改进版:

2)现代写法

代码如下:

6.swap()

代码如下:

总结



前言

        vector实现时最难想的是赋值重载时的现代写法,很妙,最有趣的是两种迭代器的失效问题。与数组相同,但又多少有点不同。


一、vector是什么?

        vector是一个序列式容器(其中的元素都可序(ordered),但是未必有序(sorted)),本质上是可变数组,尾插尾删效率较高。

二、vector如何实现

        源代码中vector的实现使用到了alloc(空间配置器),但是鉴于自身技术水平,个人只在本文中阐述如何实现无alloc版本的vector。

二、vector的迭代器(原生指针)

        因为vector是一段连续的物理存储空间,实现增删查改时,需要对位置进行++、--、+、-、+=、-=、->,这些都是普通指针就具备的能力,所以vector提供的是Random Access Iterators。

代码如下(示例):

template<typename T>
    class vector
	{
	public:		
        typedef T valueType;
		typedef T* iterator;
    private:
		iterator _start;
		iterator _finish;
		iterator _endOfStorage;
    }

三、vector的数据结构

        vector是一段连续的物理空间,其中SGI版本使用了三个指针去管理这个变长数组、使用了两个unsigned int变量去标识有效字符与容量

图解:


四、vector的构造及内存管理

        与之前学习数据结构实现的动态顺序表内存管理其实大同小异:一开始有一块默认大小的内存;

增:

        ①内存不够再存新元素:扩容,一般扩至原capacity的2倍

        ②内存够:直接存。

删:

        ①减少元素个数但是不缩容(大部分版本)。

1.push_back()

代码如下:

void push_back(const valueType& x) 
		{
			// 增容
			if (_finish == _endOfStorage)
			{
				reserve((capacity() == 0) ? 4 : capacity() * 2);
			}
			// 尾插
			*_finish = x;
			++_finish;
		}

2.pop_back()

 代码如下:

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

3.insert()

        原stl给出的模板中,insert()是返回了一个迭代器,指向插入元素的位置。        

        目的:若迭代器已经失效,尽管在增容操作中更新迭代器的值进行规避风险,但是调用时的迭代器仍为野指针(失效状态),若后续操作需要使用插入元素位置,那就会在此出现问题,所以可以再用这个迭代器接受insert()的返回值来进行更新。

图解:

代码如下:

// 随机插入
		iterator insert(iterator pos, const valueType& val)
		{
			// 最小在头插,最大在尾插
			assert(pos >= _start);
			assert(pos < _finish);
			//增容,会使迭代器失效
			if (_finish == _endOfStorage)
			{
				// 更新迭代器的值
				size_t len = pos - _start;
				reserve((capacity() == 0) ? 4 : capacity() * 2);
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = val;
			// 元素个数+1,_finish+1
			++_finish;

			return pos;
		}

需要注意的是insert()迭代器失效问题: 

        当插入时,vector进行了扩容操作,这一步会:

        ①使用一块新的、够大的tmp空间去拷贝原来_start指向的数据

        ②将原来的_start指向的空间释放

        ③把tmp的空间给_start

        但是此时,传进来的、想要插入数据的迭代器仍然指向原来的_start空间,这个迭代器就变成了野指针,再下一步对这个迭代器指向位置进行操作时就会非法访问,所以需要在进行扩容操作是对于迭代器也进行相应的更新,尽量避免失效问题。 

4.erase()

        原stl给出的模板中,erase()是返回了一个迭代器,指向删除元素的位置的下一个位置。

        目的:同insert()相同。

        Q:为什么指向的是操作位置的下一个位置?

        A:因为删除操作本质上是对数据的覆盖,这样一来,从操作位置开始向后的数据向前挪动也相当于操作位置向后移动了一位(指向了下一个位置)。 

图解:

代码如下:

// 随机删除
		iterator erase(iterator pos)
		{
			// 最小头删,最大尾删
			assert(pos >= _start);
			assert(pos < _finish);

			auto begin = pos + 1;
			while (begin != _finish)
			{
				*begin = *(begin + 1);
				++begin;
			}
			// 删除一个元素,_finish向前移动
			--_finish;
			// 返回的是删除元素的下一个元素
			return pos;
		}

需要注意的是erase()的迭代器失效问题:

        erase()的迭代器失效主要是因为迭代器的意义改变,当然,也有野指针的问题。

        ps:但是这个无法通过我们改变erase()实现去规避。

        ①意义改变:当数据向前挪动时,这个迭代器已经不再是我们想要的迭代器了,他一次性向后跳了两个位置。

        ②野指针:当数据向前挪动时,迭代器可能跳过了_finish(当第一次与_finish相遇时,begin++的操作还在继续进行),从而继续向后移动,宛如脱缰野马,永远不可能再停下(与_finish相遇才会结束)。

给出三种验证的例子:

        均是删除给定数据中的偶数元素:        

        1 2 3 4 5 - > 迭代器意义改变,但是刚好这个bug没有使结果出现问题

        1 2 3 4 - > 迭代器

        1 2 4 5 - > 4被跳过(迭代器每次相当于向后移动2位),未删除

图解:


五、vector的reserve与resize

1.reserve()

        改变的是容器的容量,需要进行开辟空间、释放空间、拷贝数据的操作,效率不是很高,也正因为扩容的效率不高,所以才有了_endofStorage和_finish去标识容量与元素个数。

代码如下:

void reserve(size_t newCapacity)
		{
			// 扩容
			valueType* tmp = new valueType[newCapacity]();
			size_t sz = size();
			// 如果有数据,需拷贝到新空间
			if (_start)
			{
				memcpy(tmp, _start, sizeof(valueType) * size());
			}
			// 释放旧空间
			delete[] _start;
			_start = tmp;
			_finish = _start + sz;
			_endOfStorage = _start + newCapacity;
		}

需要注意的是memcpy()的浅拷贝问题:

        因为memcpy()进行的是位操作,如果copy的是内置类型(int、char等)那么直接使用memcpy()会效率很高很方便;但是牵扯到指针,这会导致内存错误:

        当两个指针的内容相同,那就意味着它们指向了同一块空间,如果一个指针释放,另一个指针仍对其进行操作就会使得内存发生错误。

改进版:

        使用for循环自己一个元素一个元素进行拷贝。

代码如下:

void reserve(size_t newCapacity)
		{
			// 扩容
			valueType* tmp = new valueType[newCapacity]();
			size_t sz = size();
			// 如果有数据,需拷贝到新空间
			if (_start)
			{
				//memcpy(tmp, _start, sizeof(valueType) * size());
				for (size_t i = 0; i < sz; i++)
				{
					tmp[i] = _start[i];
				}
			}
			// 释放旧空间
			delete[] _start;
			_start = tmp;
			_finish = _start + sz;
			_endOfStorage = _start + newCapacity;
		}

2.resize()

        改变的是元素的个数(size)。

        ①如果newSize比size小,那么就发生了删除,直接改变_finish即可,但是不缩容(以防后续操作频繁删除增容、频繁向操作系统申请、释放内存造成抖动)。

        ②如果newSize比size大,那么就可能发生了扩容(需检查)。

                1.若给出初始化的值,那么根据给出的值进行初始化

                2.若无给出的值,默认是空,在模板中是const vaueType& val = valueType(),构造了空的对象进行缺省参数的赋值。

代码如下:

void resize(size_t newSize, const valueType& x = valueType())
		{
			// newSize比size短,截断
			// 但是容量不变
			if (_finish + newSize < _endOfStorage)
			{
				_finish = _start + newSize;
			}
			// 长,增长并初始化
			else
			{
				reserve(newSize);
				while (_finish != _start + newSize)
				{
					*_finish = x;
					_finish++;
				}
			}
		}

六、vector类的基本成员函数

1.default构造

        三个原生指针,初始化为空指针即可。

代码如下:

// 构造
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{}

2.copy构造

1)传统写法

        传统写法就是自己一步一步实现深拷贝的过程。

代码如下:

// copy构造
		// 传统写法
		vector(const vector<valueType>& obj)
		{
			_start = new valueType[obj.size()];
			_finish = _start + obj.size();
			_endOfStorage = _start + obj.capacity();

			memcpy(_start, obj._start, obj.size() * sizeof(valueType));
		}

需要注意的仍然是memcpy()的浅拷贝:

        我们应该使用for循环去自己实现深拷贝避免内存问题。

改进版:

// 传统写法
		vector(const vector<valueType>& obj)
		{
			_start = new valueType[obj.size()];
			_finish = _start + obj.size();
			_endOfStorage = _start + obj.capacity();

			//memcpy(_start, obj._start, obj.size() * sizeof(valueType));
			for (size_t i = 0; i < size(); i++)
			{
				_start[i] = obj._start[i];
			}
		}

2)现代写法

        使用tmp变量,让其使用构造函数构造出我们想要copy的对象,然后将tmp和我们将要copy的对象进行交换,从而帮助我们实现copy构造。

        swap(tmp, *this)这一点就是将*this中的垃圾扔给了tmp,tmp还把自己有用的数据全部交给了*this,就好比是外卖员替你送了外卖还得帮你扔垃圾。

        但是很明显的是:已有的构造函数并不能根据传进来的obj对tmp进行构造。我们需要重载实现一个支持用迭代器区间的copy构造函数(stl本来也支持这样的方式构造)。(见下一点)

        ps:copy构造也是构造,别忘了初始化!

代码如下:

// 现代写法
		//类模板的成员函数也可以变成函数模板
		vector(const vector<valueType>& obj)
			:_start(nullptr)
			,_finish(nullptr)
			,_endOfStorage(nullptr)
		{
			vector<valueType> tmp(obj.begin(), obj.end());

			// “外卖员送饭又倒垃圾的例子”
			swap(tmp);
		}

3.用迭代器区间的copy构造函数

        我们实现为了模板函数,这一点是在套娃:

        本来就是类模板的成员函数,其可以用函数模板去实现。

        目的:

        其他的迭代器(string、int、char等)都可以通过迭代器去构造。

// 迭代器版的copy构造
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{
			while (first != last)
			{
				this->push_back(*first);
				first++;
			}
		}

4.析构

        析构是释放空间,因为三个指针指向的是同一段空间,所以只需要释放_start。

        然后将它们置为空。

        ps:new时候使用的[],所以delete也需要[]进行释放。

代码如下:

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

七、其他的一些函数

1.获取size

        回想我们之前的结构:

        size就是_finish与_start之间的距离,使用两个指针相减即可得到。

代码如下:

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

2.获取capacity

        同size()。

代码如下:

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

ps:但是需要注意的是这两个成员函数最好实现为const成员函数,因为在copy构造与的default构造时传的参数均使用了const。(常对象与非常对象都可以用const)

3.访问元素

        数组可以通过下标访问。

代码如下:

const valueType& operator[](size_t i)
		{
			return _start[i];
		}

4.迭代器 -> 使用范围for

        实现为const,原理同size()、capacity()。

代码如下:

// 迭代器
		// 必须是begin与end(同名)才能使用范围for
		iterator begin() const
		{
			return _start;
		}
		iterator end() const
		{
			return _finish;
		}

5.赋值重载

        赋值重载与copy构造总是很相像:

        ①赋值重载是相对于两个已经存在的对象之间的操作。

        ②copy构造是相对于一个即将创建一个已经存在的对象之间的操作。

        

        但是它们最终要达到的目标都是相同的:让两个对象相等。

        

        所以在实现过程上也大同小异。

        实现赋值重载需要注意的点是:

        ①判断是否给自己赋值

        ②需要传引用返回,减少拷贝

        ③传进来的函数参数需要是const的引用,防止修改+减少拷贝。

        ④返回值是*this

1)传统写法

                与copy构造相同的是,它也要实现:释放旧空间、开辟新空间、拷贝数据。

// 传统写法
		vector<valueType>& operator=(const vector<valueType>& obj)
		{
			// 判断是否给自己赋值
			if (this == &obj)
			{
				return *this;
			}

			delete[] _start;
			//_start = (valueType*)calloc(obj.capacity(), sizeof(valueType));
			_start = new valueType[obj.capacity()];
			_finish = _start + obj.size();
			_endOfStorage = _start + obj.capacity();

			memcpy(_start, obj._start, sizeof(obj) * obj.size());
			return *this;
		}

需要注意的仍然是memcpy()的浅拷贝:

        我们使用for循环自己进行深拷贝。

改进版:

// 传统写法
		vector<valueType>& operator=(const vector<valueType>& obj)
		{
			// 判断是否给自己赋值
			if (this == &obj)
			{
				return *this;
			}

			delete[] _start;
			//_start = (valueType*)calloc(obj.capacity(), sizeof(valueType));
			_start = new valueType[obj.capacity()];
			_finish = _start + obj.size();
			_endOfStorage = _start + obj.capacity();

			//memcpy(_start, obj._start, sizeof(obj) * obj.size());
			for (size_t i = 0; i < obj.size(); i++)
			{
				_start[i] = obj._start[i];
			}
			return *this;
		}

2)现代写法

        这种写法很巧妙地使用了传值传参需要进行拷贝构造的特点,利用这一点在传参的时候就可以深拷贝出来一个obj,这样进行交换的时候就不会把原来的obj更改。

代码如下:

/ 赋值重载 v2 = v1
		// 现代写法
		vector<valueType>& operator=(vector<valueType> obj)
		{
			swap(obj);
			return *this;
		}

6.swap()

        由于copy构造与赋值重载都使用了交换,所以将它实现为了函数。

代码如下:

void swap(vector<valueType>& tmp) 
		{
			std::swap(tmp._start, _start);
			std::swap(tmp._finish, _finish);
			std::swap(tmp._endOfStorage, _endOfStorage);
		}

总结

        vector的实现是对于stl源码的一次深入理解,但是仍然没有学会alloc,只学习了一部分,自己模拟实现vector,不在于造一个更好的轮子,但是可以帮助我更好的了解它、使用它。

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

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

相关文章

《爆肝整理》保姆级系列教程python接口自动化(十二)--https请求(SSL)(详解)

简介 本来最新的requests库V2.13.0是支持https请求的&#xff0c;但是一般写脚本时候&#xff0c;我们会用抓包工具fiddler&#xff0c;这时候会 报&#xff1a;requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590) 小编…

C++:提高篇: 栈-寄存器和函数状态:windows X86-64寄存器介绍

寄存器1、什么是寄存器2、寄存器分类3、windows X86寄存器命名规则4、寄存器相关术语5、寄存器分类5.1、RAX(accumulator register)5.2、RBX(Base register)5.3、RDX(Data register)5.4、RCX(counter register)5.5、RSI(Source index)5.6、RDI(Destination index)5.7、RSP(stac…

iptables和nftables的使用

文章目录前言iptable简介iptable命令使用iptables的四表五链nftables简介nftables命令的时候nftables与iptables的区别iptables-legacy和iptables-nft实例将指定protocol:ip:port的流量转发到本地指定端口前言 本文展示了&#xff0c;iptables和nftable命令的使用。 # 实验环…

win10 安装rabbitMQ详细步骤

win10 安装rabbitMQ详细步骤 win10 安装rabbitMQ详细步骤win10 安装rabbitMQ详细步骤一、下载安装程序二、安装配置erlang三、安装rabbitMQ四、验证初始可以通过用户名&#xff1a;guest 密码guest来登录。报错&#xff1a;安装RabbitMQ出现Plugin configuration unchanged.问题…

力扣SQL刷题10

目录标题618. 学生地理信息报告--完全不会的新题型1097. 游戏玩法分析 V - 重难点1127. 用户购买平台--难且不会618. 学生地理信息报告–完全不会的新题型 max()函数的功效&#xff1a;&#xff08;‘jack’, null, null&#xff09;中得出‘jack’&#xff0c;&#xff08;nul…

基于微信小程序图书馆座位预约管理系统

开发工具&#xff1a;IDEA、微信小程序服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8项目构建&#xff1a;maven数据库&#xff1a;mysql5.7前端技术&#xff1a;vue、uniapp服务端技术&#xff1a;springbootmybatis本系统分微信小程序和管理后台两部分&#xff0c;项目采用…

索引的基本介绍

索引概述-优缺点 索引介绍&#xff1a;索引是一种高效获取数据的数据结构&#xff1b; 索引优点&#xff1a;提供查询效率&#xff1b;降低IO成本&#xff1b;怎么减低IO成本呢&#xff1f;因为数据库的数据是存放在磁盘的&#xff0c;你要操作数据就会涉及到磁盘IO&#xff0…

Windows11 安装Apache24全过程

Windows11 安装Apache24全过程 一、准备工作 1、apache-httpd-2.4.55-win64-VS17.zip - 蓝奏云 2、Visual Studio Code-x64-1.45.1.exe - 蓝奏云 二、实际操作 1、将下载好的zip文件解压放到指定好的文件夹。我的是D:\App\PHP下 个人习惯把版本号带上。方便检测错误。 2…

数组常使用的方法

1. join (原数组不受影响)该方法可以将数组里的元素,通过指定的分隔符,以字符串的形式连接起来。返回值:返回一个新的字符串const arr[1,3,4,2,5]console.log(arr.join(-)&#xff1b;//1-3-4-2-52. push该方法可以在数组的最后面,添加一个或者多个元素结构: arr.push(值)返回值…

(考研湖科大教书匠计算机网络)第四章网络层-第一、二节:网络层概述及其提供的服务

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;网络层概述&#xff08;1&#xff09;概述&#xff08;2&#xff09;学习内容二&#xff1a;网络层提供的两种服务&#xff08;1&#xff09;面向连…

nginx越界读取缓存漏洞(CVE-2017-7529)

range格式: Range: <unit><range-start>- Range: <unit><range-start>-<range-end> Range: <unit><range-start>-<range-end>, <range-start>-<range-end> range事例&#xff1a; Range: bytes500-999 //表示第…

Spring Security简介

前面我们已经完成了传智健康后台管理系统的部分功能&#xff0c;例如检查项管理、检查组管理、套餐管理、预 约设置等。接下来我们需要思考2个问题&#xff1a; 问题1&#xff1a;在生产环境下我们如果不登录后台系统就可以完成这些功能操作吗&#xff1f; 答案显然是否定的&am…

微前端-模块联邦

一、 Module Federation 模块联邦概述 Module Federation 即为模块联邦&#xff0c;是 Webpack 5 中新增的一项功能&#xff0c;可以实现跨应用共享模块。 二、快速上手 需求 通过模块联邦在容器应用中加载微应用。 应用结构 products ├── package-lock.json ├──…

程序的机器级表示part3——算术和逻辑操作

目录 1.加载有效地址 2. 整数运算指令 2.1 INC 和 DEC 2.2 NEG 2.3 ADD、SUB 和 IMUL 3. 布尔指令 3.1 AND 3.2 OR 3.3 XOR 3.4 NOT 4. 移位操作 4.1 算术左移和逻辑左移 4.2 算术右移和逻辑右移 5. 特殊的算术操作 1.加载有效地址 指令效果描述leaq S, DD…

【项目实战】32G的电脑启动IDEA一个后端服务要2min!谁忍的了?

一、背景 本人电脑性能一般&#xff0c;但是拥有着一台高性能的VDI&#xff08;虚拟桌面基础架构&#xff09;&#xff0c;以下是具体的配置 二、问题描述 但是&#xff0c;即便是拥有这么高的性能&#xff0c;每次运行基于Dubbo微服务架构下的微服务都贼久&#xff0c;以下…

使用太极taichi写一个只有一个三角形的有限元

公式来源 https://blog.csdn.net/weixin_43940314/article/details/128935230 GAME103 https://games-cn.org/games103-slides/ 初始化我们的三角形 全局的坐标范围为0-1 我们的三角形如图所示 ti.kernel def init():X[0] [0.5, 0.5]X[1] [0.5, 0.6]X[2] [0.6, 0.5]x[0…

每天10个前端小知识 【Day 12】

&#x1f469; 个人主页&#xff1a;不爱吃糖的程序媛 &#x1f64b;‍♂️ 作者简介&#xff1a;前端领域新星创作者、CSDN内容合伙人&#xff0c;专注于前端各领域技术&#xff0c;成长的路上共同学习共同进步&#xff0c;一起加油呀&#xff01; ✨系列专栏&#xff1a;前端…

I.MX6ULL内核开发9:kobject-驱动的基石

目录 一、摘要 二、重点 三、驱动结构模型 四、关键函数分析 kobject_create_and_add()函数 kobject_create()函数 kobject_init&#xff08;&#xff09;函数 kobject_init_internal(&#xff09;函数 kobject_add&#xff08;&#xff09;函数 kobject_add_varg&am…

JAVA集合专题4 ——ArrayDeque + BlockingQueue

目录ArrayDeque的特点BlockingQueue什么是BlockingQueue?什么叫阻塞队列?阻塞队列的应用场景是什么?BlockingQueue的阻塞方法是什么?BlockingQueue的四类方法codecode2ArrayDeque的特点 ArrayDeque是Deque接口子实现ArrayDeque数据结构可以表示为: 队列、双端队列、栈Arra…

C语言学习笔记(三): 选择结构程序设计

if语句 if(){} if (a1){printf("hehe");} //单独一个ifif(){}else{} int a 1, b 2;if (a b) {printf("haha"); //if else}else{printf("hehe");}if(){}else if(){} int a 1, b 2;if (a b) {printf("haha");}else if (a …