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

ops/2024/9/25 15:25:01/

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.ppmy.cn/ops/15483.html

相关文章

【C语言】多字节字符、宽字符(涉及字符集和编码)

字符集、编码&#xff1a; 字符集&#xff1a;一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称&#xff0c;包括各国家文字、标点符号、图形符号、数字等。例如&#xff1a;ASCII、Unicode、GB2312、GBK、GB18030、BIG5(繁体中文) ... 编码方式&#xff1a;符号…

Docker - HelloWorld

原文地址&#xff0c;使用效果更佳&#xff01; Docker - HelloWorld | CoderMast编程桅杆https://www.codermast.com/dev-tools/docker/docker-helloworld.html 开始之前 在学习本小节之前&#xff0c;你必须确保你正确安装了 Docker&#xff0c;正确安装 Docker 是后续学习的…

【工具类】linux常用别名

1. 【工具类】linux常用别名 1. 【工具类】linux常用别名 1.1. 使用方法1.2. cd 文件时&#xff0c;自动切到其父目录1.3. time 相关1.4. cpu 和 mem 相关 1.1. 使用方法 保存下边内容到 ~/.bashrc 文件&#xff0c;然后执行 source ~/.bashrc如果使用 zsh&#xff0c;则保…

Ubuntu下,Notepad++的安装、汉化与卸载

Notepad的作者有自己的问题&#xff0c;但必须承认的是软件本身质量还是不错的&#xff0c;有朋友感到介意&#xff0c;可以了解另一款软件&#xff1a;Notepad--&#xff0c;目前也在逐步优化中 1.安装 在终端中输入指令 sudo snap install notepad-plus-plus 等待安装即可…

static和extern关键字详解

目录 创作不易&#xff0c;如对您有帮助&#xff0c;还望一键三连&#xff0c;谢谢&#xff01;&#xff01;&#xff01; 回顾 1.作用域和声明周期 1.1作用域 1.2生命周期 2.static和extern 2.1extern 2.2static 2.2-1static修饰局部变量 2.2-2static修饰全局变量 创…

Linux 软件包工具rpmbuild

下载工具rpm-build yum search rpm-build yum install rpm-build.x86_64制作属于自己的RPM包 1.准备打包目录 ls rpmbuild/ BUILD BUILDROOT RPMS SOURCES SPECS SRPMS 2.放入软件包 cp /root/nginx-1.18.0.tar.gz rpmbuild/SOURCES/ 3.编辑spec文件 vim rpmbuild/SPECS/n…

Pandas 2.2 中文官方教程和指南(二十二)

原文&#xff1a;pandas.pydata.org/docs/ 时间增量 原文&#xff1a;pandas.pydata.org/docs/user_guide/timedeltas.html 时间增量是时间之间的差异&#xff0c;以不同的单位表示&#xff0c;例如天、小时、分钟、秒。它们可以是正数也可以是负数。 Timedelta是datetime.tim…

数组模拟几种基本的数据结构

文章目录 数组模拟单链表数组模拟双链表数组实现栈数组模拟队列总结 数组模拟单链表 首先类比结构体存储单链表&#xff0c;我们需要一个存放下一个节点下标的数组&#xff0c;还需要一个存储当前节点的值的数组&#xff0c;其次就是一个int类型的索引&#xff0c;这个索引指向…