美文网首页
第9章:顺序容器

第9章:顺序容器

作者: MrDecoder | 来源:发表于2018-10-22 17:07 被阅读15次
    • #1.顺序容器概述
    • #2.容器库概览
      • 迭代器
      • 容器类型成员
      • begin和end成员
      • 容器定义和初始化
      • 赋值和swap
      • 容器大小操作
      • 关系运算符
    • #3.顺序容器操作
      • 向顺序容器添加元素
      • 访问元素
      • 删除元素
      • 特殊的forward_list操作
      • 改变容器大小
      • 容器操作可能使迭代器失效
    • #4.vector对象是如何增长的
    • #5.额外的string操作
      • 构造string的其他方法
      • 改变string的其他方法
      • string搜索操作
      • compare函数
      • 数值转换
    • #6.容器适配器

    #.1 顺序容器概述

    下表列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同性能折中:

    • 向容器添加或从容器中删除元素的代价
    • 非顺序访问容器中元素的代价
    顺序容器类型 描述
    vector 可变大小数组。支持随机访问。在尾部之外的位置增删元素可能很慢。
    deque 双端队列。支持随机访问。在头尾位置增删元素速度很快。
    list 双向链表。只支持顺序访问。在list中任何位置进行增删操作速度很快。
    forward_list 单向链表。只支持单向顺序访问。在链表中任一位置进行增删操作速度很快。
    array 固定大小数组。只支持快速随机访问。不能增删元素。
    string 与vector相似的容器,专门用于保存字符。随机访问快。在尾部增删速度快。
    确定使用哪种顺序容器

    以下是一些选择容器的基本原则:

    • 除非你有很好的理由选择其他容器,否则应使用vector。
    • 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。
    • 如果程序要求随机访问元素,应使用vector或deque。
    • 如果程序需要在头尾位置插入和删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
    • 如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则
      • 首先,确定是否真的需要在容器中间位置添加元素。通常可以很容易地向vector追加数据。
      • 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list的内容拷贝到一个vector中。

    ==通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。==


    #2. 容器库概览

    2.1 迭代器

    与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。

    迭代器范围

    一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为begin和end。迭代器范围中的元素包含first所表示的元素以及从first开始直至last(不包含last)之间的所有元素。这种元素范围称为左闭合区间

    [begin,end)
    

    ==迭代器范围的概念是标准库的基础。==

    使用左闭合范围蕴含的编程假定

    标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和end构成一个合法的迭代器范围,则

    • 如果begin和end相等,则范围为空
    • 如果begin和end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
    • 我们可以对begin递增若干次,使得begin==end
    while(begin != end) {
        *begin = val; //范围非空,begin指向一个元素
        ++begin; //移动迭代器,指向下一个元素
    }
    

    2.2 容器类型成员

    每个容器都定义了多个类型,如size_type,iterator,const_iterator等。除了已经使用过的迭代器,大多数容器还提供反向迭代器。为了使用这些类型,我们必须显式使用其类名:

    //iter是通过list<string>定义的一个迭代器类型
    list<string>::iterator iter;
    //count是通过vector<int>定义的一个difference_type类型
    vector<int>::difference_type count;
    

    2.3 begin和end成员

    begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。begin和end有多个版本:带r的的版本返回反向迭代器;以c开头的版本返回const迭代器:

    list<string> a = {"Milton","Shakespeare","Austen"};
    auto it1 = a.begin(); //list<string>::iterator
    auto it2 = a.rbegin(); //list<string>::reverse_iterator
    auto it3 = a.cbegin(); //list<string>::const_iterator
    auto it4 = a.crbegin(); //list<string>::const_reverse_iterator
    

    ==当不需要写访问时,应使用cbeign和cend。==

    2.4 容器定义和初始化

    每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器的大小和元素初始值参数。

    类型 定义
    C c 默认构造函数。如果C是一个array,则C中元素按默认方式初始化;否则C为空。
    C c1(c2) | C c1 = c2 c1初始化为c2的拷贝。c1和c2必须是相同类型。(即,它们必须是相同类型的容器,保存的元素类型相同;对于array类型,两者的大小还必须相同)
    C c{a,b,c,...} | C c = {a,b,c,...} c初始化为初始化列表中元素的拷贝。列表中元素类型必须与c的元素类型相容。对于array类型,类表中的元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化。
    C c(b,e) c初始化为迭代器b和e指定范围中的元素的拷贝。范围中元素的类型必须与c的元素类型相容
    C seq(n) seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的。
    C seq(n,t) seq包含n个初始化为值t的元素
    将一个容器初始化为另一个容器的拷贝

    将一个新容器创建为另一个容器的拷贝的方法有两种:

    • 可以直接拷贝整个容器。
    • (array除外)拷贝由一个迭代器对指定的元素范围。

    为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且, 新容器和原容器中元素类型也可以不同,只要能将要拷贝的元素转为要初始化的容器的元素类型即可。

    //每个容器有三个元素,用给定的初始化器进行初始化
    list<string> authors = {"Milton","Shakespeare","Austen"};
    vector<const char*> articles = {"a","an","the"};
    
    list<string> list2(authors); //正确:类型匹配
    deque<string> authList(authors); //错误:容器类型不匹配
    vector<string> words(articles); //错误:容器类型必须匹配
    
    //正确:可以将const char*元素转换为string
    forward_list<string> words(articles.beign(),articles.end());
    

    ==当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。==

    列表初始化

    在新标准中,我们可以对一个容器进行列表初始化。

    //每个容器有三个元素,用给定的初始化器进行初始化
    list<string> authors = {"Milton","Shakespeare","Austen"};
    vector<const char*> articles = {"a","an","the"};
    
    与顺序容器大小相关的构造函数

    除了与关联容器相同的构造函数外,顺序容器还提供了一个构造函数,它接受一个容器大小和一个元素初始值。如果我们不提供初始值,则标准库会创建一个值初始化器:

    vector<int> ivec(10,-1); //10个int元素,每个初始化为-1
    list<string> svec(10,"hi!"); //10个string元素,每个初始化为"hi!"
    forward_list<int> ilist(10); //10个int元素,每个初始化为0
    

    ==只有顺序容器的构造函数才接受大小参数,关联容器不支持。==

    标准库array具有固定大小

    与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还必须指定容器大小:

    array<string, 10>; //类型为:保存10个string的数组
    

    为了使用array类型,我们必须同时指定元素类型和容器大小:

    array<int,10>::size_type i; //数组类型包括元素类型和数组大小
    array<int>::size_type j; //错误:array<int>不是一个类型
    

    2.5 赋值和swap

    下表列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中的元素的拷贝:

    c1 = c2; //将c1中的内容替换为c2中元素的拷贝
    c1 = {a,b,c}; //赋值后,c1大小为3
    
    容器赋值运算 定义
    c1 = c2 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型
    c = {a,b,c,...} 将c中的元素替换为初始化列表中元素的拷贝(array不适用)
    swap(c1,c2) | c1.swap(c2) 交换c1和c2中的元素。c1和c2必须具有相同的类型。swap通常比从c2向c1拷贝元素快得多
    seq.assign(b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。
    seq.assign(il) 将seq中元素替换为初始化列表il中的元素
    seq.assign(n,t) 将seq中的元素替换为n个值为t的元素
    使用assign(仅顺序容器)

    顺序容器定义了一个assign成员,允许我们从一个不同但相容类型赋值,或者从容器的一个子序列赋值。assign操作用参数所指定的元素的拷贝替换左边容器中的所有元素。例如,我们可以用assign实现一个vector中一段char *值赋予一个list中的string:

    list<string> names;
    vector<const char*> oldstyle;
    names = oldstyle; //错误:容器类型不匹配
    names.assign(oldstyle.cbegin, oldstyle.cend); //正确:可以将const char*转换为string
    

    ==由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。==

    使用swap

    swap操作交换两个相同类型容器的内容,调用swap之后,两个容器中的元素将会交换:

    vector<string> svec1(10); //10个元素的vector
    vector<string> svec2(24); //24个元素的vector
    swap(svec1, svec2);
    

    调用swap后,svec1将包含24个元素,svec2将包含10个元素。除array外,交换两个容器内容的操作保证会很快——元素本身并未交换,swap只是交换了两个容器的内部数据结构。

    ==除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。==

    2.6 容器大小操作

    除了一个例外,每个容器类型都有三个与大小相关的操作。成员函数size返回容器中元素的数目;empty当size为0时返回布尔值true,max_size返回一个大于或等于该类型容器所能容纳最大元素的值。forward_list支持max_size和empty不支持size。

    2.7 关系运算符

    每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、<、>=、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。

    比较两个容器实际上是进行容器元素的逐对比较。这些运算符的工作方式与string的关系运算类似:

    • 如果两个容器具有相同的大小且所有元素都两两对应相等,则这两个容器相等;否则,两个容器不相等。
    • 如果两个容器大小不相同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
    • 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等元素的比较结果。
    vector<int> v1 = {1,3,5,7,9,12};
    vector<int> v2 = {1,3,9};
    vector<int> v3 = {1,3,5,7};
    vector<int> v4 = {1,3,5,7,9,12};
    v1 < v2 //true;v1[2]<v2[2]
    v1 < v3 //false;所有元素都相等,但v3中元素数目更少
    v1 == v4 //true;每个元素都相等,且v1和v4大小相同
    v1 == v2 //false;v2元素数目比v1小
    
    容器的关系运算符使用元素的关系运算符完成比较

    容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算符。

    vector<Sales_data> storeA,storeB;
    if(storeA < storeB) //错误:Sales_data没有<运算符
    

    ==只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符比较两个容器。==


    #3. 顺序容器操作

    顺序容器和关联容器的不同之处在于两者组织元素的方式。

    3.1 向顺序容器添加元素

    除array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除来改变容器大小

    • 这些操作会改变容器的大小;array不支持这些操作。
    • forward_list有自己专有版本的insert和emplace;forward_list不支持push_back和emplace_back。
    • vector和string不支持push_front和emplace_front。
    向顺序容器添加元素的操作 定义
    c.push_back(t) | c.emplace_back(args) 在c的尾部创建一个值为t或由args创建的元素。返回void
    c.push_front(t) | c.emplace_front(args) 在c的头部创建一个值为t或者由args创建的元素。返回void
    c.insert(p,t) | c.emplace(p,args) 在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加元素的迭代器
    c.insert(p,n,t) 在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,则返回p
    c.insert(p,b,e) 将迭代器b和e指定范围内元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指向新添加的第一个元素的迭代器;若返回为空,则返回p
    c.insert(p,il) il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回p

    ==向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。==

    使用push_back

    push_back将一个元素追加到一个vector的尾部。除array和forward_list之外,每个顺序容器都支持push_back。

    ==当我们用一个对象来初始化容器时,或将一个对象插入到容器中,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。==

    使用push_front

    除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:

    list<int> ilist;
    //将元素添加到ilist开头
    for (size_t i = 0; i < 4; i++) {
        ilist.push_front(i); //每个元素都插入到list的新的开始位置。
    }
    

    此循环将元素0、1、2、3添加到ilist头部。每个元素都插入到list新的开始位置。即,当我们插入1时,它会被放置在0之前,2被放置到1之前,依此类推。

    在容器中的特定位置添加元素

    insert成员提供了更一般的添加功能,它允许我们在容器中的任意位置插入0个或多个元素。

    vector<string> svec;
    list<string> slist;
    
    slist.insert(slist.begin(),"Hello!");
    //vector不支持push_front,但我们可以插入到begin()之前
    //警告:插入到vector末尾之外的任何位置都可能很慢
    svec.insert(svec.begin, "Hello");
    

    ==将元素插入到vector、deque和string中任何位置都是合法的。然而,这样做可能很耗时。==

    插入范围内元素

    除了第一个迭代器参数之外,insert函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素按给定值初始化:

    svec.insert(svec.end,10,"Anna");
    

    将10个元素插入到svec的末尾,并将所有元素都初始化为string "Anna"。

    接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:

    vector<string> svec = {"quasi","simba","frollo","scar"};
    //将svec的最后两个元素添加到slist起始位置
    slist.insert(svec.begin(), svec.end() - 2, svec.end());
    slist.insert(svec.end(), {"these","words","will","go","at","the","end"});
    //运行时错误:迭代器表示要拷贝的范围,不能指定与目的位置相同的容器
    slist.insert(slist.begin(),slist.begin(),slist.end());
    

    如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器。

    使用insert的返回值

    通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:

    list<string> lst;
    auto iter = lst.begin();
    while(cin >> word) {
        iter = list.insert(iter,word); //等价于调用push_front
    }
    
    使用emplace操作

    新标准引入三个新成员——emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对用push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

    当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存Sales_data元素:

    //在c的末尾构造一个Sales_data对象
    c.emplace_back("978-0590353403",25,15.99);
    

    ==emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。==

    3.2 访问元素

    包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用:

    //在解引用一个迭代器或者调用front或back之前检查是否有元素
    if (!svec.empty()) {
        //val和val2是c中第一个元素值的拷贝
        auto val = *svec.begin(), val2 = svec.front();
        //val3和val4是c中最后一个元素值的拷贝
        auto last = svec.end();
        auto val3 = *(--last); //不能递减forward_list
        auto val4 = svec.back(); //forward_list不支持
    }  
    
    • at和下表操作只适用于string、vector、deque和array。
    • back不适用于forward_list。
    在顺序容器中访问元素的操作 定义
    c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
    c.front() 返回c中首元素的引用。若c为空,函数行为未定义
    c[n] 返回c中下标为n的元素的引用,n是一个无符号整数。若n>=c.size(),则函数行为未定义
    c.at[n] 返回下标为n的元素的引用。如果下标越界,则抛出——out_of_range异常

    ==对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误。==

    访问成员函数返回的是引用

    在容器中访问元素的成员函数(即,front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值:

    if (!svec.empty()) {
        svec.front() = "hi"; //给svec的第一个元素赋值
        auto &v = svec.back(); //获得指向最后一个元素的引用
        v = "last"; //改变svec中的元素
        auto v2 = svec.back(); //v2不是一个引用,它是svec.back()的一个拷贝
        v2 = "lst"; //未改变svec中的元素
    }
    
    下标操作和安全的随机访问

    提供快速访问的容器(string、vector、deque和array)也都提供下标运算符。下标运算符并不检查下标是否在合法范围内。使用越界下标是一种严重的程序设计错误,而且编译器并不检查这种错误。

    vector<string> svec = {"quasi","simba","frollo","scar"};
    for (auto i = 0; i < svec.size(); i++) {
        cout << svec[i] << " ";
    }
    cout << endl;
    

    3.3 删除元素

    与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。

    • 这些操作会改变容器大小,所以不适用于array。
    • forward_list有特殊版本的erase,forward_list不支持pop_back;vector和string不支持pop_front。
    顺序容器删除操作 定义
    c.pop_back() 删除c中尾元素。若c为空,则函数行为未定义。函数返回void
    c.pop_front() 删除c中首元素。若c为空,则函数行为未定义。函数返回void
    c.erase(p) 删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义
    c.erase(b,e) 删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删元素之后的元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
    c.clear() 删除c中的所有元素。返回void

    ==删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。==

    pop_front和pop_back成员函数

    pop_front和pop_back成员函数分别删除首元素和尾元素。

    while(!ilist.empty()) {
        process(ilist.front()); //对ilist的首元素进行一些处理
        ilist.pop_front(); //完成处理后删除首元素
    }
    
    从容器内部删除一个元素

    成员函数erase从容器指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可也删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的最后一个元素之后位置的迭代器。

    list<int> lst = {0,1,2,3,4,5,6,7,8,9};
    auto it = lst.begin();
    while (it != lst.end()) {
        if (*it % 2) {
            it = lst.erase(it); //删除元素
        }else {
            it++;
        }
    }
    
    删除多个元素

    接受一对迭代器的erase版本允许我们删除一个范围内的元素:

    //删除两个迭代器表示范围内的元素
    //返回指向最后一个被删元素之后位置的迭代器
    elem1 = slist.erase(elem1,elem2); //调用后,elem1 == elem2
    

    3.4 特殊的forward_list操作

    forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
    auto prev = flst.before_begin(); //表示前首元素
    auto curr = flst.begin(); //表示首元素
    while (curr != flst.end()) { //有元素处理
        if (*curr % 2) { //若元素为奇数
            curr = flst.erase_after(prev); //删除它并移动
        }else {
            prev = curr; //移动迭代器指向下一元素
            ++curr;
        }
    }
    

    3.5 改变容器的大小

    用resize来增大或缩小容器。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:

    list<int> ilist(10, 42);
    ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
    ilist.resize(25, -1); //将10个-1添加到ilist的末尾
    ilist.resize(5); //从ilist末尾删除20个元素
    

    如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效。

    3.6 容器操作可能使迭代器失效

    向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。

    在向容器添加元素后:

    • 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效。但指向插入位置之后的将会失效。
    • 对于deque,插入到除首尾元素之外的任何位置都会导致迭代器、指针和引用失效。
    • 对于list和forward_list,指向容器的迭代器、指针和引用仍有效。
    编写改变容器的循环程序

    添加/删除vector、string或deque元素的循环必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:

    vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
    auto it = vi.begin();
    while(it != vi.end()) {
        if (*it % 2) {
            it = vi.insert(it, *it); //赋值当前元素
            it += 2; //向前移动迭代器,跳过当前元素以及插入到它之前的元素
        }else {
            it = vi.erase(it); //删除偶元素
            //不应向前移动元素,iter指向我们删除的元素之后的元素
        }
    }
    

    在调用erase后,不必递增迭代器,erase返回的迭代器以及指向序列中下一个元素。调用insert后,需要递增迭代器两次。insert在给定位置之前插入新元素,然后返回指向新元素的迭代器。

    不要保存end返回的迭代器

    当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。

    //灾难:此循环的行为是未定义的
    vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
    auto begin = vi.begin(), 
         end = vi.end(); //保存尾迭代器的值是一个坏主意
    while (begin != end) { //此循环是未定义的
        ++begin;//向前移动begin,因为我们想在此元素之后插入元素
        vi.insert(begin, 8); //插入元素
        ++begin; //向前移动begin,跳过我们刚刚加入的元素
    }
        
    //更安全的方法:在每个循环步添加/删除元素后都重新计算end
    while(begin != v.end()) {
        //....
        ++begin; //向前移动begin,因为我们想在此元素之后插入元素
        begin = v.insert(begin,42); //插入新值
        ++begin; //向前移动begin,跳过我们刚刚加入的元素
    }
    

    将end操作返回的迭代器保存在一个名为end的局部变量中。在循环体中,向容器中添加了一个元素,这个操作使保存在end中的迭代器失效了。这个迭代器不再指向vi中的任何元素,或是vi中尾元素之后的位置。

    ==如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。
    必须在每次插入操作后重新调用end(),而不能在循环开始前保存它返回的迭代器。==


    #4. vector对象是如何增长的

    为了支持快速访问,vector将元素连续存储——每个元素紧挨着前一个元素存储。

    管理容量的成员函数

    shrink_to_fit只适用于vector、string和deque。
    capacity和reserve只适用于vetor和string。

    容器大小管理操作 定义
    c.shrink_to_fit() 请将capacity()减小为与size()相同的大小
    c.capacity() 不重新分配内存空间的话,c可以保存多少元素
    c.reserve(n) 分配至少容纳n个元素的内存空间

    ==reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。==

    capacity和size

    容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。

    vector<int> vi;
    //size的值为0,capacity的值依赖于具体实现
    cout << "vi:size:" << vi.size()
         << " capacity:" << vi.capacity() << endl;
    //向vi添加24个元素
    for (vector<int>::size_type i = 0; i < 24; i++) {
        vi.push_back(i);
    }
    //size为24,capacity大于等于24,具体依赖于实现
    cout << "vi:size:" << vi.size()
         << " capacity:" << vi.capacity() << endl;
    

    ==每个vector实现都可以选择自己内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。==


    #5. 额外的string操作

    5.1 构造string的其他方法

    构造string的其他方法 定义
    string s(cp,n) s是cp指向的数组中前n个字符的拷贝。此数组至少应该包含n的字符
    string s(s2,pos2) s是string s2从下标pos2开始的字符拷贝。若pos2>s2.size(),构造函数的行为未定义
    string s(s2,pos2,len2) s是string s2从下标pos2开始len2个字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符

    这些构造函数接受一个string或一个const char*参数,还接受指定拷贝多少个字符的参数。

    const char* cp = "Hello World!!!";
    char noNull[] = { 'h','i' };
    string s1(cp); //拷贝cp中的字符直到遇到空字符
    string s2(noNull, 2); //从noNull拷贝两个字符
    string s3(noNull); //未定义的,noNull不是以空字符结束
    
    substr操作

    substr操作返回一个string,它是原始string的一部分或全部的拷贝。

    s.substr(pos,n); //返回一个string,包含s中从pos开始的n个字符的拷贝。
                     //pos的默认值为0,n的默认值为s.size()-pos,即拷贝从pos开始所有的字符。
    

    5.2 改变string的其他方法

    string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。

    const char *cp = "Stately,plump Buck";
    string s("Hello World");
    s.assign(cp, 7); //替换s的内容为Stately,赋予s的是从cp指向地址开始的7个元素
    s.insert(s.size(), cp + 7); //将字符插入到s[size()]处元素之前的位置
    

    关于insert操作的说明:

    string s = "some string";
    string s1 = "some other string";
    s.insert(0, s1); //在s中0位置之前插入s1的拷贝
    s.insert(0, s1, 0, s1.size()); //在s[0]之前插入s1中s1[0]开始的s1.size()个字符
    
    append和replace函数

    string类定义了两个额外的成员函数:append和replace,这两个函数可以改变string的内容。
    append操作是在string末尾进行插入操作的一种简写形式:

    string s("C++ Primer"), s2 = s; // 将s2和s初始化为"C++ Primer"
    s.insert(s.size(), "4th Ed."); // s== "C++ Primer 4th Ed."
    s2.append(" 4th Ed."); //等价方法:将" 4th Ed."追加到s2; s==s2
    

    replace操作是调用erase和insert操作的简写形式:

    //将"4th"替换为"5th"
    s.erase(11,3); // s == "C++ Primer Ed."
    s.insert(11,"5th"); // s == "C++ Primer 5th Ed." 
    //从位置11开始,删除3个字符并插入"5th"
    s2.replace(11,3,"5th"); //等价方法:s == s2
    

    5.3 string搜索操作

    string类提供了6个不同的搜索函数,每个函数都有4个重载版本。每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为sting::npos的static成员。

    string搜索操作 定义
    s.find(args); 查找s中args第一次出现的位置
    s.rfind(args); 查找s中args最后一次出现的位置
    s.find_first_of(args); 在s中查找args中任何一个字符第一次出现的位置
    s.find_last_of(args); 在s中查找args中任何一个字符最后一次出现的位置
    s.find_first_not_of(args); 在s中查找第一个不在args中的字符
    s.find_last_not_of(args); 在s中查找args最后一次出现的位置
    指定在哪里开始搜索

    我们可以传递给find操作一个可选的开始位置。

    string::size_type pos = 0;
    while((pos = name.find_first_of(numbers,pos)) 
                                != string::npo) {
        cout << "find number at index: " << pos 
                << " element is " << name[pos] <<endl;
        ++pos; //移动下一个字符        
    }
    
    逆向搜索

    rfind成员函数搜索最后一个匹配,即子字符串最靠右的出现位置:

    string river("Mississippi");
    auto first_pos = river.find("is"); //返回1
    auto last_pos = river.rfind("is"); //返回4
    

    5.4 compare函数

    根据s是等于、大于还是小于参数指定的字符串,s.compare()返回0、正数或负数。

    5.5 数值转换

    int i = 42;
    string s = to_string(i); //将整数i表示为字符表示形式
    double d = stod(s); //将字符串s转换为浮点数
    

    要转换为数值的string中的第一个非空白符必须是数值中可能出现的字符:

    #include <iomanip>
    
    void main() {
        string s2 = "pi = 3.1415926";
        string s3 = s2.substr(s2.find_first_of("+-.1234567890"));
        double d = stod(s3);
        cout <<std::setprecision(10)<< d << endl; //设置输出精度
    }
    

    #6. 容器适配器

    除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。适配器是标准库中一个通用的概念。

    定义一个适配器
    每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。

    deque<int> deq;
    stack<int> stk(deq); //从deq拷贝元素到stk
    //在vector上实现的空栈
    stack<string, vector<string>> str_stk;
    
    栈适配器

    栈默认基于deque实现,也可以在list或vector之上实现。

    栈操作 定义
    s.pop() 删除栈顶元素,但不返回该元素值
    s.push(item) | s.emplace(args) 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造
    s.top() 返回栈顶元素,但不将元素弹出栈
    stack<int> stk; //空栈
    for (size_t i = 0; i < 10; i++) {
        stk.push(i); //保存0-9十个数
    }
    while(!stk.empty()) {
        int value = stk.top(); //返回栈顶元素,但不将元素弹出栈
        cout << value;
        stk.pop();
    }
    
    队列适配器

    queue默认基于deque实现,priority_queue默认基于vector实现;
    queue也可以用list或vector实现,priority_queue也可以用deque实现。

    queue和priority_queue操作 含义
    q.pop() 返回queue的首元素或priority_queue的最高优先级的元素,但不删除此元素。
    q.front() 返回首元素或尾元素,但不删除此元素
    q.back() 只适用于queue
    q.top() 返回最高优先级元素,但不删除该元素[只适用于priority_queue]
    q.push(item) | q.emplace(args) 在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item,或者由args构造

    标准库queue使用一种先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到对尾,而离开队列的对象从队首被删除。

    priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。

    相关文章

      网友评论

          本文标题:第9章:顺序容器

          本文链接:https://www.haomeiwen.com/subject/qaigzftx.html