【C++】STL—— vector 模拟实现
创始人
2025-05-29 23:06:54
0

文章目录

  • 📕 vector 简介
  • 📕 vector 模拟实现
    • 框架
    • 构造函数
    • ★ 拷贝构造、赋值重载 ★
    • resize() 、reserve()
    • _push_back() 、_pop_back()
    • ★ 迭代器使用、迭代器失效问题 ★
  • 📕 源代码

📕 vector 简介

vector 是 C++ 的 STL 标准库中的一个容器,它主要使用的是泛型编程的思想,即使用模板。
vector 是表示可变大小数组的序列容器。它里面可以存储任意数据类型的数据,除了内置类型,还可以有自定义类型,它和数组明显的区别就是——vecctor的大小可以动态改变。当然了,不同平台下的增长方式不一样,如下图是VS 和 Linux 下的测试vector 标准库的增长方式。
请添加图片描述

📕 vector 模拟实现

框架

为了和 STL 标准库中的 vector 区分,自己实现的可以用命名空间包含起来,这里我取名为 simulate,是模拟的意思。
由于 vector 是一个容器,可以存放各种类型的数据,所以要使用模板。

namespace simulate
{templateclass vector{public:private:T* _start=nullptr;T* _finish=nullptr;T* _end_of_storage=nullptr;};
}

构造函数

如下,构造函数有多个函数重载。初始化列表可以使用缺省值代替(在申明的时候给成员变量缺省值)。

同时,这里也使用了模板,模板参数只能是迭代器区间(如果传其他的会出错),构造迭代器区间内的数据成为一个vector对象。

		// 直接用缺省值替代初始化列表vector(){}vector(size_t n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){*(_start + i) = val;++_finish;  // 不要忘了++ _finish ,一开始忘记了,所以打印不出来}}vector(int n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){*(_start + i) = val;++_finish;  // 不要忘了++ _finish ,一开始忘记了,所以打印不出来}}// 写了模板,但是不加上上面的int参数重载,会报错,因为vector(10,5)会调用模板函数templatevector(InputIterator begin, InputIterator end){reserve(end - begin);while (begin != end){_push_back(*begin);++begin;}}

但是,为什么上面有两个构造函数的非常像,区别仅仅是第一个参数的类型呢?
如下,这是使用自己定义的vector,初始化后vector里面有10个5。

simulate::vector vv(10, 5);

如果没有第一个参数是 int 类型的构造函数,如下。如果是上面一行代码那样构造,传入的两个参数都被认为是 int 类型,而目前有两个参数的构造函数中,一个可以被认为是 (size_t ,int) ,另一个由于是模板,所以可以被认为是(int,int) ,编译器会选择后者
但是实际上,后者由于不传入迭代器,而是实实在在的数据,就会造成错误,因为函数体内部有解引用
所以要多一个函数重载,来重载这种情况,编译器会优先选择参数是(int,int)的,而不是使用模板。
请添加图片描述

★ 拷贝构造、赋值重载 ★

拷贝构造就涉及到深拷贝和浅拷贝的问题。
如果 vector 里面存放的数据是内置类型,根据拷贝构造方面的知识,深、浅拷贝都没有关系。
但是如果 vector 里面存放的是自定义类型,并且该类型涉及资源管理,那就必须要深拷贝。

如下图,vector 容器里面存放的是 string 类型的数据,根据对 string 类型的模拟实现,我们可以大概知道其实现原理。string 类有一个成员变量 _str 指向一串字符串。现在将 v 里面的数据拷贝到 vv 里面,如果是浅拷贝,那么就会如下图,导致 v[n] 和 vv[n] 里面的数据完全一样, _str,_size,_capcatiry ,那么 _str 指向同一块空间在析构的时候,会造成重复析构,导致崩溃

请添加图片描述

如下,深拷贝就不会出现上述问题,完全独立的空间。
请添加图片描述
那么如何实现深拷贝呢?vector是一个容器,由于使用泛型编程的思想,里面可以存放的数据类型各种各样,也许有的自定义类型,里面有多个指针分别指向不同的空间呢?我们如何知道每一次要深拷贝多少块空间呢?
所以,在这里做一个统一的标准显然是不现实的。也不能用memcpy,因为memcpy是直接把一个 vector 里面的数据拷贝到 另一个 vector,就会造成上图的浅拷贝问题。
其实,我们可以直接调用 赋值重载 ,这也是为什么这两个函数放在一起的原因。

试想一下,现在有 vector< stirng > v,里面存放了一定量的数据, vector vv(v) ,和上图一样的操作。只不过,拷贝构造内部使用的是 赋值重载 —— vv[0] = v[0] …… ,由于赋值符号两边都是string类型的数据,会调用 string 自己的赋值重载,string 内部实现的赋值重载当然是没有任何问题的!!!!其他的类型也是,内部实现的赋值重载是深拷贝,所以,这里就很好地解决了上述问题!

		vector(const vector& v){_start = new T[v.capacity()];for (int i = 0; i < v.size(); i++){_start[i] = v._start[i];  // 会调用自己类型的赋值重载}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}

但是,如果出现了 vector< vector< string > > v ;这样的情况呢?又要拷贝构造使得vector< vector< string > > vv(v) ;容器里面存放的是容器,由于拷贝构造内部是 vv[0] = v[0] …… 赋值符号两边都是 vector< string > 类型的数据,都是 vector 类型,所以会调用 vector 自己的赋值重载如下。
还是以上述的 vector< vector< string > > v 为例, vv[0] = v[0] ,调用 vector 类型的赋值重载,即调用下面的函数。v[0] 是 vector< string > 类型的数据,该类型的数据,就像上面的两张图片一样,内部存放了一个个 string 类型的数据,如下所示。所以 _start[i] 是 string 类型的数据的指针,_start[i] = val._start[i]; 会调用 string 类型的赋值重载,那当然是没有问题的。

请添加图片描述

		// 一个 vector 赋值给另一个 vectorvector operator=(const vector& val){_start = new T[val.capacity()];for (int i = 0; i < val.size(); i++){_start[i] = val._start[i];}_finish = _start + val.size();_end_of_storage = _start + val.capacity();return *this;}

以此类推,即使是更多层次的 vector 嵌套,也可以通过不断调用 vector 类里面的赋值重载,直到里面的类型不是 vector,而是其他的类型,而这些其他的类型是实现了深拷贝的赋值重载的,所以完全是行得通的。

resize() 、reserve()

这两个函数都和 string 类里面的两个函数类似,所以这里也不过多赘述。

		void resize(size_t n,T x=T()){if (n < size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = x;_finish++;}}}void reserve(int n){if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start)  // 非空,复制数据,删除原空间。为空则不需要这样做{memcpy(tmp, _start, sizeof(T) * sz);delete[] _start;}_start = tmp;_finish = tmp + sz;_end_of_storage = tmp + n;}}

_push_back() 、_pop_back()

尾插其实也和 string 类差别不大,考虑扩容。
这里也可以再深入思考一下,锻炼思维。如果参数类型 T 是 vector< string > ,那么尾插会是一次成功的吗?当然是的, *_finishi 和 x 都是T类型的数据,也就是 vector< string > ,所以会调用 vector 自己的赋值重载;然后由于其内部数据是 string 类型的,所以vector 的赋值重载内部,又会进一步调用 string 类型的赋值重载,标准库里面的string类的赋值重载,自然是没有什么问题的。

		void _push_back(const T& x){if (_finish == _end_of_storage){int sz = capacity() == 0 ? 4 : 2 * capacity();reserve(sz);}*_finish = x;++_finish;}void _pop_back(){assert(!empty());--_finish;}

★ 迭代器使用、迭代器失效问题 ★

vector 的使用,非常需要注意的一点就是迭代器失效问题。如下代码是 vector 中迭代器的设计。

		typedef T* iterator;typedef const T* const_iterator;iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin()const{return _start;}const_iterator end()const{return _finish;}

如下,是使用迭代器设计 insert() 函数,由于是插入,所以要考虑扩容的问题,但是如果真的扩容了,那么原本传参的 pos 迭代器指向的位置,就可能是错误的了。因为扩容后,可能开辟一块新空间,那么指向旧空间某一处的 pos 迭代器,也就失去了意义,因为旧空间在 reserve 的过程中已经被释放了。

所以,要用一个变量来存储 pos 迭代器指向的位置 和 初始位置 的长度,这里用的是 len 。下面的就是普通的后移插入过程。

		iterator insert(iterator pos , const T& x){assert(pos >= _start);assert(pos < _finish);size_t len = pos - _start;if (_finish == _end_of_storage){reserve(2 * capacity());pos = _start + len;  // 扩容之后,_start 改变了}iterator tmp = _finish - 1;while (tmp >= pos){*(tmp + 1) = *tmp;--tmp;}*pos = x;_finish++;return pos;}iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);iterator tmp = pos + 1;while (tmp != _finish){*(tmp-1) = *tmp;tmp++;}--_finish;return pos;}

接下来就是迭代器失效的情况,这是在使用 vector 迭代器的时候会出现的问题。
现在有一个 vector< int > v; v里面存储着一些数据,要求删除所有偶数数据。

像下面一样去写(simulate 是我用来包含自己的 vector 的命名空间,防止和标准库中的冲突),就可以了吗?可以画图尝试理解,如下图。当删除一个数据的时候,由于删除这一动作本身就会使得后面的数据前移,然后又++,就导致直接跳过了一个数据。
而如果最后一个数据是偶数,那么会删除数据并跳过 v.end() ,导致 while() 的循环条件一直满足,从而陷入死循环。

这就是典型的迭代器失效问题,原本的迭代器,在经过 插入/删除 之后,导致迭代器不是指向原本的那个数据,后续使用就可能出错。

请添加图片描述

	simulate::vector::iterator it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);}++it;}

所以,在删除数据之后,就不能再让迭代器++。如下代码,加上一个 else 就可以,这样子就可以使得删除之后不进入 else 的代码块。同时,it 也要接收 erase() 的返回值,其返回值也是 iterator ,指向被删除的后一个数据。

	while (it != v1.end()){if (*it % 2 == 0){it=v1.erase(it);}else ++it;}

📕 源代码


#pragma once
#include
#includenamespace simulate
{templateclass vector{public:typedef T* iterator;typedef const T* const_iterator;// 用缺省值替代初始化列表vector(){}vector(size_t n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){*(_start + i) = val;++_finish;  // 不要忘了++ _finish ,一开始忘记了,所以打印不出来}}vector(int n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){*(_start + i) = val;++_finish;  }}// 写了模板,但是不加上上面的int参数重载,会报错,因为vector(10,5)会调用模板函数templatevector(InputIterator begin, InputIterator end){reserve(end - begin);while (begin != end){_push_back(*begin);++begin;}}vector(const vector& v){_start = new T[v.capacity()];for (int i = 0; i < v.size(); i++){_start[i] = v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}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 _end_of_storage - _start;}bool empty(){return _start == _finish;}T& operator[](size_t x){assert(x < size());return _start[x];}// 自己要写赋值重载,赋值重载和拷贝构造类似,一个 vector 复制给另一个 vectorvector operator=(const vector& val){_start = new T[val.capacity()];for (int i = 0; i < val.size(); i++){_start[i] = val._start[i];}_finish = _start + val.size();_end_of_storage = _start + val.capacity();return *this;}const T& operator[](size_t x)const{assert(x < size());return _start[x];}void resize(size_t n,T x=T()){if (n < size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = x;_finish++;}}}void reserve(int n){if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start)  // 非空,复制数据,删除原空间。为空则不需要这样做{memcpy(tmp, _start, sizeof(T) * sz);delete[] _start;}_start = tmp;_finish = tmp + sz;_end_of_storage = tmp + n;}}void _push_back(const T& x){if (_finish == _end_of_storage){int sz = capacity() == 0 ? 4 : 2 * capacity();reserve(sz);}*_finish = x;++_finish;}void _pop_back(){assert(!empty());--_finish;}// 用迭代器,可能会出现迭代器失效的情况iterator insert(iterator pos , const T& x){assert(pos >= _start);assert(pos < _finish);size_t len = pos - _start;if (_finish == _end_of_storage){reserve(2 * capacity());pos = _start + len;  // 扩容之后,_start 改变了}iterator tmp = _finish - 1;while (tmp >= pos){*(tmp + 1) = *tmp;--tmp;}*pos = x;_finish++;return pos;}iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);iterator tmp = pos + 1;while (tmp != _finish){*(tmp-1) = *tmp;tmp++;}--_finish;return pos;}~vector(){delete[] _start;_finish = _end_of_storage = nullptr;}private:T* _start=nullptr;T* _finish=nullptr;T* _end_of_storage=nullptr;};
}

相关内容

热门资讯

Altium Designer... 目录Altium Designer(AD)软件使用记录15-PCB布线部分之优化和DRC处理一、线路...
通俗易懂了解Hadoop(更新... 从本书第5、6、7、8章,学习云计算开发相关知识 这是第五章 文章目录Hadoo...
LeetCode-198. 打... 目录暴力递归动态规划 题目来源 198. 打家劫舍 暴力递归 class Solution {pub...
js学习11(客户端存储) 目录 web storage IndexDB   web storage ### 前言࿱...
target.closest妙... 首先看下MDN:Element.closest() - Web APIs | MDN ...
并发编程(一)-Thread ... 一、什么是线程线程(英语:thread)是操作系统能够进行...
小白学Pytorch系列--T... 小白学Pytorch系列–Torch API (9) Spectral Ops stft 短时傅立...
Java二叉树的前中后序遍历 Java二叉树的前中后序遍历1.前序遍历1.1前序遍历概念1.2前序遍历习题2.中序遍历2.1中序遍...
遗传算法原理及案例解析 一、遗传算法原理 遗传算法—进化算法(Genetic Algorithm GA...
朴素贝叶斯学习报告 报告 朴素贝叶斯算法描述公式:  案例计算步骤: 一个数据集中有两个样本...
算法小课堂(一)暴力枚举 、 目录 一、概念 1.1相关概念 1.2应用场景 1.3局限性 二、相关问题 2.1例题1:统计 ...
OpenHarmony之doc... Docker使用示例 docker移植至OpenHarmony的过程可参考:https...
懒人专用高并发:Actor模型 传统多线程实现方式 public class MultiThreadExample implemen...
WEB安全 HTML基础 1.简单的HTML页面架构 charset  编码 gbk gbk2...
算法基础---基础算法(二) 文章目录 高精度         高精度加法高精度减法高精度乘法高精度除法前缀和 一维前缀和二维前缀...
【Docker】镜像的原理定制... 文章目录镜像是什么UnionFS(联合文件系统)Docker镜像加载原理...
vue3常用 Composit... 二、常用 Composition API 官方文档 1.拉开序幕的setup语法糖 理解࿱...
【MySQL】实验二 简单查询 目录 1. 查询课程代号为1301的成绩不及格的成绩信息 2. SQL查询:查询employee的j...
spring启动时加载外部配置... 平常同学们使用spring搭建工程时一些应用配置信息(例如数据库的连接配置、中间件的连...
《他是谁》爆火,优酷的成功并非... 今年国产电视剧市场又进入了新一轮的爆款时代,观众在前面刚送走《三体》《狂飙》ÿ...