【C++】vector的简化模拟实现

文章目录

  • 1. 主要结构
  • 2. 默认成员函数
  • 3. 迭代器
  • 4. 容量相关
    • 1. size和capacity
    • 2. reserve
    • 3. resize
  • 5. 数据访问
  • 6. 数据修改
    • 1. push_back
    • 2.pop_back
    • 3. insert
    • 4.erase
    • 5.swap
    • 6.clear

1. 主要结构

参照SGI版本的vector实现,使用三个指针来维护这样一段内存空间

template<class T>
class vector
{
public:
    //成员函数
private:
    T* _start;
    T* _finish;
    T* _endOfStorage;
};

图示结构如下:

image-20230418160124874

那么,对于vector的模拟实现和string的实现就有所不同了。

2. 默认成员函数

✅对于默认构造函数,我们不需要对其进行开辟空间等操作,所以直接在初始化列表给定三个成员变量空指针即可

vector()
:_start(nullptr)
,_finish(nullptr)
,_endOfStorage(nullptr)
{}

✅对于给定长度和值的构造函数,我们需要开辟空间,然后把val都填进去,但是这里对于开辟空间的操作,我们复用reserve即可,这样如果出现某些需要对开辟空间操作的修改,只需要修改reserve即可,提高了代码的可维护性

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

✅对于迭代器区间的初始化,按照迭代器的自增行为遍历整个迭代器区间,并将每个元素尾插到vector中。

template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
    while (first != last)
    {
        push_back(*first);
        ++first;
    }
}

✅对于拷贝构造,我们同样有经典写法和现代写法两种,由于现代写法的可读性更优,所以这里对于经典写法我们就不再赘述。现代写法,我们的想法是找到一个“工具人”tmp来帮我们构造,这里用到了迭代器区间初始化的方式来初始化tmp,然后再通过swap函数将tmp与this指针指向的对象进行交换从而实现了拷贝构造。

vector(const vector<T>& v)
    :_start(nullptr)
    , _finish(nullptr)
    , _endOfStorage(nullptr)
{
    vector<T> tmp(v.begin(), v._end());
    swap(tmp);
}

✅对于析构函数,我们要做的事情就是释放new申请的资源,然后将成员变量置空即可。

~vector()
{
    delete[] _start;
    _start = _finish = _endOfStorage = nullptr;
}

✅对于赋值重载,我们同样找到了一个“工具人”tmp,在传参的过程中,使用传值的方式,构建了一个变量tmp,然后使用在vector类内部实现的swap交换this指针指向的类和tmp。

vector<T>& operator=(vector<T> tmp)
    : _start(nullptr)
    , _finish(nullptr)
    , _endOfStorage(nullptr)
{
	swap(tmp);
    return *this;
}

3. 迭代器

vector和string的底层都是动态数组,所以对于vector类来说,原生指针也能够很好的支持迭代器行为。这里我们只实现正向迭代器的const和非const版本。

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

4. 容量相关

1. size和capacity

由于vector和string的成员变量不同,所以这里我们的size和capacity不能直接得到,需要进行运算

image-20230419172014232

//按照图示的理解,由于指向数组的指针都是前闭后开,所以直接使用指针相减即可
size_t size()
{
    return _finish - _start;
}
size_t capacity()
{
    return _endOfStorage - _start;
}

2. reserve

如果我们做了扩容操作,就一定是异地扩容,所以对于_finish和_endOfStorage的处理就要注意一下,提前将size保存一下,否则如果在_start重新赋值之后再调用size就会出现问题。

void reserve(size_t n)
{
    if (capacity() < n)
    {
        size_t OldSize = size();
        T* tmp = new T[n];
        memcpy(tmp, _start, sizeof(T) * size());
        delete[] _start;
        _start = tmp;
        _finish = _start + OldSize;
        _endOfStorage = _start + n;
    }
}

那么,这样做了修改之后是不是就没有问题了呢?

这里注意一下,我们写的vector是一个类模板,其中存放的元素类型可以是任意类型,如果这里vector中存放的元素类型是需要深拷贝的,例如vector中存放的是string对象的时候,结构如下图。

image-20230420162151069

按照我们刚刚实现的逻辑,首先开辟一块新的空间,然后按照值拷贝的方式将vector中存放的元素拷贝到tmp中,然后调用vector的析构函数,析构的时候对于string对象,自动调用string的析构函数,将string指向的空间,即上述的绿色部分的空间释放,然后导致新开辟的空间中的string对象全部都无效。最终在该vector对象生命周期结束时,再次调用析构函数,导致其中的每个string对象都析构两次,程序崩溃(或者在结束之前对其中的stirng对象做操作,导致出现其他问题使程序崩溃)

那么应该怎么解决这个问题呢?

不能调用库函数memcpy执行值拷贝,而是自己重新写一个拷贝的操作

void reserve(size_t n)
{
    if (capacity() < n)
    {
        size_t OldSize = size();
        T* tmp = new T[n];
        //memcpy(tmp, _start, sizeof(T) * size());
        if (_start)
        {
            for (size_t i = 0; i < OldSize; ++i)
            {
                tmp[i] = _start[i];
            }
            delete[] _start;
        }
        _start = tmp;
        _finish = _start + OldSize;
        _endOfStorage = _start + n;
    }
}

3. resize

这里resize的用法是和之前string中resize的用法相同,所以设计思路也是相同的,一共分成三种情况:

  1. 当传入的n>capcaity时,进行扩容操作,然后再填充数据
  2. 当 size < n < capacity时,直接填充数据
  3. 当n < size时,更改_finish的指向即可
void resize(size_t n, const T& val = T())
{
    if (n > capacity())//n>capacity
    {
        reserve(n);
    }
    if (n > size())//size < n < capacity
    {
        while (*_finish > _size + n)
        {
            *_finish = val;
            ++_finish;
        }
    }
    else
        _finish = _start + n;
}

这里补充一下,在上面的代码中,可以看到给val的缺省值是T(),而不是一个确定的值,这是因为存在vector中的元素可以是任意类型,有可能是自定义类型,如果使用某一个值可能无法构造出该类型的对象,所以使用T()构建一个T类型的匿名对象。

对于内置类型,怎么会有默认构造函数呢?

✅在C++中,为了支持这种写法,对于内置类型,也支持了使用类型()的方式创建对象

5. 数据访问

针对数据访问,我们同样只实现一个[]的重载即可

T& operator[](size_t pos)//const版本
{
    assert(pos < size());
    return _start[pos];
}
const T& operator[](size_t pos) const//非const版本
{
    assert(pos < size());
    return _start[pos];
}

6. 数据修改

1. push_back

老规矩,首先判断容量,如果已经没有容量插入就先扩容,然后在_finish指向的位置插入值,然后让_finish向后挪动一位

void push_back(const T& val)
{
    if (_finish == _endOfStorage)
    {
        size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
        reserve(newCapacity);
    }
    *_finish = val;
    ++_finish;
}

2.pop_back

显而易见,直接使_finish向前挪动一位即可,但是最好首先进行一下判空,这里顺便也实现以下empty

bool empty()
{
    return _start == _finish;
}
void pop_back()
{
    if (!empty())
    {
        --_finish;
    }
}

3. insert

对于insert,我们在上一篇博客中讲到了迭代器失效问题,给出的解决办法就是设定返回值类型为迭代器,所以这里对insert的设计就很清晰了。

iterator insert(iterator pos, const T& val)
{
    assert(pos >= _start);
    assert(pos <= _finish);
    if (_finish == _endOfStorage)
    {
        size_t len = pos - _start;//这里注意以下,要保存pos的相对位置,否则扩容之后会导致pos位置失效
        size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newCapacity);
        pos = _start + len;//重新确定pos位置
    }
    //挪动数据
    iterator end = _finish - 1;
    while (end >= pos)
    {
        *(end + 1) = *end;
        --end;
    }
    *pos = val;
    ++_finish;
    return pos;
}

4.erase

同样的erase也会导致迭代器失效,所以最终我们也重新返回一个有效的pos值

iterator erase(iterator pos)
{
    assert(pos >= _start);
    assert(pos < _finish);
    iterator begin = pos + 1;
    while (begin < _finish)
    {
        *(begin - 1) = *begin;
        ++begin;
    }
    --_finish;
    return pos;
}

5.swap

由于vector和string一样,底层都是动态数组,所以swap的形式也非常像

void swap(vector<T>& v)
{
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_endOfStorage, v._endOfStorage);
}

6.clear

clear的作用就是将数据清空,但是不销毁对象,所以直接改变_finish即可

void clear()
{
    _finish = _start;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/13743.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《数据结构》---术语篇

目录 前言: 一.术语 1.1数据 1.2数据结构 1.3逻辑结构和物理结构 二.数据类型和抽象数据类型 ​​​​​​​ ❤博主CSDN&#xff1a;啊苏要学习 ▶专栏分类&#xff1a;数据结构◀ 学习数据结构是一件有趣的事情&#xff0c;希望读者能在我的博文切实感受到&#xff0c…

同为科技(TOWE)防雷科普篇(二)——雷击灾害急救方法大全

前 言 当雷击发生时&#xff0c;空气中的各种微粒互相碰撞和摩擦便会使该空气介质两面的正负电荷的量持续积累&#xff0c;这时加于该空气介质的电压也会同时增加&#xff0c;当局部电压达到当时条件下空气的击穿电压时&#xff0c;该空气介质的局部便会发生电击穿而持续成为等…

使用ChatGPT完成程序开发——目标:不写一行代码完成图像识别并点击

本文作为一个使用AI开发的思路&#xff0c;让更多的人可以利用AI完成一些简单的程序&#xff0c;文中使用的是国内镜像GTP3.5 源码: GitHub - kasimshi/testCV: AI编写的OpenCV图像识别例子 GTP镜像: 知汇 对AI描述我们要做的功能&#xff0c;让它给给初步的思路和方向 作为新…

[译] 实战 React 18 中的 Suspense

> 原文&#xff1a;https://dev.to/darkmavis1980/a-practical-example-of-suspense-in-react-18-3lln React 18 带来了很多变化&#xff0c;它不会破坏你已经编写过的代码&#xff0c;并且有很多改进和一些新概念。 它也让很多开发人员&#xff0c;包括我&#xff0c;意识到…

Web 开发会话技术之 -Cookie介绍以及源码分析和图分析以及Cookie的生命周期--路径--中文乱码的分析和代码示例

目录 Web 开发会话技术之 -Cookie 会话 基本介绍 1. 什么是会话&#xff1f; 2. 会话过程中要解决的一些问题&#xff1f; cookie 技术 cookie 介绍 二说 cookie cookie 可以用来做啥 cookie 基本使用 cookie 常用方法 cookie 底层实现机制-创建和读取 Cookie Crea…

Linux-初学者系列——篇幅7_文本编辑和处理命令

文本编辑和处理命令-目录 一、系统基本编辑命令安装vim软件工具包语法格式&#xff1a; 1、vim编辑命令模式01 普通模式02 编辑模式03 命令模式 2、编辑文件技巧01 批量删除多行指定信息02 批量增加多列指定信息03 编辑常见问题错误1&#xff1a;没有指定编辑信息错误2&#xf…

Flink高手之路5-Table API SQL

文章目录 Flink 中的Table API & SQL一、Table API & SQL 介绍1. 为什么要Table API和SQL2. Table API & SQL的特点3. Table API& SQL发展历程3.1 架构升级3.2 查询处理器的选择3.3 了解-Blink planner和Flink Planner具体区别如下&#xff1a;3.4 了解-Blink …

基于GPS/北斗卫星技术的无盲区车辆调度系统

基于GPS/北斗卫星技术的无盲区车辆调度系统 现代车辆调度系统是一种集全球卫星定位技术&#xff08;GPS&#xff09;、地理信息技术&#xff08;GIS&#xff09;和现代通信技术于一体的高科技项目。它将移动目标的动态位置&#xff08;经度与纬度&#xff09;、时间和状态等信息…

uni-app入门到实战

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f35f;欢迎来到前端初见的博文&#xff0c;本文主要讲解uni-app入门到实战&#x1f35f; &#x1f468;‍&#x1f527; 个人主页 : 前端初见 &#x1f95e;喜欢的朋友可以关注一下&#xff…

javassist 字节码处理库

目录 一、快速入门 1.1 创建class文件1.2 ClassPool的相关方法1.3 CtClass的相关方法1.4 CtMethod的相关方法1.5 调用生成的类对象 1.5.1 通过反射调用1.5.2 通过接口调用1.6 修改现有的类对象二、将类冻结三、类搜索路径四、$开头的特殊字符五、ProxyFactory的使用 我们知道J…

Linux I/O复用函数的使用情况和select接口的介绍

I/O 复用使得程序能同时监听多个文件描述符&#xff0c;这对于提高程序的性能至关重要。通常&#xff0c; 网络程序在下列情况下需要使用 I/O 复用技术&#xff1a; 1.TCP服务器同时要处理监听套接字和连接套接字 2.服务器同时要处理TCP请求和UDP请求。 3.程序同时要处理多个套…

直播预告 | 时序数据处理的云端利器:TDengine Cloud 详解与演示

当下&#xff0c;我们正处在一个万物互联的时代&#xff0c;大数据、云原生、AI、5G 等数字技术极大地方便了人们的生活&#xff0c;但智能物联网产生的海量数据却成为众多企业在数据处理上的巨大痛点。从本质来看&#xff0c;这些数据大多是产生自各种设备和传感器的时序数据&…

Spring种存取Bean的5种注解

存取Bean的五种注解 存储Bean对象两种方式1.添加一行bean2.使用注解的方式(5大注解)Controller(控制器存储)Service(服务存储)Repository(仓库存储)Component(组件存储)Configuration(配置存储)方法注解 Bean 获取Bean对象(三种)1.属性注入2.setter注入3.构造方法注入三种注入的…

springboot-分页功能

1.分页功能的作用 分页功能作为各类网站和系统不可或缺的部分&#xff08;例如百度搜索结果的分页等&#xff09; &#xff0c;当一个页面数据量大的时候分页作用就体现出来的&#xff0c;其作用有以下5个。 &#xff08;1&#xff09;减少系统资源的消耗 &#xff08;2&#…

Vue 3组件传值 、组件通信

本文采用<script setup />的写法&#xff0c;比options API更自由。那么我们就来说说以下七种组件通信方式&#xff1a; props emit v-model refs provide/inject eventBus vuex/pinia 举个例子 本文将使用下面的演示&#xff0c;如下图所示&#xff1a; 上图中…

mybatis粗心使用导致内存溢出

现象 服务响应变慢&#xff0c;线程日志也出现Java heap space内存溢出的错误&#xff0c;这个服务属于基础业务服务&#xff0c;出现问题要尽快的排查 分析 因为设置了gc日志和jmap启动相关参数 所以我们进行分析&#xff0c;这里模拟线上环境将堆大小参数调整到了128m&am…

【Linux】权限管理

文章目录 &#x1f4d6; 前言1. 什么是权限2. 权限管理2.1 Linux的用户分类&#xff1a;2.2 Liunx文件的分类&#xff1a;2.3 文件的访问权限2.4 文件访问权限的相关设置方法&#xff1a;chmod对文件权限的修改chown / chgrp 2.5 以八进制修改文件权限&#xff1a;2.6 默认权限…

Springsecurity课程笔记06-13章基于数据库的方法授权

动力节点Springsecurity视频课程 6 密码处理 6.1 为什么要加密&#xff1f; csdn 密码泄露事件 泄露事件经过&#xff1a;https://www.williamlong.info/archives/2933.html 泄露数据分析&#xff1a;https://blog.csdn.net/crazyhacking/article/details/10443849 6.2加密…

IJKPLAYER源码分析-常用API

前言 本文简要介绍IJKPLAYER的几个常用API&#xff0c;以API使用的角度&#xff0c;来审视其内部运作原理。这里以iOS端直播API调用切入。 调用流程 init 创建播放器实例后&#xff0c;会先调用init方法进行初始化&#xff1a; - (IJKFFMediaPlayer *)init {self [super ini…

计算机网络复习题+答案

文章目录 导文题目一、单项选择题二、填空题三、判断改错题,判断下列命题正误,正确的在其题干后的括号内打“√”,错误的打“”,并改正。四、名词解释五、简答题六、应用题导文 计算机网络复习题 题目 一、单项选择题 在应用层协议中,主要用于IP地址自动配置的协议是: (…
最新文章