目录
本文测试环境为 编译器 gcc 13.1
vector 是 STL 中的一个顺序容器,它给我们提供了一个动态数组,那它的动态扩充和收缩到底是如何实现的呢?
1. vector 对象
我们在 main 函数中使用默认构造函数创建一个 vector<int> vec; 对象
vector<int> vec;
这个对象的大小为 24 字节,3 个指针的大小
对象的内部表示如图所示
这是源码中的定义
struct _Vector_impl_data {
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;
// ...
}
这三个指针是什么含义后面再介绍
这三个指针变量本身是在栈上的,它们的值,也就是地址,是堆上的地址
也就是说,vec 对象在栈上,它通过这三个指针维护了堆上的一块连续空间,来实现一个连续、动态的数组
2. vector 大小 size 和 容量 capacity
想要了解 vec 的底层模型就必须先了解这两个概念
- size:是指数组元素的个数
- capacity:是指当前可用空间能够容纳的元素的个数
上面默认构造的 vec 对象的大小和容量都为 0
现在我们再看一下 vec 对象中的三个指针的含义,分别为:
- start:指向第一个元素
- finish:指向最后一个元素的下一个位置
- end_of_storage:指向当前可用空间的下一个位置
现在我们改变一下 vec 对象的大小和容量
vec.reserve(4);
vec.resize(2);
现在 vec 的大小是 2,容量是 4
看下面的图就清楚大小和容量了
上图所示的空间是堆上的空间,vec 容器所管理的元素是在堆上的
start 和 finish 之间就是我们口头说的数组
start 和 end_of_storage 之间则是当前可用的空间,一共有 4 个容量,我们已经使用了 2 个元素了,还能再使用 2 个,如果使用完了,再向这个 vec 对象添加元素,就需要进行动态扩充了
3. vector 成员函数
通过上面两张图,相信大家已经大概了解 vector 在内存中的模型了
vec 对象通过这三个指针来完成动态数组的功能,下面来看看 vector 提供的成员方法是如何实现的吧
3.1 迭代器
我们首先搞懂 vector 的迭代器是怎么实现的
vector 迭代器是一个随机访问迭代器,需要提供 ++、- - 、+ i、- i、[ i ] 、比较等操作
这样的话完全可以使用指针来实现
1. __normal_iterator
在 vector 定义里,gcc 中是这样定义的
typedef __normal_iterator<pointer, vector> iterator;
typedef __normal_iterator<const_pointer, vector> const_iterator;
其中的 pointer 其实就是 T*
来看看这个 __normal_iterator
template<typename _Iterator, typename _Container>
class __normal_iterator
{
protected:
_Iterator _M_current;
// ...
}
内部就是一个 T* 指针
下面是部分它提供的接口
_GLIBCXX_CONSTEXPR __normal_iterator() _GLIBCXX_NOEXCEPT
: _M_current(_Iterator()) {
}
通过 T* 指针来构造
_GLIBCXX20_CONSTEXPR
reference
operator*() const _GLIBCXX_NOEXCEPT
{
return *_M_current; }
_GLIBCXX20_CONSTEXPR
pointer
operator->() const _GLIBCXX_NOEXCEPT
{
return _M_current; }
_GLIBCXX20_CONSTEXPR
reference
operator[](difference_type __n) const _GLIBCXX_NOEXCEPT
{
return _M_current[__n]; }
_GLIBCXX20_CONSTEXPR
__normal_iterator&
operator++() _GLIBCXX_NOEXCEPT
{
++_M_current;
return *this;
}
// ...
可以看到,其实就是对普通指针 T* 的封装
2. vector 迭代器
那么从 vector 中得到迭代器访问数组,其实就是得到 vector 中的 start 和 finish 指针了,这两个指针之间就是当前管理的元素
iterator begin()
{
return iterator(start); }
iterator end()
{
return iterator(finish); }
所以 end( ) 不是最后一个元素,而是最后元素的下一个
得到了 iterator 就可以像普通指针一样访问这个数组了
3.2 容量
通过上面的图,我们很容易想到 size( ) 和 capacity( ) 如何实现
size 只需要 start 和 finish 两个指针相减
size_t size() const
{
return size_t(finish - start); }
capacity 只需要 start 和 end_of_storage 两个指针相减
size_t capacity() const
{
return size_t(end_of_storage - start); }
empty( ) 用来检查 vector 是否为空,是指容器管理的元素个数是否为 0
bool empty() const
{
return start == finish; }
// 源码是这样,一个意思
_GLIBCXX_NODISCARD _GLIBCXX20_CONSTEXPR
bool
empty() const _GLIBCXX_NOEXCEPT
{
return begin() == end(); }
3.3 元素访问
可以通过下标和 at 函数来访问 vector 中的元素
reference operator[](size_t n)
{
return *(start + n); }
reference at(size_t n)
{
if (n >= this->size())
__throw_out_of_range_fmt(__N("vector::_M_range_check: __n "
"(which is %zu) >= this->size() "
"(which is %zu)"),
n, this->size());
return (*this)[n];
}
[ ] 和 at 区别就是 at 会进行越界检查,越界会抛出异常
可以使用 front 和 back 来访问第一个和最后一个元素
reference front()
{
__glibcxx_requires_nonempty();
return *begin();
}
reference back