C++ STL -- vector

本质

本质上就是一个数组,存储区域在内存中连续,但相比于静态数组,其可以在运行时动态调整大小(push_back,pop),无需手动管理内存

动态调整 -- 内存管理

vector中有两个状态信息来维护内存管理:capacity和size。
capacity:表示当前分配到的内存空间大小
size:表示当前内部的元素数量
当size大于了capacity就需要重新分配

重新分配

当出现重新分配时,vector会被分配到一块新的内存空间,然后将原有数据复制到新的内存空间,然后对原有数据空间进行释放。这样保持了连续性。

动态扩容

vector采用了指数增长的策略进行动态扩容,即当需要扩容时,vector将容量翻倍增长。这种扩容方式不仅避免了频繁的内存分配,还确保了插入操作具有常数时间复杂度

简易实现

通过编写MyVector类实现类似std::vector的增、删、获取、遍历四种功能

成员变量

主要是一个数组指针、数组大小以及数组容量

    //动态数组头指针
    T* elements;
    //动态数组大小
    size_t size;
    //动态数组容量
    size_t capacity;

构造函数和析构函数

默认构造函数就是将头指针置为空,大小和容量置为0

MyVector():elements(nullptr),capacity(0),size(0){}

析构函数采用delete清除已经分配的内存

~MyVector() {delete[] elements;}

拷贝构造函数,通过复制传入的另一个类对象初始化改对象,首先为大小和容量进行赋值,然后利用new和std::copy对指针进行赋值

MyVector(const MyVector& v):size(v.size),capacity(v.capacity)
{
  elements = new T[capacity];
  std::copy(v.elements,v.elements+size,elements);
}

拷贝复制函数,通过重载=实现,需要先判断两者是否相等

MyVector &operator=(const MyVector& v)
{
  if(this != &v)
  {
    delete[] elements;
    size = v.size;
    capacity = v.capacity;
    elements = new T[capacity];
  std::copy(v.elements,v.gelements+size,elements);
  }
  return *this;
}

相关操作

reserve使用对数组的扩容

void reserve(size_t newcapacity)
{
  if(newcapacity > capacity)
  {
    T* newelements = new T[newcapacity];
    std::copy(elements,elements+size,newelements);
    delets[] elements;
    capacity = newcapacity;
    elements = newelemenys;
  }
}

push_back函数实现在数组尾部增加元素,判断大小是否超过容量

void push_back(const T& value)
{
  if(size >= capacity)
  {
    reserve(capacity == 0 ? 1 : 2*capacity);
  }
  elements[size++] = value;
}

通过重载[]运算符实现对下标的索引

T& operator[](int index)
{
  if(index >= size)
  {
    throw std::out_of_range("invalid Index: out of range");
  }
  return elements[index];
}

insert通过传入下标和数值实现元素的插入

void insert(int index, const T& value)
{
  if (index > size)
  {
    throw std::out_of_range("Index out of range");
  }
  else if(capacity == size) // 扩容
  {
    reserve(capacity == 0 ? 1:2*capacity);
  }
  //将index到末尾size的元素向后移动
  for(size_t i = size;i>index;i--)
  {
    elements[i] = elements[i-1];
  }
  elements[index] = value;
  size++;
}

begin()和end()是为了实现迭代器而设计的

    // 使用迭代器遍历数组的开始位置
T *begin()
{
  return elements;
}
  
// 使用迭代器遍历数组的结束位置
T *end()
{
  return elements + size;
}

pop_back()弹出数组末尾元素只需要将数组元素减一就可以实现了

void pop_back()
{
  size--;
}

vector相关

扩容过程

一般的扩容过程涉及到以下几个步骤

  1. 分配一个更大的内存块、通常是当前大小的两倍
  2. 将当前所有元素移到新分配的内存块中
  3. 销毁旧元素,并释放旧内存块
  4. 插入新元素

push_back 和 emplace_back

其实现效果都是向vector的末尾添加元素
push_back会对给定对象进行拷贝或者移动构造,将元素添加导末尾,即会先通过构造函数将value给构造出来,然后再调用拷贝构造函数,添加到末尾
emplace_back则使用给定的参数直接在vector末尾构造一个元素,无需拷贝或者移动构造,只需要在末尾调用构造函数构造即可

//例如对于vector<pair<int,int>> nums
//push_back
nums.push_back({a,b});
//emplace_back
nums.empalce_back(a,b);

减少vector的占用空间

在C++ 11中引入了shrink_to_fit来请求移除未使用的容量,会尝试将容量压缩至size,打不保证一定压缩

检查vector是否为空

使用empty()方法,empty()方法是常数时间,因为一般通过检测尾节点来实现判断
而size()方法通常是遍历整个数组获取长度

迭代器失效

如果对vector进行重新分配,那么其所有指向元素的迭代器都会失效
如果在vector中间插入元素,那么从该点到末尾的所有迭代器都会失效。
为了避免这些问题,最好使用remove() remove_if()方法搭配erase()方法来删除元素

如果vector的元素类型为指针,需要考虑什么问题

  1. 内存管理:在清除vector的头指针之前,要保证每个元素分配的内存空间被清理掉
  2. 所有权和生命周期:在对vector元素访问期间,要保证这些指针指向的对象是有效的。同时需要清楚的定义谁拥有这些对象和修改时机
  3. 异常控制:当创建和填充vector时需要需要一个机制来处理已经分配的内存,避免内存泄漏
  4. 智能指针:一般建议使用智能指针(unique_ptr或则shared_ptr)来当作指针类型,vector被销毁后,会自动销毁
  5. 避免悬垂指针:当指向的对象失效时,要确保没有悬垂指针指向无效的内存地址,同样vector被重新分配后,这些指针也会失效
  6. 深拷贝和浅拷贝:如果是指针类型,在复制vector时,需要考虑是复制指针+对象还是只复制指针

作者:XTG111

出处:https://www.cnblogs.com/XTG111/p/18145137

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   XTG111  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示