第二讲

应具备的基础

  • C++基本语法

  • 模板(Template)基础

    令你事半功倍

  • 数据结构(Data Structures)和算法(Algorithms)概念

    令你如鱼得水

书籍:

  • 《Algorithms + Data Structures = Programs》—— Niklaus Wirth,1976

源代码的分布(VC, GCC)

所谓标准库,指规格是标准的,接口是标准的,但是不同的编译器可能有不同的实现。

标准库版本,Visual C++

标准库版本,GNU C++

Ubuntu下的C++源码路径:/usr/include/c++

OOP(面向对象编程) vs. GP(泛型编程)

  • OOP:Object-Oriented programming
  • GP:Generic Programming

C++标准库并不是用面向对象的概念设计出来的,面向对象的概念就是有封装、继承、多态(虚函数)。

标准库是利用泛型编程的概念设计出来的。

  • OOP将数据和操作关联到一起;
  • list容器中有sort排序操作,为什么不像vector或者deque一样使用全局的排序函数呢?因为标准库的sort算法用到的迭代器需要一定的条件(RandomAccessIterator),而这个条件是list提供的迭代器所不能满足的,所以链表不能像vectordeque 一样使用全局的 sort 算法。
  • 如果容器中有sort 算法,则排序的时候使用容器中的排序算法;否则使用全局的排序算法。

  • vectordeque 容器中没有sort排序方法,只能使用全局的排序算法,这就是将数据和操作分开了,通过迭代器进行关联。

技术基础:操作符重载and模板(泛化,全特化,偏特化)

阅读C++标准库源码的必要基础

  • Operator Overloading 操作符重载
  • Templates 模板

Operator Overloading,操作符重载

Class Templates, 类模板

Function Templates,函数模板

Member Templates,成员模板

Specialization,特化

例1:

例2:

例3:

Partial Specialization,偏特化

两个模板参数,绑定其中一个(个数的偏特化);本来泛化可以接受任意类型,但是如果类型是指针,指向的类型还是由 T 决定,就进入struct iterator_traits<T*>(范围的偏特化)

分配器allocators

标准层面上,分配内存最终都会调用到malloc,然后各个操作系统调用不同的System API,得到内存。

先谈operator new() 和 malloc()

malloc 分配出来的比你需要的内存多,有很多附加的东西。需要的size越大,overhead(附加的内存)比例就较小;需要的size越小,附加的东西的比例就越大。

VC6 STL对 allocator 的使用

  • 使用示例,如下容器使用的分配器是标准库中的allocator

  • 标准库中的allocator的实现:

说明:

  1. 分配内存的时候调用的是allocatorallocate函数,调用流程就变成了:allocate -> Allocate -> operator new -> malloc
  2. 回收内存的时候调用的是allocatordeallocate函数,调用流程:deallocate -> operator delete -> free
  3. 直接使用分配器就是如图中所示的“分配512 ints”,allocator<int>()生成一个匿名对象,使用该对象进行函数的调用;
  4. VC的分配器没有做任何独特的设计,只是调用了 C 的mallocfree来分配和释放内存,且接口设计并不方便程序员直接使用,但是容器使用就没问题。

BC5 STL对allocator的使用

  • 使用示例,BC5 STL中的一下容器使用的分配器是标准库的allocator

  • BC5 中标准库的allocator的实现:

说明:和VC一样,allocator最终也是通过 C 的 mallocfree来分配和释放内存的。

G2.9 STL对 allocator的使用

  • G2.9 标准库中的分配器allocator的实现:

说明:G2.9 标准库中的allocator 和 VC、BC一样,最终都是调用mallocfree进行内存的分配和释放。但是G2.9的STL中并没有使用标准库中的分配器,这个文件并没有被包含在任何STL头文件中,G2.9的STL容器使用的是alloc分配器。

  • G2.9 STL容器使用的分配器是alloc

  • G2.9 中的 alloc 的实现的行为模式:

说明:

  1. 这个alloc分配器的主要目的是减少malloc被调用的次数,因为malloc会带着额外开销;
  2. 设计了16条链表,每条链表负责不同大小区块的内存,#0负责8bytes大小的区块,#1负责16bytes大小的区块,以此类推…,超过这个分配器能管理的最大的区块128bytes,就仍然要调用malloc函数分配;
  3. 所有的容器,当它需要内存的时候都来向这个分配器要内存;容器中元素的大小会被调整为8的倍数;
  4. 更多内容,可查看C++内存管理机制。

G4.9 STL对allocator的使用

  • G4.9 标准库中的分配器allocator的实现:

说明:这个分配器也是直接调用的mallocfree进行内存的分配和回收,并没有其他的操作。

  • G4.9 STL容器使用的分配器是std::allocator

说明:G4.9 的STL的容器使用的分配器是标准库的allocator分配器,而没有使用G2.9中的alloc分配器,为什么呢?那么G2.9中的分配器alloc在G4.9的版本中还在吗?

  • G2.9中的alloc分配器在G4.9中还在,只是名称变了,名称现在是__pool_alloc

说明:

  1. 之所以说__pool_alloc就是G2.9的alloc,因为相关的数值都仍然存在:管理8的倍数的区块,最大可以管理128字节的区块,有16条链表;
  2. 这种比较好的分配器仍然是可以使用的,使用方式如图中所示的“用例”,第二个模板参数指定分配器,__pool_alloc所在的命名空间为__gun_cxx

容器之间的实现关系与分类

容器 — 结构与分类

说明:

  1. 如图中的注释说明,缩排表达的关系是复合,就是set 里面有rb_treemap里面有rb_treemultiset/multimap里面有rb_tree

  2. 同理,heap中有vectorpriority_queue中有vectorpriority_queue中有heapstackqueue中都有一个deque

  3. 图中的左右两侧表示的是容器要操作数据必须有的指针或元素,这个整体的大小,不包括数据,通过sizeof计算:

深度探索list

G2.9的list

说明:

  1. list中只有一个数据成员node,类型是link_type,而link_type就是list_node*,所以node就是一个指针,那么sizeof(list) = 4;
  2. __list_node中有两个指针prevnext,以及数据data,所以这就是一个双向链表,注意这里的prevnext指针是void*类型的,这种写法可以使用,但是必须进行强转型,这种方式不太好,在G4.9中已经进行了改善,这两个指针就指向__list_node类型;
  3. list中每个元素并不单纯的只有元素本身,还会多耗用两个指针prevnext,所以容器list向它的分配器要内存的时候,就是要“两个指针+数据”这么大的内存,而非只是数据这么多的内存;
  4. 链表是非连续的空间,所以它的Iterator不能是指针,因为Iterator模拟指针,就要能进行++这些操作,但是如果listIterator进行++ 操作不知道指到哪里去了;所以Iteartor必须足够聪明,当进行++操作的时候知道要指向list的下一个节点;
  5. 除了vectorarray外的所有容器的iterator都必须是class,它才能成为一个智能指针;
  6. 最后一个节点的下一个节点一定要加一个空白节点(图中的灰色节点),为了符合STL的「前闭后开」区间;begin()得到链表的第一个节点,end()得到链表的最后一个节点的下一个节点,即图中的空白节点;这是实现上的一个小技巧,不但是双向的,而且是环状的。

list’s iterator

  • 概览listiterator

说明:

  1. iterator要模拟指针,所以有大量的操作符重载;
  2. 所有的容器中的iterator都要做5个typedef,如上图中的所示的(1)(2)(3)(4)(5);
  • iteartor++操作的实现:

说明:

  1. i++叫做postfix form,++i叫做prefix form,因为无论是前缀还是后缀形式,都只有i 这个参数,C++中为了区分这种情况,规定了operator++()无参表示前缀,此时的i已经变成调用这个函数的对象本身了;operator++(int)有参表示后缀;
  2. self& operator++()函数可以成功的将node进行移动,指向下一个节点;
  3. self operator++(int)函数的流程是先记录原值,然后进行操作,最后返回原值。注意:
    • 此时的记录原值的操作:self tmp = *this;并不会调用重载的operator*函数,因为这行代码先遇到了=运算符,所以会调用拷贝构造函数,此时的*this已经变成了拷贝构造函数里面的参数;
    • ++*this 调用了重载的operator++()函数;
  4. 注意返回值的差别。之所以有差别是向整数的++操作看齐:整数里面是不允许进行两次后++的,所以这里iteratoroperator++(int)为了阻止它做两次后++操作,返回值不是引用;整数中是允许做两次前++的操作,所以iteratoropeartor++()返回值是引用。
  • iterator*->操作符的实现

说明:

  1. operator* 就是获得node指针指向的节点的data数据;
  2. operator->获取node指针指向的节点的data数据的地址;
  • 小结

    1. list是个双向链表,因为每个节点除了有data,还有nextprev指针;
    2. 所有的容器的iterator都有两大部分:(1)一些typedef;(2)操作符重载
  • G4.9相比G2.9的改进:

说明:

  1. iterator的模板参数只有一个,容易理解;
  2. G4.9中的指针prevnext是本身的类型,而不再是void*;

G4.9的list

说明:

  1. 相比G2.9的list,G2.9的list更加复杂了;
  2. 因为行为模式已经在G2.9中知道了,所以没有必要再去看G4.9了;
  3. 和 G2.9一样,链表是环状双向的,刻意在环状list最后加了一个空白节点,用来符合STL的「前闭后开」区间;
  4. 在G2.9的list图中看到了,sizeof(list)在G2.9中是4,因为只有一个指针;而在G4.9中是8,为什么是8呢?
    • G4.9的list中本身没有数据,所以size = 0;但是它有父类_List_base,所以父类多大,它就多大;
    • _List_base中的数据为_M_impl,所以这个数据多大,_List_base就多大;
    • _M_impl类型为_List_impl,而_List_impl中的数据类型是_List_node_base;
    • _List_node_base中有两个指针,所以sizeof(list) = 8

迭代器的设计原则和Iterator Traits的作用与设计

设计Traits实现希望你放入的数据,能够萃取出你想要的特征。标准库中有好几种Traits,针对type的有type traits;针对characters,就有char traits;针对pointer,有pointer traits,… 。这里,只看iterator traits。

Iterator需要遵循的原则

说明:

  1. iterator是算法和容器之间的桥梁,这样算法能知道要处理的元素的范围,容器将begin()end() 传出去交给算法,算法知道了范围且可以通过iterator进行移动,++或–,将元素一个一个地取出来;
  2. 算法在处理数据的过程中可能需要知道iterator的性质,因为它需要做动作,可能会选择最佳化的动作;
  3. 举例:有一个rotate算法,会想要知道iterator的哪些属性?
    • 想要知道iterator的分类(iteartor_traits<_Iter>::iterator_category()),有的迭代器只能++,或者只能–,有的可以跳着走,得到分类以便可以选取最佳的操作方式;
    • 想要知道iterator的difference_type,两个iterator之间的距离;
    • 想要知道iterator的value_type,指的是迭代器指向的元素的类型,比如在一个容器中放了10个string类型的元素,那么这个value_type就是string
  4. 算法提问,迭代器回答。这样的提问在C++标准库开发过程中设计出 5 种,这 5 种叫做iterator的associated types(相关类型):
    • iterator_category
    • difference_type
    • value_type
    • reference
    • pointer
  5. iterator必须提供这5种相关类型,以便回答算法的提问。

Iterator 必须提供的 5 种 associated types

说明:

  1. 标准库中用ptrdiff_t来表示两个迭代器之间的距离,这个ptrdiff_t也是C++中定义的,但是如果实际存放的元素的头和尾的距离超过了这个ptrdiff_t类型表示的范围,那这就失效了;
  2. 因为list是个双向链表,所以这里使用了bidirectional_iterator_tag来表示iterator_category;
  3. 可以看到,这里并没有traits,那么为什么还要谈到iterator traits呢?因为如果 iterator 不是 class,就不能进行typedef,如果iterator是native pointer,即C++中的指针,它被视为一种退化的 iterator。所以当调用算法的时候,传入的可能是个指针,而不是泛化指针,不是个迭代器,那此时算法怎么提问呢?此时,才需要设计出 traits。

Traits,特性,特征,特质

说明:

  1. 图中的“萃取机”必须能区分它所收到的iterator,到底是以class设计的iterator还是native pointer的iterator;

说明:

  1. 因为算法不知道iterator是什么类型,所以不能直接提问,而是间接问。将iterator放入traits,算法问traits:value type是什么?
  2. 然后traits问iterator或指针:value type是什么?
    • 若traits要问的对象是class iterator,则进入图中的①
    • 若traits要问的对象是 pointer,则进入②或者③
  3. 为了应付指针的形式,增加了中间层 iterator traits,利用了偏特化分离出指针和const指针;
  4. 这一页回答了算法的value_type的提问;

完整的iterator_traits

说明:如果是iterator,则进入泛化版本;如果是指针,则进入偏特化版本。算法问traits,当traits发现手上的东西是指针的时候,就由traits替它回答。

各式各样的Traits

深度探索vector

vector是一种动态增长的数组,当空间不够的时候,要到内存的其他地方开辟空间,并将原来的数据移动到新的空间,这才是扩充,不能在原来的空间进行扩充。

G2.9的vector

说明:

  1. 当前vector的容量是8,目前已经存放了 6 个元素;
  2. 如果已经放了8个元素,要再放第9个元素的时候,就要进行扩充,如图中所示,二倍成长。容器回到内存中去找到另外的空间,要求是当前空间的2倍。当次数较多的时候,申请的空间就越来越大,如果最后找不到2倍大的空间,容器的生命就结束了,不能再放入元素了;
  3. 只需要三个指针startfinishend_of_storage就能控制整个容器,因此,sizeof(vector) = 12;
  4. 所有的容器,如果带了连续的空间,就必须提供[]运算符重载函数;
  5. vector的 二倍成长 到底是怎么回事呢?
  • vector的二倍成长的实现

说明:

  1. vector的成长发生在放入元素的时候,此处即push_back函数;
  2. 之所以在insert_aux函数中也做了finish == end_of_storage的判断,是因为insert_aux除了被push_back函数调用外,还可能被其他函数调用,在那种情况下,就需要做检查;

  1. 没有备用空间的时候,先记录下原来的size,分配原则:如果原大小为0,则分配1;如果原大小不为0,则分配原大小的2倍,前半段用来放置元数据,后半段准备用来放新数据;
  2. 确定了长度之后,使用分配器的allocate函数进行内存空间的分配;
  3. 然后将原来vector的内容拷贝到新的vector;
  4. 为新的元素设定初值;(要放入的第9个元素);
  5. 因为insert操作也会使得空间发生增长,也会调用到这个insert_aux,所以要把安插点之后的内容也进行拷贝;
  6. 每次成长都会大量调用拷贝构造函数和析构函数(析构原来vector中的元素),需要很大的成本;

G2.9的vector’s iterator

说明:

  1. vector的空间是连续的,按理说可以直接使用指针作为迭代器,vector类中的iterator也的确是指针;
  2. 当算法要提问iterator的时候,就通过iterator_traits进行回答;当前的iterator是个指针,当它丢给萃取机萃取的时候,就是图中箭头指向的T*,因此萃取机就会进入偏特化的版本——struct iterator_traits<T*>

G4.9的vector

说明:

  1. G4.9的vector也有三个指针,_M_start_M_finish_M_end_of_storage,所以 sizeof(vector) = 12

G4.9的vector’s iterator

说明:

  1. G4.9的vectoriterator经过层层推导就是T*外包裹一个iterator adapter,使得能支持 5 种 associated types;

  1. 算法向iterator提问的时候,将iterator放入萃取机中,因为此时的iterator是个object,所以走图中的灰色细箭头这一条路径;
  2. 而在iterator内部本身就定义了 5 种 associated types;
  3. 绕了这么大一圈,最后和G2.9的iterator达到的是一样的效果;

深度探索array

TR1的array

说明:

  1. array相比vector更加简单,因为在C和C++语言中本身就存在数组,为什么要将数组包装成一个容器来使用呢?因为变成容器之后,就要遵循容器的规律、规则,即需要提供iterator迭代器,而这个迭代器又要提供五种相关的类型以便于让算法可以询问一些必要的信息,算法才能决定采取哪种最优的动作,如果没有进行这样的包装,array就被摒弃在六大部件之外,就不能享受算法、仿函数等与其交互的关系。
  2. 上述的是TR1(Technique report 1) 版本,是C++的过渡版本,介于C++1.0和C++2.0之间;
  3. array不能扩充,所以必须指定大小,如array<int, 10> myArray;
  4. 没有构造函数,也没有析构函数;
  5. 因为array是连续的空间,所以它的迭代器可以用指针来单纯的指针来表现,不用再设计单独的class;

G4.9的array

说明:

  1. 数组的写法

    int a[100]; //OK
    int[100] b; //fail
    typedef int T[100];
    T c; //OK
    

    即此处的_M_elems变量是个数组;

深度探索forward_list

说明:forward_list是个单向链表,相比双向链表更加简单,因此此处不再赘述。

深度探索deque、queue和stack

容器deque

G2.9的deque

说明:

  1. deque是分段连续,deque是个vector,其中的每个元素都是一个指针,这些指针分别指向不同的buffer;
  2. 如果当前空闲的最后一个buffer使用完了,要继续push_back,那么新分配一个buffer,并将其deque当前图上的倒数第二个空白位置指向这个buffer即可,这就是往后扩充;
  3. 同理,如果第一个空闲的buffer用完了,要继续push_front,再分配一个buffer,用deque中的第一个空白位置指向新分配的buffer即可,这就是向前扩充;
  4. 图中的蓝色部分是迭代器,deque的迭代器是class,其中包含了curfirstlastnode四个部分:
    • 其中的node指的就是图中deque中指向buffer的指针,我们在这里把它暂时称为控制中心。一个迭代器能知道控制中心在哪里,当迭代器要++或–的时候就能够跳到另一个分段,因为分段的控制全部在这里;
    • firstlast指的是node所指向的buffer的头和尾(前闭后开),标识出buffer的边界,如果走到了边界,就要跳到下一个buffer;
    • cur就是当前迭代器指向的元素;
  5. 几乎所有的容器都维护了两个迭代器startfinish,分别指向头和尾;几乎所有的容器都提供两个函数,begin()end(),其中begin()传回startend()传回finish

说明:

  1. 图中是G2.9的deque;
  2. 数据部分的map的类型是T**,占 4 个字节;
  3. 代码中的iterator中的数据为下图的deque's iterator中所示的curfirstlastnode,都是指针,所以deque’s iterator的大小为 16 字节,
  4. 那么一个deque的大小为“两个迭代器 + map + map_size" = 16 * 2 + 4 + 4 = 40 bytes;
  5. deque是个模板类,有三个模板参数,第一个参数表示元素类型,第二个参数是分配器的类型,第三个参数是指每个buffer容纳的元素个数,允许指定buffer容纳的元素个数,默认值为0,deque_buf_size函数会根据该模板参数决定buffer中能容纳的元素具体个数;

deque<T>::insert()

说明:

  1. 聪明在于插入数据的时候会判断要插入的位置是离前面比较近还是后面比较近,离哪边近,就推动哪边的元素,因为每次推动元素都要调用构造函数和析构函数,挺花费时间的;

  1. insert_aux首先检查要插入的点往前和往后,哪边需要移动的元素index哪边更少;即找到离头还是尾的距离更近,将距离近的哪边的元素进行推动以便放入新值;
  2. 在安插点上设定新的值;

deque如何模拟连续空间

说明:

  1. font()返回第一个元素,back()返回最后一个元素,这里是利用finish进行倒推;
  2. size() 就是元素的个数,注意这里的finish - start,其中迭代器一定是对-进行了操作符重载;

  1. operator*就是取值,迭代器取值就是获取迭代器的cur指向的值;
  2. operator-统计首尾迭代器之间的元素个数;

  1. operator++(int)调用operator()operator--(int)调用operator--(),都是只移动一个位置
  2. operator++()就是移动当前元素,移动之后检查是否到达buffer的边界,如果到了下一个边界,就跳到下一个buffer的起点;operator--()同理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9WvgOkW-1641201057150)(…/…/…/Library/Application Support/typora-user-images/image-20211223174035925.png)]

  1. operator+=就是移动多个位置,先判断是否会跨越buffer;如果跨越buffer,要先计算跨越几个,然后退回控制中心,跨越之后再决定有几个要走

  1. operator-=利用了operator+=
  2. operator[]将迭代器移动到第n个位置,获得第n个元素

小结:deque的迭代器通过操作符重载可以欺骗使用者自己是连续,这种欺骗是善意的,可以让使用者更好地使用deque。

G4.9的deque

说明:

  1. G4.9版本相比G2.9版本,又是从一个单一的class变成了复杂的多个class;每个容器的新版本都会设计为如图的这种继承和组合的关系;
  2. 此时的 sizeof(deque<int>) = 40,和G2.9版本的大小一样,就是数据成员_M_impl的大小,即_Deque_impl类中的数据成员的大小;
  3. G4.9的 deque 的模板参数只有两个,不允许指派buffer size;
  4. _M_map指向的是控制中心,它是用vector来存放指向不同buffer的指针的,当它的空间不够的时候,会二倍增长,将当前的vector拷贝到新的空间的中段,为了让左边和右边有空闲,使得可以向前和向后扩充;

容器queue

说明:

  1. 可以看到,deque是双向进出,stack是先进后出,queue是先进先出,那么只需要在queue和stack中内含deque,然后封锁住其中某些动作;
  2. 如图中所示,queue类中的数据成员是deque类型,所有的操作都是转去调用deque的接口来完成;

容器stack

说明:

  1. queue类似,stack 内含了一个 deque,所有的操作都是转调用deque的接口完成。

queue和stack,关于其 iterator 和底层结构

1、stack或queue都不允许遍历,也不提供iterator

说明:

  1. stackqueue 都可以选择 listdeque 作为底层结构,默认是 deque,如图中所示,选择 list 作为底层结构也是可以的,可以成功编译和执行;
  2. stackqueue都 不允许 遍历,也不提供 iterator,因为stack的行为是先进后出,queue的行为是先进先出,如果允许任意插入元素的话就会干扰了这个行为模式,而stackqueue的行为是被全世界公认的,所以不允许放元素,而放元素要靠迭代器,所以根本就不提供迭代器,要取东西的时候只能从头或者尾拿;

2、queue不可选择vector作为底层结构,stack可选择vector作为底层结构

说明:

  1. stack 可以选择vector作为底层结构;
  2. queue不可以选择vector作为底层结构;部分接口不能转调用vector,如图中所示的pop(),不能成功转调用,因为vector中没有pop_front这个成员函数,部分失败了;
  3. 通过queue测试使用vector作为底层结构的启示:使用模板的时候,编译器不会预先做全面的检查,用到多少检查多少。

3、stack和queue都不可选择set或map作为底层结构

说明:

  1. stackqueue 都不可以选择 setmap作为底层结构,因为转调用的时候,调用不到正确的函数的话,这个结构就剔除了,不能作为候选;
  2. 图中示范了stack选择set作为底层结构出现的的错误,第一行编译通过,是因为上面说过编译器预先不会做前面的检查;而stack<string, map<string>> c;queue<string, map<string>> c; 编译无法通是因为map使用的时候是key和value都要设置,这里使用错误,所以编译无法通过。

深度探索RB_tree

之前谈到的容器都是 Sequence Containers,从本章开始,要讲解关联式容器,它非常有用,因为它查找和插入都很快。关联式容器可以想象成一个小型的数据库,数据库就是希望用key找到value,而关联式容器就带着这样的性质。在标准库中,关联式容器底层使用两种结构作为技术支持——红黑树和哈希表。

红黑树简介

说明:

  1. Red-Black tree(红黑树)是平衡二叉查找树(balanced binary search tree)中常被使用的一种。平衡二叉查找树的特征:排列规则有利于 searchinsert,并保持适度平衡——无任何节点过深。
  2. rb_tree 提供 ”遍历“ 操作及 iterators。按正常规则(++ite) 遍历,便能获得排序状态(sorted)。【注:begin()记录的是最左的节点,end()记录最右的节点】
  3. 我们不应使用 rb_tree 的 iterators 改变元素值(因为元素有其严谨排列规则)。编程层面(programming level)并未阻绝此事。如此设计是正确的,因为 rb_tree 即将为 set 和 map 服务(作为其底部支持),而 map 允许 元素的data 被改变,只有元素的key 才是不可被改变的。
  4. rb_tree 提供两种 insertion 操作: insert_unique()insert_equal()。前者表示节点的key一定在整个 tree 中独一无二,否则安插失败;后者表示节点的 key 可重复。

G2.9 容器rb_tree

  • 标准库中红黑树的实现

说明:

  1. rb_tree是一个模板类,模板参数:

    • Value:key 和 data 合成 value,其中的data也可能是其他的数据合起来的;
    • KeyOfValue:如何取出value中的key;
    • Compare:比较函数/仿函数;
    • Alloc:分配器,默认为alloc
  2. 数据部分:
    • node_countrb_tree中的节点数量;
    • header:指向rb_tree_node的指针;
    • key_comparekey的大小比较规则;Compare仿函数,没有数据成员,所以大小为0,任何的编译器,对于大小为0的class,创建出来的对象的size一定为1;
  3. 所以数据部分一共的大小是 9,但是因为内存对齐,以4的倍数进行对齐,所以 9 要调整为 12;
  4. 图中的双向链表中的天蓝色节点,是一个虚空节点,为了做「前闭后开」区间,刻意放入的,不是真正的元素;红黑树中的header也是类似的,刻意放入的,使得代码实现更加简单;

  1. 直接使用rb_tree示例:

    rb_tree<int, int, identity<int>, //仿函数,重载了operator()函数,告诉红黑树要如何取得key,GNU C 独有的,不是标准库的一部分less<int>, //key比较大小的方式,less是标准库的一部分alloc>
    myTree;
    

使用容器rb_tree

G4.9 容器_Rb_tree

说明:

  1. G4.9版本相比G2.9版本,类结构发生了变化;
  2. OO思想里面,类中包含一个指针指向另一个类,主体本身不做任何事情,都是通过指针指向的另一个类做事情,这中手法叫做Handle-Body;
  3. setmap里都各有一个_Rb_tree
  4. 此时的_Rb_tree的数据的大小取决于_M_impl这个数据成员的大小,而_M_impl类型是_Rb_tree_implRb_tree_impl中的数据成员_M_node的类型是_Rb_tree_node_base,其中包含了四个数据成员:3 个指针,1个_Rb_tree_color(enum枚举类型) = 24 bytes;

使用容器_Rb_tree

G4.9版本相比于G2.9部分名称发生了改变,如下红色的部分就是改变的部分:

深度探索set,multiset

  1. set/multisetrb_tree为底层结构,因此有「元素自动排序」特性。排序的依据是 key,而set/multiset 元素的 value 和 key 合一:value 就是 key

  2. set/multiset 提供 ”遍历“操作及 iterators。按正常规则(++ite) 遍历,便能获得排序状态(sorted)。

  3. 我们无法 使用 set/multiset 的 iterators 改变元素值(因为key 有其严谨排列规则)。set/multiset 的 iterator 是其底部的 RB tree 的 const_iterator, 就是为了禁止 user 对元素赋值。【注:讲解 rb_tree 的时候说到的是”不应“,因为Value 中的 data是可以更改的,是合理的;但是这里是”无法“,可见set在设计上就限制了不能修改,之所以不能修改,是因为set的key就是value,如果修改的话,改的就是key,这是不可以的。】

  4. set 元素的 key 必须独一无二,因此其 insert() 用的是 rb_tree 的 insert_unique()

  5. multiset 元素的 key 可以重复,因此其insert() 用的是 rb_tree 的 insert_equal()

容器set

说明:

  1. set的模板参数有三个:Key的类型;Key的大小比较规则,默认值为less<Key>;分配器,默认值为alloc
  2. set中有个红黑树变量t
  3. set中拿 iterator 的时候拿的是 rb_treeconst_iterator,这个迭代器是不允许对元素进行修改的;
  4. set的所有操作,都转调用底层 t 的操作。从这层意义来看,set 未尝不是个 container adapter;
  5. 之前说到 key 和 data 合起来这一整包是 value,从 value 中取出 key 用identityset 里面取出 key 也就需要用 identityidentity 是GNU C中才有的;

VC6 容器set

  • VC6 不提供 identity(),那么其 setmap 如何使用 RB-tree?

说明:

  1. VC6中自己实现了一个内部类_Kfn,写法和 GNU C 中的identity 的实现是一样的,即自己实现。

使用容器multiset

深度探索map,multimap

说明:

  1. 每个元素即value包含了key 和 data,key不能修改,但是 data 可以修改;

G2.9 的容器map

说明:

  1. select1st:从value中取出第一个,即取出key;map拿出key的方式就是select1st;
  2. map的迭代器就是红黑树的迭代器,红黑树的迭代器并没有禁止任何事情呀?那是如何做到用它不能修改key,但是能修改data的呢?如上例所示,使用者map<int,string>放入两个类型,被map包成一个 pair,而这个pair被当成红黑树的第二个模板参数,map自动地将key设置成 const,所以 key 放入之后无论如何都不能被修改,因为它是 const。set 中不能修改 key 是因为使用的迭代器是红黑树的 const_iterator,而map不允许通过迭代器修改key,是因为包装成 pair的时候将key设置成了 const;
  3. select1st是GNU C独有的;

VC6 的容器map

  • VC6 不提供 select1st(),那么 map 如何使用 RB-tree?

说明:

  1. 自己实现一个和select1st功能一样的类_Kfn,重载operator() 函数,所以是个函数对象/仿函数,将pairfirst 数据传回;

使用容器multimap

容器map,独特的operator[]

说明:

  1. map[]操作:如果key存在,则返回 key 对应的 data;如果key不存在,那么会创建一个pair,使用默认值作为data,当前的key为key;
  2. 使用lower_bound查找元素value,如果找到了,则返回一个iterator指向其中第一个元素;如果没有,就返回该元素应该插入的位置,即返回iterator指向第一个「不小于value」的元素。

使用容器map

深度探索hashtable

  • 引子

说明:假设有N个object,每个有一个对应的编号,当空间足够的时候,就将object放到对应编号的位置上;当空间不足的时候,object的编号 % 表的长度,此时就可能出现多个object应落在同一个位置,出现了碰撞

【侯捷】C++STL标准库与泛型编程(第二讲)相关推荐

  1. 侯捷C++课程笔记03: STL标准库与泛型编程

    本笔记根据侯捷老师的课程整理而来:STL标准库与泛型编程 pdf版本笔记的下载地址: 笔记03_STL标准库与泛型编程,排版更美观一点(访问密码:3834) 侯捷C++课程笔记03: STL标准库与泛 ...

  2. STL标准库及泛型编程

    1-认识headers.版本.重要资源 C++ Standard Library Standard Template Library 标准库 > STL 标准库以header files形式呈现 ...

  3. 【侯捷】C++STL标准库与泛型编程(第一讲)

    前言 所谓Generic Programming(GP,泛型编程),就是使用 template(模板)为主要工具来编写程序.本课程第二讲开宗明义阐述了 GP 与 OOP(Object Oriented ...

  4. 【侯捷】C++STL标准库与泛型编程(第三讲)

    第三讲 算法的形式 C++标准库的算法,是什么东西? 说明: 算法Algorithm 是个 function template,标准库中的算法都长成如下这样: template<typename ...

  5. 【侯捷】C++STL标准库与泛型编程(第四讲)

    第四讲 1.一个万用的Hash Function 说明: 1.1 针对自定义类型的哈希函数的编写方式 自定义类型: #include <functional> class Customer ...

  6. C++拾取——使用stl标准库实现排序算法及评测

    今天看了一篇文章,讲各种语言的优势和劣势.其中一个观点:haskell非常适合写算法,因为使用者不用去关心具体的计算机实现,而只要关注于操作语义.这让它在专心研究算法的人中非常受欢迎.所以很多时候,语 ...

  7. STL标准库-容器-set与map

    STL标准库-容器-set与multiset C++的set https://www.cnblogs.com/LearningTheLoad/p/7456024.html STL标准库-容器-map和 ...

  8. C++的STL标准库学习(queue)队列(第四篇)

    queue容器基本概念 Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素.  也就是说输入的数据要 ...

  9. C++的STL标准库学习(stack)栈

    stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,形式如图所示.stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法 ...

最新文章

  1. 学者要研究真问题做真学问
  2. 根据数据库中不同的值显示不同的图片
  3. 解决oracle主键问题,解决renren-security使用oracle主键问题
  4. 为什么以太网的最小数据帧长度为64字节?
  5. Python3教程Web开发实战梳理-day7(看着不错)
  6. AESNI/XData勒索病毒来袭 目前主要在乌克兰传播 它居然还能使用硬件加速加密过程...
  7. 用 docker-compose 启动 WebApi 和 SQL Server
  8. 如何在android客户端中做到自动检查数据更新?,UpdateHelper
  9. 【0】Zookeeper QA
  10. Jquery取得iframe中元素的几种方法Javascript Jquery获取Iframe的元素、内容或者ID,反之也行!...
  11. java 线程间通信方式_「转」JAVA多线程之线程间的通信方式
  12. printf,sprintf,vsprintf 区别【转】
  13. 云痕大数据 家长登录_智学网家长学生查分入口:www.zhixue.com
  14. 【Uniapp 原生插件】芯烨云打印机插件
  15. Maven的下载和配置(一)
  16. 书单 电影单 电视剧单
  17. 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?(递归与动态规划)
  18. LaTeX如何输出反斜杠 \
  19. 酒水知识(六大基酒之白兰地_Brandy)
  20. Mac下浏览器安装证书

热门文章

  1. 阿里云轻量服务器WordPress镜像建网站教程(图)
  2. agx 安装ros opencv_ROS基础
  3. python自动评论_selenium+python 的微博自动转赞评功能实现
  4. 开发微信小程序:创建小程序实例
  5. canvas路径,描边与填充
  6. python时间序列分析航空旅人_大佬整理的Python数据可视化时间序列案例,建议收藏(附代码)...
  7. kafka 消费机制
  8. SpringBoot - 单元测试利器Mockito入门
  9. 使用css中的white-space:pre-wrap;让html浏览器显示空白空格符
  10. 语法-07-复合词,接尾词