C++ prime (第五版) 第九章 小结


本章是第三章内容的延续和扩展。完成本章学习后,对标准库顺序容器的掌握就完整了。

要点:

1、标准库容器是模板类型,用来保存给定类型的对象。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。顺序容器有公共的标准接口:如果两个顺序容器都提供一个特定的操作,那么这个操作在两个容器中具有相同的接口和含义。与之相对的,在11章介绍的有序和无序的关联容器,则根据关键字的值来存储元素。

2、标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配。

具体内容梳理如下:

1.1 顺序容器概述

(1)类型:

vector : 大小可变数组,支持快速随机访问(元素的下标),在尾部之外的位置插入或删除元素可能很慢。

array:    大小固定数组,支持快随访,不能添加或删除元素。

string:  和vector相似,专用于保存字符。随机访问快(元素的下标),在尾部插入或删除快。

deque:双段队列,支持快随访,在头尾位置删除或插入快。

list:       双向链表,只支持双向顺序访问,在任何位置进行删除或插入都很快。

forward_list: 单向链表,只支持单向顺序访问,在任何位置进行删除或插入都很快。

(2)比较:

string和vector将元素保存在连续的内存空间。由于元素是连续存储的,由元素的下标来计算其地址是十分快速的。但是,在两种容器的中间位置添加和删除元素会非常耗时。

list和forward_list两个容器的设计目的就是令容器任何未知的添加和删除操作都很快速。但是,不支持元素的随机访问。

(3)选择:

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

如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。

如果你不确定应该使用哪种容器,那么可以在程序中只使用vector和list的公共操作:使用迭代器,不使用下标操作,避免随机访问。在必要时选择使用vector或list都很方便。

1.2 容器库

(1)顺序容器几乎可以保存任意类型的元素。(如:vector<vector<string>> lines;   //lines是一个vector,其元素类型是string的vector)。几乎所有容器都提供的操作,参见书本p.295 表9.2。

(2)迭代器

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

forward_list迭代器不支持递减运算器(--)(可能和单向顺序访问有关)。

迭代器范围由一对迭代器表示,两个迭代器分别指向同一容器中的元素或者是为元素之后的位置。(通常为begin()和end(),左闭合区间[begin, end)).对构成范围的迭代器的要求:1,它们指向同一容器的元素,或者容器最后一个元素之后的位置。2,反复递增begin来到达end。end不在begin之前。如果begin和end相等,则范围为空。如果不等,则范围至少包含一个元素,且begin指向范围中的第一个元素。如:while(begin != end){ *begin = val; ++begin;}

反向迭代器是一种反向遍历容器的迭代器。

begin和end有多个版本:带r的版本返回反向迭代器;以c开头的版本则返回const迭代器。begin是被重载过的,有两个版本:其中一个是const成员函数,也返回const迭代器;另一个是返回普通迭代器,可以对容器元素进行修改。auto it8 = a.cbegin();//不管a是什么类型,auto就是const_iterator类型。不能对容器元素进行修改。当不需要写访问时,应使用cbegin和cend.

(3)容器定义和初始化

每个容器类型都定义了一个默认构造函数。具体见p299表9.3。

将一个新容器创建为另一个容器的拷贝的方法有两种:一种是直接拷贝整个容器,要求两个容器的容器类型和元素类型都必须相同。第二种是(除array外)拷贝由一个迭代器对指定的元素范围(不要求容器类型是相同的,且新容器和原容器的元素类型也可以不同,只要能将要拷贝的元素转换即可),可以使用这种构造函数来拷贝一个容器中的子序列。

对于与顺序容器大小相关的构造函数,如果元素类型是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。否则,除了大小参数外,还必须指定一个显式的元素初始值。

大小是array类型的一部分,所以使用array(固定大小,一个默认构造的array是非空的)类型时,必须同时指定元素的类型和大小。

(4)赋值与swap(交换)

容器赋值运算具体细则见表9.4.

特别注意:赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效(可能会导致内存的重新分配)。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效。

顺序容器(除array外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。

除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效(swap只是交换了两个容器的内部数据结构)。(string存储的是字符串,在string变量中真正存储字符串的是一个叫_Ptr的指针,指向string所存储字符串首地址,而字符串没有固定地址,存储在一个临时内存区域,所以当字符串发生改变时,会发生内存的重新分配,所以会导致迭代器、引用和指针失效。)

(5)容器大小操作

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

(6)关系运算符

关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。

只有当其元素类型也定义了相应的比较运算符时(有些类并未定义相应的比较运算符),我们才可以使用关系运算符来比较两个容器。

1.3顺序容器操作-顺序容器所特有的操作

(1)向顺序容器添加元素

除array外,所有标准库容器都提供灵活的内存管理。详见表9.5.

向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效(存储空间的重新分配)。

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

1、list<string> slist; slist.insert(slist.begin(), "Hello");//等价于调用slist.push_front("Hello");

2、list<string> lst; auto iter = lst.begin(); 

while(cin >> word)

         iter = lst.insert(iter, word);//等价于调用push_front

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

3、c.emplace_back("978-0590353403", 25, 15.99);

等价于 c.push_back(Sales_data("978-0590353403", 25, 15.99));

(2)访问元素

如果容器中没有元素,访问操作的结果是未定义的。

包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。在调用front和back(或解引用begin和end返回的迭代器)之前,要确保容器非空。如果容器为空,if中操作的行为将是未定义的。

在容器中访问元素的成员函数(即,front、back、下标和at)返回的都是引用(auto &v = c.back();//获得指向最后一个元素的引用  auto v2 = c.back();//v2不是一个引用,它是c.back()的一个拷贝 )。我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但是如果下标越界,at会抛出一个out_of_range异常。

(3)删除元素

删除元素具体操作细则见表9.7:P311

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

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

(4)特殊的forward_list操作

相关操作细节见表9.8;P313

当在forward_list中添加或者删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向前驱(单向链表,当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素。我们需要访问其前驱,以便改变前驱的链接。)。在一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除操作所影响的元素。为了支持操作,forward_list定义了before——begin,返回的是首前迭代器。

(5)改变容器的大小

可以用resize来增大或缩小容器,array不支持。细则见表9.9.

(6)容器操作可能使迭代器失效的问题

使用失效的迭代器、指针或引用是严重的运行时错误。因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这对vector、string和deque尤为重要。

添加或删除vector、string和deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用和指针。

添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器。

1.4 vector容器大小管理

当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。这样,就不需要每次添加新元素都重新分配容器的内存空间了。

vector和string提供了管理容量的成员函数。capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。reserve操作允许我们通知容器它应该准备保存多少个元素。

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

只有当迫不得已时vector容器才可以分配新的内存空间。只有执行insert操作时size与capacity相等,或者调用resize或reserve时给定的大小超过当前capacity,vector才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于具体实现。

1.5 string的额外操作

(1)构造string的其他方法

详见表9.11:P321

substr操作返回一个string,它是原始string的一部分或全部的拷贝。可以传递给substr一个可选的开始位置和计数值。

(2)改变string的其他方法

string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。除此之外,它还定义了额外的append(尾后添加)和replace(插入)函数。具体实现见表9.13:P323.

(3)string的搜索操作

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

(2,逆向搜索,标准库提供了由右至左搜索的操作,rfind成员函数搜索最后一个匹配。

(4)compare函数 类似于c语言中的strcmp,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。

(5)数值转换

具体实现细则见表9.16;P328.

1.6容器适配器

不同的适配器就是不同的接口,不同的实现方式(数据结构?)。

一个适配器是一种机制,能使某事物的行为看起来像另外一种事物一样。

栈(先进后出)默认基于deque实现,也可以在list或vector之上实现。queue(队列(先进先出)适配器)默认基于deque实现,priority_queue默认基于vector实现。具体实现细则见表9.18表9.19.










猜你喜欢

转载自blog.csdn.net/sinat_37815269/article/details/80351231