C++primer小结9-10章

第九章 顺序容器

9.1 顺序容器概述

所有顺序容器都提供了访问快速顺序访问元素的能力,但是在以下方面有不同的性能折中:

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

9.2 容器库

9.2.1 迭代器

与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
迭代器范围的概念是标准库的基础。
第二个迭代器从来不会指向范围中的最后一个元素,而是指向尾元素之后的位置。
begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。

9.2.4 容器定义和初始化

每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者(array除外)拷贝由一个迭代器对指定的元素范围。
为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过当传递迭代器来拷贝一个范围时,就不要求容器类型是相同的。而且新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。两个迭代器分别标记想要拷贝的第一个元素和尾元素之后的位置。

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

在新标准中,我们可以对一个容器进行列表初始化。
除了与关联容器相同的构造函数外,顺序容器还提供另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值。如果我们不提供元素初始值,则标准库会创建一个值初始化器。

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

当定义一个array时,我们必须同时指定元素类型和大小。array<int, 10>
由于大小是array类型的一部分,array不支持普通的容器构造函数,这些构造函数都会确定容器的大小。
与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。
值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。
array也要求初始值的类型必须与要创建的容器类型相同。

9.2.5 赋值和swap

赋值运算符将其左侧容器中的全部元素替换为右边容器中元素的拷贝。
与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型。
由于右边运算对象的大小可能与左边运算对象的大小不同,因此array类型不支持assign,也不允许用花括号包围的值列表进行赋值。

swap(c1, c2) c1.swap(c2)
交换c1和c2中的元素。c1和c2具有相同的类型。swap通常比从c2向c1拷贝元素快得多。
assign操作不适用于关联容器和array。

赋值运算操作会导致左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效。(容器类型为array和string的情况除外)。
使用assign(仅顺序容器)
顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。

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

assign的第二个版本接收一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素。
swap操作交换两个相同类型容器的内容。调用swap后,两个容器中的元素将会交换。
除array外,交换两个容器内容的操作会很快——元素本身并未交换,swap只是交换了两个容器的内部数据结构。
除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
swap两个array会真正交换它们的元素。

9.2.6 容器大小操作

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

9.2.7 关系运算符

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

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

9.3 顺序容器操作

9.3.1向顺序容器添加元素

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

c.push_back(t)
c.push_front(t)  //vector和string不支持
c.insert(p,t)
c.insert(p,n,t)
c.insert(p,b,e)
c.insert(p,i1)
向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。

当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。

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

push_back和push_front操作提供了一种方便地在顺序容器尾部或头部插入单个元素的方法。insert成员提供了更一般的添加功能,允许我们在容器中任意位置插入0个或多个元素。insert函数将元素插入到迭代器所指定的位置之前。

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

如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器。
在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器。如果范围为空,不插入任何元素,insert操作会将第一个参数返回。
使用emplace操作
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们使用emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。

9.3.2 访问元素

c.back()
c.front()
c[n]
c.at(n)  //返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常
对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误

在容器中访问元素的成员函数返回的都是引用。
提供快速随机访问的容器也都提供下标运算符。

9.3.3 删除元素

c.pop_back()
c.pop_front()
c.erase(p) //删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代器
c.erase(b, e) //删除迭代器b和e所指范围内的元素
c.clear() //删除c中的所有元素
删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。
指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
删除元素的成员函数并不检查其参数。在删除元素之前,必须确保它们是存在的。

9.3.4 特殊的forward_list操作

lst.before_begin()  //返回指向链表首元素之前不存在的元素的迭代器
lst.insert_after(p, t)  //在迭代器p之后的位置插入元素
emplace_after(p, args)  //使用args在p指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。
lst.erase_after(p)  //删除p指向的位置之后的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。 

9.3.5 改变容器大小

我们可以用resize来增大或缩小容器。
resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。如果容器中保存的是类类型元素,且resize向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数。

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

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。
添加/删除vector、string或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。
如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。

9.4 vector对象是如何增长的

向vector或string中添加元素:如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。
当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可以用来保存更多的新元素。
vector和string类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动。capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。reverse操作允许我们通知容器它应该准备保存多少个元素。
reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。
容器的size是指它已经保存的元素的数目;而capicity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
只要没有操作需求超出vector的容量,vector就不能重新分配内存空间。

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

9.5 额外的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个字符的拷贝。
这些构造函数接受一个string或一个const char*参数,还接受(可选的)指定拷贝多少个字符的参数。当我们传递给它们的
是一个string时,还可以给定一个下标来指出从哪里开始拷贝。

通常当我们从一个const char* 创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。

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

string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。此外,它还定义了额外的insert和erase版本。
sriing类定义了两个额外的成员函数:append和replace,这两个函数可以改变string的内容。
append操作是在string末尾进行插入操作的一种简写形式。
replace操作是调用erase和insert的一种简写形式。
string搜索操作
string类提供了6个不同的搜索函数。每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。该类型是一个unsigned类型。

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中的字符

除关系运算符外,还提供了一组compare函数,与C标准库的strcmp函数很相似。根据s是等于、大于还是小于参数给定的字符串,s.compare返回0、整数或负数。
新标准库引入了多个函数,可以实现数值数据与标准库string之间的转换:

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

9.6 容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue、priority_queue。
适配器是标准库中的一个通用概念。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。

第十章 泛型算法

泛型算法:称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;称它们是“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型,以及还能用于其他类型的序列。

10.1 概述

大多数算法都定义在头文件algorithm中。一般情况下,这些算法并不直接操作容器,而是遍历两个迭代器指定的一个元素范围来进行操作。
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。

关键概念:泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。算法用于不会改变底层容器
的大小。算法可能改变容器中元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。

10.2 初识泛型算法

除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。

10.2.1 只读算法

find、cout、accumulate函数都是只读算法。accumulate定义在头文件numeric中。
accumulate函数接收三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。它的类型决定了函数中使用哪个加法运算符以及返回值的类型。
对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()。但是如果计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数。
另一个只读算法是equal,用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。
那些只接收一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

10.2.2 写容器元素的算法

fill(vec.begin(), vec.end(), 0)
算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。
fill_n(vec.begin(), vec.size(), 0)
fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。
向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

插入迭代器
插入迭代器是一种向容器中添加元素的迭代器。
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。copy返回的是其目的位置迭代器(递增后)的值。

10.2.3 重排容器元素的算法

sort() sort算法接受两个迭代器,表示要排序的元素范围。
unique() unique算法重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。unique返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么。
标准库算法对迭代器而不是容器进行操作。因此,算法不能(直接)添加或删除元素。

10.3 定制操作

谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词和二元谓词。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。
排序算法
可以使用stable_sort算法,稳定排序算法维持相等元素的原有排序。
find_if算法用来查找第一个具有特定大小的元素。find_if算法接受一对迭代器,表示一个范围。第三个参数是一个谓词。find_if算法对输入序列中的每个元素调用给定的这个谓词,它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。
lambda表达式
我们使用过的仅有的两种可调用对象是函数和函数指针。还有两种可调用对象:重载了函数调用运算符的类,以及lambda表达式。
一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。
一个lambda具有一个返回类型,一个参数列表和一个函数体。但是lambda可能定义在函数内部。
与普通函数不同,lambda必须使用尾置返回来指定返回类型。
lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符()。
Note:如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数。一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
空捕获列表表明此lambda不使用它所在函数中的任何局部变量。
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
for_each算法,接受一个可调用对象,并对输入序列中每个元素调用此对象。
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字。

10.3.3 lambda捕获和返回

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
值捕获
引用捕获
当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。
建议:尽量保持lambda的变量捕获简单化。
隐式捕获
为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。
可变lambda
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。
一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型。
指定lambda返回类型

10.3.4 参数绑定

对于那种只在一两个地方使用的操作,lambda表达式是最有用的。如果需要在很多地方使用相同的操作,通常应该定义一个函数。
标准库bind函数
我们可以解决向check_size传递一个长度参数的问题,方法是使用bind的标准库函数。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
bind的参数
我们可以用bind绑定给定可调用对象中的参数或重新安排其顺序。
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中.但是,与lambda类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。
标准库ref函数,函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。

10.4 再探迭代器

除了为每个容器定义的迭代器之外,标准库在iterator中还定义了额外几种迭代器。
包括:

  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
  • 流迭代器:这些迭代器被绑定到输入或者输出流上,可用来遍历所有关联的IO流。
  • 反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
  • 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。

插入器有三种类型,差异在于元素插入的位置:

  • back_inserter创建一个使用push_back的迭代器
  • front_inserter创建一个使用push_front的迭代器
  • inserter创建一个使用insert的迭代器

标准库定义了可以用于IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator<T> in(is);  //in从输入流is读取类型为T的值
ostream_iterator<T> out(os);  //out将类型为T的值写到输出流os中
istream_iterator<T> end;  //读取类型为T的值的istream_iterator迭代器,表示尾后位置。
ostream_iterator<T> out(os, d);  //out类型将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组。
in1 == in2  in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等。
*in  返回从流中读取的值
in->mem  与(*in).mem的含义相同
++in,in++  使用元素类型所定义的>>运算符从输入流中读取下一个值。与以往一样,前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值。
out = val;<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容。
*out,++out, out++;  这些运算符是存在的,但不对out做任何事情。每个运算符都返回out。

反向迭代器就是容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器会移动到前一个元素,递减一个迭代器会移动到下一个元素。
反向迭代器需要递减运算符。
reverse_iterator的base成员函数,会返回反向迭代器对应版本的普通迭代器。
反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。

10.5 泛型算法结构

算法所要求的迭代器操作可以分为5个迭代器类别。每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。

迭代器类别
输入迭代器 只读不写;单遍扫描,只能递增
输出迭代器 只写不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算

10.6 特定容器算法

对于list和forward_list,应该优先使用成员函数版本的算法而不是通用算法。

lst.merge(lst2);
lst.remove(val);
lst.reverse();
lst.sort();
lst.unique();

链表类型还定义了splice算法,此算法是链表数据结构特有的。
lst.splice(args);

链表特有的操作会改变容器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值