zoukankan      html  css  js  c++  java
  • C++STL

    STL,全称standard template library,标准模板库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,STL 基本上达到了各种存储方法和相关算法的高度优化。

    三个最为普遍的STL版本:

    1. HP STL

      其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。

    2. P.J.Plauger STL

      由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。

    3. SGI STL

      由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

    我们一般使用的STL也是SGI STL

    源码:https://blog.csdn.net/jmw1407/category_10122107.html

    STL的六大部件和联系

    • 容器(containers)
    • 算法(algorithm)
    • 迭代器 (iterator)
    • 适配器 (adapter)
    • 分配器 (allocator)
    • 仿函数 (functor)

    容器通过分配器获取数据存储空间,算法通过迭代器存取容器数据,仿函数协助算法完成不同策略,适配器修饰仿函数

    容器:各种数据结构,来存放数据,从实现角度来看是一种类模板;

    算法:各种算法的封装,从实现角度来看是一种函数模板;

    迭代器:是容器与算法之间的胶合剂,即泛型指针;

    仿函数:一般函数指针可视为狭义的仿函数;

    空间配置器:负责空间的配置与管理;

    适配器(配接器):用来修饰容器或仿函数的迭代器接口。通俗的讲,就是像改锥的接口,装上不同的适配器,才能装上不同的改锥头。

    容器(containers)

    当程序中存在对时耗要求很高的部分时,数据结构的选择就显得尤为重要,有时甚至直接影响程序执行的成败。

    为了重复利用已有的实现好的数据结构,引入了STL内部封装好的数据结构-容器,需要包含各自头文件

    STL的容器主要利用了C++的模板特性来实现。需要注意:

    • 容器缓存了节点,节点类要确保支持拷贝(否则出现浅拷贝问题,导致崩溃)
    • 容器中的一般节点类,需要提供拷贝构造函数,并重载等号操作符(用来赋值)
    • 容器在插入元素时,会自动进行元素的拷贝。

    容器详解:https://www.jianshu.com/p/497843e403b4

    常用成员函数

    • begin:返回指向容器中第一个元素的迭代器
    • end:返回指向最后一个元素所在位置后一个位置的迭代器
    • size:返回已经保存的元素数目
    • capacity:返回当前容器容量,capacity永远不可能小于size,只要小于,就增加一半并向下取整,值得注意的是,至少增加1
    • max_size:返回容器最大支持容量,这个数是一个很大的常整数,可以理解为无穷大。这个数目与平台和实现相关
    • empty:判断容器是否为空
    • reserve:修改的是capacity的大小,无法调整为比size小的数(调整比原来size小视为无效操作)
    • resize:修改的是size的大小,未赋值的默认为0,
      • 可以调整为比capacity大的数,此时会进行重新配置
      • 缩小和扩大容器时,可能导致迭代器,指针,引用失效

    容器有些可以使用下标访问,使用方式如数组,但可能发生访问越界,使用at则会检查

    容器的大小size一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效

    内存的重新配置耗时较多,遵循:不到万不得已,不要重新分配内存原则。可通过Reserve()保留适当容量或者创建容器时提供足够空间

    std::vector<int> v;				//1.这是空的vector
    std::vector<int> v(5);			//2.这是一个capacity为5,size为5,全默认为0的容器
    std::vector<int> v{0,0,0,0,0}	//3.等价于2
    vector<int> v(5,1);				//4.这是一个capacity为5,size为5,全为1的容器
    

    序列化容器

    共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序。

    array、vector、deque、list 和 forward_list

    容器 特性 底层实现
    array 固定大小数组 数组
    vector 可变大小数组1.支持快速随机访问 2.顺序存储 3.迭代器失效(插入,删除,扩容时) 数组
    List 支持快速增删 双向链表
    forward_list 只支持单向顺序访问 单向链表
    queue 插入只可以在尾部进行,删除、检索和修改只允许从头部进行。按照先进先出的原则。 默认deque
    stack 操作的项是最近插入序列的项,按照后进先出的原则 默认deque
    deque 支持首尾(中间不能)快速增删,也支持随机访问,底层数据结构为一个中央控制器和多个缓冲区 双向队列

    通常,使用vector是最好的选择

    vector
    底层结构

    vector占用一块连续分配的内存,一种可以存储任意类型的动态数组。使用 3 个迭代器(指针):

    Myfirst 指向的是 vector 容器对象的起始字节位置;Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。

    扩容的本质

    另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

    1. 完全弃用现有的内存空间,重新申请更大的内存空间;
    2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
    3. 最后将旧的内存空间释放。

    遍历

    • []方式,如果越界或出现其他错误,不会抛出异常,可能会崩溃,可能数据随机出现
    • at方式,如果越界或出现其他错误,会抛出异常,需要捕获异常并处理

    常见操作

    • 插入:

      • insert函数,结合迭代器位置插入指定的元素。如果迭代器位置越界,会抛出异常。
      • emplace() 是C++11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。
      • 不同点
        • insert可以一次插入多个,但emplace只能一次插入一个
        • emplace() 在插入元素时,是在容器的指定位置直接构造元素,效率更高
        • 通过 insert() 函数向 vector 容器中插入 testDemo 类对象,需要调用类的构造函数和移动构造函数;先单独生成,再将其复制(或移动)到容器中
        • 当拷贝构造函数和移动构造函数同时存在时,insert() 会优先调用移动构造函数。
    • 删除

      • erase(iterator)函数,用迭代器指定某个位置或区域删除;删除后会返回当前迭代器的下一个位置。

      • clear():删除 vector 容器中所有的元素,使其变成空的 vector 容器。

        • 该函数会改变 vector 的size=0,但其容量capacity仍然保存不变
        • 当存储元素是object时,会调用析构函数;当存储元素是指针时,并不会调用这些指针所指对象析构函数(所以指针注意自己delete)
        • 如果你想同时做到清空vector的元素和释放vector的容量, 你可以使用swap
        • 使vector离开其自身的作用域,从而强制释放vector所占的内存空间,总而言之,释放vector内存最简单的方法是vector<int>.swap(v1)
    • remove() 的实现原理是,在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做上标记,等待找到新的非目标元素将其覆盖。

      • vector<int>demo{ 1,3,3,4,3,5 };
        //交换要删除元素和最后一个元素的位置
        auto iter = std::remove(demo.begin(), demo.end(), 3);
        
        
      - 在对容器执行完 remove() 函数之后,由于该函数**并没有改变容器原来的大小和容量**,因此无法使用之前的方法遍历容器,而是需要向程序中那样,借助 remove() 返回的迭代器完成正确的遍历。
      
        - 因此,如果将上面程序中 demo 容器的元素全部输出,得到的结果为 `1 4 5 4 3 5`。
      
    迭代器失效

    迭代器失效是对容器insert ,push_back,erase操作不当引起的。

    erase分为两种情况

    第一种情况:

    使用erase删除某一个结点之后,vector迭代器虽然还是指向当前位置,而且也引起了元素前挪,但是由于删除结点的迭代器就已经失效,指向删除点后面的元素的迭代器也全部失效,

    所以不能对当前迭代器进行任何操作;需要对迭代器重新赋值或者接收erase它的返回值;(即当前迭代器失效,需重新赋值)

    第二种情况:erase返回擦除的当前点的下一个位置,越界了。

    push_back造成扩容后迭代器完全失效,需从头开始

    list

    底层双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据

    • distance函数可以求出当前的迭代器指针it距离头部的位置,也就是容器的指针
      • 用法: distance(v1.begin(), it)
    • 删除
      • erase是通过位置或者区间来删除,主要结合迭代器指针来操作
      • remove是通过值来删除
    deque

    非连续存储结构,存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。

    提供了两级数组结构, 第一级类似vector的动态数组(map),存储指针,指向那些真正用来存储数据的各个连续空间,;另一级连续存放实际元素。扩容与vector类似

    迭代器实现

    1. 迭代器在遍历 deque 容器时,必须能够确认各个连续空间在 map 数组中的位置;
    2. 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中。
    template<class T,...>
    struct __deque_iterator{
        ...
        T* cur;
        T* first;
        T* last;
        map_pointer node;//map_pointer 等价于 T**
    }
    

    deque相当于vector与list的折中

    • 如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
    • 如果你需要大量的插入和删除,而不关心随机存取,则应使用list
    • 如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque

    关联式容器

    STL为什么选择红黑树而不是AVL-tree?

    红黑树的平衡性是比AVL-tree弱的,但是搜索效率几乎相等。两者的插入和删除操作都是O(logn),但是就旋转操作而言,AVL-tree是O(n),而红黑树是O(1),任何不平衡都会在三次旋转之内解决

    • 红黑树适合查找
    • 哈希表适合增删,增删时间复杂度接近O(1)

    根据红黑树的原理,可以实现O(lgn)的查找,插入和删除。

    容器 特性 底层结构
    set 存储的关键字就是value,有序,不重复 红黑树
    multiset 存储的关键字就是value,有序,可重复 红黑树
    map 存储的是有序,不重复 红黑树
    multimap 存储的是有序,可重复 红黑树

    map与set的insert

    insert返回的是pair<iterator, bool>类型,pair的第一个属性表示当前插入的迭代器的位置,第二个属性表示插入是否成功的bool值

    map

    只有map有[]操作符

    当key在map中存在时,[]完成的是读操作。当key不存在是,[]完成一个写操作。写入一个新的pair<key,T()>。

    为啥有unordered_map,还需要map

    • log(n)不一定比常数大
    • 哈希表是以空间换时间,需要内存消耗;且hashtable创建需要时间
    • 如果考虑效率,特别当元素达到一定数量级时,用hash_map。
    • 考虑内存,或者元素数量较少时,用map。

    hashtable如何避免地址冲突?

    1)线性探测:先用hash function计算某个元素的插入位置,如果该位置的空间已被占用,则继续往下寻找,知道找到一个可用空间为止。

    其删除采用惰性删除:只标记删除记号,实际删除操作等到表格重新整理时再进行。

    2)二次探测:如果计算出的位置为H且被占用,则依次尝试H+12,H+22等(解决线性探测中主集团问题)。

    3)链表:每一个表格元素中维护一个list,hash function为我们分配一个list,然后在那个list执行插入、删除等操作。

    总结

    vector deque list set mutliset map multimap
    内存结构 单端数组 双端数组 双向链表 红黑树 红黑树 红黑树 红黑树
    随机存取 对key而言是
    查找速度 非常慢 对key而言快 对key而言快
    插入删除 尾端 头尾两端 任何位置 - - - -

    deque可以看成vector和list的折中,随机存取比vector慢,

    无序关联容器

    又叫哈希容器,底层结构是hash table

    在已提供有 4 种关联式容器的基础上,又新增了各自的“unordered”版本(无序版本、哈希版本),提高了查找指定元素的效率。

    即unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。

    STL中的hashtable使用的是开链法解决hash冲突问题

    集合 底层实现 是否有序 数值是否可重复 能否修改数值 查询效率 增删效率
    set 红黑树 有序 O(logn) O(logn)
    multiset 红黑树 有序 O(logn) O(logn)
    unordered_set 哈希表 无序 O(1) O(1)
    映射 底层实现 是否有序 数值是否可重复 能否修改数值 查询效率 增删效率
    map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn)
    multimap 红黑树 key有序 key可重复 key不可修改 O(logn) O(logn)
    unordered_map 哈希表 key有序 key不可重复 key不可修改 O(1) O(1)

    算法(algorithm)

    算法部分主要由头文件组成。

    其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等

    STL中算法大致分为四类:
    1)非可变序列算法:指不直接修改其所操作的容器内容的算法。
    2)可变序列算法:指可以修改它们所操作的容器内容的算法。
    3)排序算法:对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作。
    4)数值算法:对容器内容进行数值计算。

    对集合的查找,最好不要用通用函数find(),它对集合使用的时候,性能非常的差,最好用集合自带的find()函数,它针对了集合进行了优化,性能非常的高。

    sort算法要求随机访问迭代器,包括array,deque,string,vector

    迭代器 (iterator)

    尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,所以对不同容器数据进行遍历的操作方法应该是类似的。

    可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开,并且不需暴露该对象的内部表示。

    因此引入中介-迭代器,泛型指针,是一种智能指针,重载了*,++,==,!=,=运算符。

    标准库为每一种标准容器定义了一种迭代器类型,容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。

    基础迭代器分为

    迭代器名 功能 应用
    输入迭代器 只读不写,单遍扫描,只能递增 istream
    输出迭代器 只写不读,单遍扫描,只能递增 ostream,inserter
    前向迭代器 可读写,多遍扫描,只能递增 forward_list
    双向迭代器 可读写,多遍扫描,可递增递减 list,set,multiset,map,multimap
    随机访问迭代器 随机读写 vector,deque,array,string
    //创建vector
    vector<int> v1;
    //遍历-迭代器遍历
    for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    
    //遍历-迭代器逆向遍历
    for (vector<int>::reverse_iterator it = v1.rbegin(); it != v1.rend(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    

    适配器(adapter)

    一种用来修饰容器(container)或仿函数(functor)或迭代器(iterator)接口的东西。

    • 改变functor接口者,称为functor adapter,

    • bind

    • 改变container接口者,称为container adapter

      • 封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。

      • 底层可以更换不同容器

        //可以自定义底层容器
        std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
        
      • 分类

        • stack 栈适配器,默认deque
        • queue 队列适配器,默认deque
          • push(x) -- 将一个元素放入队列的尾部。
          • pop() -- 从队列首部移除元素。
          • peek() -- 返回队列首部的元素。
          • empty() -- 返回队列是否为空。
        • priority_queue 优先权队列适配器,默认vector容器,堆存储结构
          • top 访问队头元素
          • empty 队列是否为空
          • size 返回队列内元素个数
          • push 插入元素到队尾 (并排序)
          • emplace 原地构造一个元素并插入队列
          • pop 弹出队头元素
          • swap 交换内容
        #include<queue>//头文件
        priority_queue<Type, Container, Functional>//定义
        //对于基础类型 默认是大顶堆
        priority_queue<int> a; 
        //等同于 priority_queue<int, vector<int>, less<int> > a;
        priority_queue<int, vector<int>, greater<int> > c;  //这样就是小顶堆
        
        
        //STL中的仿函数less<T>和greater<T>的使用范围仅限于基本类型,当优先队列需要保存我们自定义的数据类型时,
        //我们需要重载小于号(operator <)或者重写仿函数
        //注意:小顶堆>,大顶堆<
        //方法1:重写仿函数
        struct mycomparison 
        {
        	bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) 	  	
            {
                return lhs.second > rhs.second;//小顶堆
            }
        };
        //方法2:类运算符重载<,就无需更换仿函数
        struct tmp1 
        {
            int x;
            tmp1(int a) {x = a;}
            bool operator<(const tmp1& a) const
            {
                return x < a.x; //大顶堆
         	}
        };
        
        priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison>pri_que;
        priority_queue<tmp1>pri_que2;
        
    • 改变iterator接口者,称为iterator adapter。

      • 迭代器适配器模板类的内部实现,是通过对以上 5 种基础迭代器拥有的成员方法进行整合、修改,甚至为了实现某些功能还会添加一些新的成员方法。
      • 分类
        • 反向迭代器(reverse_iterator)
        • 插入型迭代器(insert_iterator)
        • 流迭代器(istream_iterator / ostream_iterator)流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator)
        • 移动迭代器(move_iterator)

    分配器(allocator)

    负责空间配置与管理。是一个实现了动态空间配置、空间管理、空间释放的class template。

    STL的内存配置器在我们的实际应用中几乎不用涉及,它只是在背后默默为各种容器做了大量工作,为容器分配并管理内存,实现统一内存管理

    SGI-STL的空间配置器有2种,默认的空间配置器是第二级的配置器。

    1. 仅仅对c语言的malloc和free进行了简单的封装,
    2. 设计到小块内存的管理等,运用了内存池技术

    alloc

    SGI使用时std::alloc作为默认的配置器

    • alloc把内存配置和对象构造的操作分开,分别由alloc::allocate()::construct()负责
    • 内存释放和对象析够操作也被分开分别由alloc::deallocate()和::destroy()负责

    这样可以保证高效,因为对于内存分配释放和构造析构可以根据具体类型(type traits)进行优化。

    内存分配

    STL内存分配分为两级

    • 当申请的内存大于128byte时,就启动第一级分配器,通过malloc直接向系统的堆空间分配
    • 当申请的内存小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表free_list组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。

    8~128byte,刚好到128个字节,这就是为什么大于128bytes要使用第一级配置器了,因为,第二级配置器最多只能分配128bytes,

    原因

    • 小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,从而更快分配小对象内存
    • 避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。

    仿函数(functor)

    又叫做函数对象,可以实现类似于函数一样的对象类型,函数最直接的调用形式就是:返回值 函数名(参数列表),仿函数实现了operator()操作符,使用类似于函数

    一般函数指针、回调函数可视为狭义的仿函数。以操作数的个数划分,可分为一元和二元仿函数

    为什么要使用仿函数呢?

    1).仿函数比一般的函数灵活,且可以实现代码复用,又不必维护公共变量

    2).仿函数有类型识别,可以作为模板参数。

    3).执行速度上仿函数比函数和指针要更快的。

    class less 
    { 
    	bool operator()(int a,int b){ 
    		return a<b?; 
    	} 
    }; 
    int main()
    { 
    	int a=1,b=2; 
    	bool isLess = less (a,b); 
    	cout <<isLess<< '
    '; 
    	return 0; 
    }
    

    如果要自定义仿函数,并且用于STL接配器,那么一定要从binary_function或者,unary_function继承。

    其实binary_function只是做一些类型声明而已,但为了方便,安全,可复用性

  • 相关阅读:
    springboot基本注解
    Mybatis之简单注解
    java再次学习
    在线html编辑器
    分享
    cyberduck的SSH登录
    ie67的冷知识
    css特效
    小程序分享
    css特效博客
  • 原文地址:https://www.cnblogs.com/AMzz/p/14731545.html
Copyright © 2011-2022 走看看