『C++成长记』vector模拟实现

🔥博客主页:小王又困了

📚系列专栏:C++

🌟人之为学,不日近则日退

❤️感谢大家点赞👍收藏⭐评论✍️

 

目录

一、存储结构

二、默认成员函数

📒2.1构造函数

📒2.2拷贝构造

📒2.3赋值重载

三、容量操作

📒3.1获取有效元素个数

📒3.2获取空间容量大小

📒3.3使用reverse扩容

四、数据访问

📒4.1下标访问

📒4.1迭代器访问

 五、修正操作

📒5.1尾插数据

📒5.2尾删数据

📒5.3在任意位置插入数据

📒5.4在任意位置删除数据


🗒️前言:

vector是STL容器之一,其底层实现类似于数据结构顺序表,相当于string来说得益于泛型模板的加持使得vector可以变为任何类型,且是可以动态扩容。接下来我们就通过vector各种接口的模拟实现来深入了解vector。

一、存储结构

        与string底层结构不同,vector底层空间结构为三个指针:

namespace bit
{
    template<class T> //模板参数T
    class vector
    {
    public:
        typedef T* iterator;		//指针重命名为迭代器
        typedef const T* const_iterator;
    private:
        iterator _start; 
        iterator _finish; 
        iterator _end_of_storage; 
	};
}
  • _start:指向空间的起始地址
  • _finish:指向最后一个数据的下一个地址,相当于_size
  • _end_of_stroage:指向空间最后一个最末地址,相当于_capacity

小Tips:由于vector使用了模板,所以函数实现都在头文件中,防止因为模板导致的链接错误的问题!

二、默认成员函数

📒2.1构造函数

        我们在这里实现三个版本,分别是:默认构造函数带参构造函数迭代器区间构造。 

  • 默认构造函数:初始化三个指针置空即可
  • 带参构造函数:初始化n个T类型的value值在对象中
  • 迭代器区间构造:通过其他容器迭代器或指针迭代插入其所有值
//默认构造函数
vector()
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{}

vector () = default //强制编译器生存默认构造

//构造n个T类型数据
vector(size_t n, const T& value = T()) 
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{
    reserve(n);
    for (int i = 0; i < n; ++i)
    {
        *(_finish++) = value;
    }
}

//迭代器区间构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{
    while (first != last) //迭代器插入数据
    {
        push_back(*first);
        ++first;
    }
}

注意:我们实现了带参构造函数的n为size_t类型的版本,如果我们使用带参构造函数实例化,则会发生非法间接寻址的错误!这是因为size_t是整型,实例化T数据类型也是整型,此时编译器会自动匹配最合适的构造函数,于是匹配到了迭代器区间构造。我们只要写一个n为int类型的带参构造参数去匹配,就可以解决问题。

📒2.2拷贝构造

        对于拷贝构造我们要考虑深浅拷贝的问题,我们希望当一个vector对象拷贝另一个对象时新对象开辟新的空间拷贝数据,而不是两个对象共用同一块空间,否则会析构两次造成内存泄漏。

vector(const vector<T>& v)
{
    reserve(v.capacity());
    for (auto e : v)
    {
        push_back(e);
    }
}

📒2.3赋值重载

        赋值重载与拷贝构造的问题类似,也要注意深拷贝问题;区别于拷贝构造的地方在于不需要新建对象,但是需要判断是否为同一个对象避免重复开空间。

//传统写法
vector<T>& operator=(const vector<T>& v)
{
    if (&v != this) 
    {
        clear(); 
        reserve(v.capacity()); 
        for (int i = 0; i < v.size(); ++i)
		{
            *(_finish++) = v._start[i]; 
        }
    }
    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 = v3
vector<T>& operator=(vector<T> v)
{
    swap(v);
    return *this;
}

三、容量操作

📒3.1获取有效元素个数

size_t size() const
{
    return _finish - _start;
}

📒3.2获取空间容量大小

size_t capacity() const
{
    return _end_of_storage - _start;
}

 小Tips:vevtor是使用3个指针对空间进行管理,使用_finish(有效数据指针)减去_start(空间首地址)就能得出有效数据个数,容量同理。我们只是对数据进行访问不涉及修改,所以使用const修饰this指针。

📒3.3使用reverse扩容

void reserve(size_t n)
{
    if (n > capacity())
    {
        size_t oldsize = size();//获取扩容前有效数据个数
        T* tmp = new T[n];
        if(_start)
        {
            //memcpy(tmp, _start, sizeof(T*) * size());
            for (size_t i = 0; i < oldsize; i++)
            {
                tmp[i] = _start[i];
            }
            delete[] _start;
        }
        _start = tmp;
        _finish = _start + oldsize;
        _end_of_storage = _start + n;
    }
}
//错误写法:_finish = _start + size() ;

小Tips我们要用oldsize记录扩容前有效数据的个数,按照错误写法size()是用扩容前的_finish和扩容后的_start来计算的,结果是错误的。当string类型的对象要扩容时,memcpy对数据进行的是浅拷贝,两个对象指向同一块空间,会析构两次造成内存泄漏。

四、数据访问

📒4.1下标访问

        下标访问是通过重载 [ ] 运算符实现的,在下标pos正确的情况下,返回当前下标字符的引用,否则assert报错。

T& operator[](size_t pos)
{
    assert(pos < size());
    return _start[pos];
}

const T& operator[](size_t pos) const //const引用版本,不可以修改
{
    assert(pos < size()); 
    return _start[pos];
}

         at 函数我们可以复用运算符[ ]来实现。 at 函数的检查手段是抛异常。

T& at(size_t pos) 
{ 
    return (*this)[pos]; 
}

const T& at(size_t pos) const 
{ 
    return (*this)[pos]; 
}

📒4.1迭代器访问

typedef T* iterator;
typedef const T* const_iterator;

const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }

iterator begin() { return _start; }
iterator end() { return _finish; }

//范围for
for (auto e : v1)
{
    cout << e << " ";
}
cout << endl;

 五、修正操作

📒5.1尾插数据

        在尾插前检查容量是否充足,空间不够扩容,然后直接插入_finish的空位下即可,_finish指针移动到下一个空位。

void push_back(const T x)
{
    if (_finish == _end_of_storage)
    {
        size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newcapacity);
    }
    *_finish = x;
    ++_finish;

    //insert(end(), x);    //复用
}

📒5.2尾删数据

        尾删只需要--_finish,但需要判断size是否>0。

void pop_back()
{
    assert(size() > 0);
    --_finish;
}

📒5.3在任意位置插入数据

        当我们传递一个迭代器在pos位置插入数据时,可能涉及容器扩容,如果扩容,那么迭代器是旧空间的迭代器,则会导致迭代器失效,因为原有空间已经被释放,但迭代器还是指向原空间(那么就是野指针),所以我们在插入或删除后要更新迭代器。

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

小Tips:如果要进行扩容我们用len来记录扩容前从_startpos位置数据的个数,然后扩容后更新pos的位置。

📒5.4在任意位置删除数据

        删除元素也会有迭代器失效的问题,可能会越界访问 。

iterator earse(iterator pos)
{
    assert(pos >= _start);
    assert(pos < _finish);
    iterator it = pos + 1;
    while (it != _finish)
    {
        *(it - 1) = *it;
        it++;
    }
    --_finish;
    return pos;    //返回删除元素位置的下一个元素的迭代器
}

🎁结语: 

     本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位读者三连支持。文章有问题可以在评论区留言,博主一定认真认真修改,以后写出更好的文章。你们的支持就是博主最大的动力。

相关推荐

  1. C++ | vector模拟实现

    2024-07-13 12:12:02       25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-13 12:12:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-13 12:12:02       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-13 12:12:02       58 阅读
  4. Python语言-面向对象

    2024-07-13 12:12:02       69 阅读

热门阅读

  1. TensorFlow系列:第三讲:MobileNetV2使用介绍

    2024-07-13 12:12:02       24 阅读
  2. MySQL上亿数据查询优化:实践与技巧

    2024-07-13 12:12:02       20 阅读
  3. Jetson-AGX-Orin gstreamer+rtmp+http-flv 推拉流

    2024-07-13 12:12:02       21 阅读
  4. Mybatis-Plus最优化持久层开发

    2024-07-13 12:12:02       19 阅读
  5. 389. 找不同

    2024-07-13 12:12:02       21 阅读
  6. Python水平怎么样才能就业?

    2024-07-13 12:12:02       50 阅读
  7. Git: fatal: cannot lock ref‘HEAD‘: Unable to create

    2024-07-13 12:12:02       26 阅读
  8. 大数据学习之 scala基础(补充)

    2024-07-13 12:12:02       24 阅读
  9. Perl中的切分艺术:深入探索split函数的神秘力量

    2024-07-13 12:12:02       22 阅读
  10. 【面试题】Golang 之Channel底层原理 (第三篇)

    2024-07-13 12:12:02       22 阅读
  11. 数据结构

    2024-07-13 12:12:02       26 阅读
  12. MySQL 面试题

    2024-07-13 12:12:02       25 阅读
  13. 【笔记】在虚拟机中输入 yum makecache报错

    2024-07-13 12:12:02       26 阅读
  14. 如何使用键盘优雅地使用浏览器

    2024-07-13 12:12:02       29 阅读
  15. C++ 开源库

    2024-07-13 12:12:02       28 阅读