《C++Primer(第5版)》第九章笔记

Posted qq_34132502

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C++Primer(第5版)》第九章笔记相关的知识,希望对你有一定的参考价值。

元素在顺序容器中的顺序与其加入容器时的位置相对应。
所有的容器都共享公共的接口,但不同的容器按不同的方式对其进行扩展。

9.1 顺序容器概述

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

  • 向容器添加或从容器删除元素的代价
  • 非顺序访问容器中元素的代价
    在这里插入图片描述
    除了固定大小的array外,其他容器都提供高效、灵活的内存管理。我们可以添加和删除元素,扩张和收缩容器的大小。

确定使用哪种顺序容器

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

选择容器的基本原则:

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

9.2 容器库概览

  • 某些操作是所有容器类型都提供的
  • 另外一些操作仅针对顺序容器、关联容器、无序容器
  • 还有一些操作只适用于一小部分容器

对容器可以保存的元素类型的限制

顺序容器几乎可以保存任意类型的元素。

虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自己的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
例如,顺序容器构造函数的一个版本接受容器大小参数,它使用了元素类型的默认构造函数。但某些类没有默认构造函数。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:

// 假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10, init);		// 正确:提供了元素初始化器
vector<noDefault> v2(10);			// 错误:必须提供一个元素初始化器

在这里插入图片描述
在这里插入图片描述

9.2.1 迭代器

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

表3.6列出了容器迭代器支持的所有操作,其中有一个例外不符合公共接口特点——forward_list迭代器不支持递减运算符(–)。 表3.7列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器。我们不能将它们用于其他任何容器类型的迭代器。

迭代器范围

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

一个迭代器范围由一对迭代器表示,这两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这种元素范围被称为左闭合区间,即[begin, end)

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

  • if (begin == end)
  • if (begin != end)
  • 我们可以递增若干次,使得begin == end

9.2.2 容器类型成员

每个容器都定义了多个类型,如size_typeiteratorconst_iterator。除此之外海域反向迭代器。
与正向迭代器相比,各种操作的含义也都发生了颠倒。例如,对一个反向迭代器执行++操作,会得到上一个元素。

剩下的就是类型别名了,通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。 如果需要元素类型的一个引用,可以使用referenceconst_ reference。这些元素相关的类型别名在泛型编程中非常有用。
为了使用这些类型,我们必须显式使用其类名:

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

9.2.3 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

9.2.4 容器定义和初始化

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

将一个容器初始化为另一个容器的拷贝

  1. 直接拷贝整个容器。此时,两个容器类型、元素类型必须匹配
  2. 拷贝有一个迭代器对指定的元素范围(array除外)。此时,容器类型可以不同,而且只要拷贝的元素可以转换为新容器的元素类型即可。
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

list<string> list(authors);			// 正确:类型匹配
deque<string> authList(authors);	// 错误:容器类型不匹配
vector<string> words(articles);		// 错误:元素类型不匹配
// 正确:可以将const char*元素转换为string
forward_list<string> words(articles.begin(), 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> ivec(10);		// 10个int元素,每个都初始化为0
deque<string> svec(10);			// 10个string元素,每个都为空

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

标准库array具有固定大小

当定义一个array时,除了指定元素类型,还要制定容器大小:

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

array大小固定的特性也影响了它所定义的构造函数的行为。与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化,就像一个内置数组中的元素那样。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行:

array<int, 10> ial;		// 10个0
array<int, 10> ia2 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
array<int, 10> ia3 = {42};	// ia3[0]为42,其余为0

值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array可以

array<int, 10>digits = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
array<int, 10>copy = digits;

array不仅要求类型相同,还要求大小相同,因为大小是array类型的一部分。

9.2.5 赋值和swap

赋值运算符可用于所有容器

c1 = c2;
c1 = {a, b, c};

在这里插入图片描述
使用assign(仅顺序容器)

assign操作用参数所指定的元素替换左边容器中的所有元素。例如,我们可以用assgin实现将一个vector中的一段char *值赋予一个list中的string:

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

这段代码中对assign的调用将names中的元素替换为迭代器指定的范围中的元素的拷贝。assign的参数决定了容器中将有多少个元素以及它们的值都是什么。

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

assign的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:

// 等价于slist1.clear()
// 后跟slist1.insert(slist1.begin(), 10, "Hiya");
list<string> slist1(1);		// 1个元素,为空string
slist.assign(10, "Hiya");	// 10个元素,每个都是"Hiya"

使用swap

swap操作交换两个相同类型容器的内容:

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

调用swap之后,svec1包含20个元素,svec2包含10个元素。
swap操作很快,只需要常数时间(除array),元素本身并为交换,只是交换了两个容器的内部数据结构。
这意味着,除string外,指向容器的迭代器、引用和指针,在swap操作之后都不会失效,但这些元素已经属于不同容器了
例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。
如:

	vector<int> a = { 0, 1, 2 };
	vector<int> b = { 3, 4, 5, 6, 7, 8 };

	auto a_begin = a.begin();
	auto a_end = a.end();
	auto b_begin = b.begin();
	auto b_end = b.end();

	for (auto it = a_begin; it != a_end; ++it) cout << *it << " ";
	cout << endl;
	for (auto it = b_begin; it != b_end; ++it) cout << *it << " ";
	cout << endl;

	swap(a, b);

	for (auto it = a_begin; it != a_end; ++it) cout << *it << " ";
	cout << endl;
	for (auto it = b_begin; it != b_end; ++it) cout << *it << " ";
	cout << endl;

在这里插入图片描述
由此可见a、b经过swap之后,迭代器并未指向新容器的元素,仍指向老容器的元素

赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容教会不会导致指向容器的迭代器、引用和指针失效(array、string除外)

9.2.6 容器大小操作

除了一个例外(forward_list只支持emptymax_size),每个容器类型都有三个与大小相关的操作:

  • size:返回容器中的元素数目
  • empyt:如果size为0返回true
  • max_size:返回一个大于或等于该容器所嗯呢该容纳的最大元素数的值

9.2.7 关系运算符

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

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

9.3 顺序容器操作

顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。

9.3.1 向顺序容器添加元素

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

在这里插入图片描述
在一个vectorstring的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

使用push_back

除了arrayforward_list之外,每个顺序容器都支持push_back

当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。

使用push_front

除了push_backlistforward_listdeque还支持名为push_front的类似操作,此操作将元素插入到容器头部
注意,dequevector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_frontdeque保证在容器首位进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首位之外的位置插入元素会很耗时

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

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

每个insert函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以insert函数将元素插入到迭代器所指定的位置之前。例如:

slist.insert(iter, "Hello");

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

插入范围内元素

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

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

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

vector<int> v = {0, 1, 2, 3};
// 将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
// 运行时错误:迭代器表示要拷贝的范围,不能指向目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());

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

使用insert的返回值

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

list<string> lst;
auto iter = lst.begin();
while (cin >> word)
	iter = lst.insert(iter, word);	// 等价于调用push_back

insert返回的迭代器恰好指向这个新元素

使用emplace操作

新标准引入了三个新成员——emplace_frontemplaceemplace_back这些操作构造而不是拷贝元素。这些操作分别对应push_frontinsertpush_back,允许我们将元素放置在容器头部、指定位置之前、容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定c保存Sales_data元素:

// 在c的末尾构造一个Sales_data对象
// 使用三个参数的Sales_data构造函数
c.emplace_back("978-0590353403", 25, 15.99);
// 错误:没有接受三个参数的push_back版本
c.push_back("978-0590353403", 25, 15.99);
// 正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99));

其中对emplace_back的调用和第二个push_back调用都会创建新的Sales_data对象。在调用emplace_back时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中。

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

9.3.2 访问元素

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

包括array在内的每个顺序容器都有一个front成员函数,但除了forward_list之外的所有顺序容器都有个back成员函数

程序可以用两种不同方式来获取中的首元素和尾元素的引用。直接的方法是调用frontback而间接的方法是通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获得尾元素的引用。

在这里插入图片描述

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

访问元素的成员函数(frontbackat)返回的都是引用。
如果容器是一个const对象,则返回值是const的引用。

下标操作和安全的随机访问

提供快速随机访问的容器(stringvectordequearray)也都提供下标运算符。

如果我们希望确保下标时合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常

vector<string> svec;	// 空vector
cout << svec[0];		// 运行时错误:svec中没有元素
cout << svec.at(0);		// 抛出一个out_of_range异常

9.3.3 删除元素

与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式:
在这里插入图片描述

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

删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它是存在的。

pop_front和pop_back成员函数

pop_frontpop_back成员函数分别删除首元素和尾元素。与vectorstring不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back
与元素访问成员函数类似,不能对一个空容器执行弹出操作。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:

while (!ilist.empty()) {
	process(ilist.front());	// 对ilist的首元素进行处理
	ilist.pop_front();		// 完成处理后删除首元素
}

从容器内部删除一个元素

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

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.earse(elem1, elem2);	// 调用后,ele1 == ele2

为了删除一个容器中的所有元素,我们既可以调用clear,也可以调用beginend获得的迭代器作为参数调用earse

slist.clear();
slist.erase(slist.begin(), slist.end());

9.3.4 特殊的forward_list操作

forward_list是一个单链表,正因为它是单链表,所以在插入、删除某一元素需要获取这个元素的前驱,所以并未定义insertemplaceearse,取而代之的是insert_afteremplace_afterearse_after
为了删除elem3,应该使用指向elem2的迭代器调用erase_after
为了支持这些操作,forward_list也定义了before_begin,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(亦即在链表首元素之前添加删除元素)

在这里插入图片描述

当在forward_list中添加或删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向其前驱。例如,从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;	// 移动迭代器current,指向下一个元素,prev指向curr之前的元素
		++curr;
	}
}

9.3.5 改变容器大小

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

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

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

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

9.3.6 容器扫做可能是迭代器失效

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指

以上是关于《C++Primer(第5版)》第九章笔记的主要内容,如果未能解决你的问题,请参考以下文章

C++Primer第5版学习笔记

SPRING IN ACTION 第4版笔记-第九章Securing web applications-004-对密码加密passwordEncoder

C Primer Plus(第六版)第九章 编程练习答案

SPRING IN ACTION 第4版笔记-第九章Securing web applications-001-SpringSecurity简介(DelegatingFilterProxyAbstr(

SPRING IN ACTION 第4版笔记-第九章Securing web applications-003-把用户数据存在数据库

[学习笔记—Objective-C]《Objective-C-基础教程 第2版》第九章 内存管理