【C++STL】模拟实现vector容器

简介: 【C++STL】模拟实现vector容器

前言

本文带你进入vector的模拟实现,对于vector,是我们深入学习STL的必要途径。


一、vector的成员函数

根据库的实现方式,成员函数如下:

iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;

c++11开始可以对成员变量使用缺省值,在这里我们可以使用缺省值。

二、增删查改工作

说明size()和capapcity()

size的大小为_finish - _start

capacity的大小为_end_of_storage - _start

2.1reserve()

该函数的作用是:扩容。

void reserve (size_type n);
  • 思路:
  • 1.如果要扩容的大小n小于capacity,则不会缩容。
  • 2.否则执行扩容。
  • 先申请一块n大小的空间
  • 拷贝原空间的数据到新空间,并释放原空间
  • 将新空间的数据交给_start管理。
//扩容
void reserve(size_t n)
{
  //防止有些情况是直接调用
  if (n > capacity())
  {
    size_t oldsize = size();//必须要记录旧的size,旧的size是用旧的_finish - _start获得的
    iterator tmp = new T[n];
    //memcpy(tmp, _start, sizeof(T) * size());
    //这里如果使用memcpy函数的话,如果vector对象是自定义类型,比如string,会造成浅拷贝问题。
    for (size_t i = 0; i < oldsize;i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
    {
      tmp[i] = _start[i];
    }
    delete[] _start;
    _start = tmp;
    //_finish = _start + size();
    //如果这样用的话,size()是用旧的_finish - 新的_start,不是连续的空间,没有意义
    _finish = _start + oldsize;//_finish必须更新,旧的_finish迭代器已经失效了
    _end_of_storage = _start + n;
  }
  //tmp不用处理,出了作用域自动调用析构
}

注意:在扩容时,如果是自定义类型,比如string,使用memcpy函数进行拷贝数据,会出现浅拷贝问题。

v1会拷贝给v2,v2的指针仍然指向v1的string对象所指向的空间。这就造成了两次调用析构函数释放同一块空间的问题。

正确的解法是:

在拷贝时调用string的赋值运算符重载,完成深拷贝。

还有一个点是:

注意:在这里会出现迭代器失效的问题。

此时空间已满,则需要重新申请空间。

重新申请空间后,_start和_end_of_stortage都会指向新的空间,唯独_finish还指向旧的已经被释放的空间,此时如果这样更新_finish位置:

_finish = _start + size();

这里的size()是用旧的_finish - 新的_start得到,是没有意义的。

解决方案:

保存旧的size,随后_start加上旧的size即为_finish的位置。

2.2 resize()

该函数的功能是:重新改变vector容器的大小,让其到达n。新的空间默认初始化为val

思路:

  • 1.如果n小于size,则让_finish指向_start + n位置。
  • 2.如果n大于等于size,先调用reserve函数进行扩容,再将新的空间填充为val。
void resize(size_t n, const T& val = T())
{
  if (n < size())
  {
    _finish = _start + n;
  }
  else
  {
    //扩容到n,如果n<capacity(),则啥事不做,否则,扩容到n
    reserve(n);
    //将多的空间全部设置成缺省值
    while (_finish != _start + n)
    {
      *_finish = val;
      ++_finish;
    }
  }
}

注意这里的缺省值为T(),是一个T类型的匿名对象。

不管T是什么,都可以使用模板实例化出具体的匿名对象,即使是内置类型,比如int,也可以实例化出int(),这里的值为0

2.3 insert()

在这里我们只实现最常用的接口。

该函数的作用是:

在pos位置插入模板对象val。

思路:

  • 插入元素前先检查容量
  • 如果容量满了,则调用reserve函数执行扩容操作
  • 然后从pos位置开始将其之后的元素全部都往后挪。
  • 再在pos位置插入val。
void insert(iterator pos, const T& val)
{
  assert(pos <= _finish);
  //插入前先检查扩容
  if (_finish == _end_of_storage)
  {
    size_t oldpos = pos - _start;//记录相对位置
    size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
    reserve(newcapacity);
    pos = _start + oldpos;
  }
  //记录旧的size,防止出现迭代器失效问题。
  //将pos之后的位置往后挪,再插入
  iterator end = _finish - 1; // reserve之后,_finish也跟着更新了。
  while (end >= pos)
  {
    *(end + 1) = *end;
    --end;
  }
  *pos = val;
  _finish += 1;
}

注意:insert在扩容时仍然会出现迭代器失效的问题。

解决方案:提前记录pos的相对位置,即oldpos = pos - _start;

然后再扩容完成后,更新pos 的位置,pos = _start + oldpos;

否则在扩容后,旧的pos迭代器就已经失效了。

2.4 erase()

该函数的功能是:删除pos位置的值。

//在删除最后一个位置或者只剩下一个待删除元素时,会出现迭代器失效问题。
iterator erase(iterator pos)
{
  assert(pos < _finish);
  iterator end = pos;
  while (end != _finish)
  {
    *end = *(end + 1);
    ++end;
  }
  --_finish;
  return pos;
}

有一个需要注意的点:当我们删除元素后,pos迭代器就会失效,因为空间已经释放,这时候不能使用pos迭代器。

解决方法:返回删除位置的下一个元素的位置,即pos位置。

然而,在只剩下一个待删除元素或者删除最后一个位置的元素时,不能再使用pos迭代器。

2.5 push_back()和pop_back()

顾名思义,这两个函数的功能是在最后一个位置插入元素和删除最后一个位置的操作。

直接复用insert和erase函数即可,这里不再赘述。

三、[]重载和迭代器

typedef T* iterator;
typedef const T* const_iterator;

3.1begin迭代器

返回_start地址即可。

iterator begin()
{
  return _start;
}
const_iterator begin() const
{
  return _start;
}

3.2 end迭代器

返回_finish地址即可。

iterator end()
{
  return _finish;
}
const_iterator end() const
{
  return _finish;
}

3.3 []运算符重载

思路:只需给定pos下标即可。
在此之前需要判断pos位置不能超过整个顺序表的size大小。

T& operator[]( size_t pos)
{
  assert(pos < size());
  return *(_start + pos);
}
//不可修改的
const T& operator[]( size_t pos) const
{
  assert(pos < size());
  return *(_start + pos);
}

四、默认成员函数

4.1构造函数

构造函数可分为3种:

  • 无参构造
  • 构造n个val对象
  • 根据迭代区间构造

对于无参的构造,我们只需要初始化成员函数即可。

  • 1.无参构造
//无参构造函数
vector()
//:_start(nullptr)
//,_finish(nullptr)
//,_end_of_storage(nullptr)
{}
  • 2.构造n个val对象
//构造n个对象
vector(size_t n, const T& val = T())
  :_start(nullptr)
  , _finish(nullptr)
  , _end_of_storage(nullptr)
{
  //1.开空间
  reserve(n);
  //2.将空间填充为val对象
  for (size_t i = 0; i < capacity(); i++)
  {
    _start[i] = val;
  }
  //也可以复用resize
  //resize(n, val);
}

注意:这里在构造时必须初始化,如果不初始化,在调用reserve函数开空间时会出现访问野指针的问题。

也可以调用resize函数进行拷贝构造n个对象。

  • 3.使用迭代区间进行构造

同理,如果不初始化,会出现扩容后访问野指针的情况。

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

4.2 拷贝构造函数

  • 1.拷贝构造传统写法
//拷贝构造,必须实现深拷贝
//传统写法
//v1(v)
vector(const vector<T>& v)
  //这里必须初始化,否则扩容时会访问不属于自己的空间
  :_start(nullptr)
  ,_finish(nullptr)
  ,_end_of_storage(nullptr)
  //如果私有成员给了缺省值,就不用再再次初始化了
{
  reserve(v.capacity());
  //memcpy(_start, v._start, sizeof(T) * v.size());
  //这里使用memcpy的错误跟扩容的错误一样,不再赘述
  for (size_t i = 0; i < v.size(); i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
  {
    _start[i] = v._start[i];
  }
  _finish = _start + v.size();
}

拷贝构造需要注意的几个问题:

  • 1.拷贝构造同构造函数一样,如果不初始化,在开空间时会出现访问野指针的情况。
  • 2.拷贝构造必须进行深拷贝,否则如果vector的对象是自定义类型,比如string,会出现两次释放同一块空间的问题。

4.3 析构函数

释放_start指向的空间,然后全部置为空即可。

~vector()
{
  if (_start)
  {
    delete[] _start;
    _start = _finish = _end_of_storage;
  }
}

总结

vector的模拟实现就到此结束了,如果对你有帮助的话,不妨来一个关注吧!

相关文章
|
24天前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
55 21
|
2月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
42 1
|
2月前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
69 7
|
3月前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
135 4
|
3月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
140 5
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
89 2
|
3月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
100 0
|
2月前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
67 0
|
3月前
|
存储 设计模式 C++
【C++】优先级队列(容器适配器)
本文介绍了C++ STL中的线性容器及其适配器,包括栈、队列和优先队列的设计与实现。详细解析了`deque`的特点和存储结构,以及如何利用`deque`实现栈、队列和优先队列。通过自定义命名空间和类模板,展示了如何模拟实现这些容器适配器,重点讲解了优先队列的内部机制,如堆的构建与维护方法。
60 0
|
4月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
98 5