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

news2024/9/21 8:00:23

文章目录

  • 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.coloradmin.cn/o/440698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ACL访问控制列表简介和配置演示

一.ACL功能和特点 1.功能 2.特点 二.ACL种类 1.基础ACL&#xff1a; 2.增强ACL&#xff1a; 三.配置演示 1.基础ACL&#xff1a; 2.增强ACL&#xff1a; 一.ACL功能和特点 1.功能 对感兴趣的路由 (控制层面)进行设置策略 对感兴趣的流量 (数据层面)进行设置策略 2.…

Activity启动模式的生命周期

四种启动模式 1.standard android:launchMode"standard" 默认的标准启动模式&#xff0c;每次启动当前Activity&#xff0c;任务栈中都添加一个当前Activity的实例。按返回键时&#xff0c;表现出退出多个当前Activity的现象。 MainActivityOne和MainActivityTwo都…

DPText-DETR原理及源码解读

一、原理 发展脉络&#xff1a;DETR是FACEBOOK基于transformer做检测开山之作&#xff0c;Deformable DETR加速收敛并对小目标改进&#xff0c;TESTR实现了端到端的文本检测识别&#xff0c;DPText-DETR做了精度更高的文字检测。 DETR 2020 FACEBOOK&#xff1a; 原理 https://…

c/c++:函数的作用,分类,随机数,函数定义,调用,申明,exit()函数,多文件编程,防止头文件重复

c/c&#xff1a;函数的作用&#xff0c;分类&#xff0c;随机数&#xff0c;函数定义&#xff0c;调用&#xff0c;申明&#xff0c;exit()函数&#xff0c;多文件编程&#xff0c;防止头文件重复 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大…

Spring启动及Bean实例化过程来看经典扩展接口

目录 一、Spring启动及Bean实例化过程 二、分析其对应经典扩展接口 三、对开发的指导意义 备注&#xff1a;以下总结只是一些基本的总结思路&#xff0c;具体每个扩展接口的应用后续进行分析总结。 一、Spring启动及Bean实例化过程 Spring启动及Bean实例化的过程&#xff0…

6 款顶级 Android 数据恢复软件列表

数据丢失可能会扰乱您的个人/企业生活&#xff0c;如果手动完成&#xff0c;可能很难恢复丢失的数据。Android 数据恢复软件是解决此问题的完美解决方案。这些工具可帮助您快速轻松地从 Android 设备恢复丢失的数据。它可以帮助您恢复照片、视频、笔记、联系人等。 我研究了市…

1. C++使用Thread类创建多线程的三种方式

1. 说明 使用Thread类创建线程对象的同时就会立刻启动线程&#xff0c;线程启动之后就要明确是等待线程结束&#xff08;join&#xff09;还是让其自主运行&#xff08;detach&#xff09;&#xff0c;如果在线程销毁前还未明确上面的问题&#xff0c;程序就会报错。一般都会选…

webserve简介

目录 I/O分类I/O模型阻塞blocking非阻塞 non-blocking&#xff08;NIO&#xff09;IO复用信号驱动异步 webServerHTTP简介概述工作原理HTTP请求头格式HTTP请求方法HTTP状态码 服务器编程基本框架两种高效的事件处理模式Reactor模式Proactor模拟 Proactor 模式 线程池 I/O分类 …

字节岗位薪酬体系曝光,看完感叹:不服真不行

曾经的互联网是PC的时代&#xff0c;随着智能手机的普及&#xff0c;移动互联网开始飞速崛起。而字节跳动抓住了这波机遇&#xff0c;2015年&#xff0c;字节跳动全面加码短视频&#xff0c;从那以后&#xff0c;抖音成为了字节跳动用户、收入和估值的最大增长引擎。 自从字节…

最全MySQL8.0实战教程

文章目录 最全MySQL8.0实战教程一.前言1.计算机语言概述2.SQL的概述2.1 语法特点2.2 MySQL的安装2.2.1 方式1&#xff1a;解压配置方式2.2.2 方式2&#xff1a;步骤安装方式 二. 数据库DDL操作1.DDL概念2.对数据库常用操作 最全MySQL8.0实战教程 长路漫漫&#xff0c;键盘为伴&…

【Linux进阶篇】启动流程和服务管理

目录 &#x1f341;系统启动 &#x1f343;Init和Systemd的区别 &#x1f343;运行级别和说明 &#x1f341;Systemd服务管理 &#x1f343;6和7命令区别 &#x1f343;systemd常用命令 &#x1f341;系统计划调度任务 &#x1f343;一次性任务-at &#x1f343;batch &#x1…

论文 : Multi-Channel EEG Based Emotion Recognition Using TCNBLS

Multi-Channel EEG Based Emotion Recognition Using Temporal Convolutional Network and Broad Learning System 本文设计了一种基于多通道脑电信号的端到端情绪识别模型——时域卷积广义学习系统(TCBLS)。TCBLS以一维脑电信号为输入&#xff0c;自动提取脑电信号的情绪相关…

自然语言处理 —— 01概述

什么是自然语言处理呢? 自然语言处理就是NLP,全名为Natural Language Processing。 一、NLP的困难 1. 歧义 (1)注音歧义 (2)分词歧义 (3)结构歧义 (4)指代歧义 (5)语义歧义 (6)短语歧义

javascript简单学习

简介&#xff1a; javascript 是脚本语言 javascript是轻量级的语言 javascript是可插入html页面的编程代码 javascript插入html页面后&#xff0c;可由所有现代浏览器执行 以下是JavaScript的一些基本概念&#xff1a; 1. 变量&#xff1a;变量用于存储数据值&#xff0…

每日学术速递4.13

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Slide-Transformer: Hierarchical Vision Transformer with Local Self-Attention(CVPR 2023) 标题&#xff1a;Slide-Transformer&#xff1a;具有局部自注意力的分层视觉变换器 …

一、vue之初体验-两种方式引入vue

一、Vue引入方式-CDN <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"widthdevice-width, initial-s…

开源问答社区软件Answer

什么是 Answer &#xff1f; Answer 是一个开源的知识型社区软件。您可以使用它快速建立您的问答社区&#xff0c;用于产品技术支持、客户支持、用户交流等。 Answer是国内SegmentFault 思否团队开发的技术问答社区&#xff0c;Answer 不仅拥有搭建问答平台&#xff08;Q&A…

界面控件DevExtreme使用指南 - 折叠组件快速入门(一)

DevExtreme拥有高性能的HTML5 / JavaScript小部件集合&#xff0c;使您可以利用现代Web开发堆栈&#xff08;包括React&#xff0c;Angular&#xff0c;ASP.NET Core&#xff0c;jQuery&#xff0c;Knockout等&#xff09;构建交互式的Web应用程序&#xff0c;该套件附带功能齐…

MySQL - C语言接口-预处理语句

版权声明&#xff1a;本文为CSDN博主「zhouxinfeng」的原创文章&#xff0c;原文链接&#xff1a;https://blog.csdn.net/zhouxinfeng/article/details/77891771 目录 MySQL - C语言接口-预处理语句预处理机制特点&#xff1a;预处理机制数据类型函数:预处理机制步骤&#xff1…

集群聊天服务器项目(三)——负载均衡模块与跨服务器聊天

负载均衡模块 为什么要加入负载均衡模块 原因是&#xff1a;单台服务器并发量最多两三万&#xff0c;不够大。 负载均衡器 Nginx的用处或意义**&#xff08;面试题&#xff09;** 把client请求按负载算法分发到具体业务服务器Chatserver能和ChatServer保持心跳机制&#xf…