【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.0vector(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());//扩容时一定要注意迭代器失效问题!及时更新pospos = _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.hqwc.cn/news/636700.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python爬虫笔记1

1 爬虫介绍 爬虫概述&#xff1a; 获取网页并提取和保存信息的自动化程序 1.获取网页 2.提取信息 css选择器 xpath 3.保存数据&#xff08;大数据时代&#xff09; 4.自动化 爬虫&#xff08;资产收集&#xff0c;信息收集&#xff09; 漏扫&#xff08;帮我发现漏洞&#xff…

mac电脑mysql下载与安装

mysql下载地址 历史下载地址 MySQL :: Download MySQL Community Server (Archived Versions) mac 版下载 mac版本分为 Intel 处理器 和 M系列处理器。 从 8.0.26开始&#xff0c; mysql 支持M系列处理器。 以前的都只有Intel 处理器的。 Intel 处理器选择 x86_64 M 系列处理…

【PCL】教程conditional_euclidean_clustering 对输入的点云数据进行条件欧式聚类分析...

[done, 3349.09 ms : 19553780 points] Available dimensions: x y z intensity 源点云 Statues_4.pcd 不同条件函数output.pcd 【按5切换到强度通道可视化】 终端输出&#xff1a; Loading... >> Done: 1200.46 ms, 19553780 points Downsampling... >> Done: 411…

色彩的魔力:渐变色在设计中的无限可能性

夕阳&#xff0c;天空&#xff0c;湖面&#xff0c;夕阳...随着距离和光影的变化&#xff0c;颜色的渐变色&#xff0c;近大远小、近实远虚的透视&#xff0c;为大自然营造了浪漫的氛围。延伸到UI/UX设计领域&#xff0c;这种现实、惊艳、独特的渐变色也深受众多设计师的喜爱。…

nodejs大文件上传

安装依赖 1.express 帮我们启动服务&#xff0c;并且提供接口 2.multer 读取文件&#xff0c;存储 3.cors 解决跨域 项目的目录结构&#xff1a; 前端代码&#xff1a; <input type"file" /><script>const file document.queryselector(input)// 分隔…

Git merge的版本冲突实验

实验目的 发现 两个分支的 相同文件 怎样被修改 才会发生冲突&#xff1f; 实验过程 1.初始状态 现在目前有1.py、2.py两个文件&#xff0c;已经被git管理。现在我想制造冲突&#xff0c;看怎样的修改会发生冲突&#xff0c;先看怎么不会发生冲突。 目前仓库里的版本是这样…

数字陷波器的设计

数字陷波器的设计 陷波器&#xff1a;一种特殊的带阻滤波器&#xff0c;其阻带在理想情况下只有一个频率点&#xff0c;主要用于消除某个特定频率的干扰。 例子 设计一个数字陷波器将输入信号中的50Hz工频干扰信号滤除&#xff0c;尽可能保留其他频率成分&#xff0c;设系统…

SpringMVC深解--一起学习吧之架构

SpringMVC的工作原理主要基于请求驱动&#xff0c;它采用了前端控制器模式来进行设计。以下是SpringMVC工作原理的详细解释&#xff1a; 请求接收与分发&#xff1a; 当用户发送一个请求到Web服务器时&#xff0c;这个请求首先会被SpringMVC的前端控制器&#xff08;Dispatche…

09 MySQL--操作真题

1. 用一条 SQL 语句&#xff0c;查询出每门课程都大于 80 分的人。 分析&#xff1a; 去重查询出存在课程小于 80 分的人&#xff0c;设为集合A查询不在集合 A 中的人 # 第一步&#xff1a;找小于等于80分的学员姓名 select distinct name from t_student where fenshu <…

Docker - 安装

原文地址&#xff0c;使用效果更佳&#xff01; Docker - 安装 | CoderMast编程桅杆https://www.codermast.com/dev-tools/docker/docker-install.html MacOS安装 1.使用 Homebrew 安装 brew install docker安装成功 如果你的电脑没有安装Docker&#xff0c;则会自动进行安…

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

文章目录 ☃️概述☃️源码 ☃️概述 RedisObject 是 Redis 中表示数据对象的结构体&#xff0c;它是 Redis 数据库中的基本数据类型的抽象。在 Redis 中&#xff0c;所有的数据都被存储为 RedisObject 类型的对象。 RedisObject 结构体定义如下&#xff08;简化版本&#xf…