在 C++ 标准模板库(STL)的众多组件中,vector无疑是最常用且功能强大的数据结构之一。它就像一个智能的动态数组,既能像普通数组一样高效地随机访问元素,又能根据数据量自动调整大小,解决了传统数组固定大小的局限性。那么,vector是如何实现这些强大功能的呢?答案就藏在其底层基于数组的实现方式中。今天,我们就一同深入探索,如何用数组来构建vector的核心功能。
一、数组与 vector:一脉相承的 “进化关系”
在开始构建vector之前,我们先来回顾一下数组。数组是一种线性数据结构,它在内存中占据一段连续的空间,就像一排整齐排列的储物格,每个储物格都有自己固定的编号(即数组的下标)。通过下标,我们可以快速地访问数组中的任意元素,这种随机访问的特性使得数组在数据读取和处理上具有很高的效率。
然而,传统数组有一个明显的缺陷 —— 它的大小在创建时就固定了。一旦创建完成,我们无法轻易改变它的容量。这就好比一个固定大小的储物间,如果物品数量超出了储物间的容量,我们就会陷入困境。而vector正是为了解决这个问题应运而生,它基于数组进行扩展和优化,在保留数组随机访问优势的同时,实现了动态调整大小的功能,堪称数组的 “进化版”。
二、动态数组:vector 的核心存储结构
vector的核心是一个动态数组。所谓动态数组,就是它能够根据数据的插入和删除,自动调整自身的大小,以适应数据量的变化。为了实现这一功能,vector需要在底层维护几个关键信息:
(一)数据存储区
这是vector存放实际数据的地方,本质上就是一个普通的数组。它就像vector的 “仓库”,所有的元素都被有序地存放在这个 “仓库” 的各个 “格子” 里。当我们向vector中插入元素时,数据就会被放入这个存储区;当我们访问元素时,也是从这里获取数据。
(二)元素个数
vector需要记录当前已经存储了多少个元素,这个信息就像是 “仓库管理员” 手中的账本,记录着每个时刻 “仓库” 里物品的数量。通过这个记录,我们可以知道vector当前的实际使用规模,也能在进行插入、删除等操作时,判断是否需要调整数组的大小。
(三)容量大小
容量表示vector当前能够容纳的最大元素数量,它是 “仓库” 当前的最大容量上限。当我们不断向vector中插入元素,一旦元素个数达到了容量大小,vector就需要进行扩容操作,重新分配一块更大的内存空间,将原有的数据复制到新空间中,以满足更多数据存储的需求。
动态内存分配策略:当容量不足时,vector会分配一块新的内存空间,将原有元素复制到新的内存中,然后释放原内存,这种策略确保了元素在内存中的连续性,但也带来了插入操作的额外开销。
为了确保平均时间下的插入操作具有常量时间复杂度,而不是线性复杂度。Vector使用指数翻倍策略扩容,每次扩容容量变成原始容量两倍
具体vector使用方法可以参考:
三、手写MyVector实现std::vector核心功能
#include <iostream>
template<typename T>
class MyVector
{
private:
T* elements_;
size_t size_;
size_t capacity_;
public:
//基本构造函数
MyVector(): elements_(nullptr), size_ (0), capacity_(0) {}
//析构函数,删除elements_释放空间
~MyVector()
{
delete[] elements_;
}
//拷贝构造(避免浅拷贝)
MyVector(const MyVector &other): capacity_(other.capacity_), size_(other.size_)
{
elements_ = new T[capacity_];
std::copy(other.elements_, other.elements_ + size_, elements_);
}
//迭代器构造方法
MyVector(const T* begin, const T* end): capacity_(end - begin), size_(end - begin)
{
elements_ = new T[capacity_];
for(size_t i = 0; i < end - begin; i++)
{
elements_[i] = *(begin+i);
}
}
//(元素个数,元素值)构造方法
MyVector(int n, const T& value = T()):capacity_(n), size_(n)
{
elements_ = new T[capacity_];
for (size_t i = 0; i < n; i++)
{
elements_[i] = value;
}
}
//扩容方法,为保证元素内存连续,将原始数组的元素全部复制到新数组,最后释放原始数组空间
//这种策略保证容器元素的内存连续特性,从而支持O(1)的下标访问,但带来了插入操作效率相对较低的问题
void reserve(size_t newcapacity)
{
if(newcapacity > capacity_)
{
T* newelements = new T[newcapacity];
std::copy(elements_,elements_+size_,newelements);
delete[] elements_;
elements_ = newelements;
capacity_ = newcapacity;
}
}
//移除未使用的容量,减少内存使用。(c++11引入,但在std::vector中这只是一个请求,并不保证容量会减少)
void shrink_to_fit()
{
T* newelements = new T[size_];
std::copy(elements_,elements_+size_,newelements);
delete[] elements_;
elements_ = newelements;
capacity_ = size_;
}
//重载=操作符,支持拷贝赋值操作
MyVector &operator=(const MyVector& other)
{
if(this != &other)
{
delete[] elements_;
capacity_ = other.capacity_;
size_ = other.size_;
elements_ = new T[capacity_];
std::copy(other.elements_, other.elements_ + size_, elements_);
}
return *this;
}
//下标访问支持
T &operator[](const size_t idx)
{
if(idx >= size_)
{
throw std::out_of_range("Index out of range!\n");
}
return elements_[idx];
}
//末尾添加元素
void push_back(const T &value)
{
//动态扩容,每次size和capacity相等后容器容量翻倍,这种策略确保了平均情况下的插入操作具有常数时间复杂度,而不是线性时间复杂度。
//但是这种策略带来了空间冗余(capacity > size)
if(size_ == capacity_)
{
reserve(capacity_ == 0 ? 1: 2 * capacity_);
}
elements_[size_++] = value;
}
//删除末尾元素
void pop_back()
{
if(size_ > 0)
{
size_--;
}
}
//向idx位置插入新元素(根据索引删除操作也要注意保证内存连续,删掉之后索引后面的元素前移,这里不实现了)
void insert(size_t idx, const T& value)
{
if(idx >= size_)
{
throw std::out_of_range("Index out of range!\n");
}
if(size_ == capacity_)
{
reserve(capacity_ == 0 ? 1: 2 * capacity_);
}
for(size_t i = size_; i > idx; i--)
{
elements_[i] = elements_[i - 1];
}
elements_[idx] = value;
size_++;
}
//清空容器(不是释放空间)
void clear()
{
size_ = 0;
}
//迭代器
T* begin()
{
return elements_;
}
T* end()
{
return elements_ + size_;
}
size_t Size() const
{
return size_;
}
size_t Capacity() const
{
return capacity_;
}
bool empty()
{
return size_ == 0 ? true : false;
}
};
以上代码实现了std::vector的核心功能,除了Size,Capacity两个接口意外都与std::vector保持一致。
下面来测试这个类
int main()
{
//基本构造测试
MyVector<int> vec_1;
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
//push_back测试和动态扩容测试
vec_1.push_back(10);
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
vec_1.push_back(20);
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
vec_1.push_back(30);
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
//删除冗余空间测试
vec_1.shrink_to_fit();
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
//插入测试
vec_1.insert(1,15);
std::cout << "capacity: " << vec_1.Capacity() << std::endl;
//不同遍历方法测试
std::cout << "============================" << std::endl;
for(int i = 0; i < vec_1.Size(); i++)
{
std::cout << vec_1[i] << std::endl;
}
std::cout << "============================" << std::endl;
for(auto i : vec_1)
{
std::cout << i << std::endl;
}
std::cout << "============================" << std::endl;
for(auto i = vec_1.begin() ; i < vec_1.end(); i++)
{
std::cout << *i << std::endl;
}
std::cout << "============pop_back================" << std::endl;
//pop_back测试
vec_1.pop_back();
for(int i = 0; i < vec_1.Size(); i++)
{
std::cout << vec_1[i] << std::endl;
}
std::cout << "==============vec_2==============" << std::endl;
//不同构造方法测试
MyVector<int> vec_2(3,6);
for(int i = 0; i < vec_2.Size(); i++)
{
std::cout << vec_2[i] << std::endl;
}
std::cout << "==============vec_3==============" << std::endl;
//拷贝构造
MyVector<int> vec_3(vec_2);
for(int i = 0; i < vec_3.Size(); i++)
{
std::cout << vec_3[i] << std::endl;
}
std::cout << "==============vec_4==============" << std::endl;
//迭代器构造
MyVector<int> vec_4(vec_2.begin(),vec_2.end());
for(int i = 0; i < vec_4.Size(); i++)
{
std::cout << vec_4[i] << std::endl;
}
return 0;
}
输出结果:
注意看插入过程中容量的变化,在构造完成后capacity=0,插入第一个元素后capacity=1,接着插入capacity=2,下一次再扩容的时候capacity变成了4,而不是3,扩容过程是翻倍的,不是线性的。