当前位置: 首页 > news >正文

【C++】——vector深度剖析模拟实现

低头赶路,敬事如仪


目录

1、模拟vector

1.1底层结构

1.2构造析构

1.3尾插扩容

1.4迭代器

1.5增删查改

1.6模拟中的注意事项

2、vector模拟补充

2.1迭代器区间构造问题

2.2memcpy深浅拷贝问题

2.3动态二维数组的模拟及遍历


1、模拟vector

想要模拟实现自己的vector,得知其所以然!

分析一下STL源码里的vector底层成员变量

可以看到是三个迭代器类型成员变量,迭代器类型是什么呢?

经过typedef的底层指针,而T类型是模版类的参数。

大致框架如图:

1.1底层结构

根据我们刚才所查看的源码,我们要使用三个迭代器,要使用迭代器,我们可以使用指针进行模拟。

namespace xc
{//设置成模板 兼容各种参数 但是不能分离编译否则链接错误template<class T>class vector{public:typedef T* iterator;private:iterator _start = nullptr;//指向容器开始iterator _finish = nullptr;//指向有效数据下一个位置iterator _end_of_storage = nullptr;//指向空间容量的下一个位置};
}

写出三个迭代器(指针)后,我们可以接着实现构造函数:需要初始化三个迭代器,所以我们给予初始值nullptr。然后进行开辟空间。

1.2构造析构

实现常用的构造析构以及赋值重载

/*	vector(){}*///C++11 强制生成默认构造vector() = default;//优先匹配构造函数,防止非法的间接寻址问题vector(size_t n, const T& val = T()){reserve(n);for (size_t i=0; i < n; i++){push_back(val);}}vector(int n, const T& val = T()){reserve(n);for (int i=0; i < n; i++){push_back(val);}}vector(long long n, const T&val = T()){reserve(n);for (long long i=0; i < n; i++){push_back(val);}}//类模板的成员函数还可以继续是函数模板template<class InputIterator>  //写成模板是为了兼容所有容器vector(InputIterator first, InputIterator last){while(first != last){push_back(*first);++first;}}// v1(v)vector(const vector<T>& v){reserve(v.size());for (auto& e : v){push_back(e);}}void clear(){_finish = _start;}v1=v//vector<T>& operator=(const vector<T>&v)//{//	if (this != &v)//	{//		clear();//		reserve(v.size());//		for (auto& e : v)//		{//			push_back(e);//		}//	}//	return *this;//}void swap(vector<T>&v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}//现代写法 V1=vvector<T>& operator=(const vector<T> v){swap(v);return *this;}~vector(){if (_start){delete[]_start;_start = _finish = _end_of_storage = nullptr;}}

1.3尾插扩容

想要实现尾插的操作,根据之前的经验,可以知道需要复用一些常用的简单接口(size() capacity() reserve() 等)

size_t size()const { return _finish - _start; }size_t capacity()const { return _end_of_storage - _start; }bool empty()const { _start == _finish; }//void reserve(size_t n)
//{
//	if (n > capacity())
//	{
//		T* tmp = new T[n];
//		memcpy(tmp, _start, sizeof(T));
//		delete[]_start;//		_start = tmp;
//		_finish = _start + size();//这是错误的  _start 已经是扩容后的 start了 而size调用的是 finish - start
//									//解决方法 可以先更新 finish 或者记录一下 size的值
//		_end_of_storage = _start + n;
//	}
//}void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();T* tmp = new T[n];memcpy(tmp, _start, size()*sizeof(T));delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}void reseize(size_t n, T val = T())//内置类型也具有的构造析构等函数行为 但是并没有概念
{if (n < size()){_finih = _start + n;}else{reserve(n);while (_finsh < _start+n){*_finish++ = val;}}
}void push_back(const T& x)
{//扩容if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish++ = x;
}

注意:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

1.4迭代器

所需要实现的迭代器其实很简单,对指针 typedef 即可

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

实现了简单的迭代器,我们可以测试一下 迭代器遍历和范围for遍历

void test_vector1()
{vector<int> v;v.push_back(1);v.push_back(1);v.push_back(1);v.push_back(1);vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << ' ';++it;}cout << endl;for (auto e : v){cout << e << ' ';}cout << endl;
}

这里我们为了后续方便封装一个打印函数

template<class T>
void print_vector(const vector<T>& v)
{vector<T>::const_iterator it = v.begin();while (it != v.end()){cout << *it << ' ';++it;}cout << endl;
}

可是这居然报错

为什么呢?没有实例化的类模板里面取东西,编译器是分不清 const_iterator 是静态成员变量还是类型的,在前面加一个 typename 显示告诉编译器是类型即可

template<class T>
void print_vector(const vector<T>& v)
{typename vector<T>::const_iterator it = v.begin();while (it != v.end()){cout << *it << ' ';++it;}cout << endl;
}

1.5增删查改

void insert(iterator pos, const T& x)
{if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}iterator end = _finish;while (pos < end){*end = *(end - 1);--end;}*pos = x;++_finish;
}

看起来没什么问题,但是存在着迭代器失效的风险

pos仍指向原来的空间!记录相对位置,修正一下pos即可

void insert(iterator pos, const T& x)
{if (_finish == _end_of_storage){size_t len = pos - _start;//记录相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish;while (pos < end){*end = *(end - 1);--end;}*pos = x;++_finish;
}

值得注意的是STL里的 insert有返回值!

这是为什么呢?还是防止迭代器失效的一种措施。

void test_vector2()
{vector<int>v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);print_vector(v);/*	v.insert(v.begin() + 1, 10);print_vector(v);*/int x;cin >> x;// x==2auto pos = find(v.begin(), v.end(), x);//没找到返回 last 左闭右开区间if (pos != v.end()){v.insert(pos, 40);//pos 可以理解为下标位置 也可以理解为原来位置数据前面插入一个数据print_vector(v);(*pos) *= 10;//期望将x乘以10 但是是插入的40*10  并没有指向原来的数据//归为迭代器失效一类}print_vector(v);}

如果这里还发生了扩容呢??就是将push的5给删掉

这是什么鬼?我们不是已经修正了pos的位置吗?显然这里的p已经失效了!形参是无法改变实参的,我们修正了pos,但是影响不了p

传const 引用过去影响实参。但不是常量引用的话 ,下面代码会报错!

因为函数调用返回的参数是临时变量,且临时变量具有常性。

而事实上加了const引用里面的pos就无法修正了!我们的思路错了,STL的操作是加一个返回值 (返回的是新插入数据的下标)

iterator insert(iterator  pos, const T& x)
{assert(pos <= _finish && pos >= _start);if (_finish == _end_of_storage){size_t len = pos - _start;//记录相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish;while (pos < end){*end = *(end - 1);--end;}*pos = x;++_finish;return pos;
}

所以 insert后不能再访问find 的 p 想要访问得更新!

增删查改:

iterator insert(iterator  pos, const T& x)
{assert(pos <= _finish && pos >= _start);if (_finish == _end_of_storage){size_t len = pos - _start;//记录相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish;while (pos < end){*end = *(end - 1);--end;}*pos = x;++_finish;return pos;
}iterator erase(iterator pos)
{assert(pos < _finish && pos >= _start);iterator it = pos + 1;while (it != end()){*(it - 1) = *it;++it;}--_finish;return pos;
}size_t find(T val = T())
{for (auto it = _start; it < _finish; it++){if (*it == val){return it - _start;}}return -1;
}T& operator[](size_t i)
{assert(i < size());return _start[i];
}const T& operator[](size_t i)const
{assert(i < size());return _start[i];
}void pop_back()
{assert(!empty());--_finish;
}

1.6模拟中的注意事项

  • 规定:类模板在没有实例化时,迭代器无法读取!编译器不能区分这里const_iterator是类型还是静态成员变量。要想解决:第一可以在前面加上typename用来证明这里是类型;第二用auto,让编译器判断为类型
  • 内置类型是没有构造函数的概念,但为了兼容模板(T val = T( ) ),可以被视为具有默认构造函数的行为
void reseize(size_t n, T val = T())//内置类型具有的构造析构等函数行为,并没有对应概念
{if (n < size()){_finish = _start + n;}else{reserve(n);while (_finish < _start+n){*_finish++ = val;}}
}
  •  c++11中有强制生成默认构造的操作,即使类中已有其他构造函数,也能强制生成。eg:vector( ) = default

  • 类里面可以用类名替代类型(特殊化),类外面规定:类名不能代表类型
//类里面可以用类名替代类型(特殊化)
//vector & operator=(vector v)
vector<T>& operator= (vector<T> v)
{swap(v);return *this;
}
  • 类模板的成员函数,还可继续是函数模板
//类模板的成员函数还可以继续是函数模板
template<class InputIterator>  //写成模板是为了兼容所有容器
vector(InputIterator first, InputIterator last)
{while(first != last){push_back(*first);++first;}
}

2、vector模拟补充

2.1迭代器区间构造问题

//C++11 强制生成默认构造
vector() = default;vector(size_t n, const T& val = T())
{reserve(n);for (size_t i; i < n; i++){push_back(val);}
}//类模板的成员函数还可以继续是函数模板
template<class InputIterator>  //写成模板是为了兼容所有容器
vector(InputIterator first, InputIterator last)
{while(first != last){push_back(*first);++first;}
}

这个是对迭代器区间进行的构造函数,思路很简单,把迭代器区间的数据依次尾插就可以了(这里之所以另外使用一个新的模版,而不是使用vector类的模版,是为了兼容其他容器类型)。这样就可以通过一个现有的类型来构造容器。
但是出乎意料的是出现了一个问题: C2100 非法的间接寻址 (编译层面的问题) 。非法的间接寻址的造成原因有很多:

  1. 空指针引用:当一个指针没有被初始化或者为NULL时,对它进行间接寻址操作会导致非法访问
  2. 野指针引用:当一个指针超出了它所指向的内存范围,或者已经被释放但仍然被引用时,进行间接寻址操作也会导致非法访问。
  3. 类型不匹配:如果试图将指针转换为不兼容的类型进行间接寻址,也会导致非法访问。

为什么报错在迭代器构造呢?因为编译器会寻找最合适的函数,但是这里与我们期望所违背!!

为了解决编译器选择的问题,可以多枚举几个构造函数:

vector(size_t n, const T& val = T())
{reserve(n);for (size_t i=0; i < n; i++){push_back(val);}
}vector(int n, const T& val = T())
{reserve(n);for (int i=0; i < n; i++){push_back(val);}
}
vector(long long n, const T&val = T())
{reserve(n);for (long long i=0; i < n; i++){push_back(val);}
}

这样可以做到优先匹配vector(int n,T val = T());,我们的问题也就解决了。 

2.2memcpy深浅拷贝问题

我们测试一下自定义类型的vector(这里以string为例):

void vector_test6() {vector<string> v1;v1.push_back("11111");v1.push_back("22222");v1.push_back("33333");v1.push_back("44444");//再次插入扩容v1.push_back("55555");print_vector(v1);
}

程序崩溃了

分析:
<1> memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
<2> 如果拷贝的是内置类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃

解决方法:

void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();T* tmp = new T[n];//memcpy(tmp, _start, size()*sizeof(T));//浅拷贝for (size_t i = 0; i < old_size; i++){tmp[i] = _start[i];//自定义类型的话 也是会调用自定义类型的赋值重载}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

2.3动态二维数组的模拟及遍历

结构框架图:

//二维数组
vector<int>v(3, 1);
vector<vector<int>>vv(5, v);vv[2][1] = 2;
//与上述等价  vv.operator[](2).operator[](1) = 2;
for (size_t i = 0; i < vv.size(); i++)
{for (size_t j = 0; j < vv[i].size(); j++){cout << vv[i][j] << ' ';}cout << endl;
}
cout << endl;for (auto it = vv.begin(); it != vv.end(); it++)
{for (auto jt = (*it).begin(); jt != (*it).end(); jt++){cout << *jt << ' ';}cout << endl;
}
cout << endl;auto it1 = vv.begin();
/*auto it2 = (*it1).begin();*/
while (it1 != vv.end())
{auto it2 = (*it1).begin();//更新it2 指针在堆上分配,所以可以看见内存不是连续分配的while (it2 != (*it1).end()){cout << *it2 << ' ';it2++;}cout << endl;it1++;
}
cout << endl;for (auto row : vv)
{for (auto column : row){cout << column << ' ';}cout << endl;
}
cout << endl;

应用:杨辉三角

// 以杨慧三角的前n行为例:假设n为5
void test2vector(size_t n)
{// 使用vector定义二维数组vv,vv中的每个元素都是vector<int>vector<vector<int>> vv(n);// 将二维数组每一行中的vecotr<int>中的元素全部设置为1for (size_t i = 0; i < n; ++i)vv[i].resize(i + 1, 1);// 给杨慧三角出第一列和对角线的所有元素赋值for (int i = 2; i < n; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}
}

构造一个vv动态二维数组,vv中总共有n个元素,每个元素 都是vector类型的,每行没有包含任何元素,n为5时如下所示:

相关文章:

  • 命名导出和默认导出的用法区别
  • RabbitMQ08_保证消息可靠性
  • 2、在LVGL模拟器中了解部件的基础属性
  • 智慧城市主要运营模式分析
  • 【后端开发】JavaEE初阶——计算机是如何工作的???
  • 常见的TTL,RS232,RS485,IIC,SPI,UART之间的联系和区别
  • YOLOv8 Flask整合问题
  • 什么是Agent智能体?
  • 2024年信息安全企业CRM选型与应用研究报告
  • 探索 Android DataBinding:实现数据与视图的完美融合
  • 中间件:maxwell、canal
  • SpinalHDL之语义(Semantic)(一)
  • Python办公自动化案例:将Excel数据批量保存到Word表格中
  • spring boot启动报错:so that it conforms to the canonical names requirements
  • 验收测试:从需求到交付的全程把控!
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • Bootstrap JS插件Alert源码分析
  • co模块的前端实现
  • egg(89)--egg之redis的发布和订阅
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • js 实现textarea输入字数提示
  • MobX
  • MQ框架的比较
  • MySQL用户中的%到底包不包括localhost?
  • Netty 4.1 源代码学习:线程模型
  • PHP面试之三:MySQL数据库
  • Python中eval与exec的使用及区别
  • React组件设计模式(一)
  • select2 取值 遍历 设置默认值
  • spring学习第二天
  • Travix是如何部署应用程序到Kubernetes上的
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • vue脚手架vue-cli
  • 等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规
  • 关于字符编码你应该知道的事情
  • 警报:线上事故之CountDownLatch的威力
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 每天一个设计模式之命令模式
  • 双管齐下,VMware的容器新战略
  • mysql面试题分组并合并列
  • scrapy中间件源码分析及常用中间件大全
  • 说说我为什么看好Spring Cloud Alibaba
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ​2020 年大前端技术趋势解读
  • ​io --- 处理流的核心工具​
  • ​香农与信息论三大定律
  • # Panda3d 碰撞检测系统介绍
  • # 透过事物看本质的能力怎么培养?
  • #Linux(权限管理)
  • #考研#计算机文化知识1(局域网及网络互联)
  • #我与Java虚拟机的故事#连载16:打开Java世界大门的钥匙
  • (C++哈希表01)
  • (Java企业 / 公司项目)点赞业务系统设计-批量查询点赞状态(二)
  • (二)WCF的Binding模型
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真