C++primer笔记——第九章【顺序容器】

【第九章】 顺序容器
顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。

9.1 顺序容器概述
    1、顺序容器类型
    1)vector        可变大小数组。支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢
    2)deque        双端队列。支持快速随机访问。在头尾位置插入、删除速度很快
    3)list            双向链表。只支持双向顺序访问。在list中任何位置进行插入、删除速度很快
    4)forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入、删除速度都很快
    5)array        固定大小数组。支持快速随机访问,不能添加或删除元素
    6)string        与vector相似的容器,但专用于保存字符。随机访问块。在尾部插入,删除速度快
    除了固定大小的array外,其他容器都提供高效灵活的内存管理。可以添加和删除元素,扩张和收缩容器的大小
    总结:
    1)string和vector将元素保存在连续的内存空间中。由于元素连续存储,所以由元素的下标来计算其地址很快。但是在容器中间位置添加或删除元素会非常耗时。
    2)list和forward_list 两个容器设计的目的是令容器任何位置添加和删除操作都很快。作为代价,这两个容器不支持元素的随机访问,只能通过遍历整个容器来访问其中的一个元素
    3)deque支持快速随机访问,与string和vector类似,在中间位置添加、删除元素很慢,但在两端很快
    4)forward_list和array是新标准的类型。array不支持添加和删除元素以及改变容器大小的操作。forward_list没有size操作因为保存或计算其大小会比手写链表多出额外的开销
    
确定使用哪种顺序容器:
    1)若程序要求在容器中间插入或删除元素,用list或forward_list 
    2)若程序要求在容器头尾位置插入或删除元素,但不会在中间插入或删除元素,用deque
    
9.2 容器库概览
    一般来说,每个容器都定义在一个头文件中,文件名和类型名相同。容器均定义为模板类,所以必须提供额外信息来生成特定的容器类型。
    
    容器操作:
    1)构造函数
    C c;                // 默认构造函数,构造空容器
    C c1(c2);            // 构造c2的拷贝C1
    C c(b, e);            // 构造c,将迭代器b和e指定的范围内的元素拷贝到c,array不支持
    C c{a, b, c..};        // 列表初始化c
    2)赋值与swap
    c1 = c2;            // 将c1中的元素替换为c2中的元素
    c1 = {a, b, c..};    // 将c1中的元素替换为列表中的元素,array不支持
    a.swap(b);            // 交换a和b的元素
    swap(a, b);
    3)大小
    c.size();            // c中的元素数目
    c.max_size();        // c可保存的最大元素数目
    c.empty();            // 若c中存储了元素,返回false,否则返回true
    4)添加,删除元素
    c.insert(args);        // 将args中的 元素拷贝进c
    c.emplace(inits);    // 使用inits构造c中的一个元素
    c.erase(args);        // 删除args指定的元素
    c.clear();            // 删除c中的所有元素,返回void
    5)关系运算符
    ==,!=                // 所有容器都支持相等(不等)运算符
    < > <= >=            // 无序关联容器不支持关系运算符
    6)获取迭代器
    c.begin(),c.end()    // 返回指向c的首元素和尾元素之后位置的迭代器
    c.cbegin(),c.cend()    // 返回const_iterator
    
9.2.1 迭代器
    1、forward_list迭代器不支持递减运算符--
    
    2、一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置。标记了容器中元素的一个范围。
    
使用左闭合[begin, end)范围蕴含的编程假定:
    1)如果begin和end相等,则范围为空
    2)如果不等,则范围至少包含一个元素,且begin指向该烦恼为中的第一个元素
    3)可以对begin递增若干次,使得begin==end,end不可以在begin之前
    
练习9.4 编写函数,接受一对指向vector<int>的迭代器和一个int值。在两个迭代器指定的范围中查找给定的值,返回一个bool值来指出是否找到
#include <iostream>
#include<vector>
using namespace std;

bool FindVal(vector<int>::iterator begin, vector<int>::iterator end, int val)
{
    while (begin != end)
    {
        if (*begin == val)
        {
            return true;
        }
        ++begin;
    }
    return false;
}

int main() {
    vector<int> ivec = { 1, 3, 5, 7, 9 };
    cout << FindVal(ivec.begin(), ivec.end(), 3) << endl;
    cout << FindVal(ivec.begin(), ivec.end(), 8) << endl;
    system("pause");
    return 0;
}

练习9.6 
    与vector和deque不同,list的迭代器不支持<运算,只支持递增,递减,==以及!=运算。
    原因在于这几种数据结构实现上的不同。vector和deque将元素在内存中连续保存,而list则是将元素以链表方式存储。
    因此前者可以方便地实现迭代器的大小比较来体现元素的前后关系。而在list中,两个指针的大小关系与它们指向的元素的前后关系不一定是吻合的,实现<运算将很困难
    
9.2.2 容器类型成员
    每个容器都定义了多个类型,我们已经使用过三种size_type, iterator, const_iterator
    除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。
    
    例如,为了索引int的vector中的元素,应该使用迭代器类型vector<int>::iterator类型
    为了读取string的list中的元素,应该使用list<string>::value_type,因为value_type表示元素类型
    为了写入数据,需要(非常量)引用类型,因此应该使用list<string>::reference
    
9.2.3 begin和end成员
    begin和end有多个版本:带r的版本返回反向迭代器,以c开头的版本返回const迭代器:
    list<string> a = {"aa", "bb"};
    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

9.2.4 容器定义和初始化
    
将一个容器初始化为另一个容器的拷贝:
    将一个新容器创建为另一个容器的拷贝的方法有两种:
    1)直接拷贝整个容器:两个容器的类型及其元素类型必须匹配    
    2)拷贝由一个迭代器对指定的元素范围:不要求容器类型相同,元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可
    list<string> authors = {"aa", "bb", "cc"};
    vector<const char*> articles = {"a", "b", "c"};
    
    list<string> list2(authors);    // 正确,类型匹配
    deque<string> authList(authors);// 错误,容器类型不匹配
    vector<string> words(articles);    // 错误,容器类型必须匹配
    forward_list<string> words(articles.begin(), articles.end());    // 正确,可以将const char*元素转换为string
    
列表初始化:
    在新标准中,可以对一个容器进行列表初始化,这样做时,显式指定了容器中每个元素的值。对于除array之外的容器类型,初始化列表还隐含地指定了容器的大小
    
与顺序容器大小相关的构造函数:
    如果元素类型是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。
    如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。
    vector<int> ivec(10, -1);        // 10个int元素,每个都初始化为-1
    list<string> svec(10, "hi");    // 10个string,每个都初始化为hi
    forward_list<int> ivec(10);        // 10个元素,每个都初始化为0
    deque<string> svec(10);            // 10个元素,每个都是空string
    只有顺序容器的构造函数才接受大小参数,关联容器并不支持
    
标准库array具有固定大小:
    与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:
    array<int, 42>        // 类型为:保存42个int的数组
    
    为了使用array类型,我们必须同时指定元素类型和大小:
    array<int, 10>::size_type i;        // 数组类型包括元素类型和大小
    array<int>::size_type j;            // 错误,array<int>不是一个类型
    由于大小也是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会确定容器的大小。
    
    一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化,就像一个内置数组中的元素那样。
    
    如果我们队array进行列表初始化,初始值的数目必须等于或小于array的大小
    如果初始值数目小于array的大小,则他们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。
    如果元素类型是一个类类型,那么该类必须有一个默认构造函数,使值初始化能够进行:
    array<int, 10> ia1;            // 10个默认初始化的int
    array<int, 3> ia2={0,1,2};    // 列表初始化
    array<int, 10> ia3={42};    // ia3[0]为42,剩余元素为0
    
    值得注意的是,虽然不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制
    int digs[3] = {0, 1, 2};
    int cpy[3] = digs;        // 错误,内置数组不支持拷贝和赋值
    array<int, 3> digits = {0, 1, 2};
    array<int, 3> copy  = digits;    // 正确,只要数组类型匹配即合法
    与其他容器一样,array也要求初始值的类型必须要与创建的容器类型相同。此外array还要求元素类型 和大小也一样,因为大小是array类型的一部分
    
练习9.11 列出6种创建和初始化vector对象的方法
    1)vector<int> ilist1;    // 默认初始化,vector为空。size返回0,表明容器中没有元素。capacity返回0,表明未分配存储空间
    2)vector<int> ilist2(ilist);    // ilist2初始化为ilist的拷贝,ilist必须与ilist2类型相同,等价于vector<int> ilist2=ilist;
    3)vector<int> ilist = {1,2,3};    // ilist初始化为列表中元素的拷贝,列表中的元素类型必须与ilist的元素 类型相容,等价于vector<int> ilist{1,2,3}
    4)vector<int> ilist3(ilist.begin()+2,ilist.end()-1);    // ilist3初始化为两个迭代器指定范围中的元素的拷贝,范围中的元素类型必须与ilist3的元素类型相容
    5)vector<int> ilist(7);    // 默认初始化,ilist中将包含7个元素,每个元素进行缺省的值初始化。
    6)vector<int> ilist(7,3);    // 指定值初始化,ilist被初始化为包含7个值为3的int
    
9.2.5 赋值和swap
    1、如果两个容器原来大小不同,赋值运算后两者的大小都与右边的原大小相同
    
    2、与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型
    
    3、由于右边运算对象的大小可能与左边运算对象的大小不同,因此array类型不支持assign,也不允许用花括号包围的值列表进行赋值
    
    4、容器赋值运算:
    1)c1 = c2;            // 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型
    2)c = {a, b, c...}    // 将c1中元素替换为初始化列表中元素的拷贝(array不适用)
    3)swap(c1, c2);    // 交换c1和c2中的元素。必须具有相同的类型,swap通常比从c2向c1拷贝元素快得多。等价于c1.swap(c2)
    assign操作不适用于关联容器和array
    4)seq.assign(b, e);// 将seq中的元素替换为迭代器b和e所表示的范围中的元素,迭代器b和e不能指向seq中的元素
    5)seq.assign(il);    // 将seq中的元素替换为初始化列表il中的元素
    6)seq.assign(n, t);// 将seq中的元素替换为n个值为t的元素
    赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效
    
使用assign(仅顺序容器)
    5、赋值运算要求左边和右边的运算对象具有相同的类型。
        而顺序容器(除array外)还定义了一个名为assign的成员,允许从一个不同但相容的类型赋值,或者从容器的一个子序列赋值
    list<string> names;
    vector<const char*> old;
    names = old;    // 错误,容器类型不匹配
    // 正确,可以将const char*转换为string
    names.assign(old.cbegin(), old.cend());
    assign的第二个版本接受一个整型值和一个元素值,它用指定数目且具有相同给定值的元素替换容器中原有的元素
    
使用swap:
    6、swap操作交换两个相同类型容器的内容。调用swap之后,两个容器中的元素将会交换
    
    7、swap只是交换了两个容器的内部数据结构,元素本身并未交换,除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间完成
    
    8、元素不会被移动意味着除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是在交换之后这些元素已经属于不同的容器了
    例如:假设iter在swap之前指向svec1[3]的string,那么swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效
    
    9、swap两个array会真正交换它们的元素。
    
9.2.6 容器大小操作
    成员函数size,empty,max_size
    
9.2.7 关系运算符
    1、每个容器类型都支持相等运算符(==和!=),除了无序关联容器外的所有容器都支持关系运算符(>,>=,<,<=)
    
    2、关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
    
    3、只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器
    
    总结就是,容器的比较操作需要有两个条件:2和3
    
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
使用push_back:
    1、除了array和foward_list之外,每个顺序容器(包括string)都支持push_back,将在容器的尾部创建一个新元素,该元素为原来的拷贝,随后对容器中元素的改变不影响原始对象
    
使用push_front:
    2、将容器插入到容器的头部,list、forward_list、deque容器都支持该操作
    
    3、deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front
    
在容器中特定位置添加元素:
    4、insert成员允许我们在容器中任意位置插入0个或多个元素。将元素插入到迭代器所指定的位置之前。
    
    5、虽然某些容器不支持push_front操作,但它们对于insert操作并无限制,因此可以将元素插入到容器开始的位置,而不必担心容器是否支持push_front
    
插入范围元素:
    6、insert的另一个版本接受一个元素数目和 一个值,它将制定数量的元素添加到制定位置之前,这些元素都按给定值初始化:svec.insert(svec.end(),10,"aa");
        将10个元素插入到svec的末尾,并将所有元素初始化为"aa"
        
    7、insert的第三个版本接受一对迭代器或一个初始化列表,将给定范围中的元素插入到指定位置之前
    slist.insert(slist.begin(), v.end()-2, v.end());
    slist.insert(slist.end(), {"aa","bb"});
    该版本返回指向第一个新加入元素的迭代器。
    
使用insert的返回值:
    通过使用insert的返回值,可以再容器中一个特定的位置反复插入元素:
    list<string> lst;
    auto iter = lst.begin();
    while(cin>>word)
        iter = lst.insert(iter, word);
    可以看到,这里不用递增iter,因为insert返回的迭代器刚好指向插入的新元素,将此迭代器赋予iter并重复循环,每步while循环就会将一个新元素插入到iter之前
    
使用emplace操作:
    8、新标准引入了三个新成员--emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。分别对应push_front、insert和push_back
    
    9、当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。
    而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员是用这些参数在容器管理的内存空间中直接构造元素。
    
    10、emplace成员函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配
    
9.3.2 访问元素
    1、包括array在内的每个顺序容器都有一个front成员函数
    2、除了forward_list之外的所有顺序容器都有一个back成员函数。
    这两个操作分别返回首元素和尾元素的引用。
    所以,有两种方式获取c中首元素和尾元素的引用:
    1)直接方法:调用front和back,如auto val = c.front, val1 = c.back();
    2)间接方法:通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获得尾元素的引用。如auto val=*c.begin(),val1=*(--c.end());
    
    3、注意,在调用front和back之前,要确保c非空,用if(!c.empty())
    
    4、在顺序容器中访问元素的操作:
    1)c.back()        返回c中尾元素的引用。不适用于forward_list
    2)c.front()    返回c中首元素的引用
    3)c[n]            返回c中下标为n的引用,n是一个无符号整数
    4)c.at(n)        返回下标为n的元素的引用,如果下标越界,抛出out_of_range异常
    其中,at和下标操作只适用于string,vector,deque和array
    
访问成员函数返回的是引用:
    5、在容器中返回元素的成员函数(以上4种)返回的都是引用。如果容器是一个const对象,则返回值是const引用。如果不是,则返回值是普通引用,可用来改变元素的值:
    if(!c.empty()){
        c.front() = 42;
        auto &v = c.back();        // 获得指向最后一个元素的引用
        v = 1024;                // 改变c中的元素
        auto v2 = c.back();        // v2不是一个引用,它是c.back()的一个拷贝
        v2 = 0;                    // 未改变c中的元素
    }
    如果我们使用auto变量来保存这些函数的返回值,并希望使用此变量来改变元素的值,要记得将变量定义为引用类型
    
下标操作和安全的随机访问:
    6、提供快速随机访问的容器,也都提供下标运算符。
    
    7、如果我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但是下标越界,at会抛出一个out_of_range异常
    
9.3.3 删除元素
    1、删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效
    
pop_front和pop_back成员函数:
    2、分别删除首元素和尾元素
    
    3、与vector和string不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back

从容器内部删除一个元素:
    成员函数erase从容器中指定位置删除元素。可以:
    1)删除由一个迭代器指定的单个像素,
    2)也可以删除由一对迭代器指定的范围内的所有元素。
    两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,删除i之后将返回指向j的迭代器
    
    
9.3.4 特殊的forward_list操作
    forward_list是单向链表,在一个单向链表中,没有简单的方法来获取一个元素的前驱。出于这个原因,在一个forward_list中添加或删除元素的操作时通过改变给定元素之后的元素来完成的

    所以forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作。
    此外,还定义了一个before_begin,返回一个首前迭代器,这个迭代器允许我们在链表首元素之前并不存在的元素之后添加或删除元素
    
    
9.3.5 改变容器大小
    可以用resize来增大或缩小容器,array不支持resize。
    
    如果当前大小大于所要求的大小,容器后部的元素会被全部删除;若当前大小小于新大小,会将新元素添加到容器后部
    
    resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。
    如果容器保存的是类类型元素,且resize向容器中添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数
    
9.3.6 容器操作可能使迭代器失效
    
9.4 vector对象是如何增长的
管理容量的成员函数:
    capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。
    reserve操作允许我们通知容器它应该准备保存多少个元素
    
    1、容器大小管理操作:
    1)c.shrink_to_fit()    // 将capacity()减少为与size()相同大小,只适用于vector,string和deque
    2)c.capacity()            // 不重新分配内存空间的话,c可以保存多少元素
    3)c.reserve(n)            // 分配至少能容纳n个元素的内存空间
    
    2、reserve并不改变容器中元素的数量,仅影响vector预先分配多大的内存空间。
    只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。如果需求大小大于当前容量,reserve至少分配与需求一样大的空间
    如果需求大小小于或等于当前容量,reserve什么也不做
    
9.5 额外的string操作
9.5.1 构造string的其他方法
    1、
    1)string s(cp, n)        // s是cp指向的数组中前n个字符的拷贝。此数组至少应包含n个字符
    2)string s(s2,pos2)    // s是string s2从下表pos2开始的字符的拷贝。
    3)string s(s2,pos2,len2)//s是s2从下标pos2开始len2个字符的拷贝
    这些构造函数接受一个string或一个const char*参数,还接受(可选的)指定拷贝多少个字符的参数
    const char *cp = "hello world!!!";        // 以空字符结束的数组
    char noNull[] = {'h', 'i'};                // 不是以空字符结束
    string s1(cp);                            // 拷贝cp中的字符直到遇到空字符,s1=="hello world!!!"
    string s2(noNull);                        // 未定义,noNull不是以空字符结束,只能拷贝两个字符
    string s3(cp+6,5);                        // 从cp[6]开始拷贝5个字符
    通常当我们从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止
    
substr操作:
    2、substr操作返回一个string,它是原始string的一部分或者全部的拷贝,可以传递给substr一个可选的开始位置和计数值
    子字符串操作:s.substr(pos, n) 包含s中从pos开始的n个字符的拷贝。pos的默认值为0,n的默认值为s.size()-pos,即拷贝从pos开始的所有字符
    
9.5.2 改变string的其他方法
    
append和replace函数:
    string类定义了两个额外的成员函数:append和replace
    1)append操作是在string末尾进行插入操作的一种简写形式
    如s.insert(s.size(),"aa")  等价于s.append("aa")
    2)replace操作时调用erase和insert的一种简写形式
    s.replace(11,3,"aa");    // 从位置11开始,删除三个字符并插入aa
    插入的文本不必要与删除的文本一样长。
    
9.5.3 string搜索操作
    string类提供了6个不同的搜索函数,每个函数都有4个重载版本。
    搜索操作返回指定字符出现的下标,如果未找到则返回npos
    1)s.find(args)                // 查找s中args第一次出现的位置
    2)s.rfind(args)            // 查找s中args最后一次出现的位置
    3)s.find_first_of(args)    // 在s中查找args中任何一个字符第一次出现的位置
    4)s.find_last_of(args)        // 在s中查找args中任何一个字符最后一次出现的位置
    5)s.find_first_not_of(args)    // 在s中查找第一个不在args中的字符
    6)s.find_last_not_of(args)        // 在s中查找最后一个不在args中的字符
    
9.5.4 compare函数
    
9.5.5 数值转换
    string和数值之间的转换:
    1)to_string(val)            // val可以是任何算术类型
    2)stoi(s,p,b)                // 返回s的起始子串的数值,返回值类型可以是int,long,unsigned long,long long等,
    3)stol(s,p,b)                // p是size_t指针,用来保存s中第一个非数值字符的下标,p默认为0,即函数不保存下标
    4)stof(s,p),stod(s,p)...
    
9.6 容器适配器
    除了顺序容易外,标准库还定义了三个顺序容器适配器:stack,queue和priority_queue
    
定义一个适配器:
    每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。
    例如,假定deq是一个deque<int>,我们可以用deq来初始化一个新的stack:stack<int> stk(deq);
    
    默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的
    
    所有适配器都要求容器具有添加和删除元素的能力,因此,适配器不能构造在array之上。
    
    stack只要求push_back,pop_back,和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造stack
    具体见P329
    
 

猜你喜欢

转载自blog.csdn.net/CSDN_dzh/article/details/81276469
今日推荐