• 标准模板库(STL)


    组件:

      容器:管理某类对象的集合。

      迭代器:用来在一个对象集合内遍历元素。

      算法:处理集合内的元素。


    容器

    • 序列式容器:顾名思义。array,vector,deque,list 和 forward_list(c++11)。
    • 关联式容器:已排序集合。元素位置取决于value(或者key -- 如果元素是个key--value 对)和给定的某个排序准则。set,multiset,map 和 multimap。
    • 无序容器:无序集合,每个元素的位置无关紧要,唯一重要的是某特定元素是否位于此集合内。值或安插顺序都不能影响元素的位置,元素的位置有可能在容器生命中被改变。unordered_set, unordered_multiset, unordered_map 和 unordered_multimap。 unordered容器在TR1中引入。

    序列式容器常被时限为array或者linked list 。

    关联式容器常被实现为binary tree。

    无序容器常被实现为hash table。

    序列式容器

      重点提一下 TR1中新加入的array。

      一个array对象乃是在某个固定大小的array内管理元素。因此,不可以改变元素个数,只能改变元素值。必须在建立时就指明其大小。array允许随机访问,只要有索引。包含在头<array>中

      eg:

    //array container of 5 string elements
    array<string,5> col = {"hello","world"};
    
    array<string,5> col2; //默认情况下被default构造函数初始化,对基础类型而言,初值不明确

    list:list不提供随机访问,比如要访问第10个元素,就必须走完前9个。这比vector和deque提供的摊提式常量时间,效率差太多。但优势是快速插入和删除,只需改变链接即可。

    std::forward_list 是支持从容器中的任何位置快速插入和移除元素的容器。不支持快速随机访问。它实现为单链表,且实质上与其在 C 中实现相比无任何开销。与 std::list 相比,此容器提在不需要双向迭代时提供更有效地利用空间的存储。

    在链表内或跨数个链表添加、移除和移动元素,不会非法化当前指代链表中其他元素的迭代器。然而,在从链表移除元素(通过 erase_after )时,指代对应元素的迭代器或引用会被非法化。

    std::forward_list 满足容器 (Container) (除了 operator== 的复杂度始终为线性和 size 函数)、具分配器容器(AllocatorAwareContainer) 和顺序容器 (SequenceContainer) 的要求。

    参考链接:为什么forward_list不支持push_back ,  如何使用std::forward_list 单链表 

    关联式容器

    • set : 元素依据value自动排序,每个元素只能出现一次,不许重复
    • multiset:和set的唯一差别就是可以重复
    • map:每个 元素都是键值对,key是排序准则的基准。每个key只准出现一次
    • multimap:和map的唯一差别就是,元素可以重复,即key可以重复

    实际上所有这些关联式容器通常都由二叉树实现。multimap也可以用作字典。

    无序容器

    对于无序容器而言,某次插入操作可能对其他元素位置造成影响(rehashing,删除操作不会造成rehashing)。我们唯一关心的是,某个特定元素是否位于容器内。可以理解为无序容器是个袋子。

    无序容器常以hash table实现出来,内部结构是一个“由linked list组成”的array。通过某个hash函数计算,确定元素落于这个array的位置。Hash函数运算的目标是:让每个元素的落点(位置)有助于快速访问任何一个元素。

    但前提是hash函数本身必须足够快。但这个完美的hash函数不一定存在或者被找到,抑或是由于它造成array耗费巨额内存而显得不切实际,因此,退而求其次的hash函数可能让多个元素落在同一个位置上。所以,设计上就让array的元素再被放进一个linked list中,如此一来array的每个位置就得以存在一个以上的元素。

      无序容器优点是,查找一个带有特定值的元素,其速度肯能快过关联式容器。事实上无序容器提供的是摊提式常量复杂度,前提是有一个良好的hash函数。然而这并不简单,你可能需要提供许多内存作为bucket。

    stl定义了下面这些无序容器:

    • unordered set 无需元素集合。元素不允许重复
    • unordered map 键值对。每个key唯一。
    • unordered multiset 允许元素重复
    • unordered multimap 允许key重复,可以作为字典

    容器适配器

     Container Adapter是预定义的容器,提供的是一定限度的接口,用于处理特殊需求。这些容器都是根据基本容器定义实现的。

    • stack 顾名思义。对元素采用LIFO管理策略。
    • Queue  FIFO策略。
    • Priority queue  期内元素拥有优先权。优先权奶水给予程序员提供的排序准则而定义的。效果类似于这样一个缓冲区:下一元素永远是容器中优先权最高的元素。

    迭代器

    自c++11起,可以使用range-based for循环处理所有元素。

    • Operator* 返回当前位置上的元素值,
    • Operator ++  迭代器前进到下一元素
    • operator == 和 != 判断两个迭代器是否相同
    • Operator =   对迭代器赋值

    任何容器定义两种迭代器类型:

      1  container::iterator 以“读/写”模式遍历元素

      2 container::const_iterator 以“只读”模式遍历元素

    cbegin() 和 cend() 

      c++11起,关键字auto可以代替迭代器的精确类型 ,其可以使程序比较精简,或者当容器类型变化时,程序整体仍保持较佳的强壮性。  缺点是迭代器丧失常量性,可能引发“计划外赋值”风险。因为 auto pos = coll.begin();会使pos成为一个非常量迭代器。那么,cbegin() 和 cend() 就是返回了一个类型为const_iterator的对象。

    为了避免循环中大量的拷贝行为,每个元素的copy构造函数和析构函数都被调用,可以使用const reference

    std::vector<int> vec;
    for(const auto& elem : vec){
       cout << elem ;      
    }

    迭代器种类

    根据能力不同,迭代器被划分为5种不同的类别。STL预先定义好的所有容器,其迭代器均属于以下三种分类:

    • 前向迭代器   只能够以累加操作符向前迭代。class forward_list的迭代器就是属于此类,unordered_set , unordered_multiset ,  unordered_map 和 unordered_multimap都至少是此类。
    • 双向迭代器   递增递减都行。list,set,map, multimap,multiset提供的迭代器都属此类
    • 随机访问迭代器  不但具备双向迭代的所有属性,还具备随机访问的能力。它们提供了迭代器算术运算的必要操作符。vector,dequeue,array和string提供的迭代器都属于此类。

    除此之外,stl还定义了两个类别:

    • 输入型迭代器   向前迭代时能够读取/处理 value. Input stream迭代器就是这样的例子。
    • 输出型迭代器  向前迭代时能够涂写value。Inserter和output stream迭代器都是此类。

    迭代器之适配器

      Insert Iterator(安插型迭代器)

      它可以使算法以安插的方式而非覆写的方式运作。使用它可以解决算法的“目标空间不足”的问题。是的,他会促使目标区间的大小按需要成长。

    #include <algorithm>
    #include<iterator>
    #include<list>
    #include<vector>
    #include<deque>
    #include<set>
    #include<iostream>
    
    using namespace std;
    
    int main() {
        list<int> coll1 = { 1,2,3,4,5,6,7,8,9 };
    
        //复制coll1的元素到coll2,by appending them
        vector<int> coll2;
        copy(coll1.cbegin(),coll1.cend(), //source
            back_inserter(coll2));        //destination
    
        //复制coll1的元素到coll3中,by inserting them at the front,反转了元素顺序
        deque<int> coll3;
        copy(coll1.cbegin(),coll1.cend(),    //source
            front_inserter(coll3));            //destination
    
        //只有inserter工作于关联式集合
        set<int> coll4;
        copy(coll1.cbegin(),coll1.cend(),
            inserter(coll4,coll4.begin()));
    }

    此例运用了三种预定义的insert iterator:

    1 Back inserter(安插于容器最末端):其内部调用push_back(),在容器末端插入元素(即追加动作)。只有在提供push_back() 的容器中back inserter才能派上用场。在c++标准库中的此类容器有vector,deque,和string。

    2 Front inserter(安插于容器最前端) 其内部元素调用push_front(),将元素安插与容器的最前端。c++中可用的容器为deque,list和 forward_list。

    3 General inserter :一般性的inserter,作用是在“初始化时接受之第二实参”所指位置的前方插入元素。内部调用 成员函数insert(),并以新值和新位置作为实参传入。所有STL容器都提供insert()成员函数,因为,这是唯一可用于关联式容器身上的一种预定义inserter。

    Stream Iterator(串流迭代器)

    #include <algorithm>
    #include<iterator>
    #include<string>
    #include<vector>
    #include<iostream>
    
    using namespace std;
    
    int main() {
        vector<string> coll;
    
        //read all words from the standard input
        //-source :all strings until end-of-file(or err)
        //-detination :coll(inserting)
    
        copy(istream_iterator<string>(cin),        //start of source
            istream_iterator<string>(),            //end of source
            back_inserter(coll));                //destination
    
        sort(coll.begin(),coll.end());
    
        //print all elements without duplicates
        //-source :coll
        //-destination: 标准输出
        unique_copy(coll.begin(),coll.end(),        //source
            ostream_iterator<string> (cout,"
    "));    //destination
    }

    具体介绍查阅stream 迭代器

    反向迭代器

    move 迭代器

    更易型算法

    移除(Removing)元素

    算法remove() 自某个区间删除元素。然而如果用它来删除容器所有的元素,行为肯定会令人吃惊。例如:

    int main() {
        list<int> coll;
        for (int i = 0; i < 7; ++i) {
            coll.push_front(i);
            coll.push_back(i);
        }
    
        //print all elements of the collection
        cout << "pre: ";
        copy(coll.cbegin(), coll.cend(), ostream_iterator<int>(cout, " "));
        cout << endl;
        //remove all elements with value 3
        remove(coll.begin(),coll.end(),3);
    
        cout << "post: ";
        copy(coll.cbegin(),coll.cend(),
            ostream_iterator<int>(cout," "));
        cout << endl;
    }

    输出为:

    pre:   6 5 4 3 2 1 0 0 1 2 3 4 5 6
    post: 6 5 4 2 1 0 0 1 2 4 5 6 5 6

    显然,这不符合预期。

    事实上,remove() 函数返回了新的终点。可以利用该终点获得新区间,缩减后的容器大小,或者获得被删除元素的个数。下面是改进版本:

    int main() {
        list<int> coll;
        for (int i = 0; i < 7; ++i) {
            coll.push_front(i);
            coll.push_back(i);
        }
    
        //print all elements of the collection
        cout << "pre: ";
        copy(coll.cbegin(), coll.cend(), ostream_iterator<int>(cout, " "));
        cout << endl;
        //remove all elements with value 3
        list<int>::iterator end = remove(coll.begin(),coll.end(),3);
    
        copy(coll.begin(), end,
            ostream_iterator<int>(cout, " "));
        cout << endl;
        //print numbers of removed elements
        cout << "member of remove elements:" << distance(end, coll.end()) << endl;
        coll.erase(end,coll.end());
    
        cout << "post: ";
        copy(coll.cbegin(),coll.cend(),
            ostream_iterator<int>(cout," "));
        cout << endl;
    }

    pre: 6 5 4 3 2 1 0 0 1 2 3 4 5 6
    6 5 4 2 1 0 0 1 2 4 5 6
    member of remove elements:2
    post: 6 5 4 2 1 0 0 1 2 4 5 6

    end正是“被修改集合”经过元素移除动作后的新的逻辑终点。

    当然,你可以单一一句完成上述过程:

    coll.erase(remove(coll.begin(),coll.end(),3),coll.end());

    为何算法不自己调用erase()呢,这个问题正好点出STL为获取弹性而付出的代价。通过“以迭代器为接口”,STL将数据结构和算法分离开。迭代器不过是“容器内部某一位置的抽象”。一般而言,迭代器对自己所属的容器一无所知,任何“以迭代器访问容器”的算法,都不得通过迭代器调用容器类所提供的任何成员函数。

    更易Associative(关联式)和Unordered(无序)容器

    更易型算法(指的是那些会移除(remove),重拍(reorder),修改(modify)元素的算法)若用于关联式容器或者无序容器,会出问题。关联式容器和无序容器不能被当作操作目标,原因很简单:如果更易维护型算法用于关联式和无序容器上,会改变某些位置上的值,进而破坏容器本身对次序的维护。为了避免累及内部次序,关联式容器和无序容器的所有迭代器均被声明为指向常量的value或key。

    那如何实现从关联容器和无序容器删除元素呢? ====调用它们的成员函数。如果earse()函数。

    以函数作为算法的实参

    有些算法可以接受用户自定义的辅助函数,借以提高弹性和能力。这些函数将被算法内部调用。

    以函数作为算法实参的实例示范

    最简单的是for_each()算法,它针对区间内的每一个函数,调用一个由用户指定的函数。

    void print(int elem){
      cout << elem ;
    }
    int main(){
      vector<int> coll{1,2,3,4};
      for_each(coll.cbegin(),coll.cend(),
        print);
      cout << endl;
    }

    算法以数种态度来面对这些辅助函数:有的是为可有可无,有的视为必要。你可以利用它们来指定查找准则,排序准则,或定义某种操作使其在“容器元素转换至另一容器”时被调用。

    int Aquare(int elem){
       return elem*elem ;
    }
    int main(){
      vector<int> coll{1,2,3,4};
    vector<int>  coll2;
        transform(coll.cbegin(),coll.cend(),  //source
        back_inserter(coll2),                      //destination
        squre);                                         //operation
      cout << endl;
    }

    此例是将coll的值平方,然后转移至coll2.

    判断式

    Predicate是一种特殊的辅助函数。会返回bool值。常被用来指定作为排序准则或者查找准则。判断式可能有一或两个操作数。

    并非任何返回bool的单参函数或双参函数都是合法的判断式,stl要求,面对相同的值,predicate必须得出相同的结果。

    双参数函数:

    class Student{
    public:
        string name() const;
    };
    
    bool StudentSortCriterion(const Student& s1, const Student&s2) {
        return s1.name() < s2.name();
    }
    
    int main() {
        deque<Student> coll;
        //...
        sort(coll.begin(),coll.end(),StudentSortCriterion);
    }

    单参数函数:略

    使用Lambda

    一篇小小的博文

    函数对象

    传递给算法的“函数型实参”不一定得是函数,可以是行为类似函数的对象。这种对象称为函数对象,或称为仿函数

    定义一个函数对象

    任何东西,只要其行为像函数,它就是函数。

    什么才算具备函数行为呢?所谓函数行为,是指可“使用小括号传递实参,借以调用某个东西”。例如:

    functon(arg1,arg2);

    对象如果也要如此,只需 定义operator(),并给予合适的参数类型:

    class X{
     public:
        return-value operator(arguments) const;
    };

     现在,你可以把这个class 的对象当作函数来调用了:

    X fo;
    ...
    fo(arg1, arg2);  //call operator for function object fo

    上述调用等同于;

    fo.operator()(arg1, arg2);

    一个完整的例子:

    class PrintInt
    {
    public:
        void operator() (int elem) const {
            cout << elem << " ";
        }
    };
    
    int main() {
        vector<int> coll{1,2,3,4,5,6,7,8,9};
    
        for_each(coll.cbegin(), coll.cend(),
            PrintInt());
        cout << endl;
    
    }

    比起寻常函数,仿函数带来如下优点:

    1 函数对象是一种带状态的函数。行为像pointer 的对象我们称之为smart pointer。同理,“行为像function”的对象可以称之为“smart function”,因为它们的能力超越了operator()。函数对象可以拥有成员对象和成员变量,这意味着函数对象拥有状态。事实上,在同一时间点,相同类型的两个不同的函数对象所表述的相同技能,,可具备不同的状态,这在寻常函数中是不可能的。另一个好处是,你可以在运行期初始化它们---当然必须在它们被使用(调用)之前。

    2 每个函数对象都有自己的类型。寻常函数,唯有在其签名式(signature)不同时,才算类型不同。而函数对象即使签名相同,也可以有不同的类型。事实上由函数对象定义的每一个函数行为都有其自己的类型。这对于“运用template实现泛型编程”是一个卓越贡献,因为这样一来我们便可以将函数行为当作template参数来运用。这使得不同类型的容器可以使用同类型的函数对象作为排序准则。也可以确保你不会在“排序准则不同”的集合间赋值,合并,比较。你甚至可以设计函数对象的继承体系,以此完成某些特别的事情,比如在一个总体原则下确立某些特殊的情况。

    3 函数对象通常比寻常函数速度快。就template概念而言,由于更多细节在编译期就已确定,所以通常可能进行更好的优化。所以,传入一个函数对象(而非寻常函数)可能获得更好的执行效能。

    下面是几个例子:

    对集合内的每个元素加上一个特定的值,如果在编译期便确定这个值,可以使用寻常函数完成工作:

    void add10(int& elem) {
        elem += 10;
    }
    
    void f1() {
        vector<int> coll{ 1,2,3,4,5 };
        for_each(coll.begin(), coll.end(),
            add10);
    }
    
    //如果你需要数个不同的特定值,而且他们在编译期都已经确定,可以使用template
    
    template <int theValue>
    void add(int& elem) {
        elem += theValue;
    }
    
    void f1() {
        vector<int> coll{ 1,2,3,4,5 };
        for_each(coll.begin(), coll.end(),
            add<10>);
    }

    如果在执行时期处理这个特定的值,那就麻烦了。你必须在函数被调用之前先将这个数值传给该函数。这通常会导致生成若干全局变量,让“算法的调用者”和“算法所调用的函数”都能看到和用到他们。这样不好。

        如果两次用到该函数,每次用到的特定值不同,而且都是在执行期才处理那些特定值,那么寻常函数根本无能为力。要么传入一个标记,要么干脆写2个函数。你是否有过这样的经验:手握一个函数,他有个static变量用以记录状态(state),而你需要这个函数在同一时刻内有另外一个不同的状态?于是你只好拷贝整份函数定义,华为两个不同的函数。这正是先前所说的问题。(这段话本可以不抄,但抄下来下次看的时候就能直接点题)

        如果使用函数对象,我们就可以写出更具智能的函数达成目的。对象可以有自己的状态,可以被正确初始化。下面是个完整的例子:

    template <typename T>
    inline void PRINT_ELEMENTS(const T& coll, const std::string& optstr = "") {
        std::cout << optstr;
        for (const auto& elem : coll) {
            std::cout << elem << ' ';
        }
        std::cout << std::endl;
    }
    
    class AddValue
    {
    
    private:
        int theValue;     //the value to add
    public:
        //初始化
        AddValue(int v) : theValue(v) {
        }
        //函数调用
        void operator()(int& elem) const {
            elem += theValue;
        }
    
    };
    
    int main() {
        list<int> coll{ 1,2,3,4,5 };
        PRINT_ELEMENTS(coll, "initialized:    ");
    
        for_each(coll.begin(),coll.end(),
            AddValue(10));
    
        PRINT_ELEMENTS(coll, "after adding 10:    ");
    
        for_each(coll.begin(), coll.end(),
            AddValue(*coll.begin()));
    
        PRINT_ELEMENTS(coll, "after adding first of element:    ");
    }

    运行结果:

    initialized: 1 2 3 4 5
    after adding 10: 11 12 13 14 15
    after adding first of element: 22 23 24 25 26

    表达式AddValue(10)生出一个AddValue对象,并以10为初值。AddValue构造函数将这个值保存在成员theValue中。第二次也是这个机能。

    预定义的函数对象

    一个例子,作为排序准则的函数对象。Operator < 之默认排序准则乃是调用less<>,所以,如果你声明:set<int> coll; 会被扩展为set<int,less<int>> coll; 同样,反向排列这些元素也可以:set<int, greater<int>> coll;

    另一个运用“预定义函数对象”的地点是STL算法。考虑下面的代码:

    template <typename T>
    inline void PRINT_ELEMENTS(const T& coll, const std::string& optstr = "") {
        std::cout << optstr;
        for (const auto& elem : coll) {
            std::cout << elem << ' ';
        }
        std::cout << std::endl;
    }
    
    int main() {
        deque<int> coll = { 1,2,3,5,7,11,13,17,19 };
        PRINT_ELEMENTS(coll, "initialized:  ");
        transform(coll.cbegin(), coll.cend(),coll.begin(),negate<int>());
        PRINT_ELEMENTS(coll, "negated:  ");
        transform(coll.cbegin(),coll.cend(),    //first source
            coll.cbegin(),                        //second source
            coll.begin(),                        //destination
            multiplies<int>());                    //operation
        PRINT_ELEMENTS(coll, "squared:  ");
    }

    运行结果:

    initialized: 1 2 3 5 7 11 13 17 19
    negated: -1 -2 -3 -5 -7 -11 -13 -17 -19
    squared: 1 4 9 25 49 121 169 289 361

    negate<int>() 根据class template negate<> 生成一个函数对象,将传入的int设为负。transform()算法使用此一运算,将第一集合内的所有元素转移到第二个集合。如果转移目的地是自己,那么就是对集合内的所有元素设负值。

    下面的求平方则用了transform()的另一种形式,以某个给定的运算,将两集合内的元素处理后写入第三个集合。本例中三个集合实际上是同一个,所以,自身求平方。

    Binder

    你可以使用特殊的function adapter(函数适配器),或者所谓的binder,将预定义的函数对象和其他数值结合为一体。一个完整的例子:

  • 相关阅读:
    java中next()、nextInt()、nextLine()区别
    原码、反码、补码及移位运算
    微信小程序开发流程(适用于新手学习)
    Dubbo 常用的容错机制
    分布式系统性能注意点
    陌上人如玉,公子世无双!
    五步工作法
    四个凡是
    Javacpu 和内存问题排查步骤
    开启JMC功能
  • 原文地址:https://www.cnblogs.com/gardenofhu/p/9318215.html
Copyright © 2020-2023  润新知