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相关
扩容过程
一般的扩容过程涉及到以下几个步骤
- 分配一个更大的内存块、通常是当前大小的两倍
- 将当前所有元素移到新分配的内存块中
- 销毁旧元素,并释放旧内存块
- 插入新元素
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的元素类型为指针,需要考虑什么问题
- 内存管理:在清除vector的头指针之前,要保证每个元素分配的内存空间被清理掉
- 所有权和生命周期:在对vector元素访问期间,要保证这些指针指向的对象是有效的。同时需要清楚的定义谁拥有这些对象和修改时机
- 异常控制:当创建和填充vector时需要需要一个机制来处理已经分配的内存,避免内存泄漏
- 智能指针:一般建议使用智能指针(unique_ptr或则shared_ptr)来当作指针类型,vector被销毁后,会自动销毁
- 避免悬垂指针:当指向的对象失效时,要确保没有悬垂指针指向无效的内存地址,同样vector被重新分配后,这些指针也会失效
- 深拷贝和浅拷贝:如果是指针类型,在复制vector时,需要考虑是复制指针+对象还是只复制指针
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!