C++STL学习第二讲中(从源代码角度讲解STL)

第二讲 从源代码角度讲解STL(中)

Sequence Containers 讲解:

5. 深度探索 list 容器

1. G2.9版的 list
  • 内容:
    • data:
      • 只有一个数据:list_node* node;
      • 由图中可以看出,node指向一个空白节点(尾部后,头部前)
    • typedef:各种声明 list_node、list_node* 等;
    • functions:各种函数 push_back() 等;
  • 大小:list 本身的 sizeof() 为4个字节;【32位系统,一个指针4个字节】
  • list 实质是一个双向环形链表
    • 为了符合STL容器左闭右开区间的原则,list 增加了一个空白 list_node,形成了环形结构;
    • list 的数据 node 就是指向这个空白 list_node;
  • list 扩容方式:
    • 由于 list 是离散空间分布,所以每次扩容一个,按需扩容;
2. G2.9版的 list_node
  • 内容:
    • data:由于 list 是环形链表,因此 list_node 除了数据部分,还需要有两个指针(prev、next):
    • 每次插入一个元素,都要额外花销两个指针的空间大小
  • 设计缺陷
    • list_node 两个指针指向的 object 一定是 list_node,所以 prev 和 next 类型应该是 list_node*;
    • 但G2.9设计成了 void*(万能指针),这样在使用时还需要转型,没有必要;(这在G4.9有进一步完善)
      在这里插入图片描述
3. G2.9版的 list_iterator
  • 内容:

    • typedef:iterator 必备的五个声明(图中的 (1)~(5) )+ 其他声明;
    • data:list_node* 类型的 node;
    • functions:各种模仿 pointer 操作的函数;
  • 大小:sizeof 为4个字节(一个指针);
    在这里插入图片描述

  • operator++ 操作的实现:

    • 对于 int,编译器允许连续前置++,但不允许连续后置++;(37图左下角)

      • 对于实现数值型的操作符重载,预期的行为应该对标 int 的操作符操作;
    • 前置++(postfix form):

      • T& operator++() {
        	...	// 实现对应的++操作
            return *this;
        }
        
      • 为了对标 int 可以连续前置++,函数返回reference

    • 后置++(prefix form):

      • (const) T operator++(int) {
            T tmp = *this;
        	++this;	// 调用前置++运算符
            return tmp;
        }
        
      • 后置++调用拷贝构造创建临时对象,调用前置++实现自增操作;

      • 为了对标int不能连续后置++,函数返回value

      • 个人感受:个人觉得如果函数只返回value,返回的临时对象一样还是可以继续执行后置++;为了阻止这种行为,应该返回 const value;

    • 为什么不让 i++++ 也合法呢?

      • i++是先返回i值再 +1,所以不可能返回i,那就只能先建立局部变量来保存 i 的初值,然后再返回局部变量**(局部变量不允许返回引用)**,但返回了局部变量之后,如果再连着进行下一次 ++ 运算,参与运算的就是这个局部变量的值了,所以此时 i++++ 其实等效与 i++,也就没有存在的意义了。
        在这里插入图片描述
4. G4.9版的 list
  • 改善部分:

    • list_iterator 模板参数只有一个(之前是三个);
    • list_node 的 prev、next 指针类型修改为 List_node*(之前是 void*);
      在这里插入图片描述
  • G4.9版的 list 整体结构:

    • 相比G2.9版的 list 结构(35图上方框),要复杂很多;
  • G4.9版的 sizeof 大小:

    • 在容器之间关系中,G2.9版的 list 大小为4个字节,只有一个 pointer 数据;
    • G4.9版的 list 大小为8个字节
      • list class 没有数据,所以 list 大小 == _List_base 大小;
      • _List_base 只包含一个_list_impl,所以 _List_base 大小 == _list_impl 大小;
      • _list_impl 只包含一个_list_node_base,所以 _list_impl 大小 == _list_node_base大小;
      • _list_node_base 有两个 pointer,所以有8个字节,所以 list 有8个字节;
    • G4.9版 list 也有空白节点:
      • 空白节点的内容,只有两个 pointer(刚好对应那8个字节),没有 data 部分;
        在这里插入图片描述

6. iterator

1. iterator 需要遵循的原则
  • iterator 必须提供5个 associated types(相关型别 / 相关类型):
    • iterator_category:iterator本身的类别;
    • difference_type:iterator之间距离的类型;
    • value_type:iterator所指内容的类型;
    • reference;iterator所指内容的引用类型;
    • pointer;iterator所指内容指针类型;
  • algorithm 和 iterator 的关系:
    • algorithm 向 iterator 提问(需要知道一些associated types),iterator 需要为 algorithm 提供回答(iterator 必须提供5个 associated types);
      在这里插入图片描述
2. iterator_traits
1. 为什么要 iterator_traits
  • 以 list_iterator 为例:list_iterator 声明了5个 associated types,algorithm 想要”提问“,直接采用右边这种形式就可以得到答案;
  • 那为什么还要 iterator_traits 呢?
    • 原因:如果 container 的 iterator 是 pointer,这种提问方式就失效了,因为 pointer 并不是 class,class 才能 typedef;
    • 解决:加一个中间层 traits,用来区分 iterator 的类型;
      在这里插入图片描述
2. traits介绍
  • traits 必须能区分它所收到的 iterator,是 以class设计出来的iterator 还是 pointer

    • pointer 无法定义 associated types,因为 pointer 不是 class,但它的 associated types 很直观;
    • class 有能力(通过typedef)定义 associated types;
      在这里插入图片描述
  • algorithm、iterator_traits、iterator 三者之间的关系:

    • algorithm 向 iterator_traits 获取需要的 associated type:
      • 如果 iterator 是 class iterator,则 iterator_traits 再向 iterator 获取;
      • 如果 iterator 是 non-class iterator,就使用 iterator_traits 特化的版本提供。
  • 疑问:

    • 为什么要偏特化 <const T*> 版本,仅对 <T*> 进行偏特化不够么?
      • 不够的,如果只对 <T*> 进行偏特化,iterator_traits<const T*>::value_type 的类型就是 const T。这一般不会是我们想要的(如下),所以必须对 <const T*> 也进行特化,使 iterator_traits<const T*>::value_type 的类型为 T。
      • 【因为 value_type 的主要目的是用来声明暂时变量,声明一个不能修改的暂时变量(const 变量)没什么意义;】
        在这里插入图片描述
  • 完整的 iterator_traits :

    • 有两个偏特化版本:<T*> 、<const T*> ;
    • 注意:<const T*> 特化版本的 pointer、reference 是 const 类型
      在这里插入图片描述

7. 深度探索 vector

1. G2.9版的 vector
  • 内容:

    • data:
      • 三个指针 T*(iterator 是 pointer):start、finish、end_of_storage;
      • 三个指针的指向位置如图左侧;
    • typedef:各种声明;
    • functions:各种函数;
  • 大小:vector 本身的 sizeof() 为12个字节【32位系统,一个指针4个字节,总共三个指针】;
    在这里插入图片描述

  • vector扩容方式:

    • 每次可用大小不够时,vector采取两倍成长
      • 即另外在内存中找一块两倍大小的连续空间,并将原来的元素转移到新的空间上;
    • vector 的每次扩容都会伴随大量的拷贝构造和析构函数的调用
    • 扩容代码:
      • insert_aux(Auxiliary 辅助)函数不只是提供给 push_back 使用;还有其他的函数会使用;
      • 在这里插入图片描述
      • 除了原本空间是0大小,其他情况都是2倍成长;
      • 黄色部分:数据复制代码;
        • 把数据分为了三部分进行复制操作:
          • ① 插入点之前的 data;
          • ② 插入的 data;
          • ③ 插入点之后的 data;
      • 在这里插入图片描述
2. G2.9版的 vector iterator
  • 由于 vector 是连续空间分布,所以 vector’s iterator 是 pointer

  • 获取 associated types 过程:

    • algorithm 向 iterator_trait 获取 associated types;
    • 因为 vector’s iterator 是 pointer,所以使用 iterator_trait <T*> 特化版本;
      在这里插入图片描述
3. G4.9版的 vector
  • G4.9版的 vector 整体结构:
    • 从原本的单一类实现,变成了多个类实现,加入了多个继承和包含;
  • G4.9版的 sizeof 大小:
    • G2.9版的 vector 大小为12个字节,有3个 pointer 数据;
    • G4.9版的 vector 大小也是12个字节
      • 只有 _Vector_impl class 有三个 pointer;
  • 【设计缺陷:】
    • _Vector_impl 继承 std::allocator 采用了 public 继承;
    • public 继承意味着二者是 is-a 的关系,但 vector 和 allocator 明显不是 is-a 关系;
    • 所以应该采用 private 继承,虽然使用 public 继承功能上也不会出错就是了;
      在这里插入图片描述
4. G4.9版的 vector iterator
  • vector‘s iterator 用 object 进行了包装,不是单纯的定义为 pointer,但归根到底,vector’s iterator 类型还是 T*;
    在这里插入图片描述
  • G4.9版的 vector 通过 traits 获取 associated types 就不是使用特化版本了;
    • 因为经过包装,iterator 已经是一个 object,所以 traits 使用泛化版本;
      在这里插入图片描述

8. 深度探索 array

0. 补充说明
  • C++1.0,也叫C++98(1998年);
  • C++2.0,也叫C++11(2011年);
  • TR1版本是C++1.0(1998年)~ C++2.0(2011年)之间的过渡版本;
1. TR1版的 array
  • data:使用了语言本身的数组;
  • iterator:pointer;
    在这里插入图片描述
2. G4.9版的 array
  • 经过几层包装,把 data 的定义放在别的 object;
    在这里插入图片描述

9. 深度探索 forward_list

1. forward_list 介绍
  • 内容和list类似;
  • forward_list 是单向链表,它的 iterator 类别是单向的 Forward iterator;
  • forward_list 有一个空的head节点;

10. 深度探索 deque

1. G2.9版的 deque iterator
  • 由于 deque 不是连续空间分布(是分段连续),所以deque_iterator 是一个 class
  • 内容:
    • data(4个指针):
      • T* cur:指向当前元素;
      • T* first:指向当前 buffer 的首部;
      • T* last:指向当前 buffer 的尾部;
      • T** node:指向当前 buffer 在 map 的位置;【因为 map 存放的内容是 pointer,所以 node 就是一个指向 pointer 的 pointer;】
  • 大小:一个 deque iterator 的 sizeof 为16个字节
    在这里插入图片描述
2. G2.9版的 deque
  • deque是分段连续(并不是整体连续),如图;
    • deque 维护一个 vector map,map 的内容存放指针,每个指针都指向一段连续的等量空间 buffer;
    • 【但 deque 内部实现会制造假象,让用户使用起来误以为是整体连续的;】
  • 内容:
    • data:
      • iterator start:其中的 cur、first、last 和 node 的信息指向第一个 buffer;(在55图中有表示)
      • iterator finish:其中的 cur、first、last 和 node 的信息指向最后一个 buffer;(在55图中有表示)
      • map_pointer map:指向 map,也是一个 T** 类型;
      • size_type map_size:map 数组的大小;(unsigned int 或者 unsigned long,4个字节)
  • 大小:deque 的 sizeof 为16*2 + 4 + 4 == 40个字节(32位系统,一个指针4个字节);
  • 扩容过程:
    • 每次扩充一个buffer;
    • 当push_back(…)空间不足时,获取一块新的 buffer ,在 map 尾部新增一个指针,指向新的buffer;
    • 当push_front(…)空间不足时,获取一块新的 buffer,在 map 头部新增一个指针,指向新的buffer;
    • 如果 map 空间不够,会采用两倍成长(vector),在 copy 的过程中,deque 会把数据 copy 到新 map的中段,使两边都有等量的预留空间。
  • G2.9版的 deque 允许指定每个 buffer 的大小(第三个参数,如图右上角);
    在这里插入图片描述
3. deque::insert()
  • 步骤:
    1. 判断插入点是否是头部,是调用 push_front(…);
    2. 判断插入点是否是尾部,是调用 push_back(…);
    3. 判断插入点距离头部、尾部,哪一段更近(与中部比较):
      1. 离头部更近,将插入点之前的元素向头部推动;
      2. 离尾部更近,将插入点之后的元素向尾部推动;
    4. 在插入点设置新值;
      在这里插入图片描述
      在这里插入图片描述
4. deque 如何模拟连续空间
  • 都是 iterator 的功劳,依靠 iterator 的操作符重载
    在这里插入图片描述
1. operator*() 和 operator->()
  • operator*() :返回 cur 所指内容;

  • operator->():使用 operator*(),返回 cur 所指内容的地址;

2. operator-(const self& x) const
  • 计算两个 iterator 的距离,分为三部分:
    • buffer的大小 * 两个 iterator 所指 buffer 之间的 buffers 数量;
    • 当前 iterator 所指 buffer 的元素数量;
    • x 所指 buffer 的元素数量;
      在这里插入图片描述
3. 前后置 ++ 和 前后置 –
  • 右下角的 set_node() 是跳转 buffer 函数;
  • self& operator++():
    • 先自增,然后判断是否到达 buffer 尾部;是,则要跳转到下一个 buffer 头部;
  • self operator++(int):
    • 调用前置 ++,返回临时变量;
  • self& operator–():
    • 先判断是否 buffer 头部,是,则要跳转到上一个 buffer 的尾部;再自减;
  • self operator–(int):
    • 调用前置 --,返回临时变量;
      在这里插入图片描述
4. operator+=(difference_type n) 和 operator+(difference_type n)
  • operator+=(difference_type n) :
    • 移动前,先计算移动到达的位置,判断目标位置是否在同一个 buffer 中;
      • 是,则直接修改 cur;
      • 不是,则要计算跨越了多少个 buffer,并切换到正确的 buffer;再移动剩余位置到目标点;
  • operator+(difference_type n) 【注意,这是传入一个数值,不是 iterator】:
    • 调用 operator+=(difference_type n);
      在这里插入图片描述
5. operator-=(difference_type n)、operator-(difference_type n) 和 operator[](difference_type n)
  • operator-=(difference_type n) :
    • 调用 operator+=(difference_type n);加一个负值;
  • operator-(difference_type n) 【注意,这是传入一个数值,不是 iterator】:
    • 调用 operator-=(difference_type n);
  • operator[](difference_type n)
    • 调用 operator+(difference_type n);
      在这里插入图片描述
5. G4.9版的 deque
  • G4.9版的 deque 整体结构:
    • 从原本的单一类实现,变成了多个类实现,加入了多个继承和包含;
  • G4.9版的 sizeof 大小:
    • G2.9版的 deque 大小为40个字节;
    • G4.9版的 deque 大小也是40个字节
      • 只有 _Deque_impl class 有 data;
  • 设计缺陷:一样有继承的缺陷;
  • _Deque_base 不能指定 buffer size 了,少了一个参数;
    在这里插入图片描述

11. queue 和 stack

1. queue 和 stack 介绍
  • queue 和 stack 是 adaptor,缺省底层容器是 deque;

  • queue 和 stack 的所有操作,都是依靠底层容器的操作来实现的;

  • queue:
    在这里插入图片描述

  • stack:
    在这里插入图片描述

2. queue 和 stack 不提供 iterator
  • 判断有没有 iterator,可以看它能不能进行 insert 操作;
  • queue 和 stack 都有特殊的行为,insert 会打乱这种行为,所以 queue 和 stack 没有 iterator,也就不能遍历;
    在这里插入图片描述
3. 可选底层容器
1. 都可以选择的容器
  • queue 和 stack 都可以选择的底层容器:list 或者 deque;
    在这里插入图片描述
2. 一方可以选择的容器
  • queue 对底层容器的要求更高,因为 queue 需要两端操作;
    • queue 不能选择 vector 作为底层容器,stack 可以选择 vector 作为底层容器;
  • 这里透露出一点:编译器对模板没办法全面检查,定义时编译器不会报错,只有在你使用时,才会检查是否合法;
    在这里插入图片描述
3. 都不能选择的容器

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值