【C++】详解STL容器之一的 vector

简介: 【C++】详解STL容器之一的 vector

概述

vector是STL的容器之一。vector的底层结构类似于数组——在内存中开辟一块连续的空间。与数组不同的是vector可以动态的改变空间的大小(扩容或缩容)。

vector一般不会缩容,而是会经常的扩容——扩容的大小总比用户需要的多,这和vector的扩容机制有关。

vector不支持原地扩容,会新开辟一块更大的空间。将旧空间的值浅拷贝给新空间,然后释放旧空间。vector的空间的动态改变是有代价的,尤其是数据量特别大时,不会轻易缩容。

不同平台的扩容原则是一样的,但扩容的细节并不相同。VS下是根据旧空间的1.5倍扩容,g++是根据旧空间的2倍扩容


迭代器

vector的迭代器不需要像list的那样把普通指针进行封装,

因为vector维护的是一段线性空间,它的底层是一块连续的空间。普通的指针刚好能完成数据的随机访问,空间的遍历,数据的存取等。下面是迭代器的源码定义。

templat <class T, class Alloc = alloc>
class vector
{
//......
protected:
iterator start;
iterator finish;
iterator end_of_storage;
//......
}

T*就是vector的迭代器。如果vector存的是int类型的数据,vector的迭代器就是int*类型的指针。存的如果是string类(自定义类型)类型的数据,vector的迭代器就是string*类型的指针。

迭代器失效

扩容会引发迭代器失效。首先获取了一个空间的迭代器,然后这个空间发生了扩容,此时这个迭代器是指向旧空间的,旧空间会被归还给操作系统。这种情景下的迭代器失效可以理解为野指针问题。VS环境下会强制报错。如下图

数据结构

vector管理线性空间用了三个迭代器,如下是源代码定义

templat <class T, class Alloc = alloc>
class vector
{
//......
protected:
iterator start;
iterator finish;
iterator end_of_storage;
//......
}

start——指向空间的开头

finish——指向有效数据的下一个位置

end_of_storage——指向有效空间的下一个位置


优点和缺点

优点支持随机访问,排序消耗的时间复杂度比其他容器低

有如下代码,验证三大容器vector, list, deque,在相同数据量,相同数据,数据的顺序也相同的情况下,用vector容器的排序有多大优势。在release环境下验证

#include<vector>
#include<list>
#include<deque>
#include<algorithm>
#include<time.h>
#include<iostream>
 
void Test()
{
//数据量
  int N = 100000;//十万
  //int N = 1000000;//百万
  //int N = 10000000;//千万
  //int N = 100000000;//一亿  
  
 
  std::vector<int> v; //三大容器
  std::list<int> l;
  std::deque<int>d;
 
  for (int i = N; i > 0; i--) //插入相同的数据,数据顺序也相同
  {
    int e = rand();    
    v.push_back(e);
    l.push_back(e);
    d.push_back(e);
  };
 
  clock_t begin1 = clock(); //时间函数
  sort(v.begin(), v.end()); 
  clock_t end1 = clock(); 
 
  clock_t begin2 = clock(); 
  l.sort();
  clock_t end2 = clock(); 
 
  clock_t begin3 = clock(); 
  sort(d.begin(), d.end());    
  clock_t end3 = clock(); 
 
  printf("vector的用时是%d毫秒\n", end1 - begin1); 
  printf("list的用时是%d毫秒\n", end2 - begin2); 
  printf("deque的用时是%d毫秒\n", end3 - begin3);
 
  
}
 
int main()
{
  Test();
  return 0;
}

结果展示

即使排了一亿个数据,快排加vector容器也只用了4秒。list的底层是链表用了将近两分钟,效率低下。deque是一个类似于vector和list的结合体的容器,用了20秒。vector最引以为豪的优势——排序的效率高。原因在于vector的底层是连续的空间,支持随机访问,只需O(1)复杂度便可访问任意位置的数据。

在空间足够的情况下,尾部插入,尾部删除的效率为O(1)

缺点头部插入,头部删除,随机位置插入,随机位置删除,效率为O(N)。原因很简单:这些操作都需要挪动数据。如果数据量大,并且要频繁的头插头删,这便是堪比一个O(N^2)的算法。在标准库的接口中,并没有直接给头插,头删的接口。


接口介绍

begin

获取第一个数据位置的迭代器或const迭代器

end

获取最后一个有效数据的下一个位置的迭代器或const迭代器

rbegin

获取最后一个有效数据的迭代器或const迭代器

rend

获取第一个有效数据的前一个位置的迭代器

resize

改变vector容器有效数据的个数,改变为n个数据。如果n小于有效空间,删数据。n大于有效空间,扩容,并把多余的有效数据初始化为val

reseve

改变容器的有效空间。n小于有效空间时,不做处理。n大于有效空间时,把空间开至n或更大。

insert

在迭代器position之前插入val
在迭代器position之前插入n个val
在迭代器position之前插入迭代器区间first到last的元素

erase

删除迭代器position位置的元素,或删除迭代器区间first到last的元素

其他一些接口

size

获取数据个数
capacity 获取容量大小
empty 判断是否为空

push_back

尾插
pop_back 尾删

operator[] 

 像数组一样访问

模拟实现

框架

namespace bit
{
  template<class T>
  class vector
  {
  public:
    typedef T* iterator;
    typedef const T* const_iterator;
private:
    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _endofstorage = nullptr;
  };
}

获取迭代器

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

深浅拷贝

过vector容器存的是自定义类型,它们的数据可能会指向某一块空间。当我们想要新的vector容器拷贝旧的vector容器数据时,新的vector容器是否要额外开空间储存自定义类型指向的空间的数据,便涉及深浅拷贝问题。

如下示意图

上文已经提到扩容时是浅拷贝,当 vector 需要扩容时,它会分配一个新的更大的内存块,然后将原来的元素拷贝到新的内存中,并释放原来的内存。这确保了在扩容时,原有元素的地址不会改变,从而避免了深拷贝的开销

而在拷贝构造函数和赋值运算符中,vector 会执行深拷贝,即它会复制其中的每个元素,而不是简单地复制指向内存的指针。这样,每个vector 对象都有自己独立的内存存储其元素,互不影响,也避免了浅拷贝可能带来的问题。

有了上述了解,模拟实现一下赋值重载

赋值重载

现在写法

void swap(vector<T>& v)
    {
      std::swap(_start, v._start);
      std::swap(_finish, v._finish);
      std::swap(_endofstorage, v._endofstorage);
    }
 
  
    vector<T>& operator=(vector<T> v)
    {
      swap(v);
 
      return *this;
    }

reseve

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

resize

void resize(size_t n, const T& val = T())
    {
      if (n < size())
      {
        _finish = _start + n;
      }
      else
      {
        reserve(n);
 
        while (_finish != _start + n)
        {
          *_finish = val;
          ++_finish;
        }
      }
    }

拷贝构造

vector(const vector<T>& v)
    {
      _start = new T[v.capacity()];
      //memcpy(_start, v._start, sizeof(T)*v.size());
      for (size_t i = 0; i < v.size(); i++)
      {
        _start[i] = v._start[i]; //赋值重载
      }
 
      _finish = _start + v.size();
      _endofstorage = _start + v.capacity();
    }

构造

vector(size_t n, const T& val = T())
    {
      resize(n, val);
    }

析构

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

insert

iterator insert(iterator pos, const T& x)
    {
      assert(pos >= _start && pos <= _finish);
 
      if (_finish == _endofstorage)
      {
        size_t len = pos - _start;
 
        size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newcapacity);
 
        // 解决pos迭代器失效问题
        pos = _start + len;
      }
 
      iterator end = _finish - 1;
      while (end >= pos)
      {
        *(end + 1) = *end;
        --end;
      }
 
      *pos = x;
      ++_finish;
 
      return pos;
    }

erase

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

其他

void push_back(const T& x)
    {
      insert(end(), x);
    }
 
    void pop_back()
    {
      erase(--end());
    }
 
    size_t capacity() const
    {
      return _endofstorage - _start;
    }
 
    size_t size() const
    {
      return _finish - _start;
    }
 
    T& operator[](size_t pos)
    {
      assert(pos < size());
 
      return _start[pos];
    }
 
    const T& operator[](size_t pos) const
    {
      assert(pos < size());
 
      return _start[pos];
    }

本篇内容就到这里啦

相关文章
|
1月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
69 5
|
1月前
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
21 1
|
1月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
52 1
|
1月前
|
编译器 C++
【C++】—— vector模拟实现
【C++】—— vector模拟实现
|
1月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
23 0
|
1月前
|
算法 C++ 容器
C++之打造my vector篇(下)
C++之打造my vector篇(下)
27 0
|
1月前
|
存储 编译器 C++
C++之打造my vector篇(上)
C++之打造my vector篇(上)
28 0
|
1月前
|
算法 C++ 容器
【C++】—— vector使用
【C++】—— vector使用
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
39 4
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
下一篇
无影云桌面