• 侯捷——2.C++标准库 体系结构与内核分析


    侯捷——2.C++标准库 体系结构与内核分析

    1. C++标准库 vs STL

    在这里插入图片描述

    在这里插入图片描述
    C++标准库包含STL。标准库都用 std 这个命名空间包装起来。

    2. STL体系结构基础介绍

    在这里插入图片描述
    allocators(分配器)是给containers(容器)分配内存的。当要创建容器时,必须先有分配器分配内存。在创建容器时,可以指定分配器,也可以不指定,编译器会默认指定分配器。

    在这里插入图片描述

    2.1 复杂度

    在这里插入图片描述

    2.2 迭代器采用前闭后开区间

    在这里插入图片描述
    begin()指向第一个元素,而end()指向最后一个元素的下一个元素。

    2.3 range-based for statement(since C++11)

    在这里插入图片描述

    2.4 auto关键字

    在这里插入图片描述

    3. 容器——结构与分类(1)

    在这里插入图片描述
    循序式容器,是根据你放进去的顺序来决定顺序的。
    Array、Vector和Deque的底部都是在内存中占用的空间都是连续的。
    List和Forward-List都是空间可以不连续的。

    在这里插入图片描述
    关联式容器,就是Set/MultiSet和Map/Mulimap,底部实现都是红黑树。

    关联式容器都是内部有序的,而Set和Map是内部元素不可重复的,MultiSet和MultiMap是内部元素可以重复的。

    Unordered Containers(无序容器),内部元素都是无序的,底部实现是哈希表。

    在这里插入图片描述

    3.1 Deque

    在这里插入图片描述
    Deque(双向队列),其实它内部是有可能不连续的,但为了让它看起来是连续的,则每当读到一个buffer最后一个元素时,要自动跳到邻近buffer的第一个元素。

    stack(栈)和queue(队列)底部都是deque实现的,只是限制了deque的进出元素的方式。
    在这里插入图片描述
    在这里插入图片描述
    stack和queue是没有iterator的,因为stack是先进后出,queue是先进先出,如果有iterator的话,就有可能会通过iterator修改它们中间的值,从而破坏规则。

    3.2 无序容器(unordered)

    在这里插入图片描述
    无序容器包括unordered_set、unordered_multiset、unordered_map和unordered_multimap。它们底部实现是哈希表,如上图,哈希表的位置称之为篮子。篮子的个数会随着元素的增加而扩充。
    例如当篮子只有10个的话,当元素达到10个,篮子数量就会扩充到20个,然后把原来的元素打散,按照新的表达式计算出各个元素的新位置。

    所以一般篮子的个数都会比元素的个数多。

    4. 分配器(allocator)

    容器的背后需要一个东西来支持它内存的使用,这个东西就是分配器。


    容器的模板都有第二参数,就是分配器。但可以不指定,会自动使用默认的分配器。

    4.1 用法

    在这里插入图片描述
    在这里插入图片描述

    不推荐单独使用分配器。

    5. OOP(Object-Oriented programming) vs GP(Generic programming)

    5.1 OOP

    OOP(面向对象编程)一般是将 datas(数据) 和 methods(方法)合并在一起的。
    在这里插入图片描述
    list无法使用全局的sort()。需要自己实现。
    在这里插入图片描述

    5.2 GP

    GP是将数据和方法分开来的。
    在这里插入图片描述
    容器和算法要取得联系得通过迭代器。

    vector和deque就可以使用全局的sort()

    在这里插入图片描述
    采用GP的好处:

    1. 容器和算法可各自工作,互不影响,用迭代器沟通即可。
    2. 算法通过迭代器确定操作范围,并通过迭代器取得容器中的元素。

    如果类中有实现sort(),则使用自己类中的。如果没有,则使用全局的。

    在这里插入图片描述

    6. 操作符重载和模板(泛化、全特化、偏特化)

    6.1 全特化

    当某个类型有更好的处理方法时,可以使用特化。就是指定某种类型时,调用对应的方法。要注意特化的语法表示,如下图所示。
    在这里插入图片描述

    6.2 偏特化

    在这里插入图片描述

    7. 分配器(allocators)

    分配器的好坏会影响容器的效率。因为分配器是为容器分配内存的,而容器中元素的进出都会关系到内存的分配。

    7.1 先谈operator new()和malloc()

    所有的内存分配动作最终都会跑到malloc()下去。

    在这里插入图片描述
    operator new()也不例外。malloc在分配内存时,会多分配出 32+4 字节的灰色部分,会两块红色的cookie部分,还有根据情况而定的绿色填充部分。所以当你想要分配的内存比较小的时候,灰色和红色这些内存是会固定分配的,所以会导致,实际上使用的内存占比比较小,而当你要申请的内存比较大的时候,则实际使用的内存占比就会比较大。

    7.1 分配器的源码

    以下是VC6低下的allocator的源码:
    在这里插入图片描述
    allocator两个比较重要的函数就是allocate()deallocate()allocate()底部会调用operator new(),而operator new()会调用malloc(),所以归根结底就是allocate()会调用malloc()。而deallocate()也同理,底部会调用operator delete(),而operator delete()底部会调用free()

    分配器在申请和释放内存时都需要指定大小,这是很不方便的,释放时很难去记住当初申请的大小。所以一般不推荐直接使用分配器。

    VC下的 allocator 没有特殊的设计,背后只是调用 malloc 和 free 来实现内存的分配和释放。

    VC下、BC下和GNU下的allocator的实现都差不多。但GNU下实现的allocator不怎么会去使用它,而是使用alloc

    在这里插入图片描述
    alloc 的主要改变是为了避免每分配一个元素需要在该元素上下消耗的8byte的cookie(记录大小),因为每次malloc都带来额外的内存开销。每个元素根据是 8 的几倍挂到上图对应的区域。当对应倍数下的链表没有内存时,系统还是会使用 malloc 向系统申请一大片内存。当然一使用malloc就会有额外开销。

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

    在这里插入图片描述
    本图以缩排形式表达“基层与衍生层”的关系。这里所谓的衍生,并非继承而是复合。
    例如set、map、multiset和multimap与rb_tree(红黑树)的关系就是复合关系,也就是set、map、multiset和multimap里有rb_tree,它们底部需要红黑树作为支撑。

    9. 深度探索list

    在这里插入图片描述
    这里的 __list_node 节点设计中有一些小缺陷,该结构体左右节点指向的是 void* ,也就是在使用时还得注意转型。一般来说,节点两端指向的都是自己本身这种类型。

    除了array和vector,其它容器的 iterator 都要自己通过写出 iterator 类来实现,才能使 iterator 变聪明,可以通过 ++ 而指向下一个。

    9.1 list的iterator

    在这里插入图片描述
    iterator少不了操作符重载,因为要使它像指针。

    operator++()是前置的++,相当于是i++;而为了区分前置跟后置,所以后置的加了一个参数,作为区分,这个参数是没有用到的,operator(int)

    在这里插入图片描述
    C++不允许连续两次后置++。
    为了前置++可以两次,所以它的返回类型是引用。为了使后置++不可以两次,所以它的返回类型不是引用。

    在这里插入图片描述

    9.2 list不同版本的比较

    在这里插入图片描述
    在这里插入图片描述

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

    10.1 Iterator 需要遵循的原则

    在这里插入图片描述
    value_type 指的是容器的类型,如果容器的类型是int,则 value_type 为 int。
    difference_type指的是两个iterator之间的距离,应该用什么类型来表现,如 unsigned_int。

    迭代器需要回答算法的五个问题:
    iterator 必须定义的五种类型,iterator_category(),difference_type,value_type,reference和pointer。以此来回答算法提出的问题。虽然reference和pointer从来没有被使用到。
    在这里插入图片描述

    ptrdiff_t是比编译器已经定义好了的,好像是unsigned_long。

    当iterator不是一个class时,而是一个自然指针(相当于一种退化的iterator),这个时候算法想要问它问题,就问不出什么,为了解决这个问题,引入了trait。

    10.2 Iterator Traits的作用与设计

    在这里插入图片描述
    Iterator Traits(萃取机)必须要能区分iterator到底是class类型的,还是不是class类型的。可以把迭代器放进萃取机,问它一些事情,比如问迭代器是哪种类型等。

    traits相当于中介。

    在这里插入图片描述

    在这里插入图片描述

    11. vector深度探索

    容器是一个前闭后开的区间。finish和end_of_storage都指向下一个。
    在这里插入图片描述
    vector有三个iterator变量,分别是start(指向容器的开始)、finish(指向容器中最后一个元素的下一个)和end_of_storage(指向容器的末端的下一个)。当finish与end_of_storage相等时,如果还继续放入元素,则空间会以两倍原先的空间进行扩展,而这种扩展不会在原来的地方进行扩展,而是在别处寻找两倍的空间进行扩展。

    11.1 源码(G2.9)

    在这里插入图片描述
    容器只要是存储在连续空间的就会提供operator[]

    在这里插入图片描述
    在这里插入图片描述
    每次的扩展就会引发大量的拷贝动作,而拷贝就会调用拷贝构造函数。而原来的空间也要释放,所以会调用析构函数。所以每次的扩展会调用大量的拷贝构造函数和析构函数,这是一个大的成本,在使用vector时要注意。

    11.2 vector的迭代器

    vector的迭代器不用设计得太复杂,因为vector内存是连续的,不用像list一样,把iterator单独设计成一个class,原因是list是内存不连续的。而vector的iterator只需要设计成指针就行了。
    在这里插入图片描述

    11.3 G4.9版本

    在这里插入图片描述
    在这里插入图片描述
    G4.9的iterator是一个class,不是一个单纯的指针。
    在这里插入图片描述

    12. array、forward_list(单向链表)深度探索

    12.1 array

    在这里插入图片描述
    G4.9 版本
    在这里插入图片描述

    12.2 forward_list(单向链表)

    在这里插入图片描述

    13. deque(双向队列)、stack和queue的深度探索

    在这里插入图片描述
    deque的底层,有一个vector(也就是图中的map)用来装指针,指针指向buffer(缓冲区),当deque想往前扩充时,只需要创建一个buffer,然后在vector前面的空地方用一个指针指向它就好。往后扩充也是同理。

    deque是一个分段连续的容器,连续是假象,分段是事实。

    13.1 源码

    在这里插入图片描述
    size_type类型的元素大小为4。

    一个deque对象本身占40字节(两个iterator,2*16,加上4+4)。

    13.2 deque的iterator

    在这里插入图片描述

    13.3 deque< T >::insert()

    因为deque可以两端插入,所以在进行元素插入时可以比较聪明。当插入的位置处于deque所有元素的偏前面时,移动的是将插入位置前面的所有元素,而不是去移动后面的,这样做的目的是提高效率,因为每次的移动都需要调用构造函数和析构函数。
    在这里插入图片描述

    在这里插入图片描述

    13.4 deque如何模拟连续空间

    全是deque iterators的功劳。
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述
    set_node()也可以用来往前跳。

    +=和+是实现iterator进行多个元素的跳转。
    在这里插入图片描述
    进行多个元素的跳转有两种情况,第一种是要跳的距离还是在同一个缓冲区内,这种情况就比较简单;第二种情况是跳的距离要跨越缓冲区,这时就得继续需要跨越多少个缓冲区,如何在存储缓冲区指针的vector种,先移动对应的几个,到达对应的缓冲区,然后再移动到对应的位置。

    在这里插入图片描述

    G4.9版本
    在这里插入图片描述
    在这里插入图片描述

    13.5 queue的源码

    在这里插入图片描述
    stack和queue底部容器都是deque,只是封住某些端口。

    13.6 stack的源码

    在这里插入图片描述

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

    在这里插入图片描述
    queue和stack的底层结构默认是deque,但也可以选择list。速度的话,是deque比较快。

    还有一个需要注意的点就是,stack和queue都不允许遍历,所有也不提供iterator。 因为一个是先进后出,一个是先进先出,如果允许在指定位置进行插入的话,就会干扰到原来的模式。

    在这里插入图片描述
    注意: stack还可以选择vector作为底层结构,而queue不能。

    在这里插入图片描述
    还有就是stack和queue不可以选择set或map作为底层结构。因为只要一些函数无法调用,就没办法实现原本的模式。

    14. 容器rb_tree(红黑树)

    在这里插入图片描述
    我们不应该使用 rb_tree 的 iterators 改变元素值(因为元素尤其独特的排列规则),例如图中 6 改成 60 是不被允许的。但编程层面并未杜绝此事。不过这样设计是正确的,因为 rb_tree 是为 set 和 map 服务(作为其底部支撑),而 map 允许元素的 data 部分被改变,只有元素的 key 才是不可被改变的。

    rb_tree 提供两种插入操作:insert_unique() 和 insert_equal()。前者表示插入的 key 在整个 tree 中是独一无二的,否则插入失败;后者表示插入的 key 是可以重复的。

    在这里插入图片描述
    第三个模板参数KeyOfValue表示从value中取出key的方式。

    在这里插入图片描述
    size_typeunsigned_int,所以大小为4字节;link_type是指针,所以大小也为4字节;key_compare是一种仿函数,仿函数是一个大小为0的class,但会给它分配1字节的大小。所以rb_tree整个类的大小为9,编译器的默认对齐方式为4字节,所以它的大小为12字节。

    G4.9的红黑树
    在这里插入图片描述

    15. 容器set和multiset

    value包含key和data。
    set/multiset 元素的 value 和 key 合一:value就是key,相当于就是没有data。

    在这里插入图片描述

    在这里插入图片描述
    identity 就是将key看成value,取value时,返回key。

    15.1 set in VC6

    在这里插入图片描述
    VC6里的_Kfn等同于identity()。

    16. 容器map和multimap

    在这里插入图片描述

    在这里插入图片描述
    map不允许通过迭代器来修改key,它是通过在pair中,直接让key为const。

    16.1 map in VC6

    在这里插入图片描述
    VC6里没有select1st(),这个函数的作用是查找容器中的对应的key。

    16.2 multimap

    在这里插入图片描述
    multimap不可使用 [] 做插入操作。
    在这里插入图片描述
    当用[]想map插入元素时,首先会先检查map里有没有该value,如果有,则用 iterator 指向其中第一个value(如果是multimap的话,有多个value相等的);如果没有,则 iterator 会找到最适合插入该value的位置。

    17. 容器hashtable(哈希表)

    在这里插入图片描述

    18. 迭代器的分类

    在这里插入图片描述
    算法看不见容器,对其一无所知。所以,它所需要的一切信息都必须从迭代器(iterators)取得,而 iterarors (有containers提供)必须能完成 algorithm 的要求,才能搭配该 algorithm 的所有操作。

    在这里插入图片描述
    迭代器的分类如上图所示,使用结构体来表现,而且有的还是继承关系。

    注意上面的连线,Forward-List(单向链表)用的迭代器是forward_iterator_tag(单向迭代器);Array、Vector、Deque(虽然它底层不是完全连续) 用的迭代器是random_access_iterator_tag(随机访问迭代器);List(双向链表)、Set/Multiset、Map/Multimap 用的迭代器是 bidirectional_iterator_tag(双向迭代器)。

    unordered_set/multiset 和 unordered_map/multimap 视情况而定。

    18.1 自己编写能够得到容器迭代器类型的代码

    当我们想要用代码来识别当前容器的迭代器是什么类型时,可以使用下图这种方法,使用萃取机(iterator_traits< I >::iterator_category)来获得当前容器的迭代器的类型。根据编写函数重载的函数,就可以输出对应容器迭代器的类型。
    在这里插入图片描述
    注意: istream_iterator 的类型就是input_iterator;ostream_iterator 的类型就是 output_iterator。

    18.2 利用标准库自身的标识(各种容器的迭代器的iterator_category 的typeid)

    要记得包含同文件 #include< typeinfo > 。
    在这里插入图片描述

    18.3 迭代器分类对算法的影响

    18.3.1 一个例子(求两个迭代器之间的距离)

    在这里插入图片描述
    distance()是一个算法,用于计算两个迭代器之间的距离。但它的距离的类型不是 int,迭代器中有一种数据类型是用来代表两个迭代器之间的距离的(difference_type)。所以要把我们的迭代器类型传给萃取机,让萃取机帮我们找到对应的 difference_type,所以这个函数的返回类型是 iterator_traits< InputIterator >::difference_type。

    它有一个对外开放的版本,底部调用就有两个版本,根据迭代器的类型来选择对应的函数。注意上面代码中的 category() ,这种语法的使用,就是一个临时对象,type name加上括号可以代表一个临时对象。

    在数据很多的情况下,使用第二个 distance 会比第一个快很多。

    这这里就可以看出,为什么迭代器类型要用结构体来表示而不是用枚举类型?
    使用结构体来表示迭代器类型可以方便地封装相关数据和行为,并实现多态性,从而提高代码的灵活性和可复用性。例如上面的例子,五种迭代器类型,而只有两种迭代器类型的函数,这时,如果是 forward_iterator_tag 或者是 bidirectional_iterator_tag 类型就会直接调用第一种函数。

    18.3.2 一个例子(让迭代器移动对应的距离)

    在这里插入图片描述
    跟上面是一样的道理。

    18.3.3 另一个例子(拷贝函数)

    在这里插入图片描述
    上面memmove()是C语言的一个拷贝字节的函数,属于低阶动作,速度较块;当最后判断是< T*,T* >或< const T*,T* >的话,就会经过 Type Traits 的判断,如果是不重要的(has trivial op=())话,就调用 mommove()处理,如果是重要的就用__copy_d()处理。

    总之就是根据参数的类型去调用最快的函数。

    18.3.4 另一个例子(destroy)

    在这里插入图片描述

    19. 算法源码剖析(11个例子)

    20. 仿函数(Functors)

    functors 是六大部件中大多时候比较会自己编写的一种,就是写一些能融入到STL(标准库)里的东西就是这种东西。 仿函数又称函数对象,专门为算法服务,当算法需要一些指定的规则(操作)时,就可以使用functors;例如,sort算法中我们可以自己指定排序的顺序,就可以创建一个functors,然后把functors传进去。

    在这里插入图片描述

    functors有三大类:算术类、逻辑运算类和相对关系类(比大小)。仿函数的特点就是重载小括号运算符。

    在这里插入图片描述

    20.1 举个例子

    在这里插入图片描述
    当我们自己实现的仿函数不继承STL里的类时就没有融入到STL里去,其实标准库中的 functors 都会有继承关系,虽然上面 myobj 和 less< int >() 实现的效果是一样,但 myobj 没有继承 STL 中的 binary_function或unary_function,就没有融入到STL中去,虽然上面执行没问题,但如果再执行某些操作,就会出现问题。因为它没有达到STL的全部要求。

    20.2 为什么要继承(functors的可适配条件)

    在这里插入图片描述
    STL规定每个可适配的函数都应挑选上面两个其中的一个来继承,因为函数的适配器(Adapter)会向仿函数提问题。可适配的意思就是可以被修改以达成某种操作。

    unary_function适合单操作数的情况,binary_function适合双操作数的情况。这两个类都没有成员变量和成员函数,所以理论上大小为0,但编译器会让它为1。当它们作为父类被继承时,给子类增加的大小就为0。

    在这里插入图片描述
    所以 less 继承 binary_function 后没有任何坏处,所增加的大小为0,而且有了 binary_function 里的三种数据类型,这三个数据类型可以回答适配器的问题。

    所以当我们想要我们写的 functors 可以被修改,被适配就需要选择上面两种中合适的一种去继承。

    总结: 仿函数就是一个类或结构体里面重载小括号,这样的类或结构体创建出来的对象就叫做函数对象或仿函数。在大大小小的算法中,我们为了搭配算法就会写出相应的仿函数出来。但如果我们没有去继承上面两种类的话,在未来就没有被改造的机会。如果我们想要我们写的 functors 能融入到STL的体系中去,就必须选择其中合适的一种去继承。

  • 相关阅读:
    智哪儿评测:既要便捷、安全和智能,也要颜值:萤石极光人脸视频锁Y3000FV体验评测
    软件设计模式系列之二十四——模板方法模式
    Stylegan3卡在Setting up PyTorch plugin “upfirdn2d_plugin“...怎么办?一招教你解决!
    虹科分享 | 软件供应链攻击如何工作?如何评估软件供应链安全?
    代码随想录算法训练营第五十天|股票问题专题(2)
    java计算机毕业设计ETC用户自驾游推荐系统MyBatis+系统+LW文档+源码+调试部署
    Java线程池ThreadPoolExecutor详解(一篇就够了)
    P2444 [POI2000] 病毒
    毕业设计 深度学习OCR中文识别 - opencv python
    SSM+优秀宿舍评选系统毕业设计-附源码221511
  • 原文地址:https://blog.csdn.net/weixin_45604295/article/details/134042303