跟我学C++中级篇——STL的容器Deque

本文介绍了STL中的双端队列deque,探讨了其内部实现原理,包括分段连续存储及迭代器重载,并通过示例展示了deque的基本用法。

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

 

一、顺序容器Deque

在使用vector时,发现它如果在头部进行操作的话,有点复杂,array也是如此。其实在实现一个标准的C数组时,使用的人都会发现,如果在尾部增加或者删除一个元素,其实都是很简单的。可是如果想在头部增加一个,那是真麻烦。STL库是啥?不就是为解决劳苦大众的福祉而诞生的么。没有,就创建一个嘛。deque就横空出世了,它可以解决头尾都需要操作的场景。
那它为啥能头尾都进行操作呢?做一个连续的空间?其实看deque源码就会发现,它其实并不是一个真正的像数组一样的整体的连续空间。而是分组或者说分段式的连续的空间。那么分段连续的空间怎么能让上层应用看起来像一个没分组的呢?那就需要设计一个map来管理他,通过重载迭代器的指针来实现连续的操作。看下图:

 

其实看到这儿,应该对STL中一定要拆成分配器、算法等的原因了吧。高度重用,快速扩展。

二、源码

老样子看一下它的代码:

template <class _Ty, class _Alloc = allocator<_Ty>>
class deque {
private:
    friend _Tidy_guard<deque>;
    static_assert(!_ENFORCE_MATCHING_ALLOCATORS || is_same_v<_Ty, typename _Alloc::value_type>,
        _MISMATCHED_ALLOCATOR_MESSAGE("deque<T, Allocator>", "T"));

    using _Alty           = _Rebind_alloc_t<_Alloc, _Ty>;
    using _Alty_traits    = allocator_traits<_Alty>;
    using _Alpty          = _Rebind_alloc_t<_Alloc, typename _Alty_traits::pointer>;
    using _Alpty_traits   = allocator_traits<_Alpty>;
    using _Mapptr         = typename _Alpty_traits::pointer;//=============map指针扩展
    using _Alproxy_ty     = _Rebind_alloc_t<_Alty, _Container_proxy>;
    using _Alproxy_traits = allocator_traits<_Alproxy_ty>;

    using _Scary_val = _Deque_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Deque_simple_types<_Ty>,
        _Deque_iter_types<_Ty, typename _Alty_traits::size_type, typename _Alty_traits::difference_type,
            typename _Alty_traits::pointer, typename _Alty_traits::const_pointer, _Ty&, const _Ty&, _Mapptr>>>;

    ......
    //C++11增加
    void shrink_to_fit() {
       size_type _Oldcapacity = _DEQUESIZ * _Mapsize();
       size_type _Newcapacity = _Oldcapacity / 2;

       if (_Newcapacity < _DEQUESIZ * _DEQUEMAPSIZ)
           _Newcapacity = _DEQUESIZ * _DEQUEMAPSIZ;

       if ((empty() && 0 < _Mapsize())
           || (!empty() && size() <= _Newcapacity && _Newcapacity < _Oldcapacity)) { // worth shrinking, do it
           deque _Tmp(_STD make_move_iterator(begin()), _STD make_move_iterator(end()));
           swap(_Tmp);
       }
   }

  ......
  using _Mybase::operator-;

  _NODISCARD _Deque_iterator operator-(const difference_type _Off) const {
      _Deque_iterator _Tmp = *this;
      return _Tmp -= _Off;
  }

  _NODISCARD reference operator[](const difference_type _Off) const {
      return const_cast<reference>(_Mybase::operator[](_Off));
  }
......
}

其实如果明白了讲过的组合的味道,基本就知道了这些源码的味道,不要看这么多的代码,其实有好多是显示的标志一些符号而做的,对于学习本身,用处不是很大。重点是掌握了整个源码的流程,这才是最重要的。
通过分析源码可以看出,deque的访问速度理论上讲要比vector要慢,毕竟有一个中间层的存在,获得便利的同时一定会有牺牲性能。也就是说,得到一些东西,就得舍弃一些东西,编程和实际生活一样,不可能事事完美,学会取舍,也就学会了设计的思想的入门。

三、例程

还是要看例子,毕竟重点是学习使用,而不是学习源码:

#include <deque>
#include <iostream>
void PrintDeque(const std::deque<int> & dq)
{
    std::cout << "cur deque print:" << std::endl;
    for (auto& d : dq)
    {
        std::cout << "item value:" << d << std::endl;
    }
    std::cout << "print end!" << std::endl;
}
void TestDeque()
{
    std::deque<int> d1;

    for (int num = 0; num < 10; num++)
    {
        d1.emplace_back(num);
        d1.emplace_front(10 - num);
    }

    PrintDeque(d1);
    std::deque<int>::iterator it;
    d1.erase(++d1.begin());
    int tmp = d1[9];

    std::cout << "tmp value is:" << tmp << std::endl;

    d1[9] = 1000;

    std::cout << "nine value is:" << d1[9] << std::endl;
    int f = 8;
    auto v = std::find(d1.begin(),d1.end(),f);
    std::cout << "find value:" << *v << std::endl;
    PrintDeque(d1);
}
int main()
{
    TestDeque();
    return 0;
}

运行结果是:

cur deque print:
item value:1
item value:2
item value:3
......
item value:8
item value:9
print end!
tmp value is:0
nine value is:1000
find value:8
cur deque print:
item value:1
item value:3
...
item value:10
item value:1000
...
item value:9
print end!

例程非常简单,这也是STL的目的,如果写得天花乱坠,估计更没人用了。

四、总结

std::deque单独使用还是比较少的,一般是使用它的适配器层的std::queue,这个会在适配器中进行学习。其实在于习一门技术或者具体到编程语言时,你会发现,语言的进步往往是实际应用推动的,设计时,可能觉得可有可无的,或者说干脆没有实现的,发现到实际应用中需求很大,不管出于什么原因,一般最后都会添加这个特性。这一点上,Java的模板就是一个很典型的例子。当然,包括Go语言,现在都是这样。c++/c语言自产生后,随着应用的不断的发展,对它们也提出了很多需求,以前的大佬们觉得无所谓,但是随着新语言的不断喷涌而出,大佬儿们发现如果不再进步,极有可能就被消灭,所以c++11,以及以后不断推出的版本更新,都是在拥抱变化。
你不变化,变化就淘汰你!

 

<think>嗯,用户问的是如何理解C++中的迭代器。首先,我得确定用户的基础知识水平,可能是个刚开始学C++的人,或者是已经有一定编程经验但想深入理解这个概念的人。需要从基础讲起,但也不能太啰嗦。 迭代器在C++里是个很重要的概念,特别是和STL容器一起用的时候。用户可能已经用过vector或者list,但不太清楚为什么需要迭代器,或者怎么正确使用它们。我应该先解释迭代器的作用,把它比作指针,这样用户容易理解。不过也要指出它们的不同之处,比如迭代器更安全,有些还支持更多操作。 然后,可能需要分点说明迭代器的类型,比如输入、输出、前向、双向、随机访问迭代器,每种类型的特点和常见容器对应的迭代器类型。比如vector是随机访问,list是双向,这样用户就知道为什么有些操作在vector里可以用,在list里不行。 还要提到迭代器的使用场景,比如遍历容器、算法中的应用,比如sort需要随机访问迭代器。用户可能遇到过算法不兼容的问题,这时候迭代器类型的概念就很重要了。 另外,常见的错误,比如迭代器失效的情况,特别是在修改容器时,比如在vector插入元素后,之前的迭代器可能失效。这个点需要强调,避免用户在实际编程中遇到问题。 可能还需要给出一些代码示例,比如用迭代器遍历vector,或者用advance、distance这些函数,但用户可能只是要概念性的解释,所以示例要简洁明了。或者用户可能想看到不同的迭代器类型对应的操作,比如双向迭代器只能++和--,而随机访问可以+5这样的操作。 最后,总结一下迭代器的优点,比如统一了不同容器的访问方式,增强了代码的通用性,还有和范围for循环的关系,这样用户能理解为什么C++11之后的循环可以这么方便。 还要注意使用中文,避免专业术语的直译,用更口语化的表达,但保持准确性。结构要清晰,分点分步骤,让用户容易跟上思路。可能用户需要的是实际应用中的理解,而不仅仅是理论,所以结合实例会更好。</think>理解C++迭代器可以从以下5个关键点逐步掌握: 1. **指针的进化版** - 迭代器本质是智能指针,专为容器设计 - 示例:`vector<int>::iterator it = vec.begin();` - 类似指针操作:`*it`取值,`it++`移动,`it != end()`判断终止 2. **容器与算法的桥梁** ```cpp // 统一不同容器的遍历方式 list<string> names{"Alice", "Bob"}; for(auto it = names.begin(); it != names.end(); ++it) { cout << *it << endl; } ``` - 同样的语法适用于vector/deque/set等容器 3. **能力分级(5种类型)** | 类型 | 能力 | 典型容器 | |-----------------|--------------------------|--------------| | 输入迭代器 | 只读,单次遍历 | istream | | 输出迭代器 | 只写,单次遍历 | ostream | | 前向迭代器 | 读写,多次前移 | forward_list | | 双向迭代器 | 支持双向移动 | list/map | | 随机访问迭代器 | 直接跳转(+n/-n运算) | vector/array | 4. **失效陷阱(重要!)** ```cpp vector<int> v{1,2,3}; auto it = v.begin(); v.push_back(4); // 可能导致迭代器失效! // cout << *it; // 危险操作! ``` - 修改容器容量后必须重新获取迭代器 5. **现代C++的简化用法** ```cpp // 范围for循环(C++11) for(const auto& num : vec) { // 自动处理迭代器 } // 反向迭代器 for(auto rit = vec.rbegin(); rit != vec.rend(); ++rit) { // 逆向遍历 } ``` **关键理解**:迭代器通过统一接口解耦了算法与容器,使`sort()`等算法能作用于不同数据结构。其设计体现了C++的抽象思维——用最小接口约定(如`++`、`*`等操作)实现最大灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值