【C++】STL:vector常用接口的使用和模拟实现

Hello everybody!这篇文章主要给大家讲讲vector常用接口的模拟实现,STL库中的实现一层套着一层,十分复杂,目前阶段还不适合看源代码。而模拟实现可以让我们从底层上了解这些接口的原理从而更好的使用这些接口。另外我还会讲一些在vector使用过程中特别容易出错的点!

还有就是这篇文章很有难度,类和对象基础不太好的同学可能看不懂,建议回去复习一下构造,拷贝构造和const引用,然后再来看这篇文章会好很多。当然,这些知识我也写过博客,其中描述的很详细,大家也可以去看看!

1.vector常用接口的模拟实现

这些接口的实现细节和难点我都有注释出来,如有疑问可以发在评论区,我会解答。其中print_vector是我自己在类外面写的函数,不是vector中的接口。

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace bit {
	template <class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		//在深拷贝构造写出来之前先不要写析构,否则在测试时可能会出现某个对象析构两次而崩溃
		~vector() {
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}
		//这是深拷贝构造
		vector(const vector<T>& v) {
			reserve(v.capacity());//目的是一次性扩容,防止push_back反复扩容对性能消耗过大!
			for (auto& e : v) {
				push_back(e);
			}
		}
		//这是构造函数,可以用一个容器的迭代区间构造。
		template<class InputIterator>//类模板中可以套函数模板
		vector(InputIterator first, InputIterator last) {
			reserve(last - first);
			while (first != last) {
				push_back(*first);
				first++;
			}
		}
		//这是构造函数,可以用n个T类型的变量去构造。T()是匿名对象 比如vector(),string()。
		//如果T是内置类型,比如int,那么int()就是0,char()是'/0',double()是0.0
		vector(size_t n, const T& val = T()) {
			reserve(n);
			while (n--) {
				push_back(val);
			}
		}
		//如果没有这个构造函数的话,用vector<int> v(5,8)构造时,
		//编译器会匹配vector(InputIterator first, InputIterator last),而不匹配vector(size_t n, const T& val = T())
		vector(int n=0, const T& val = T()) {
			reserve(n);
			while (n--) {
				push_back(val);
			}
		}
		//c++11新增了一个类initializer_list<T>,{1,2,3,4}的类型就是initializer_list<int>,
		//这样就支持vector<int> v({1,2,3,4})这样的写法。
		vector(std::initializer_list<T> il) {
			reserve(il.size());
			for (auto& e : il) {
				push_back(e);
			}
		}
		
		iterator begin() {
			return _start;
		}
		iterator end() {
			return _finish;
		}
		const_iterator begin() const{
			return _start;
		}
		const_iterator end() const {
			return _finish;
		}
		size_t size() const{
			return _finish - _start;
		}
		size_t capacity() const{
			return _endofstorage - _start;
		}
		T& operator[](size_t pos) {
			assert(pos < size());
			return _start[pos];
		}
		const T& operator[](size_t pos) const{
			assert(pos < size());
			return _start[pos];
		}
		void reserve(size_t n) {
			if (n > capacity()) {
				size_t old_size = size();
				T* tmp = new T[n];
				//如果T是string的话,memcpy就是一个浅拷贝,后面delete[] _start会有问题!
				//memcpy(tmp, _start, sizeof(T) * size());
				for (size_t i = 0; i < old_size; i++) {
					//调用各种容器的赋值,这是深拷贝
					tmp[i] = _start[i];
				}
				delete[] _start;
				_start = tmp;
				_finish = tmp + old_size;
				_endofstorage = tmp + n;
			}
		}
		/*形参有引用最好加上const,否则push_back("abcd"); 就传不了,
		因为隐式类型转换后,"abcd"转换成string,这是一个临时对象,临时对象具有常性,无法修改。*/
		void push_back(const T& val) {
			if (_finish == _endofstorage) {
				reserve(capacity() == 0 ? 4 : 2 * capacity());
			}
			*_finish = val;
			_finish++;
		}
		bool empty() const{
			return _start == _finish;
		}
		void pop_back() {
			/*assert(!empty());
			_finish--;*/
			erase(end()-1);//不可以用--end()。因为end()传值返回时返回的是一个临时对象,临时对象不可修改!
		}
		iterator erase(iterator pos) {
			assert(pos >= _start);
			assert(pos <= _finish);
			iterator it = pos;
			while (it<_finish-1) {
				*it = *(it+1);
				it++;
			}
			_finish--;
			return pos;
		}
		void insert(iterator pos,const T& val) {
			assert(pos <= _finish);
			assert(pos >= _start);
			if (_finish == _endofstorage) {
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());//扩容时一定要注意迭代器失效问题!及时更新pos
				pos = _start + len;
			}
			iterator it = _finish-1;
			while (it >= pos) {
				*(it + 1) = *it;
				it--;
			}
			*pos = val;
			_finish++;
		}

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

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

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

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
	template<class T>
	void print_vector(vector<T> v) {/*在深拷贝没有写出来之前不要写析构函数, 因为这里的传参是浅拷贝,
		函数结束自动调用析构,而main函数结束也会调用析构。对堆上同一块空间析构两次程序会崩溃!*/
		
		typename vector<T>::iterator it = v.begin();
		/*这里必须要加typename,告诉编译器后面是一个类型,否则编译器无法识别vector<T>::iterator 是类型还是静态成员变量。*/
		while (it != v.end()) {
			cout << *it << " ";
			it++;
		}
		cout << endl;

		for (auto e : v) {
			cout << e << " ";
		}
		cout << endl;

		for (size_t i = 0; i < v.size(); i++) {
			cout << v[i] << " ";
		}
		cout << endl;
		
	}
}

2.vector常用接口的使用

灵活使用这些接口需要对类和对象中构造,拷贝构造等函数掌握得非常牢固。构造的多种重载形式我在上面实现的时候都有给出,大家可以在上面一一对应。当然也可以去cplusplus网站中去查看vector各种接口的重载形式。

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
int main() {
	string s1("abcdef");
	std::string::iterator it1 = s1.begin();
	std::string::iterator it2 = s1.end();
	/*vector构造的使用。*/
	std::vector<int> v1;
	std::vector<int> v2(8, 1);
	std::vector<string> vv1(6, "1111111");//先用"1111111"构造string,然后在构造vextor<string>。
	std::vector<string> v3(2,s1);
	std::vector<string> v4;
	std::vector<char> v5(s1.begin(),s1.end());
	vector<int> v9({ 1,2,3,4 });//c++11支持
	/*vector拷贝构造的使用。*/
	vector<int> v6(v2);
	vector<int> v7 = v2;
	vector<int> v10{ 1,2,3,4 };//c++11支持
	vector<int> v8 = {1,2,3,4};//c++11支持,这个数组的类型是initializer_list<T>的类型,要用到构造加拷贝构造
	int i = 1;
	int b = { 1 };//c++11支持 
	int c{ 1 };//c++11支持
	/*operator= 的使用*/
	v8 = v6;
	v8 = { 1,2,3,4 };//c++11支持,这是构造加赋值

	/*算法库中的find。*/
	auto pos=find(v8.begin(), v8.end(), 3);//库中给的是函数模板,在一段迭代区间中查找某值。没找到,返回v8.end().
	cout << *pos<<v8[2] << endl;

	/*insert的使用:有三种用法。在某个位置插入:1.某个值 2.n个值 3.迭代器区间*/
	v8.insert(v8.begin(), 8);
	v8.insert(v8.begin(), 5, 8);
	v8.insert(v8.begin(), it1, it2);//也可以是自己的迭代器区间。
	
	/*erase的使用*/
	/*可以删除某个位置(迭代器),也可以删除某个迭代器区间*/

	/*max_size:这个接口没啥意义。*/

	/*resize的使用*/
	v8.resize(100, 1);//该接口不会缩容,
	/*1.如果比size小,则缩小size,第二个参数就没用了。
	2.如果>size,<capacity,则增加size,增加的元素用第二个参数初始化。
	3.>capacity 扩容加初始化。
	4.半缺省,第二个参数可省略。*/
	

	/*reserve的使用*/
	v8.reserve(10);//很简单,就是扩容。不会缩容。

	/*shrink_to_fit的使用*/
	v8.resize(10);
	v8.shrink_to_fit();//将空间缩容到size大小。

	/*emplace与insert用法相同。*/
	/*emplace_back与push_back用法相同。*/
	return 0;
}

3.使用vector接口极易出现且难以发现的错误

3.1insert引起的迭代器失效

就用我刚才实现的vector来测试:

#include "vector.h"
using namespace bit;
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	vector<int>::iterator it = v.begin();
	cout << *it << endl;
	v.insert(it, 3);//发生扩容,迭代器(it)失效,需要及时更新。
	cout << *it << endl;
	return 0;
}

运行结果:

插入一个值后,发生扩容,it指向的原空间被释放掉,it失效。

对于这个问题,STL库中也没有给出很好的解决办法。

所以我们在使用时记得更新迭代器(这要养成习惯。)

更正代码:

#include "vector.h"
using namespace bit;
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	vector<int>::iterator it = v.begin();
	cout << *it << endl;
	v.insert(it, 3);//发生扩容,迭代器(it)失效,需要及时更新。
	it = v.begin();//更新迭代器
	cout << *it << endl;
	return 0;
}

运行结果:

3.2erase引起的迭代器失效

依然用刚刚实现的vector做测试

场景一:

#include "vector.h"
using namespace bit;
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = v.begin();
	//删除偶数
	while (it != v.end()) {
		if (*it % 2 == 0) {
			v.erase(it);
		}
		it++;
	}
	print_vector(v);
	return 0;
}

运行结果:

偶数没有删完整,是因为删完一个整数时,后面的数据往前移,迭代器往后移,导致跳过了某个数据。

场景二:

#include "vector.h"
using namespace bit;
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);
	v.push_back(4);
	vector<int>::iterator it = v.begin();
	//删除偶数
	while (it != v.end()) {
		if (*it % 2 == 0) {
			v.erase(it);
		}
		it++;
	}
	print_vector(v);
	return 0;
}

运行结果:

这次是程序崩溃,原因与场景一类似,v.end()往前移,it往后移,导致错过了结束条件,野指针解引用,程序崩溃!

解决方案一:

只需要多加一个else就可以很好的解决这个问题。但这样写并不是通解,这种写法只能在Linux下跑,因为Linux下的erase不会缩容,迭代器就不会失效。

如果在VS下,用STL库中的vector,这样写也同样有问题!

在VS下,代码:

#include <iostream>
#include <vector>
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);
	v.push_back(4);
	vector<int>::iterator it = v.begin();
	//删除偶数
	while (it != v.end()) {
		if (*it % 2 == 0) {
			v.erase(it);
		}
		else {
			it++;
		}
	}
	for (auto e : v) {
		cout << e << " ";
	}
	return 0;
}

运行结果:

在VS下这种写法同样会崩溃,因为VS的迭代器有强制检查,在调用erase过后不让使用迭代器(范围for的底层就是迭代器),否则系统会杀掉该程序。

那么VS为什么要这样设计呢?因为不同平台下,STL库的底层是不太一样的,有的平台的erase不会缩容,有的平台的erase可能会缩容,一旦erase发生了缩容,迭代器指向的空间被释放掉,迭代器就失效了,所以VS规定在erase过后不能使用迭代器!

那么怎么解决这个问题呢?

同样的,还是需要及时更新迭代器。

通过查STL库我们发现,erase有一个返回值,是一个迭代器,它指向刚刚删除元素的下一个位置。换句话说就是库中的erase帮帮我们更新了迭代器,只要我们能利用上这个返回值,就能通过VS的强制检查,程序就可以跑起来啦!

erase引起的迭代器失效的通解代码

#include <iostream>
#include <vector>
using namespace std;
int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);
	v.push_back(4);
	vector<int>::iterator it = v.begin();
	//删除偶数
	while (it != v.end()) {
		if (*it % 2 == 0) {
			it=v.erase(it);
		}
		else {
			it++;
		}
	}
	for (auto e : v) {
		cout << e << " ";
	}
	return 0;
}

运行结果:

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

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

相关文章

【第34天】SQL进阶-SQL高级技巧-Window Funtion(SQL 小虚竹)

回城传送–》《100天精通MYSQL从入门到就业》 文章目录 零、前言一、练习题目二、SQL思路初始化数据什么是Window Funtion窗口函数的分类语法结构第一种写法&#xff1a;第二种写法&#xff1a; 实战体验序号函数&#xff1a;row_number()序号函数&#xff1a;rank()序号函数&…

【树莓派Linux内核开发】入门实操篇(虚拟机Ubuntu环境搭建+内核源码获取与配置+内核交叉编译+内核镜像挂载)

【树莓派Linux内核开发】入门实操篇&#xff08;虚拟机Ubuntu环境搭建内核源码获取与配置内核交叉编译内核镜像挂载&#xff09; 文章目录 【树莓派Linux内核开发】入门实操篇&#xff08;虚拟机Ubuntu环境搭建内核源码获取与配置内核交叉编译内核镜像挂载&#xff09;一、搭建…

什么是0-day漏洞,怎么防护0-day漏洞攻击

随着信息技术的快速发展&#xff0c;网络安全问题日益凸显&#xff0c;其中0day漏洞攻击作为一种高级威胁手段&#xff0c;给企业和个人用户带来了极大的风险。下面德迅云安全就对0day漏洞攻击进行简单讲解下&#xff0c;并分享相应的一些安全措施&#xff0c;以期提高网络安全…

网络空间地图测绘理论体系白皮书(2023年)02网络空间测绘研究背景(想法比较好,着重看)

01前言 02 网络空间测绘研究背景 2.1 网络空间的起源 2.2 传统测绘理论 2.3 网络空间测绘相关工作 03 测绘体系框架概念定义 3.1 网络空间 3.2 网络空间地图测绘 3.3 体系框架总体思路 04 测绘体系框架应用实践 4.1 网络空间地形图 4.2 网络空间地志图 4.3 网络空间战略图 05 总…

Python 全栈安全(一)

原文&#xff1a;annas-archive.org/md5/712ab41a4ed6036d0e8214d788514d6b 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 前言 序言 多年前&#xff0c;我在亚马逊搜索了一本基于 Python 的应用程序安全书。我以为会有多本书可供选择。已经有了很多其他主题的 Pyt…

【ZYNQ】Zynq 芯片介绍

Zynq 是 Xilinx 公司提出的全可编程 SoC 架构&#xff0c;集成了单核或多核 ARM 处理器与 Xilinx 16nm 或 28nm 可编程逻辑&#xff0c;包括 Zynq 7000 Soc&#xff0c;Zynq UltraScale MPSoC 和 Zync UltraScale RFSoC 等系列。本文主要介绍 Xilinx Zynq 7000 系列芯片架构、功…

Hadoop1X,Hadoop2X和hadoop3X有很大的区别么?

Hadoop的演进从Hadoop 1到Hadoop 3主要是为了提供更高的效率、更好的资源管理、更高的可靠性以及对更多数据处理方式的支持。下面是Hadoop 1, Hadoop 2, 和 Hadoop 3之间的主要区别和演进的原因&#xff1a; Hadoop 1 特点&#xff1a; 主要包括两大核心组件&#xff1a;HDFS&a…

simple-jwt快速入门(包含自定制)

simple-jwt快速入门(包含自定制) 目录 simple-jwt快速入门(包含自定制)安装路由层视图层全局配置前端传入参数配置文件定制登录返回格式定制payload格式自定制签发-认证 安装 pip install djangorestframework-simplejwt路由层 from rest_framework_simplejwt.views import T…

【 AIGC 研究最新方向(上)】面向平面、视觉、时尚设计的高可用 AIGC 研究方向总结

目前面向平面、视觉、时尚等设计领域的高可用 AIGC 方向有以下 4 种&#xff1a; 透明图层生成可控生成图像定制化SVG 生成 本篇&#xff08;上篇&#xff09;介绍 1、2&#xff0c;而下篇将介绍 3、4。 透明图层生成 LayerDiffuse 代表性论文&#xff1a;Transparent Imag…

Qt基础之四十六:Qt界面中嵌入第三方程序的一点心得

本文主要讲解QWidget和QWindow的区别,以及如何在QWidget中嵌入第三方程序,并完美解决在QWidget中嵌入某些程序(比如Qt程序)时出现的白边问题。 下面是嵌入QQ音乐的样子,这首歌还不错。 先用spy++查看QQ音乐的窗口信息,如果安装了Visual Studio,工具菜单里自带spy++ 然后…

Spring Boot | Spring Boot 默认 “缓存管理“ 、Spring Boot “缓存注解“ 介绍

目录: 一、Spring Boot 默认 "缓存" 管理 :1.1 基础环境搭建① 准备数据② 创建项目③ 编写 "数据库表" 对应的 "实体类"④ 编写 "操作数据库" 的 Repository接口文件⑤ 编写 "业务操作列" Service文件⑥ 编写 "applic…

Redis入门到通关之数据结构解析-QuickList

文章目录 ☃️前提概要☃️ 配置项相关☃️简要源码☃️总结 Redis中的 QuickList 是一种特殊的数据结构&#xff0c;用于存储列表类型的数据。它的设计目的是在内存中高效地存储和操作大量的列表元素&#xff0c;尤其是当列表长度很大时。 QuickList的内部结构是一个由多个节…

政安晨:【Keras机器学习示例演绎】(八)—— 利用 PointNet 进行点云分割

目录 简介 导入 下载数据集 加载数据集 构建数据集 预处理 创建 TensorFlow 数据集 PointNet 模型 排列不变性 变换不变性 点之间的相互作用 实例化模型 训练 直观了解培训情况 推论 最后说明 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论…

【PCL】教程 implicit_shape_model.cpp 3D点云数据的对象识别 利用隐式形状模型进行训练和识别...

ism_test_cat.pcd 参数&#xff1a;ism_train_cat.pcd 0 ism_train_horse.pcd 1 ism_train_lioness.pcd 2 ism_train_michael.pcd 3 ism_train_wolf.pcd 4 ism_test_cat.pcd 0 这里红点表示对应感兴趣类别的对象预测中心 ./ism_t…

字节FE:JavaScript学习路线图

JavaScript简介 JavaScript是一种高级的、解释执行的编程语言。它是互联网的三大核心技术之一&#xff0c;与HTML和CSS一同工作&#xff0c;用于创建交互式的网页。JavaScript被所有现代网页浏览器支持而不需要任何插件。它可以增强用户界面和网页的交互性&#xff0c;可以进行…

【讲解下Spring Boot单元测试】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

FineBi中创建自定义的图表

FineBi中增加自己的自定义图表组件,比如: 的相关笔记: 1 获取有哪些BI自定义图表组件:http://localhost:8080/webroot/decision/v5/plugin/custom/component/list?_=1713667435473[{"name": "图表DEMO_EK","chartType": "amap_demo&q…

GO环境及入门案例

文章目录 简介一、win GO开发环境安装二、Linux go运行环境二、GO代码入门2.1 导包案例2.2 赋值2.3 变量、函数2.4 三方库使用 简介 go不是面向对象语言&#xff0c; 其指针、结构体等比较像C&#xff0c;知名的go 开源项目有docker k8s prometheus node-exporter等 一、win …

如何在3dMax中快速打包mzp 文件?

如何在3dMax中创建mzp 文件&#xff1f; 我喜欢将我的Maxscript脚本发布为mzp文件。这是一个为3dMax构建的自解压zip文件。在mzp文件中&#xff0c;您可以捆绑Maxscript脚本文件、图片、预设或其他文件&#xff0c;并链接安装时执行的特殊操作。 在3dMax中使用大型脚本时&…

耐高温300度锅炉轴承,江苏鲁岳轴承制造的行业标杆

自润滑轴承-产品类型-耐高温轴承-不锈钢轴承-江苏鲁岳轴承制造有限公司。锅炉轴承&#xff0c;耐高温至200度-800度。 江苏鲁岳轴承制造有限公司&#xff0c;一家专注于锅炉轴承和耐高温轴承的研发与生产的企业&#xff0c;致力于为客户提供高质量、高性能的轴承解决方案。其中…