顺序容器(上)——跟我一起从C到C++(第十期)

10 顺序容器

一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖元素的值,而与元素加入容器的位置相对应。

10.1 顺序容器类型

10.1.1 顺序容器概述

typeoverview
vector可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。
deque双端队列。支持快速随机访问。在头尾位置插入或者删除元素很快。
list双向链表。仅支持双向顺序访问。在list中任何位置进行插入或者删除操作速度很快。
forward_list单向链表。仅支持单向顺序访问。在链表任何位置进行插入或者删除操作速度很快。
array固定大小数组。支持快速随机访问。不能添加或删除元素。
string与vector相似的容器,但专门用于字符保存。随机速度快。在尾部插入或者删除速度快。

看上去,好像除了固定大小的array外,其他容器都提供高效灵活的内存管理。
不过,有得必有失,就像stringvector将元素连续存储在空间中,虽然使用元素下标来计算其地址非常的快速,但在两种容器的中间位置添加或者删除元素就会非常耗时(在插入或者删除时,需要移动插入或者删除的元素位置之后的所有元素);而listforward_list就可以快速添加和删除,但代价是他们不支持元素的随机访问(访问一个元素我们必须遍历整个容器),而且相比vectordequearray,这两个容器的额外内存开销也很大。

deque相对复杂。dequestringvector类似,支持快速随机访问,在中间位置添加或者删除元素的代价较高;但deque的两端添加或者删除元素都是很快的(与listforward_list相当)。

我们再瞧瞧array,与内置数组相比,array是一种更安全、更容易使用的数组类型。与内置数组相似,array对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器的大小。
forward_list的设计目标是达到与最好的书写的单向链表数据结构相当的性能,因此, forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。

亲爱的,当你用上了C++,你就该尽可能使用C++标准库程序,而不是更原始的数据结构,如内置数组啥的。

10.1.2 选择容器指南

通常,我们按以下方式选取容器:

  • 除非你有很好的理由选择其他容器,否则就用vector
  • 如果你程序有很多小元素,且空间额外开销很重要,请不要用listforward_list
  • 如果你程序要求随机访问元素,vectordeque
  • 如果你程序要求在容器中间位置插入或删除元素,listforward_list
  • 如果你程序要求在容器头尾位置插入或删除元素,但不会在中间位置进行插入或删除,deque
  • 如果你程序只有读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则:首先确认是否真的需要在容器中间插入元素(比如有情况就能使用vector追加元素,并使用sort排序,从而避免在中间插入元素);如果确实需要,那我们就可以在输入阶段使用list,输入完成后拷贝到vector中。

10.2 容器操作

使用之前,我们引用其类型同名头文件,比如list就引入#include <list>

基本上顺序容器可以保存任意类型的元素,特别的,我们也可以定义一个容器,其元素类型是另外一个容器:

std::list<std::string> l;

还可以容器里放容器:

std::list<std::vector<std::string>> l;

而每种顺序容器都提供了一组有用的类型定义以及以下操作:

  • 在容器中添加元素;
  • 在容器中删除元素;
  • 设置容器大小;
  • (如果有的话)获取容器内的第一个和最后一个元素

10.2.1 容器定义的类型别名

类型别名含义
size_type无符号整型,足以存储此容器类型的最大可能容器长度
iterator此容器类型的迭代器类型
const_iterator元素只读迭代器类型
reverse_iterator 按逆序寻址元素的迭代器类型
const_reverse_iterator元素只读逆序迭代器类型
difference_type足够存储两个迭代器差值的有符号整型,可为负数
value_type元素类型
reference元素的左值类型,是value_type&的同义词
const_value_type元素的常量左值类型,等效于const value_type&

10.2.2 迭代器

容器迭代器支持的操作可以回顾 4.3 迭代器,但有一个例外,forward_list不支持递减运算符--
当然还有一些具体的迭代器运算,必须具体问题具体分析。

也可以看看
C++中的迭代器(2)——迭代器运算

10.2.2.1 迭代范围

范围之前说过了,这里再重复一遍:

[begin,end)

因为end指向尾元素之后的元素,故不能取。

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

也就是把之前我们编程时的经验总结一下:

  • 如果beginend相等,则范围为空。
  • 如果beginend不等,则范围内至少包含一个元素,且begin指向第一个元素。
  • 我们可以递增begin,直到begin==end

代码表示一下,你就熟悉了:

	vector<int> v{1, 3, 5, 7, 9};
    vector<int>::iterator begin = v.begin();
    auto end = v.end();
    while (begin != end)
    {
        cout << *begin << endl;
        ++begin;
    }
10.2.2.3 begin和end成员

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

c.cbegin() //返回const_iterator
c.cend()
c.rbegin() //按逆序寻址元素的迭代器
c.rend()
c.crbegin() //返回const_reverse_iterator
c.crend()

故,我们应该改写刚刚的例子:

	vector<int> v{1, 3, 5, 7, 9};
    vector<int>::const_iterator cbegin = v.cbegin();
    auto cend = v.cend();
    while (cbegin != cend)
    {
        cout << *cbegin << endl;
        ++cbegin;
    }

当然这里的const是指的底层const(不能修改指向的对象)

当不需要写访问的时候,应该使用cbegin和cend

10.2.3 容器定义和初始化

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

构造函数含义
C c默认构造函数。如果C是一个array,则c中的元素按默认方式初始化;否则c为空
C c(c2)
C c=c2
创建容器c2的副本cc2c必须具有相同的容器类型,并存放相同类型的元素。
C c(b, e)创建容器c,其中元素是迭代器b和e标示的范围内元素的副本。(array不适用)
C c{a,b,c...}
C c= {a,b,c...}
列表初始化c
C c(n)创建有n个初始化元素的容器c。此构造函数是explicit的(取消了隐式转换),(string不适用)
C c(n, t)使用n个为t的元素创建容器c,其中值t必须是容器类型C的元素类型的值,或者是可以转换为该类型的值。

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

	list<string> author = {"mico", "rich"};
    vector<const char *> articles = {"a", "an", "the"};
	deque<string> d1(author);//报错,容器类型不匹配
    deque<string> d2(author.cbegin(), author.cend());//正确
    forward_list<string> wrods(articles.cbegin(), articles.cend());//正确

10.2.4 array具有固定大小

类似是一个保存10个int型的array

array<int, 10> a;

不过,由于大小是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会隐式或者显式地确定容器大小。
而且array固定大小的特性也影响了它所定义的函数的行为。与其他容器不同,一个默认构造的array是非空的,它包含了与其大小一样多的元素(这些元素都被默认初始化了)。
当然也可以进行列表初始化,不过当初始化数目小于array大小时,这些列表初始化靠前的元素,所有剩余元素都会进行默认初始化。

内置数组是不能进行拷贝或对象赋值,但array可以。

10.2.5 赋值和swap

操作功能
c1=c2删除容器c1中所有的元素,然后将c2的元素复制给c1c1c2的类型(包括容器类型和元素类型)必须相同
c1.swap(c2)交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的是原来c1的元素。c1c2的类型必须相同。该函数的执行速度通常要比将c2复制到c1的操作快
swap(c1, c2)等价于c1.swap(c2)

除了array,swap不会进行拷贝、删除、插入,它仅仅交换了两个容器的内部数据结构(元素本身没有交换),因此可以保证在常数时间内完成。

由于容器内没有移动任何元素,故,除了string,指向容器迭代器、引用、指针都不会失效,只不过指向另外一个容器了罢了。

	vector<int> v1 = {1, 2, 3};
    vector<int> v2 = {4, 5, 6};
    auto it = v1.cbegin() + 2;//3
    swap(v1, v2);
    cout << *it << endl;//3

不过,string调用swap会导致迭代器、引用、指针失效。

与其他容器不同,swap两个array会真正交换它们的元素。
所以指针、引用、迭代器都指向了交换后的值。

	array<int, 3> a1 = {1, 2, 3};
    array<int, 3> a2 = {4, 5, 6};
    auto it = a1.cbegin() + 2;//3
    swap(a1, a2);
    cout << *it << endl;//6

swap分为成员函数和非成员函数版本。非成员函数版的swap在泛型编程中非常常见。统一使用非成员函数版本的swap是一个好习惯。

assign操作仅顺序容器(除了array)可以使用,即array和关联容器不能使用。

操作功能
c.assign(b, e)重新设置c的元素,将迭代器bc标记范围内的所有元素复制到c中。be必须不是指向c中元素的迭代器
c.assign(il)重新设置c的元素为初始化列表il的元素
c.assign(n, t)c重新设置为存储n个值为t的元素

如果你想把用不同类型的容器来进行赋值:
如果直接:

	list<string> names;
    vector<const char *> oldstyle;
    names = oldstyle;//错误,容器类型不符合

但我们可以使用assign:

	names.assign(oldstyle.cbegin(), oldstyle.cend());

10.2.6 容器大小操作

操作功能
c.size()返回容器c中元素个数,返回类型为c::size_typeforward_list不支持)
c.max_size()返回容器c可容纳的最多元素个数,返回类型为c::size_type
c.empty()返回标记容器大小是否为0的布尔值

10.2.7 关系运算符

==!=
如果两个容器具有相同的大小且所有元素都两两对应相等,则两个容器相等;否则不等。

><<=>=
如果两个容器大小不同,但较小容器中的每个元素都等于较大容器的中的对应元素,则较小容器小于较大容器;
如果两个容器都不是另外一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。

不过,只有当其元素定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。

10.3 顺序容器操作

顺序容器与关联容器不同于两者组织元素的方式。这些不同之处直接关系到元素如何存储、访问、添加和删除。之前我们介绍了所有容器的操作,现在我们来介绍顺序容器支持的操作。

10.3.1 添加元素

这些操作都会改变容器大小,故均不适用于array

操作功能
c.push_back(t)在容器c的尾部添加值为t的元素,返回void类型(forward_list不支持)
c.push_front(t)在容器c的前端添加值为t的元素,返回void类型(vectorstring不支持)
c.insert(p, t)在迭代器p所指向的元素前面插入值为t的新元素,返回指向新添加元素的迭代器
c.insert(p, n, t)在迭代器p所指向的元素前面添加插入n个值为t的新元素,返回指向新添加元素的迭代器
c.insert(p, b, e)在迭代器p所指向元素前面插入由迭代器be标记范围的元素,返回指向新添加元素的迭代器

平时我们使用push_back意为在容器末尾添加,而且在listforward_listdeque容器中还支持push_front,在容器开头添加。
dequevector一样提供了随机访问元素的能力,但它也具备了vector的不支持的push_frontdeque 能保证在容器首尾插入和删除元素都只花常数时间,但它也和vector一样,在首尾之外的位置插入元素会很耗时。

虽然一些容器不支持push_front,但我们可以使用奇招:

	list<string> l1={"this" ,"world!"};
    l1.insert(l1.cbegin(),"hello");

当然要完成push_front,还有一点,就是迭代器指向新添加的元素:

	list<string> l1={"this" ,"world!"};
    auto iter = l1.begin();
    string word;
    while (cin >> word)
        iter = l1.insert(iter, word);//等价push_front

新标准引入了三个新成员——emplace_frontemplaceemplace_back
这些操作构造而不拷贝元素,对应的是push_frontinsertpush_back

当调用pushinsert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。
而我们调用emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
c++之emplace的使用
就比如我们可以把

	list<string> l1;
	string word(5,'c');
    l1.push_back(word);

写成

	list<string> l1;
    l1.emplace_back(5,'c');

10.3.2 访问元素

操作功能
c.back()返回容器c的最后一个元素的引用,如果c为空,则该操作未定义(forward_list不适用)
c.front()返回容器c的第一个元素的引用,如果c为空,则该操作未定义
c[n]返回下标为n的元素的引用,如果n<0n>c.size(),则该操作未定义(只适用于vectordequestringarray
c.at(n)返回下标为n的元素的引用。如果下标越界,则该操作未定义(只适用于vectordequestringarray

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

if (!c.empty())
    {
        //val和val2类型相同,都是c中的第一个元素值的拷贝
        auto val = *c.begin(), val2 = c.front();
        auto last = c.end();
        //val3==val4
        auto val3 = *(--last);//如果是forward_list则不能递减
        auto val4 = c.back();//如果是forward_list则没有back
    }

在调用frontback或者解引用迭代器之前,要保证c非空,否则if中的行为将未定义。

而且,因为返回的是引用,所以有这样的性质:

	if (!c.empty())
    {
        c.front() = "kimo";
        auto &v = c.back();
        v = "rimo";//改变
        auto v2 = c.back();
        v2 = "mori";//未改变
    }

10.3.3 删除元素

这些操作都会改变容器大小,故均不适用于array

操作功能
c.pop_back()删除容器c的最后一个元素,返回void。如果c为空容器,则该操作未定义(不支持forward_list
c.pop_font()删除容器c的第一个元素,返回void。如果c为空容器,则该操作未定义(不支持vectorstring
c.erase(p)删除迭代器p所指向的元素,返回一个迭代器,它指向被删除元素后面的元素。如果p指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置,如果p本身就是指向超出末端的下一位置的迭代器,则该函数未定义(forward_list有特殊版本的erase
c.erase(b, e)删除迭代器be标记的范围内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一位置的迭代器(尾后迭代器),则返回的迭代器也指向容器末端的下一位置(尾后迭代器)
c.clear()删除容器c内的所有元素,返回void

pop_back()pop_font()这些操作都返回void,如果你需要弹出元素值,那么你需要在执行弹出操作之前进行保存。

我们在容器内部删除一个元素很简单:

	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;

或者删除多个元素

	list<int> lst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto elem1 = lst.begin();
    auto elem2 = ++lst.begin();
    elem1 = lst.erase(elem1, elem2);

删除的范围是[elem1, elem2),删除结束后elem1==elem2

10.3.4 特殊的forward_list

为了更好的理解forward_list为什么有特殊版本的添加和删除,我们可以思考假如我们在一个单链表删除了一个元素会怎么样:

就是在我们删除或添加一个元素时,我们需要访问它们之前的元素。因为这些操作上的差异,forward_list并未定义insertemplaceerase,而是定义了insert_afteremplace_aftererase_after的操作。例如我们刚刚的例子,我们为了删除elem3,应该用指向elem2的迭代器调用erase_after
为了支持这些操作,forward_list也定义了before_begin,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素之后添加或者删除元素。

操作功能
lst.before_begin()返回指向链表首元素之前的不存在的元素的迭代器,此迭代器不能解引用。
lst.cbefore_begin()返回const_iterator
lst.insert_after(p,t)
lst.insert_after(p,n,t)
lst.insert_after(p,b,e)
lst.insert_after(p,il)
在迭代器p之后的位置插入元素。t是对象,n是数量,be是表示范围的一对迭代器,il是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p就是尾后元素,则行为函数行为未定义。
lst.emplace_after(p,args)使用argsp指定的位置创建一个元素,返回一个指向这个新元素的迭代器。如果p指向的是尾后元素,则函数行为未定义。
lst.erase_after(p)
lst.erase_after(b,e)
删除迭代器p之后的元素。
或者删除b之后直到e(不包含e)的范围的元素。返回一个指向指向被删除元素之后的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向的是尾后元素,则函数行为未定义。

把刚刚的删除奇数的例子用单链表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;
        }
    }

10.3.5 改变容器大小

我们使用resize来增大或者缩小容器,当然,因为改变了容器的大小,所以array是不支持的。

操作功能
c.resize(n)调整c的大小为n个元素,若n<c.size(),则多出来的元素被丢弃;若必须添加新元素,对新元素进行初始化。
c.resize(n,t)调整c的大小为n个元素。任何新添加的元素都初始化为t

如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素加到容器后部:

	list<int> lst(10, 42);//10个42
    lst.resize(15);//把5个0加到lst的末尾
    lst.resize(25, -1);//把10个-1加到lst的末尾
    lst.resize(5);//从lst末尾删20个元素

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值