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

news2024/11/13 8:59:42

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;
}

运行结果:

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

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

相关文章

OSPF的学习笔记

1.OSPF &#xff08;1&#xff09;链路状态路由协议的路由信息并不是像距离矢量路由协议那样(邻居告诉的)&#xff0c;通过收集自身以及邻居发出的LSA(原材料)&#xff0c;并LSA放到指定仓库里面(LSDB)&#xff0c;通过SPF算法&#xff0c;以自己为根计算到达网络每个节点的最优…

LeetCode刷题实战5:最长回文子串

题目内容 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab" 解释&#xff1a;"aba"…

虚拟线程的定义及使用

0.前言 长期以来&#xff0c;虚拟线程是 Java 中最重要的创新之一。 它们是在 Project Loom 中开发的&#xff0c;自 Java 19 作为预览功能以来一直包含在 JDK 中&#xff0c;自 Java 21 作为最终版本 (JEP 444) 以来&#xff0c;它们已包含在 JDK 中。 1.虚拟线程的作用 任…

深入OceanBase内部机制:资源隔离实现的方式总结

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 目录 1. 为何HTAP需要资源隔离2. OceanBase的资源隔离机制概述租户间资源隔离租户内资源隔离物理资源隔离大查询请求的隔离优先级…

如何批量给Word文件增加前缀序号?“汇帮批量重命名”帮助你批量给word文件增加前缀序号。

批量给Word文件增加前缀序号的过程&#xff0c;对于经常处理大量文档的人来说&#xff0c;是一项既繁琐又必要的任务。首先&#xff0c;我们需要明确为什么要给Word文件增加前缀序号。在很多情况下&#xff0c;当我们需要按照一定的顺序对多个文档进行管理和归档时&#xff0c;…

【CSS】CSS实现元素逐渐消失(实现元素透明逐渐消失/模糊)

mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%);mask-image 属性用于定义一个遮罩&#xff0c;它可以隐藏元素的一部分或全部内容。在这个示例中&#xff0c;我们使用 mask-image 属性来定义一个线性渐变的遮罩&#xff0c;使得列表项的内…

适配器模式【结构型模式C++】

1.概述 适配器模式是一种结构型设计模式&#xff0c; 又称为变压器模式、包装模式&#xff08;Wrapper&#xff09; 将一个类的接口变换成客户端所期待的另一种接口&#xff0c;从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 2.结构 Target&#xff1a;适配…

Https网站接口被黑被恶意调取

背景&#xff1a; 维护的一个网站最近短信接口被黑&#xff0c;发送大量短信。起初以为是在网站内部操作&#xff0c;优化了发送短信前的操作&#xff0c;如添加图形验证码&#xff0c;屏蔽国外IP等。但后续还存在被调取情况&#xff0c;定位排查到是该接口在外部被恶意调取。 …

Pod 状态 Bsck Off,是什么情况?

k8s项目运维中&#xff0c;相信各位都遇到过 Pod 状态 Bsck Off 的情况&#xff0c;如上图&#xff1a;该情况主要原因是我们 Pod 中没有任何容器运行成功&#xff0c;而能容器能成功运行的前提是&#xff0c;容器内部是有进程的&#xff0c;只要容器中的进程不停止&#xff0c…

ATM第二弹~~~

昨天发现自己电脑的运行速度太慢的问题后&#xff0c;在autodl租了个4090 &#xff08;本来跑了4个小时快好了&#xff0c;但由于没有续上费&#xff0c;断了。。。。&#xff09; ( 2.58 每小时&#xff0c;贵死我了。。。。&#xff09; 又重新开了个4090d&#xff08;409…

论文笔记:Large Language Model for Participatory Urban Planning

202402 arxiv 大模型城市规划 引入了一个基于LLM的多代理协作框架&#xff0c;模拟规划师和数千名具有不同特征和背景的居民&#xff0c;用于参与式城市规划——>生成考虑居民多样化需求的城市区域土地利用规划为了提高讨论的效率&#xff0c;论文采用了鱼缸讨论机制&#…

二维码门楼牌管理应用平台建设:网格化管理的新篇章

文章目录 前言一、二维码门楼牌管理应用平台的建设背景二、二维码门楼牌管理应用平台的功能特点三、二维码门楼牌管理应用平台的实际应用四、二维码门楼牌管理应用平台的前景展望 前言 随着信息技术的飞速发展&#xff0c;二维码门楼牌管理应用平台的建设已成为城市网格化管理…

QT 按钮的工具提示tooltips设置字体大小颜色与背景

QT 按钮的工具提示tooltips设置字体颜色与背景 main.cpp添加 mainwindow.cpp添加全局配置&#xff1a; 构造函数中&#xff1a; QToolTip::setFont(font3); //按钮提示信息通用设置 如下&#xff1a; MainWindow_oq::MainWindow_oq(QWidget *parent) : QMainWindow(parent)…

第十四届蓝桥杯省赛C/C++大学B组真题-飞机降落

思路&#xff1a;根据数据范围N<10猜测用DFS剪枝&#xff0c;因为菜狗不会状压dp。根据题目&#xff0c;一般这种飞机的题都会用到贪心的思想。思想是每架飞机都要卡极限最早降落时间&#xff0c;从而保证后面的飞机能够有充足时间降落。 代码参考博客MQy大佬有详细解答 #i…

盲盒一番赏小程序:打开未知的惊喜之旅

在快节奏的生活中&#xff0c;人们总是渴望寻找一份属于自己的小确幸。盲盒一番赏小程序&#xff0c;正是这样一个为你带来无尽惊喜与乐趣的平台。我们精心打造这一小程序&#xff0c;让每一次点击都成为一次全新的探索&#xff0c;让每一次选择都充满无限可能。 盲盒一番赏小…

刷代码随想录有感(39):每层最大值

题干&#xff1a; 代码&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), …

装修公司网站怎么做的

装修公司网站是一个能够展示公司实力&#xff0c;提供公司服务信息的重要平台。装修公司要做好网站需要注意三点&#xff0c;分别是建设网站的目的、网站的内容和更新维护。 首先&#xff0c;建设网站的目的是非常重要的。网站可以起到展示公司实力&#xff0c;宣传公司服务的作…

mac可以玩steam吗 mac安装steam教程 苹果电脑能打steam游戏吗 苹果电脑怎么安装windows 苹果mac电脑配置AI功能的M4芯片

众所周知&#xff0c;Steam作为一个热门的游戏平台&#xff0c;深受国内外玩家的喜爱&#xff0c;平台中包含了无数的游戏&#xff0c;在作战时玩家们能够与朋友们互动聊天&#xff0c;还能匹配好友组队&#xff0c;同时还能增进与同伴的默契度。 但是最近有玩家们提问说&#…

《星尘传说》游戏完整源码(源码+引擎+客户端+服务端+教程+工具),云盘下载

《星尘传说》是一款奇幻类大型多人在线角色扮演电脑客户端游戏&#xff0c;该游戏设置有两大阵营&#xff0c;六个国家以及22个职业&#xff0c;采用3D卡通风格&#xff0c; 有兴趣的&#xff0c;可以架设个外网&#xff0c;让大家一起玩。 《星尘传说》游戏完整源码&#xff0…

Excel数据处理:数据透视表

点击任意一个单元格 点击插入、数据透视表 点击确定 按照所属区域、产品列表和金额 将求和项改为计数 显示列汇总的百分比 在透视表中双击之后查看明细数 所属区域旁边加上订购日期 对日期进行组合 点中日期任意一个单元格然后右键点击组合 按照季度分组之后 因为…