C++ STL 容器 vector

本文详细解析了STL中vector容器的工作原理,包括vector对象的内存结构、大小和容量的概念、迭代器的实现、成员函数如插入、删除和调整容量的方法,以及迭代器失效问题的讨论。重点强调了vector的动态扩展和收缩机制以及如何有效利用reserve和移动优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文测试环境为 编译器 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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值