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

本篇内容就到这里啦

相关文章
|
2月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
73 2
|
2月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
161 73
|
2月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
95 3
|
3月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
3月前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
3月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
132 1
|
3月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
3月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
25天前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
53 12
|
2月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
58 16