C++STL的vector模拟实现
文章目录
- 前言
- 成员变量
- 成员函数
- 构造函数
- push_back
- pop_back
- insert
- erase
- 析构函数
- 拷贝构造
前言
成员变量
namespace but
{template<class T>class vector{public:typedef T* iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;};
}
我们之前实现顺序表是用指向数组的的指针和数组个数和容量来维护顺序表的,这里用三个指针来实现其实大差不差。
成员函数
构造函数
vector():_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{}
push_back
void push_back(const T& x)
{if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);//避免容量为0扩容还是0的情况}*_finish = x;++_finish;
}
首先放数据怎么放呢?
直接赋值。
为什么可以直接赋值?
因为空间是new出来的,如果是内置类型,有没有初始化都可以直接赋值。
但是自定义类型没有初始化不能直接赋值。
扩容
void reserve(size_t n)
{//避免缩容//1.缩容的代价太大//2.反复缩容与扩容,降低了性能。if (n > capacity()){size_t sz = size();T* tmp = new T[n];//如果旧空间没有数据就不用拷贝了if (_start){//因为是类型不一定是字符串,所以得使用memcpymemcpy(tmp, _start, sizeof(T)*size());delete[] _start;}_start = tmp;//这里有个小坑//_finish=_start + size();//提前把size()记录下来,防止这里出错//改成tmp也可以,但是有点影响我们原本的理解,不利于维护。_finish = _start + sz;_end_of_storage = _start + n;}
}
我们把其他一些需要用到的代码补上,然后测试一下。
size_t capacity() const{return _end_of_storage - _start;}size_t size() const{return _finish - _start;}
迭代器
//这个普通迭代器实现起来也相当简单
iterator begin()
{return _start;
}iterator end()
{return _finish;
}
pop_back
删除数据好像也很简单,直接finish- -就可以,但是也有一些问题,那具体有什么问题呢?我们来看一下。
我们简单测试一下。
我们一直pop,就出问题了。
_finish一直减就走到前面去了,这样我们使用迭代器就出问题了。
我们简单改一下。
void pop_back()
{assert(!empty());--_finish;
}
bool empty()
{return _start == _finish;
}
针对const对象的访问
本质还是涉及到权限的放大,我们把成员函数都改为const就可以了。
resize()
void resize(size_t n, T val = T())//T()默认构造,是匿名对象,具体解释看下面
{if (n < size()){// 删除数据_finish = _start + n;}else{if (n > capacity())reserve(n);while (_finish != _start+n){*_finish = val;++_finish;}}
}
上面resize的缺省值能不能给0?
其实答案很明显, 显然不行,因为T是泛型编程,类型不一定是int,如果是double或者指针,对象都不行。
那问题又来了,int有默认构造函数吗?
我们在之前学习类和对象的时候知道,内置类型是没有构造函数的,但是有了模板之后它需要有。
insert
下面这段代码有什么问题?
如果容量不够,挪动数据就越界了。
除了容量不够还有没有其他什么问题?
提示一下pos==0; 好吧,其实不会发生之前模拟实现string的时候发生的无符号整型最大值的问题。
大家看这样子去测试就出问题了
注意,func(v1)是两次读取
程序运行时崩了。
先简单分析一下,这可能时内存问题,可能时数组越界。
为什么push_back 5次的时候没问题,push_back 4次就出问题了?
5个和4个的区别是什么?
insert的时候扩容出问题了。
注意看
发生了什么,扩容之后,start和finish变了,start 和finish为什么会变?
这是我们遇见的最经典的迭代器失效的问题。
pos变成野指针了。
这也就导致一直在循环的问题。
那这个怎么解决呢?
更新一下pos.
//void insert(iterator pos, const T& val)
iterator insert(iterator pos, const T& val)
{assert(pos >= _start);assert(pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);// 扩容后更新pos,解决pos失效的问题pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;return pos;
}
接下来,继续问一个问题,看前面的测试图片,insert之后能不能修改迭代器的位置。
(*pos)++;//我想修改这个3的位置。
func(v1);
程序没有报错,但是很明显不符合我们的预期。为什么?
不是已经把pos更新了吗,为什么外面还是不行呢?怎么不起作用。
因为自己写的insert是传值形参,形参的改变 不会影响实参的改变 。
怎么解决?
能不能用引用传参解决,看起来不错,实际不好。用引用传参会报错,为什么?
insert 引起迭代器失效的两种情况:
1.野指针问题
2.意义已经变了
通过返回值去解决
但是最好别用,你也不知道是什么时候失效。
insert以后我们认为pos失效了,不能再使用。
erase
现在又有一个问题,erase以后pos会不会失效?
不会,但是库里面的失效了,并且vs报错非常强烈,看下面。
报了一个断言错误。
再看一下g++下的运行。
那到底erase以后pos失效还是不失效?vs比较合理还是g++合理?
如果pos位置是4呢,那这个位置就很不合理了。
所以我们认为失效,不要访问,行为结果未定义,跟具体的编译器有关。
一定要注意,不然会被坑的很惨。
要想解决一下这种情况,我们都是用返回值来处理一下,其实本质就是不要pos跳过。
iterator erase(iterator pos)
{assert(pos >= _start);assert(pos < _finish);iterator start = pos + 1;while (start != _finish){*(start - 1) = *start;++start;}--_finish;return pos;
}
下面这个测试,连续的偶数和最后一个是偶数都能解决,就没什么大问题了。
1. vs进行强制检查,erase以后,迭代器不能访问。
2.g++没有强制检查,具体问题具体分析,结果未定义。
string有没有迭代器失效?
有,但是string不容易发生迭代器失效问题。
insert和erase不喜欢用迭代器,用的都是下标。
析构函数
~vector()
{delete[] _start;_start = _finish = _end_of_storage = nullptr;
}
下面的构造函数。
给大家看一个大坑。
先看下面的代码,这样写有没有什么问题?
程序崩了
不初始化会出各种问题。
一调试很容易看到会出现哪些问题。
加上初始化列表
vector(size_t n, const T& val = T())//T()是什么前面已经讲得很清楚了: _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(n);for (size_t i = 0; i < n; ++i){push_back(val);}
}
看一下这个构造函数。
如果是用iterator就写死了,你必须用vector的迭代器来进行初始化。
迭代器区间初始化,是必须用vectro的迭代器初始化吗?
一个容器用迭代器区间初始化,需要时任意类型的迭代器。
这里引出了另一个语法,允许一个类的成员函数再是函数模板。
// [first, last)
template <class InputIterator>
vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{while (first != last) //不能用 <=,如果是链表肯定就不行{push_back(*first);++first;}
}
如果我们不写初始化列表,可用c++11的语法,成员变量声明的时候给个缺省值
缺省值是给构造函数的初始化列表用的。
奇怪的事情发生了,编译的时候报了一个这样的错误
为什么匹配错了,匹配到上面写的那个迭代区间初始化那个?
我们知道编译器会调用最匹配的那个,仔细分析一下其实可以发现,如果推演一下的话,如果要匹配 vector(size_ t n, const T& val =T()); 的话,就会发生类型转换,而调用迭代器区间初始化的话,进行推演,如果类型是int 直接就匹配上了。
然后看迭代器区间初始化的代码,int不能解引用,就直接报错了。
怎么解决?
1.加个u, 代表我这个变量是无符号。
2…看STL的源码,看源码是怎么解决这个问题的。
我们这里可以用一个很简单的方式去就解决,提供一个重载的版本。
引用返回时有风险的,要谨慎使用,除非你想想operator【】那样,可以去修改。
给大家看一下神奇的东西。
只要类型能匹配,char可以发生转换。
最神奇的还是可以这么玩。
甚至可以是数组,为什么可以是数组?
原生指针可以是天然的迭代器,有个前提,这个原生指针是指向数组。
其实vector的迭代器,string的迭代器也可以是原生指针。
接着扩展一下。
sort
默认是升序
这是一个函数模板,它的名字叫随机访问迭代器,那随机访问是什么呢?一般底层是数组。
它可以帮我们排序,并且用起来很爽。
如果是降序,我们就这么用。
1.
2.
这两个其实是等价了。
拷贝构造
我们还涉及深浅拷贝的问题。
首先这样写,是我们经典的浅拷贝的问题。
我们先写一个传统写法的深拷贝。
vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{_start = new T[v.capacity()];memcpy(_start, v._start, sizeof(T) * v.size());_finish = _start + v.size();_end_of_storage = _start + v.capacity();
}
看下面的代码,崩了,为什么?
也就是说当我们数据是int的时候程序能正常运行,当我们的数据是string的时候就崩的。
因为memcpy也是一种浅拷贝。调用拷贝构造的时候,memcpy干了什么事情。
从起始位置开始依次把所有值拷贝下来。
这里面还有一层是我们没有考虑的,这是深拷贝里面又套了一层深拷贝。memcpy就是深层次的浅拷贝。
调用析构的时候会崩。
怎么解决呢?
我们肯定是要解决三个问题,数据是int类型,或者string类型,或者vector的vector类型。
怎样完成深拷贝呢?难道是我们自己写吗?
我们不能自己解决,因为T是模板,我们也不知道它具体是什么类型。
不能自己给它写一个深拷贝,因为它们是私有的,动不了里面的。
所以我们这里面调一个深拷贝的函数来完成。
赋值就是深拷贝。
其实我们还没有完全解决所有的问题 ,除了拷贝构造用了memcpy,扩容也用了memcpy。
修改扩容的代码。
void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start){//memcpy(tmp, _start, sizeof(T)*size());for (size_t i = 0; i < sz; ++i){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;}
}
那接着,哪里还有问题呢?vector里面的数据比如说是vector对象呢,比如vectro<vector >,给大家看一下,一个杨辉三角的例子。
测试
出错了,为什么?还有什么问题没有解决。
外面的vector是深拷贝,里面的vector是浅拷贝。
问题还是出现在拷贝构造,我们并没有自己写赋值。所以编译器还是用的默认生成的,是浅拷贝。
我们自己写个赋值就解决了。
现代写法
直接复用。
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(const vector<T>& v)
{vector<T> tmp(v.begin, v.end());swap(tmp);
}
最后一个小问题,不加模板参数语法上也允许,但是不推荐这样写。
相关文章:

C++STL的vector模拟实现
文章目录 前言成员变量成员函数构造函数push_backpop_backinserterase析构函数拷贝构造 前言 成员变量 namespace but {template<class T>class vector{public:typedef T* iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;}; }我们之前实…...

openssl 常用命令 pkcs12
openssl pkcs12 openssl pkcs12 官方文档 1. 描述 The pkcs12 command allows PKCS#12 files (sometimes referred to as PFX files) to be created and parsed. PKCS#12 files are used by several programs including Netscape, MSIE and MS Outlook. pkcs12 命令是用来创…...

2017下半年软工(桥接模式)
题目——桥接模式(抽象调用实现部分) package org.example.桥接模式;/*** 桥接模式的核心思想是将抽象部分与它的实现部分分离,使它们可以独立变化,就是说你在实现部分:WinImp、LinuxImp基础上还能加上RedHatImp&#…...

Hive 浅析
Hive是一个简单的LUA沙盒,除了基本的LUA解释器的功能以外,还提供了诸如热加载等功能。 了解HIVE的工作原理有利于了解Lua虚拟机的底层实现机理。 本文从是什么-怎么用-为什么三个维度介绍HIVE。 Hive Hive是什么 hive是一个简单的LUA应用框架,目前基于…...

C 语言中,结构体「.」与「->」的区别
简单来说 「 」的左边是结构体名字时用点符号「.」 「 」的左边是结构体指针时名字时用箭头「->」 对于要读取结构体种的数据时,有下面三种写法,操作是等价的。 struct ListNode a;struct ListNode *p1 &a;/*三种写法*/a.element 2333;p1->e…...

【Java Web学习笔记】5 - XML
项目代码 https://github.com/yinhai1114/JavaWeb_LearningCode/tree/main/xml 零、在线文档 XML系列教程 一、XML引出 1.为什么需要XML 1.需求1 :两个程序间进行数据通信? 2.需求2:给一台服务器,做一个配置文件,当服务器程序启动时,去…...

在jupyter notebook中修改其他文件的解决方案
大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…...

如何在Android中旋转屏幕时避免重新绘制Activity
如何在Android中旋转屏幕时避免重新绘制Activity 在Android开发中,设备旋转通常导致当前活动(Activity)被销毁并重新创建,这可能导致用户界面重置和不必要的资源重新加载。然而,有时我们希望避免这种行为,…...

离线环境下安装python库(推荐pip download)
离线环境下安装python库(推荐pip download) 目录 1.需求 2.失败操作(注意) 3.成功操作 4.其它参考 1.需求 机器部署web系统环境后,就不可再次联网,所以升级python web后端,需要离线安装pyt…...

ubuntu16.04安装ROS+Gazebo
ubuntu16.04安装ROS参考文章 ros安装(一键最简安装,吹爆鱼香ROS,请叫我鱼吹) ROS篇——Ubuntu快速一键安装ROS或ROS2(通用) ubuntu安装ROS melodic(最新、超详细图文教程) 配置ubuntu以及安装ros2必要环…...

手动搭建koa+ts项目框架(路由篇)
文章目录 前言一、安装koa-router二、引入koa-router并使用三、优化路由配置总结如有启发,可点赞收藏哟~ 前言 本文基于手动搭建koats项目框架(基础篇)配置接口路由 一、安装koa-router npm i -S koa-router二、引入koa-router并使用 ./sr…...

c语言:文件操作(1)
前言:为什么要使用文件 使用文件可以让程序在不同运行之间保存和读取数据。这样可以实现持久化存储,即使程序关闭后数据也不会丢失。文件也可以用于数据交换,允许不同程序之间共享信息。在 C 语言中,文件还可以用于读取配置信息&…...

运筹学经典问题(三):最大流问题
问题描述 给定一个图网络 G ( V , E ) G(V, E) G(V,E),网络中连边的权重代表最大容量,在这个图中找出从起点到终点流量最大的路径。 数学建模 集合: I I I:点的集合; E E E:边的集合。 常量&#x…...

裸机开发与Linux驱动开发的区别
一. 简介 裸机开发,即我们常说的不带系统的单片机开发。 Linux驱动开发,即带文件系统的Linux驱动的开发。 二. 裸机开发与Linux驱动开发的区别 1. 裸机开发 比较底层,跟寄存器打交道,有些 MCU提供了库。 2. Linux驱动开发…...

【蓝桥杯选拔赛真题75】Scratch行走的螃蟹 第十五届蓝桥杯scratch图形化编程 少儿编程创意编程选拔赛真题解析
目录 scratch行走的螃蟹 一、题目要求 编程实现 二、案例分析 1、角色分析...

小型洗衣机哪个牌子质量好?迷你洗衣机排名前十名
随着内衣洗衣机的流行,很多小伙伴在纠结该不该入手一款内衣洗衣机,专门来洗一些贴身衣物,答案是非常有必要的,因为我们现在市面上的大型洗衣机只能做清洁,无法对我们的贴身衣物进行一个高强度的清洁,而小小…...

MySQL_9.B-数索引
1.定义:B-树是一类树,包括B-树、B树、B*树等,是一棵自平衡的搜索树,它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点. 2.B-数产生的原因 当数据量非常大时,内存不够用,大部分数据只能存放在磁盘上,只有需要的…...

ubuntu-更改镜像源-系统初始化-安装Clion-C++编译环境-Java安装
文章目录 1.镜像配置文件及更新2.安装java sdk并配置环境变量3.安装Clion4.总结 1.镜像配置文件及更新 将sources.list备份保存为sources.list.backup,以防止有需要的时候更换回来。 sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup sudo gedit /etc/apt/source…...

c语言-动态内存管理
文章目录 一、为什么会有动态内存管理二、申请内存函数1、malloc2、free3、calloc4、realloc 三、常见的动态内存的错误四、练习 一、为什么会有动态内存管理 1.我们一般的开辟空间方式: int a 0;//申请4个字节空间 int arr[10] { 0 };//申请40个字节空间2.这样…...

【JAVA杂货铺】一文带你走进面向对象编程的构造方法 | Java| 面向对象编程 | (中)
🌈个人主页: Aileen_0v0🔥系列专栏:Java学习系列专栏💫个人格言:"没有罗马,那就自己创造罗马~" 目录 回顾 构造方法 this关键字 面试题 构造方法的类型 下节预告 代码块 🍒回顾 之前我们学习了什么是类 什么是…...

动态规划学习——通符串匹配,正则表达式
目录 编辑 一,通符串匹配 1.题目 2.题目接口 3,解题思路及其代码 二,正则表达 1.题目 2.题目接口 3.解题思路及其代码 三,交错字符串 1.题目 2,题目接口 3.解题思路及其代码 一,通符串匹配 1…...

【数据开发】Hive 多表join中的条件过滤与指定分区
1、条件过滤 left join 中 on 后面加条件 where 和 and 的区别 1、 on条件是在生成临时表时使用的条件,它不管and中的条件是否为真,都会保留左边表中的全部记录。2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有le…...

基于Java SSM框架实现高校人事管理系统项目【项目源码】计算机毕业设计
基于java的SSM框架实现高校人事管理系统演示 JSP技术介绍 JSP技术本身是一种脚本语言,但它的功能是十分强大的,因为它可以使用所有的JAVA类。当它与JavaBeans 类进行结合时,它可以使显示逻辑和内容分开,这就极大的方便了用户的需…...

[C++] 模板进阶(非类型模板参数,特化,分离编译)
文章目录 1、非类型模板参数2、模板的特化2.1 什么是模板特化2.2 函数模板特化2.3 类模板的实例化2.3.1 全特化2.3.2 偏特化 3、模板分离编译3.1 什么是分离编译3.2 模板的分离编译3.3 解决方法 4、模板总结 1、非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即…...

C++ this指针
通常情况下,类的成员函数都只涉及一个对象,即调用它的对象。但有时候方法可能涉及到两个对象,在这种情况就需要使用到C的this指针。 class Stock { private: ... double total_val; ... public: double total() const {return total_val;} }…...

解决Sortable拖动el-table表头时,由于选择列造成的拖拽顺序错乱的bug
原因 由于我的表头是由数组循环遍历生成的,而选择列不在数组内,只能在循环外定义el-table-column,造成拖动时索引错乱错误代码 <el-tableheader-dragend"headerDragend"id"out-table":data"state.sliceTable&quo…...

Plantuml之类图语法介绍(十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...

深入Docker命令行:探索常用命令和实用技巧
Docker命令行界面是每个容器开发者的得力工具。在这篇文章中,将深入探讨一系列常用的Docker命令,以及一些实用技巧,通过更丰富的示例代码,帮助大家更全面地理解和运用Docker命令行工具。 1. Docker基本命令 1.1 镜像操作 深入了…...

qt 容器QVector,QMap,QHash的常见使用与该迭代器的简单介绍
一. QVector容器是一个动态数组,可以容纳任意数量的元素,在相邻的内存中存储给定的数据类型作为一组数据,在QVector前部或中间位置插入元素都会导致内存中大量的数据元素移动,这使得操作速度会减慢.可使用迭代器对这组数据进行访问. 和其他的容器类型类似,QVector…...

两线制无源 4-20mA 回路供电隔离变送器
两线制无源 4-20mA 回路供电隔离变送器 一入一出两线制无源 4-20mA 回路供电隔离变送器 概述:JSD TAW-1001D-100L-F 系列隔离变送器是 4-20mA 两线制回路供电的电流隔离变送配电器,该隔离变送器采用电磁隔离技术,并通过输入端馈电方式,给输入端两线制仪器仪表设备供…...