• 【C++ 系列笔记】06 C++ STL


    STL

    介绍

    简介

    (Standard Template Library)标准模板库

    广义上分为三类:

    • 容器(container)
    • 算法(algorithm)
    • 迭代器(iterator)

    六大组件:

    • 容器

      一种类模板,有各种数据结构。

    • 算法

      一种函数模板,有各种常用算法。

    • 迭代器

      一种类模板,迭代器是容器与算法之间的胶合剂。

    • 仿函数(伪函数)

      一种类模板,用来协助算法完成不同的策略。

    • 适配器(配接器)

      一种用来修饰容器或者仿函数或迭代器接口的东西。

    • 空间配置器

      一种类模板,负责空间的配置和管理。

    容器

    两类:

    • 序列式容器

      如 Vector、Deque、List 等,序列式容器强调值的顺序,每个元素都有物理意义上固定的系列关系。

    • 关联式容器

      如 Set/multiset、Map/multimap 等,关联式容器是非线性的容器,元素之间没有顺序关系,例如树结构。

      JSON 就是关联式容器。

    算法

    两类:

    • 质变算法

      质变算法是运算过程会改变元素内容的算法,例如拷贝,替换,删除。

    • 非质变算法

      非质变算法是运算过程不会改变元素内容的算法,例如查找、计数、遍历。

    迭代器

    迭代器是依顺序访问某个容器所含的各个元素的方法。

    种类:

    • 输入迭代器

      提供对数据的只读访问,支持 ++、==、!=

    • 输出迭代器

      提供只写访问,支持 ++

    • 前向迭代器

      提供读写访问,并且可以向前推进迭代器,支持 ++、==、!=

    • 双向迭代器(使用较多)

      提供读写访问,并且可以前后推进迭代器,支持 ++、==

    • 随机访问迭代器(使用较多)

      提供读写访问,并可以跳跃方式访问容器的任意数据,功能最强,

      支持 ++、–、[n]、-n、<、<=、>、>=

    针对实现来说,有几种类型的迭代器:

    • iterator 普通迭代器
    • reverse_iterator 逆序迭代器
    • const_iterator 只读迭代器

    常用容器

    string 容器

    • assign 方法

      string& assign(const char* s,int n);
      // 将字符串 s 的前 n 个字符赋值给当前字符串
      
      string& assign(const strubg& s,int start, int n);
      // 将字符串 s 从 start 开始的 n 个字符赋值给当前字符串,n 从 0 开始
      
    • 字符串存取

      char& operator[](int n);
      // 访问越界就崩了
      char& at(int n);
      // 访问越界会抛出异常
      
    • 字符串查找

      正找:

      int find(const string& str, int pos = 0) const;
      // 从 pos 开始查找,返回 str 在当前字符串中第一次出现的位置。
      
      int find(const string& str, int pos, int n) const;
      // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中第一次出现的位置。
      

      倒找:

      int rfind(const string& str, int pos = npos) const;
      // 从 pos 开始查找,返回 str 在当前字符串中最后一次出现的位置。
      
      int rfind(const string& str, int pos, int n) const;
      // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中最后一次出现的位置。
      

      找不到返回 -1

    • 字符串替换

      string& replace(int pos, int n, const string& str);
      // 替换从 pos 开始的 n 个字符为字符串 str
      
    • 字符串截取(子串)

      string substr(int pos = 0, int n = npos) const;
      // 返回从 pos 开始的 n 个字符组成的字符串
      

      应用:

      获取两文本中间

      string getMiddleStr(string source,string left, string right) {
        return source.substr(source.find(left),
                             source.find(right) - source.find(left));
      }
      
    • 字符串插入和删除

      string& insert(int pos, const string& s);
      // 将 s 插入到当前字符串的 pos 位置后
      
      string& erase(int pos, int n = npos);
      // 删除从 pos 开始的 n 个字符
      
    • C-style 字符串转换

      const char* p = str.c_str();
      

    vector 容器

    (单端数组、动态数组)

    vector 内部维护一段线性空间,当空间不足时会额外开辟一段连续的更大的空间,然后进行数据的拷贝,最后丢弃原空间。

    他被称为单端数组,是因为其一端封闭,数据仅在另一端成长,这导致其头插数据的开销非常大,这个问题可由 deque 容器解决,deque 容器是双端数组,而且其拥有更小的扩容开销。

    • 头文件

      #include <vector>
      
    • 获得一个容器(构造函数)

      vector<type> v;
      // 默认构造
      
      vector<type> v(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      

      注意!end 指向的是尾元素的下一个位置。

      vector<type> v(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 数据操作

      • 拷贝

        void assign(type* begin, type* end);
        // 将 begin 到 end 指向的内存范围拷贝给自身
        
        void assign(size_t n, type elem);
        // 将 n 个 elem 拷贝给自身
        
      • 交换

        void swap(vector& v2);
        // 互换两容器的内容
        

        swap 的应用:收缩内存

        vector 在缩小长度的时候并不会缩小容量(这个结论后面会通过代码检验),那么饿我们想要收缩容量,可以使用 swap。

        vector<int>(oldV).swap(oldV);
        

        resize 后,尾指针将移动至新范围的尾部,此时通过拷贝构造实例化的新对象便仅拷贝新范围内的数据,然后通过 swap 来交换容器的指针,就拿到收缩了内存的容器。

        此后匿名对象会自行销毁。

      • 是否为空

        bool empty();
        
      • 获取容量

        size_t capacity()
        
      • 获取大小

        size_t size()
        
      • 改变大小

        void resize(size_t newSize, type val);
        // 若容器变大,则多余位置由 val 填充
        
        void resize(size_t newSize);
        // 若容器变大,则多余位置由 0 填充
        
      • 预留内存

        void reserve(const size_t newcapacity);
        
      • 数据存取

        直接访问:

        type& operator[](size_t n);
        // 访问越界就崩了
        type& at(size_t n);
        // 访问越界会抛出异常
        

        获取首尾元素:

        type& front();
        // 返回容器的首个元素的引用
        
        type& back();
        // 返回容器的尾元素的引用
        

        尾插尾删:

        void push_back(type& ele);
        void pop_back();
        

        数据插入:

        iterator insert(iterator where, size_t count, type val);
        // 从 where 处前插 count 个值 val
        
        iterator insert(iterator where, type val);
        // 从 where 处前插 1 个值 val
        
        iterator insert(iterator where, iterator first, iterator last);
        // 将 first 到 last 范围内的数据插入至 where
        

        数据删除:

        iterator erase(iterator first, iterator last);
        // 删除迭代器从 first 到 last 之间的元素
        
        iterator erase(iterator where);
        // 删除迭代器指向的元素
        
        void clear();
        //删除容器中所有元素
        
    • 迭代器

      • 先拿到迭代器

        // 首指针
        vector<int>::iterator itBegin = v.begin();
        // 尾指针(最后一个元素的下一个位置)
        vector<int>::iterator itEnd = v.end();
        
      • 方法一

        vector<int>::iterator itBegin = v.begin();
        vector<int>::iterator itEnd = v.end();
        while (itBegin != itEnd) {
          cout << *itBegin++ << endl;
        }
        
      • 方法二

        for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
          clog << *it << endl;
        }
        
      • 方法三:算法

        #include <algorithm>
        // ...
        void callback(int value) {
          cout << value;
        }
        // ...
        for_each(v.begin(), v.end(), callback);
        
      • 方法四:

        for (auto& ele : v) {
          cout << ele << endl;
        }
        

        这是基于范围的 for 循环,C++11 的语句。

        另:

        为了能跟写 python 和 js 一样爽,我实现了一个 range 类。

        class range {
         private:
          long* startp;
          long* endp;
        
         public:
          long* begin() { return this->startp; }
          long* end() { return this->endp; }
          range(long min, long max) {
            if (min > max) {
              long temp = min;
              min = max;
              max = temp;
            }
            this->startp = new long[max - min + 2];
            this->endp = startp + max - min + 1;
            for (long ele = min; ele <= max; ele++) {
              startp[ele - min] = ele;
            }
          }
          ~range() { delete[] startp; }
        };
        

        当我们想要循环指定次数时再也不用写一长串了,只需要简单的:

        for (auto& ele : range(10, 20)) {
          cout << ele << endl;
        }
        
      • 逆序遍历:

        拿到迭代器 rbegin()rend()

        // 首指针
        vector<int>::reverse_iterator itBegin = v.rbegin();
        // 尾指针(最后一个元素的下一个位置)
        vector<int>::reverse_iterator itEnd = v.rend();
        

        迭代方法不变,例如:

        vector<int>::reverse_iterator itBegin = v.rbegin();
        vector<int>::reverse_iterator itEnd = v.rend();
        while (itBegin != itEnd) {
          cout << *itBegin++ << endl;
        }
        
      • 随机访问迭代器

        vector 的迭代器是随机访问迭代器,要判断某迭代器是否是随机访问迭代器,可用下述代码测试:

        iterator it = foo.begin();
        it = it + 3;
        

        编译通过说明是随机访问迭代器,不通过说明不是。

    • vector 的空间分配策略:

      vector 维护的是一段线性空间,当空间不足时会开辟一段连续的更大的空间,然后进行数据的拷贝。

      扩展空间时,扩展量大致是当前空间的 1.5 倍。

      不会缩小空间。

      测试代码:

      #include <iostream>
      #include <vector>
      using namespace std;
      int main() {
        vector<int> v;
        int oldCapacity = 0;
        // 添加数据
        for (auto& val : range(1, 100000)) {
          v.push_back(val);
          if (oldCapacity != v.capacity()) {
            cout << v.capacity() << endl;
          }
          oldCapacity = v.capacity();
        }
          
        cout << endl;
        // 删除数据
        for (auto& val : range(1, 100000)) {
          v.pop_back();
          if (oldCapacity != v.capacity()) {
            cout << v.capacity() << endl;
          }
          oldCapacity = v.capacity();
        }
      
        system("pause");
        return EXIT_SUCCESS;
      }
      

      输出:

      1
      2
      3
      4
      6
      9
      13
      19
      28
      42
      63
      94
      141
      211
      316
      474
      711
      1066
      1599
      2398
      3597
      5395
      8092
      12138
      18207
      27310
      40965
      61447
      92170
      138255

      请按任意键继续. . .

    • 注意!

      vector 分配空间的策略如此,当容器扩展后,所有迭代器将失效。

    deque 容器

    (双端数组、没有容量)

    deque 是双端数组,其成长方向有两个,这使得其前后插入数据的开销是一个常数,解决了 vector 头插开销令人难以接受的问题。

    deque 容器的扩容也与 vector 非常不同,vector 是不断地申请新空间,不断地对数据进行拷贝迁移。

    而 deque 则是将新申请到的空间串接在一端,其内部维护着这些分段空间,维持着整体连续的假象。这也造成了其代码实现要比 vector 或 list 多得多,迭代器的架构也非常复杂。

    • 头文件

      #include <deque>
      
    • 获得一个容器(构造函数)

      deque<type> v;
      // 默认构造
      
      deque<type> v(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      

      注意!end 指向的是尾元素的下一个位置。

      deque<type> v(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 数据操作

      • 拷贝

        void assign(type* begin, type* end);
        // 将 begin 到 end 指向的内存范围拷贝给自身
        
        void assign(size_t n, type elem);
        // 将 n 个 elem 拷贝给自身
        
      • 交换

        void swap(deque& v2);
        // 互换两容器的内容
        
      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 改变大小

        void resize(size_t newSize, type val);
        // 若容器变大,则多余位置由 val 填充
        
        void resize(size_t newSize);
        // 若容器变大,则多余位置由 0 填充
        
      • 数据存取

        直接访问:

        type& operator[](size_t n);
        // 访问越界就崩了
        type& at(size_t n);
        // 访问越界会抛出异常
        

        获取首尾元素:

        type& front();
        // 返回容器的首个元素的引用
        
        type& back();
        // 返回容器的尾元素的引用
        

        双端插入:

        void push_back(type& ele);
        void push_front(type& ele);
        void pop_back();
        void pop_front();
        

        数据插入:

        iterator insert(iterator where, size_t count, type val);
        // 从 where 处前插 count 个值 val
        
        iterator insert(iterator where, type val);
        // 从 where 处前插 1 个值 val
        
        iterator insert(iterator where, iterator first, iterator last);
        // 将 first 到 last 范围内的数据插入至 where
        

        数据删除:

        iterator erase(iterator first, iterator last);
        // 删除迭代器从 first 到 last 之间的元素
        
        iterator erase(iterator where);
        // 删除迭代器指向的元素
        
        void clear();
        // 删除容器中所有元素
        
    • 迭代器

      使用方法与 vector 相同,均为随机访问迭代器。

    stack 容器

    stack(栈),是一个栈结构的容器,不提供迭代器,也不提供遍历的方法。

    stack 容器允许新增元素,移除元素,取得栈顶元素。

    • 头文件

      #include <stack>
      
    • 获得一个容器(构造函数)

      stack<type> v;
      // 默认构造
      
    • 数据操作

      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 数据存取

        void push(type& elem);
        // 压栈
        
        void pop();
        // 弹栈
        
        type& top();
        // 返回栈顶元素的引用
        
    • 迭代器

      stack 没有迭代器

    queue 容器

    queue (队列),是一个队列结构的容器,不提供迭代器,也不提供遍历的方法。

    queue 容器允许从一端新增元素,从另一端移除元素。

    • 头文件

      #include <queue>
      
    • 获得一个容器(构造函数)

      queue<type> q;
      // 默认构造
      
    • 数据操作

      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 数据存取

        void push(type& elem);
        // 往队尾添加元素
        
        void pop();
        // 从队头移除第一个元素
        
        type& back();
        // 返回最后一个元素
        
        type& front();
        // 返回第一个元素
        
    • 迭代器

      queue 没有迭代器

    list 容器

    (双向循环链表)

    list 容器是双向循环链表,插入和移除数据的开销是常数。

    它提供的迭代器是双向迭代器,而不是随机访问迭代器。由于链表的结构特殊性,指向有效数据的迭代器永远不会失效。

    • 头文件

      #include <list>
      
    • 获得一个容器(构造函数)

      list<type> l;
      // 默认构造
      
      list<type> l(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      

      注意!end 指向的是尾元素的下一个位置。

      list<type> l(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 数据操作

      • 拷贝

        void assign(type* begin, type* end);
        // 将 begin 到 end 指向的内存范围拷贝给自身
        
        void assign(size_t n, type elem);
        // 将 n 个 elem 拷贝给自身
        
      • 交换

        void swap(list& l);
        // 互换两容器的内容
        
      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 改变大小

        void resize(size_t newSize, type val);
        // 若容器变大,则多余位置由 val 填充
        
        void resize(size_t newSize);
        // 若容器变大,则多余位置由 0 填充
        
      • 数据存取

        获取首尾元素:

        type& front();
        // 返回容器的首个元素的引用
        
        type& back();
        // 返回容器的尾元素的引用
        

        双端插入:

        void push_back(type& ele);
        void push_front(type& ele);
        void pop_back();
        void pop_front();
        

        数据插入:

        iterator insert(iterator where, size_t count, type val);
        // 从 where 处前插 count 个值 val
        
        iterator insert(iterator where, type val);
        // 从 where 处前插 1 个值 val
        
        iterator insert(iterator where, iterator first, iterator last);
        // 将 first 到 last 范围内的数据插入至 where
        

        数据删除:

        iterator erase(iterator first, iterator last);
        // 删除迭代器从 first 到 last 之间的元素
        
        iterator erase(iterator where);
        // 删除迭代器指向的元素
        
        size_t remove(type& elem);
        // 删除容器中所有与 elem 值匹配的元素。
        // 注意!对于自定义类型,需要重载 == 号
        bool operator==(const Type& elem);
        
        void clear();
        // 删除容器中所有元素
        

        链表操作:

        void reverse();
        // 反转链表
        
        void sort();
        // 排序,从小到大
        
        void sort(funType* callback);
        // 排序,通过回调函数来排序
        

        注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。

    • 迭代器

      同期提供双向迭代器

    set/multiset 容器

    (关联式容器)

    set 容器

    set 容器的特点是,所有元素都会根据元素的键值自动排序,且不允许出现重复的键值。(可以插入重复的键值,但没用)

    虽然提到键值,但其不像 map 容器一样是键值对,它的元素即是键也是值。

    它的迭代器是 const_iterator,不允许修改,因为其元素关系到组织排序。

    其指向有效数据的迭代器永远不会失效。

    multiset 容器

    multiset 容器的特点和用法与 set 容器几乎完全相同,唯一的不同点在于它允许键值重复,而 set 不允许。

    两容器的底层结构是红黑树(一种平衡二叉树)。

    • 头文件

      #include <set>
      
    • 获得一个容器(构造函数)

      set<type> s;
      multiset<type> ms;
      // 默认构造
      
    • 数据操作

      • 交换

        void swap(list& l);
        // 互换两容器的内容
        
      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 改变大小

        void resize(size_t newSize, type val);
        // 若容器变大,则多余位置由 val 填充
        
        void resize(size_t newSize);
        // 若容器变大,则多余位置由 0 填充
        
      • 数据存取

        • 对祖:

          对组是库定义的一个模板类的实例,其中储存有两个数据,用于同时返回两个数据。

          类型为:

          pair<type1, type2>
          

          对于一个对祖,可以通过以下属性来获取其中存储的数据:

          p.first;
          p.second;
          

          分别对应 type1 和 type2。

          创建对祖:

          pair<int, string> p(10, "string");
          

          或者:

          pair<int, string> p = make_pair(10, "string");
          
        • 数据插入:

          pair<iterator, bool> insert(type elem);
              // 向容器中插入一个值
          
        • 数据删除:

          iterator erase(iterator first, iterator last);
              // 删除迭代器从 first 到 last 之间的元素
              
              iterator erase(iterator where);
              // 删除迭代器指向的元素
              
              size_t erase(type elem);
              // 删除容器中值为 elem 的元素
              
              void clear();
              // 删除容器中所有元素
          
        • set 查找操作:

          iterator find(type& key);
              // 查找 key 是否存在,返回迭代器,未找到范围 set.end()
              
              size_t count(type& key);
              // 返回 key 的元素个数
              
              iterator lower_bound(type& key);
              // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同
              
              iterator upper_bound(type& key);
              // 返回第一个大于 key 的迭代器。
              
              pair<iterator, iterator> equal_range(type& key);
              // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值
              
              // pair 类型为 
              pair<set<type>::iterator, set<type>::iterator> p;
              // 可以通过 p 拿到两个迭代器:
              p.first, p.second;
          

          注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。

        • 指定容器的插入规则:

          容器默认插入顺序是从小到大,如果想要自己指定插入顺序,则需要在实例化容器时额外**提供一个模板参数**。

          这个类型实参里需要提供一个重载的方法,实现一个仿函数,提供排序规则。

          例如:

          class rule {
                public:
                 bool operator()(const type& a, const type* b) const {
                     return a > b;
                 }
             };
          

          实例化容器时:

          set<type, rule> s;
          

          所以,对于自定义数据类型,需要提供一个 < 号常函数重载(因为 < 是默认排序)。

           bool operator<(const type& obj) const {
                   return /* ... */ ;
               }
          

          或者提供一个排序规则。

          set<type, rule> s;
          
    • 迭代器

      容器提供双向迭代器。

    map/multimap 容器

    (关联式容器)

    map 与 JSON 的抽象结构相似,是键值对。插入时会根据键值排序。

    元素的类型是刚才提到的 pair<T1, T2>。第一个是键,第二个是值。

    与 set/multiset 容器相同,multimap 容器允许出现相同的键值,且其底层实现也是红黑树。

    • 头文件

      #include <map>
      
    • 获得一个容器(构造函数)

      map<type1, type2> m;
      // 默认构造
      
    • 数据操作

      • 交换

        void swap(map& m);
        // 互换两容器的内容
        
      • 是否为空

        bool empty();
        
      • 获取大小

        size_t size()
        
      • 数据存取

        • 直接访问:

          type& operator[](size_t n);
          // 访问越界就崩了
          type& at(size_t n);
          // 访问越界会抛出异常
          
        • 数据插入:

          pair<iterator, bool> insert(pair<type1, type2> p);
          

          示例:

          // 1.pair
          m.insert(pair<int, string>(3, "string"));
          
          // 2.make_pair
          m.insert(make_pair(3, "string"));
          
          // 3.(一般不这样用)
          m.insert(map<int, string>::value_type(3, "string"));
          
          // 4.(伪数组) 注意! multimap 不允许这种操作
          m[1] = "string";
          // 这种方法存在一个问题
          // 当 m[1] 不存在时
          m[1]; /* 等价于 */ m[1] = NULL;
          // 对于某些类型来说,就等于 type(NULL);
          
        • 数据删除:

          iterator erase(iterator first, iterator last);
          // 删除迭代器从 first 到 last 之间的元素
          
          iterator erase(iterator where);
          // 删除迭代器指向的元素
          
          iterator erase(type1& key);
          // 删除容器中键为 key 的元素
          
          void clear();
          // 删除容器中所有元素
          
        • 查找操作:

          iterator find(type1& key);
          // 查找 key 是否存在,返回迭代器,未找到范围 set.end()
          
          size_t count(type1& key); 
          // 返回 key 的元素个数
          
          iterator lower_bound(type1& key);
          // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同
          
          iterator upper_bound(type1& key);
          // 返回第一个大于 key 的迭代器。
          
          pair<iterator, iterator> equal_range(type1& key);
          // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值
          
        • 指定容器的插入规则:

          与 set/multiset 容器相同,实例化时额外提供一个伪函数的实现,就可以了。

          map<type1, type2, rule> m;
          
    • 迭代器

      容器提供双向迭代器。

    容器总结

    迭代器类型

    • 双向迭代器

      map/multimap、set/multiset、list

    • 随机访问迭代器

      string、vector、deque、

    • 不提供迭代器

      stack、queue

    底层实现

    • 单端数组

      vector

    • 双端数组

      deque

    • 双向循环链表

      list

    • 二叉树

      set/multiset、map/multimap

    性能

    vector deque list set/multiset map/multimap
    搜寻速度 特慢
    迭代器 随机访问 随机访问 双向 双向 双向
    优点 查找效率比较高 均衡的动态增删 + 数据访问 动态增删能力强 查找效率极高 查找效率极高
    弱点 头部数据操作效率低 中间数据操作效率低 查找效率低 插入效率低 插入效率低

    deque 有较好的动态增删能力,但牺牲了中间数据的操作效率。

    list 有着效率非常高的动态增删能力,但牺牲了查找效率。

    set 和 map 以插入效率为代价来维护底层的红黑树,从而增强了在大容量数据中的查找性能。

    另外,在遍历 vector 时,使用 [] 效率很高,而遍历 deque 时则应使用迭代器。

  • 相关阅读:
    【06月18日】A股滚动市净率PB历史新低排名
    沪深300指数的跟踪基金最近1年收益排名
    主要股东近3年净买入排名
    北上资金近1周流入排行榜
    【06月12日】指数估值排名
    最近一月研报推荐次数最多的最热股票
    【06月10日】A股ROE最高排名
    JDK源码阅读-------自学笔记(九)(常用类型Integer初探)
    JDK源码阅读-------自学笔记(八)(数组演示冒泡排序和二分查找)
    JDK源码阅读-------自学笔记(七)(二维数组的浅析)
  • 原文地址:https://www.cnblogs.com/gaolihai/p/13149742.html
Copyright © 2020-2023  润新知