【STL】 模拟实现简易 vector

news2024/11/25 12:43:39

目录

1. 读源码

2. 框架搭建

3. vector 的迭代器

4. vector 的拷贝构造与赋值

拷贝构造

赋值

5. vector 的常见重要接口实现

operator[ ] 的实现

insert 接口的实现

erase 接口实现

pop_back 接口的实现

resize 接口实现

源码分享

写在最后:


1. 读源码

想要自己实现一个 vector,读源码来理解他的实现是必不可少的一个步骤,

但是,当我们拿到 vector 的源码之后,一堆代码,我们应该从何看起呢?

我们当然是从一个类的核心读起,也就是从他的成员变量开始读:

这里我们找到了他的成员变量,他的类型是 iterator,这又是个啥,

我们来溯源一下:

我们可以看到,实际上 iterator 就是一个T* 的指针类型,

而 iterator 是迭代器,这里我们也可以大致猜到,vector 的迭代器其实就是原生指针。

回归正题,那他的成员函数有什么作用呢?

这个时候我们就可通过去看他的:构造函数 + 插入接口,来进一步的了解:

 

 先来看构造函数,其他的就是一些重载,而且具体的实现也封装起来了,

但是我们看他的默认构造也看不出什么名堂来,只是都初始化成0了,

那我们还是得去看看他的插入接口:

源码的 push_back 说,如果 finish != end_of_storage 就调 construct 然后 finish++

这里我们可以先猜一下源码的意思,他给了 start,finish,end_of_storage,

那其实我们可以菜 start 是数组开始的位置,finish 是结束的位置,

end_of_storage 是数组容量的最后一个位置,那这个 if 语句的判断就是如果数组没满,

就插入一个数据,让 finish++,这里我们暂时不知道 construct 究竟是什么,

但是看源码千万不要陷进一些细节,我们先把大的框架给看好先,那这个时候,

我们就可以大概猜到,else 里面的就是需要扩容的逻辑,他调用了 insert_aux,

那我们就再去看一看这个函数:

这个函数很大,我就一点一点分析啊,

一开始是又进行一次判断,这里的 insert 不一定是只被 push_back 使用的,

所以可能其他地方调用的时候需要这一个判断:

然后我们来看 else 里面的逻辑,首先这里是扩容的策略,

如果第一次就扩容成 1,如果是其他情况就双倍扩容,

然后这里调用的是 allocate 也就是STL自己的空间配置器来要内存,

应为STL比较嫌弃 malloc 开内存的速度啊,就自己内部实现了一个内存池。

 然后这一段逻辑就是拷贝数据到新的空间,

然后又调用了 construct 把数据插入进去,这里我还是先不看他的底层实现啊:

然后最后这里就是把旧的空间释放掉,然后更新成员变量:

然后我这里再补充一个小的点:

这里使用的就是 try catch 来捕获异常的操作,为了防止内存泄漏 catch 这里有销毁内存的操作,

这个时候不得不吐槽一下老 C++ 程序员的爱好,使用宏,总是喜欢搞一堆宏,让人很难受啊。

回归正题啊,这里我们是想搞懂成员变量的含义啊,但是他的 push_back 封装的比较复杂,

所以我们再去看一个扩容的逻辑(reserve)验证我们刚刚的猜想:

其他的我们不在意啊,就来看着几个成员变量的操作,

start = tmp,这里就差不多能证实 start 指向的是数组的开头位置了,

finish = tmp + old_size,这里的 old_size 不就是以前的数据大小吗,那 finish 也没错,

end_of_storage = start + n,reserve 函数传来的 n 就是要扩容到的容量大小,

那我们就大致了解了他的成员变量的含义了。

刚刚说好的,来看看 construct 的实现是怎么样的:

发现没有,construct 其实就是一个定位 new,如果我们需要给一个自定义类型开空间,

那我们就不能直接调用 malloc 了,得调用该自定义类型的构造函数,

而这个 destroy 为什么也把它放出来呢,因为清理资源的时候,他调用的destroy,

其实就是在调用自定义类型的析构函数来清理资源。 

2. 框架搭建

那我们话不多说,直接开始写我们自己的 vector。

先来快速打个架子,让代码跑起来:

#pragma once

#include <iostream>
#include <vector>

using namespace std;

namespace xl {
	template<class T>
	class vector {
    public:
		typedef T* iterator;

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;

	public:
		vector()
			: _start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{}
		
	public:
		iterator begin() {
			return _start;
		}

		iterator end() {
			return _finish;
		}
			 
	public:
		void reserve(size_t n) {
			if (n > capacity()) {
				size_t old_size = size();
				T* tmp = new T[n];
				if (_start) {
					for (size_t i = 0; i < size(); i++) {
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + old_size;
				_end_of_storage = _start + n;
			}
		}

		void push_back(const T& x) {
			if (_finish == _end_of_storage) {
				size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(new_capacity);
			}
			*_finish = x;
			_finish++;
		}

	public:
		int size() const {
			return _finish - _start;
		}

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

我们实现了一个构造,一个 push_back,一个最基本的迭代器,

现在我们可以把代码跑起来了:

void test() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

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

这里我们是直接用范围for,因为范围for 没问题,迭代器肯定没问题。

来看结果:

这里我们再把重要的析构函数给加上:

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

3. vector 的迭代器

我们来看这样一个场景:

void Print(const vector<int>& v) {
	for (auto e : v) cout << e << " ";
	cout << endl;
}

void test2() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

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

	Print(v);
}

实际上,编译器报错了:

这是为什么呢?

我们搭建框架的时候,只实现了普通迭代器,

这里我们传参的时候加了 const 导致出现了权限放大的情况,

所以我们需要重载一份 const 迭代器:

public:
    typedef T* iterator;
    typedef const T* const_iterator;

public:
	iterator begin() {
		return _start;
	}

	iterator end() {
		return _finish;
	}

	const_iterator begin() const {
		return _start;
	}

	const_iterator end() const {
		return _finish;
	}

我们赶紧来测试一手:

void Print(const xl::vector<int>& v) {
	for (auto e : v) cout << e << " ";
	cout << endl;
}

void test2() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

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

	Print(v);
}

输出:

4. vector 的拷贝构造与赋值

拷贝构造

这里就实现一下传统写法:

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

当然啦,实现方法有很多,怎么舒服怎么来就好~

赋值

这里我就直接用现代写法啦,因为实现起来真的和方便:

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

5. vector 的常见重要接口实现

operator[ ] 的实现

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 test1() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	for (int i = 0; i < 5; i++) {
		cout << v[i] << " ";
	}
	cout << endl;
}

输出:

insert 接口的实现

void insert(iterator pos, const T& x) {
	assert(pos >= _start && pos <= _finish);

	if (_finish == _end_of_storage) {
		size_t len = pos - _start; // 防止迭代器失效的问题(扩容之后pos仍指向旧空间)
		size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(new_capacity);
		pos = _start + len;
	}
	iterator end = _finish - 1;
	while (end >= pos) {
		*(end + 1) = *end;
		end--;
	}
	*pos = x;
	_finish++;
}

实现了 insert 之后,其实我们已经不需要自己实现 push_back 了,

直接复用 insert 就行了:

void push_back(const T& x) {
	//if (_finish == _end_of_storage) {
	//	size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
	//	reserve(new_capacity);
	//}
	//*_finish = x;
	//_finish++;

	insert(end(), x);
}

我们来测试一下:

void test2() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

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

 还是这段代码,来看输出:

现在我们解决了 insert 内部的迭代器失效的问题,

再来看看这样一个场景:

void test2() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);

	xl::vector<int>::iterator it = v.begin() + 2;
	v.insert(it, 3);
	*it += 10;

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

}

我们插入了一个 3 ,然后把 3 += 10 ,应该打印出 13 才对,

但是,来看输出:

为什么会还是打印 3 呢?

我们调试来看看:

 目前为止还是正常的:

走到这里我们发现 it 指针变成随机值了,这是为什么?

我们虽然在 insert 实现的内部对扩容这里进行了防止迭代器失效的操作,

但是,形参的改变不影响实参,扩容之后旧空间就被释放了,导致了迭代器失效。 

那我们该怎么解决呢?

我们看看源码是咋实现的:(当有细节问题的时候,就可以看看源码的实现细节了)

源码里面使用的操作,是搞了个返回值,

就是返回指向新插入位置的迭代器,如果源码看不太懂,可以去看看文档是怎么说的:

这是文档对这个返回值的描述。

erase 接口实现

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

我们来测试一下:

void test3() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	xl::vector<int>::iterator it = v.begin();
	v.erase(it);

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

输出:

好像没什么问题,但其实并不是这样的,我们再来看一个场景:

void test3() {
	xl::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	xl::vector<int>::iterator it = v.begin();
	while (it != v.end()) {
		v.erase(it);
		it++;
	}

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

输出:

怎么就崩了呢?

erase 以后,迭代器是有可能会失效的,我们试试库里的:

跑刚刚的代码:

实际上,VS的库里做了强制的检查,他不让我们访问 erase 之后的迭代器,

所以我们让 it++ 程序就报错了。

那库里是怎么处理的呢?

还得看看源码是怎么样的:

我们发现,他也是通过返回值来解决这个问题的,

我们也可以很容易的看出,返回值返回的就是原来位置的迭代器,

根据这个特性,我们测试一下:

void test3() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	vector<int>::iterator it = v.begin();
	while (it != v.end()) {
		it = v.erase(it);
	}

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

输出:

确实是都删除掉了,那我来改一改我们的代码:

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

 这样就没问题了:

pop_back 接口的实现

这个就直接复用 erase 就好了:

void pop_back() {
	erase(end() - 1);
}

resize 接口实现

这里我们先补充一个新知识:

C++ 有了模板之后,对内置类型有了升级,他们也可以使用构造函数初始化,

来看代码:

void test4() {
	int i = 0;
	int j = 1;

	int a = int();
	int b = int(1);

	cout << i << " " << j << " " << a << " " << b << endl;
}

输出:

好,我们再来看这个接口:

void resize(size_t n, const T& val = T()) {
	if (n < size()) {
		_finish = _start + n;
	}
	else {
		reseve();
		while (_finish != _start + n) {
			*_finish = val;
			_finish++;
		}
	}
}

这样我们给 val 缺省值的时候,就可以覆盖自定义类型和内置类型了。

vector 的接口当然不止这些,但是最核心的我们基本都实现了,其他的接口有兴趣再实现吧~

源码分享

Gitee链接:模拟实现简易STL: 模拟实现简易STL (gitee.com)

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

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

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

相关文章

Rust 数据类型 之 类C枚举 c-like enum

目录 枚举类型 enum 定义和声明 例1&#xff1a;Color 枚举 例2&#xff1a;Direction 枚举 例3&#xff1a;Weekday 枚举 类C枚举 C-like 打印输出 强制转成整数 例1&#xff1a;Weekday 枚举 例2&#xff1a;HttpStatus 枚举 例3&#xff1a;Color 枚举 模式匹配…

opencv 图像距离变换 distanceTransform

图像距离变换&#xff1a;计算图像中每一个非零点距离离自己最近的零点的距离&#xff0c;然后通过二值化0与非0绘制图像。 #include "iostream" #include "opencv2/opencv.hpp" using namespace std; using namespace cv;int main() {Mat img, dst, dst…

【STL】模拟实现简易 list

目录 1. 读源码 2. 框架搭建 3. list 的迭代器 4. list 的拷贝构造与赋值重载 拷贝构造 赋值重载 5. list 的常见重要接口实现 operator--() insert 接口 erase 接口 push_back 接口 push_front 接口 pop_back 接口 pop_front 接口 size 接口 clear 接口 别…

数字验证码识别新思路及对opencv支持向量机机器学习总结

验证码识别框架 新问题 最近遇到了数字验证码识别的新问题。 由于这次的数字验证码图片有少量变形和倾斜&#xff0c;所以&#xff0c;可能需要积累更多的原始采样进行学习。但按照4个验证码10个数字的理论随机组合(暗含某种数字仅有少量变化&#xff0c;不然此组合数量还应更…

知识图谱--入门笔记

知识图谱–入门笔记-----整体的概念 1.什么是知识图谱&#xff1f; 描述现实世界中各个实体或者概念之间的关系&#xff0c;其构成一张海量的语义网络图&#xff0c;节点表示实体或者概念&#xff0c;边表示属性或者关系。 2.知识图谱中的三个节点 &#xff08;1&#xff09…

【LeetCode每日一题合集】2023.7.17-2023.7.23(离线算法 环形子数组的最大和 接雨水)

文章目录 415. 字符串相加&#xff08;高精度计算、大数运算&#xff09;1851. 包含每个查询的最小区间⭐⭐⭐⭐⭐解法1——按区间长度排序 离线询问 并查集解法2——离线算法 优先队列 874. 模拟行走机器人&#xff08;哈希表 方向数组&#xff09;918. 环形子数组的最大和…

会议OA项目之会议审批(亮点功能:将审批人签名转换为电子手写签名图片)

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于OA项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.主要功能点介绍 二.效果展示 三.前端…

AVLTree深度剖析(双旋)

在上一篇文章中我们提到了&#xff0c;单旋的情况&#xff08;无论是左单旋还是右单旋&#xff09;&#xff0c;都仅仅适用于绝对的左边高或者绝对的右边高 b插入&#xff0c;高度变为h1&#xff0c;我们可以来试试单旋会变成什么样子 旋完之后&#xff0c;形成了对称关系&…

自然语言处理之AG_NEWS新闻分类

前言: 先简单说明下&#xff0c;关于AG_NEWS情感分类的案例&#xff0c;网上很多博客写了&#xff0c;但是要么代码有问题&#xff0c;要么数据集不对&#xff0c;要么API过时&#xff0c;在这里我再更新一篇博客。希望对各位同学有一定的应用效果。 1、DataSets 数据集的处理…

部署mycat2

因为mycat是Java写的&#xff0c;要装jdk 下载包 jdk-8u261-linux-x64.rpm 安装 安装好后&#xff0c;查看版本 安装mycat2 解压到data目录 修改权限 把所需的jar复制到mycat/lib目录 查看MyCat目录结构 回为mycat代理连接启动时需要有一个默认的数据源&#xff0c;所以我们…

C#读取写入文件的三种方式

最新对文件的操作比较频繁。这里记录一下常用的几种文件读写的方式。 我这里使用窗体来做测试&#xff0c;例子在文末&#xff0c;可下载。 1&#xff1a;二进制读写 /// <summary>/// 二进制写入文件/// </summary>private void button1_Click(object sender, E…

PuTTY连接服务器报错Connection refused

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程

有时遇到这种需求&#xff0c;在上传文件的同时还需要带上token凭据和其它表单信息&#xff0c;那么这个需求前端可以使用FormData数据类型来实现。FormData和JSON一样也是通过body传递的&#xff0c;前者支持字符串和二进制文件&#xff0c;后者只能是字符串&#xff0c;如下图…

[CISCN 2023 初赛]go_session 解题思路过程

过程 下载题目的附件&#xff0c;是用go的gin框架写的后端&#xff0c;cookie-session是由gorilla/sessions来实现&#xff0c;而sessions库使用了另一个库&#xff1a;gorilla/securecookie来实现对cookie的安全传输。这里所谓的安全传输&#xff0c;是指保证cookie中的值不能…

STM32 USB使用记录:HID类设备(前篇)

文章目录 目的基础说明HID类演示代码分析总结 目的 USB是目前最流行的接口&#xff0c;现在很多个人用的电子设备也都是USB设备。目前大多数单片机都有USB接口&#xff0c;使用USB接口作为HID类设备来使用是非常常用的&#xff0c;比如USB鼠标、键盘都是这一类。这篇文章将简单…

向npm注册中心发布包(上)

目录 1、创建package.json文件 1.1 fields 字段 1.2 Author 字段 1.3 创建 package.json 文件 1.4 自定义 package.json 的问题 1.5 从当前目录提取的默认值 1.6 通过init命令设置配置选项 2、创建Node.js 模块 2.1 创建一个package.json 文件 2.2 创建在另一个应用程…

UE5 AI移动无动作问题

文章目录 问题背景解决方法问题背景 在使用行为树让角色移动时,出现角色行走不播放动作的情况: 解决方法 其实有2个问题导致出现这种情况 1、角色动画蓝图的问题 角色动画蓝图可能存在4个问题: ① 无播放行走动画 ② 速度的值未正常传递 ③ 播放移动动作逻辑的值判断错…

【每日一题】——C - Standings(AtCoder Beginner Contest 308 )

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

Clion开发STM32之W5500系列(DNS服务封装)

概述 在w5500基础库中进行封装&#xff0c;通过域名的方式获取实际的ip地址用于动态获取ntp的ip地址 DNS封装 头文件 /*******************************************************************************Copyright (c) [scl]。保留所有权利。****************************…

JVM对象在堆内存中是否如何分配?

1&#xff1a;指针碰撞&#xff1a;内存规整的情况下 2&#xff1a;空闲列表: 内存不规整的情况下 选择那种分配方式 是有 java堆是否规整而决定的。而java堆是否规整是否对应的垃圾回收器是否带有空间压缩整理的能力决定的。 因此当使用Serial,ParNew等带有压缩整理过程的收…