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关键字 面试题 构造方法的类型 下节预告 代码块 🍒回顾 之前我们学习了什么是类 什么是…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...