静态数组是由相同类型的元素线性排列的数据结构,在计算机上会分配一段连续的内存,对元素进行顺序存储。
其中有三个关键词,相同类型、连续内存、顺序存储。之所以这样设计,本质就是为了能做到基于下标,对数组进行 O(1) 时间复杂度的快速随机访问。
存储数组时,会事先分配一段连续的内存空间,将数组元素依次存入内存。因为数组元素的类型都是一样的,所以每个元素占用的空间大小也是一样的,这样我们就很容易用“数组的开始地址 +index* 元素大小”的计算方式,快速定位到指定索引位置的元素,这也是数组基于下标随机访问的复杂度为 O(1) 的原因。
为什么要事先分配一段内存呢?答案也很简单,因为内存空间并不是无限的。一段程序里可能有很多地方都需要分配内存,我们必然要为分配的连续内存寻找一个边界。
STL 扩容逻辑的实现
void push_back(const _Tp& __x) {//在最尾端插入元素
if (_M_finish != _M_end_of_storage) {//若有可用的内存空间
construct(_M_finish, __x);//构造对象
++_M_finish;
}
else//若没有可用的内存空间,调用以下函数,把x插入到指定位置
_M_insert_aux(end(), __x);
}
template <class _Tp, class _Alloc>
void
vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
if (_M_finish != _M_end_of_storage) {
construct(_M_finish, *(_M_finish - 1));
++_M_finish;
_Tp __x_copy = __x;
copy_backward(__position, _M_finish - 2, _M_finish - 1);
*__position = __x_copy;
}
else {
const size_type __old_size = size();
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
iterator __new_start = _M_allocate(__len);
iterator __new_finish = __new_start;
__STL_TRY {
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
construct(__new_finish, __x);
++__new_finish;
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
}
__STL_UNWIND((destroy(__new_start,__new_finish),
_M_deallocate(__new_start,__len)));
destroy(begin(), end());
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
}
}
-
push_back:
- 这个函数旨在在向量的末尾插入一个元素。
- 它首先检查是否有可用的内存空间(
_M_finish != _M_end_of_storage)。如果有,它会在末尾(_M_finish)构建元素,然后增加_M_finish。 - 如果没有剩余空间,它会调用
_M_insert_aux函数。
-
_M_insert_aux:
-
这是一个辅助函数,设计用于处理没有空间时的插入。
-
首先,它检查是否有可用的内存空间。如果有,它会在末尾构建最后一个元素(相当于复制),然后将所有元素向右移动(从您想要插入的位置到倒数第二个元素)。最后,元素
__x被复制到所需位置。 -
如果没有空间,它会进行以下操作:
- 将向量的大小加倍(除非当前大小为0,在这种情况下,它只是将新大小设置为1)。
- 为这个新大小分配内存。
- 将元素从旧存储复制到新存储,直到所需位置。
- 插入新元素。
- 复制其余的元素。
- 在上述操作期间发生异常(由
__STL_TRY和__STL_UNWIND宏表示),它会清理新存储并释放内存。 - 如果一切正常,它会销毁旧存储,释放旧内存,并更新向量的起始、结束和存储末尾指针。
-
数组,是支持 O(1) 基于下标随机访问的数据结构,在内存中是连续存储的。基于下标高效访问元素的核心就在于“相同类型”和“连续存储”的特性,当然,也带来了高昂的插入和删除的时间复杂度。动态数组之所以能看起来像是无限容量,也仅仅是因为它内置了倍增的扩容策略,每次数组大小超过容量的时候,就会触发数组的扩容机制,封装了繁琐的拷贝细节。