C---标准模板库使用教程-全-

C++ 标准模板库使用教程(全)

原文:Using the C++ Standard Template Libraries

协议:CC BY-NC-SA 4.0

一、标准模板库简介

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​1) contains supplementary material, which is available to authorized users.

本章解释了标准模板库(STL)背后的基本思想。这是为了让您全面了解 STL 中各种类型的实体是如何联系在一起的。你会看到我在这本书的这一章中介绍的所有内容的更深入的例子和讨论。在本章中,您将了解以下内容:

  • STL 中有什么
  • 如何定义和使用模板
  • 什么是容器
  • 什么是迭代器以及如何使用它
  • 智能指针的重要性及其在容器中的使用
  • 什么是算法,你如何应用它们
  • 数字库提供了什么
  • 什么是函数对象
  • 如何定义和使用 lambda 表达式

除了介绍 STL 背后的基本思想,本章还提供了一些你需要熟悉的 C++ 语言特性的简要提示,因为它们会在后面的章节中经常用到。如果您已经熟悉该主题,可以跳过这些部分。

基本思想

STL 是一套广泛而强大的工具,用于组织和处理数据。这些工具都是由模板定义的,因此数据可以是满足一些最低要求的任何类型。我假设您相当熟悉如何定义类模板和函数模板以及如何使用它们,但是我将在下一节提醒您这些的要点。STL 可以细分为四个概念库:

  • 容器库定义了用于存储和管理数据的容器。该库的模板在以下头文件中定义:arrayvectorstackqueuedequelistforward_listsetunordered_setmapunordered_map
  • 迭代器库定义了迭代器,迭代器是行为类似指针的对象,用于引用容器中的对象序列。该库在一个头文件iterator中定义。
  • 算法库定义了广泛的算法,这些算法可以应用于存储在容器中的一组元素。该库的模板在algorithm头文件中定义。
  • Numerics 库定义了广泛的数字函数,包括容器中元素集的数字处理。该库还包括用于随机数生成的高级函数。该库的模板在标题complexcmathvalarraynumericrandomratiocfenv中定义。cmath头文件已经存在一段时间了,但它在 C++ 11 标准中得到了扩展,并被包含在这里,因为它包含了许多数学函数。

使用 STL,用非常少的几行代码就可以非常容易地完成许多复杂而困难的任务。例如,无需解释,下面的代码从标准输入流中读取任意数量的浮点值,并计算和输出平均值:

std::vector<double> values;

std::cout << "Enter values separated by one or more spaces. Enter Ctrl+Z to end:\n ";

values.insert(std::begin(values), std::istream_iterator<double>(std::cin),

std::istream_iterator<double>());

std::cout << "The average is "

<< (std::accumulate(std::begin(values), std::end(values), 0.0)/values.size())

<< std::endl;

它只需要四个语句!诚然,行很长,但不需要循环;这都是由 STL 负责的。可以很容易地修改这段代码,对文件中的数据做同样的工作。由于 STL 的强大功能和广泛的适用性,它是任何 C++ 程序员的必备工具箱。所有的 STL 名称都在std名称空间中,所以我不会总是在文本中用std明确限定 STL 名称。当然,在任何代码中,我都会在必要的地方限定名字。

模板

模板是一组函数或类的参数化规范。当您在代码中使用函数模板或类模板类型时,编译器可以在必要时使用模板来生成特定的函数或类定义。还可以为参数化类型别名定义模板。因此,模板不是可执行代码——它是创建代码的蓝图或方法。编译器会忽略程序中从未使用过的模板,因此不会产生任何代码。不使用的模板可能包含编程错误,包含它的程序仍将编译和执行;模板中的错误将不会被识别,直到该模板被用来创建随后被编译的代码。

从模板生成的函数或类定义是模板的实例或实例化。模板参数值通常是数据类型,因此可以为类型为int,的参数值生成一个函数或类定义,并为类型为string的参数值生成另一个定义。参数变量不一定是类型;参数规范可以是需要整数参数的整数类型。下面是一个非常简单的函数模板示例:

template <typename T> T& larger(T& a, T& b)

{

return a > b ? a : b;

}

这是返回两个参数中较大值的函数的模板。使用模板的唯一限制是参数的类型必须允许执行>比较。类型参数T决定了要创建的模板的具体实例。编译器可以从您使用larger()时提供的参数中推断出这一点,尽管您可以显式地提供它。例如:

std::string first {"To be or not to be"};

std::string second {"That is the question."};

std::cout << larger(first, second) << std::endl;

该代码要求包含string头。编译器会将T的参数推断为类型string。如果你想指定它,你可以写larger<std::string>(first, second)。当函数参数的类型不同时,需要指定模板类型参数。例如,如果你写了larger(2, 3.5),,编译器不能推导出T,因为它是不明确的——它可能是类型int或类型double。这种用法将导致错误消息。编写larger<double>(2, 3.5)将解决问题。

下面是一个类模板的示例:

template <typename T> class Array

{

private:

T* elements;                                   // Array of type T

size_t count;                                  // Number of array elements

public:

explicit Array(size_t arraySize);              // Constructor

Array(const Array& other);                     // Copy Constructor

Array(Array&& other);                          // Move Constructor

virtual ∼Array();                              // Destructor

T& operator[](size_t index);                   // Subscript operator

const T& operator[](size_t index) const;       // Subscript operator-const arrays

Array& operator=(const Array& rhs);            // Assignment operator

Array& operator=(Array&& rhs);                 // Move assignment operator

size_t size() { return count; }                // Accessor for count

};

size_t类型别名在cstddef头中定义,代表无符号整数类型。这段代码为类型为T的元素数组定义了一个简单的模板。模板定义中出现的Array是隐含的Array<T>,如果你愿意,你可以这样写。在模板体之外——在一个外部函数成员定义中,你必须写Array<T>。赋值操作符允许将一个Array<T>对象赋给另一个,这是普通数组做不到的。如果您想禁止这个功能,您仍然需要将operator=()函数声明为模板的成员。如果不这样做,编译器将在必要时为模板实例创建一个公共的默认赋值操作符。为了防止使用赋值运算符,您应该将其指定为 deleted——如下所示:

Array& operator=(const Array& rhs)=delete;     // No assignment operator

一般来说,如果你需要定义任何一个复制或移动构造函数,复制或移动赋值操作符,或者析构函数,你应该定义所有的五个类成员,或者指定那些你不想删除的。

Note

实现移动构造函数和移动赋值操作符的类被称为具有移动语义。

size()成员是在类模板中实现的,所以默认情况下它是inline,不需要外部定义。类模板的函数成员的外部定义本身就是放在头文件中的模板——通常是与类模板相同的头文件。即使函数成员不依赖于类型参数T,也是如此,所以如果size()没有在类模板中定义,它将需要一个模板定义。定义函数成员的模板的类型参数列表必须与类模板的类型参数列表相同。下面是构造函数的定义:

template <typename T>                       // This is a function template with parameter T

Array<T>::Array(size_t arraySize) try : elements {new T[arraySize]}, count {arraySize}

{}

catch(const std::exception& e)

{

st::cerr << "Memory allocation failure in Array constructor." << std::endl;

rethrow e;

}

元素的内存分配可能会抛出异常,因此构造函数是一个函数try块。这允许异常被捕获和响应,但是异常必须被重新抛出——如果你没有在catch块中rethrow异常,它无论如何都会被重新抛出。模板类型参数在构造函数名的限定中是必不可少的,因为它将函数模板定义与类模板联系起来。注意,您没有在成员名称的限定符中使用typename关键字;它只在模板参数列表中使用。

当然,您可以为类模板的函数成员指定一个外部模板作为inline——例如,下面是如何定义Array模板的复制构造函数:

template <typename T>

inline Array<T>::Array(const Array& other)

try : elements {new T[other.count]}, count {other.count}

{

for (size_t i {}; i < count; ++i)

elements[i] = other.elements[i];

}

catch (std::bad_alloc&)

{

std::cerr << "memory allocation failed for Array object copy." << std:: endl;

}

这假设赋值操作符适用于类型T。如果在使用模板之前没有看到它的代码,您可能不会意识到对赋值操作符的依赖。这表明,对于动态分配内存的类,总是定义赋值操作符以及我前面提到的其他成员是多么重要。

Note

在指定模板参数时,classtypename关键字是可以互换的,因此在定义模板时,您可以编写template<typename T>template<class T>。因为T不一定是一个类类型,我更喜欢使用typename,因为我觉得这更能表达模板类型参数可以是基本类型也可以是类类型的可能性。

编译器实例化一个类模板,作为具有由该模板产生的类型的对象的定义的结果。这里有一个例子:

Array<int> data {40};

除非有默认实参,否则每个类模板类型形参的实参总是必需的。当这个语句被编译时,会发生三件事:创建了对Array<int>类的定义,以便识别类型;生成了构造函数定义,因为必须调用它来创建对象;创建了析构函数,因为需要它来销毁对象。这就是编译器创建和销毁data对象所需要的全部内容,因此这是它此时从模板生成的唯一代码。类定义是通过用int代替模板定义中的T生成的,但是有一个微妙之处。编译器只编译程序使用的成员函数,所以你不一定要得到整个类,这个类是通过简单地替换模板参数得到的。基于对data对象的定义,该类将被定义为:

class Array<int>

{

private:

int* elements;

size_t count;

public:

explicit Array(size_t arraySize);

virtual ∼Array();

};

您可以看到,仅有的函数成员是构造函数和析构函数。编译器不会创建创建对象不需要的任何东西的实例,也不会包含程序中不需要的模板部分。

您可以为类型别名定义模板。当您使用 STL 时,这很有用。以下是类型别名的模板示例:

template<typename T> using ptr = std::shared_ptr<T>;

该模板将ptr<T>定义为智能指针模板类型std::shared_ptr<T>的别名。有了这个模板,你可以在你的代码中使用ptr<std::string>而不是std::shared_ptr<std::string>。它显然不那么冗长,更容易阅读。下面的using指令将进一步简化它:

using std::string;

现在你可以在你的代码中使用ptr<string>来代替std::shared_ptr<std::string>。类型别名模板可以使您的代码更容易理解和键入。

集装箱

容器是 STL 功能的基石,因为 STL 的大部分内容都与它们相关。容器是以特定方式存储和组织其他对象的对象。当你使用容器时,你不可避免地会使用迭代器来访问数据,所以你也需要很好地理解这些。STL 提供了几类容器:

  • 序列容器以线性组织形式存储对象,类似于数组,但不一定存储在连续的内存中。您可以通过调用函数成员或迭代器来访问序列中的对象;在某些情况下,您还可以对索引使用下标运算符。
  • 关联容器将对象与关联的键存储在一起。通过提供对象的关联键,可以从关联容器中检索对象。还可以使用迭代器检索关联容器中的对象。
  • 容器适配器是适配器类模板,为访问存储在底层序列容器或关联容器中的数据提供了替代机制。

重要的是要认识到,除非对象是具有移动语义的类型的右值(临时对象),否则所有的 STL 容器都存储您在其中存储的对象的副本。STL 还要求 move 构造函数和赋值操作符必须被指定为noexcept,这表明它们不会抛出异常。如果将没有移动语义的类型的对象添加到容器中并修改原始对象,则原始对象和容器中的对象将会不同。但是,当您检索一个对象时,您会在容器中获得对该对象的引用,因此您可以修改存储的对象。存储的副本是使用对象类型的复制构造函数创建的。对于某些对象来说,复制可能是一个开销很大的过程。在这种情况下,最好是将指向对象的指针存储在容器中,或者假设已经为该类型实现了移动语义,则将对象移动到容器中。

Caution

不要将派生类对象存储在存储基类类型元素的容器中。这将导致派生类对象的切片。如果您想要访问容器中的派生类对象以获得多态行为,请将指向对象的指针存储在存储基类指针的容器中——或者更好的是存储指向基类型的智能指针。

容器将它们持有的对象存储在堆上,并自动管理它们占用的空间。存储类型为T的对象的容器中的空间分配由分配器管理,分配器的类型由模板参数指定。默认的类型参数是std::allocator<T>,这种类型的对象是一个分配器,为T类型的对象分配堆内存。这为您提供了提供自己的分配器的可能性。出于性能原因,您可能希望这样做,但是这很少是必要的,并且大多数时候默认分配器是好的。定义分配器是一个高级主题,我不会在本书中进一步讨论它。因此,当模板类型的最后一个模板参数表示分配器时,我将省略它。std::vector<typename T, typename Allocator>模板有一个指定为std::allocator<T>Allocator默认值,所以我把它写成std::vector<typename T>。这个解释只是为了让你知道提供一个分配器的选项在那里。

如果要将T对象存储在容器中,类型T必须满足某些要求,而这些要求最终取决于您需要对元素执行的操作。容器通常需要复制元素,并且可能需要移动和交换元素。在这种情况下,T类型的对象存储在容器中的最低要求如下:

class T

{

public:

T();                                          // default constructor

T(const T& t);                                // Copy constructor

∼T();                                         // Destructor

T& operator=(const T& t);                     // Assignment operator

};

考虑到编译器在许多情况下为上述所有成员提供了默认实现,大多数类类型应该满足这些要求。请注意,operator<()没有包含在T,的定义中,但是没有定义operator<()的类型的对象将不能用作任何关联容器(如mapset)中的键,并且排序算法(如sort()merge())不能应用于元素不支持小于运算的序列。

Note

如果您的对象类型不符合您正在使用的容器的要求,或者您以其他方式误用了容器模板,您将经常得到与标准库头文件中的代码相关的编译器错误消息。当这种情况发生时,不要急于在标准库中报告错误。在使用 STL 的代码中寻找错误!

迭代程序

迭代器是一种类模板类型的对象,其行为类似指针。只要迭代器iter指向一个有效的对象,你就可以通过写*iter来解引用它以获得对该对象的引用。如果iter指向一个类对象,你可以通过写iter->member来访问该对象的成员member。因此,你可以像使用指针一样使用迭代器。

当您以某种方式处理容器中的元素时,尤其是在应用 STL 算法时,您可以使用迭代器来访问它们。因此迭代器将算法连接到容器中的元素,而不管容器的类型。迭代器将算法从数据源中分离出来;算法不知道数据来自哪个容器。迭代器是在iterator头中定义的模板类型的实例,但是这个头包含在所有定义容器的头中。

通常使用一对迭代器来定义一系列元素;元素可以是容器中的对象、标准数组中的元素、string对象中的字符,或者支持迭代器的任何其他类型对象中的元素。范围是由指向范围中第一个元素的开始迭代器和指向最后一个元素之后的元素的结束迭代器指定的元素序列。即使序列是容器中元素的子集,第二个迭代器仍然指向序列中最后一个元素之后的元素,而不是范围中的最后一个元素。代表容器中所有元素的范围的结束迭代器不会指向任何东西,因此不能被解引用。迭代器提供了一种标准的机制来识别 STL 和其他地方的元素。元素范围的规范与元素的来源无关,因此给定的算法可以应用于来自任何来源的元素范围,只要迭代器满足算法的要求。稍后我会详细介绍不同类型迭代器的特点。

一旦理解了迭代器的工作原理,就很容易定义自己的模板函数来处理迭代器指定为参数的数据序列。然后,函数模板的实例可以应用于来自任何源的数据,这些源可以定义为一个范围;代码处理数组中的数据就像处理向量容器中的数据一样。在本书的后面,您将会看到这方面的实例。

获取迭代器

通过调用容器对象的begin()end()函数成员,可以从容器中获得迭代器;这些返回的迭代器分别指向第一个元素和最后一个元素。容器的end()成员返回的迭代器没有指向一个有效的元素,所以你不能取消引用它或者增加它。string 类如std::string也有这些函数成员,所以你也可以获得它们的迭代器。通过以容器对象为参数调用全局函数std::begin()std::end(),可以获得与容器的begin()end()函数成员返回的迭代器相同的迭代器;这些由iterator标题中的模板定义。全局begin()end()函数使用普通数组或string对象作为参数,因此提供了一种统一的获取迭代器的方式。

迭代器允许你通过递增 begin 迭代器来遍历一个范围内的元素,从一个对象移动到下一个对象,如图 1-1 所示;图中的“容器”意味着一个string对象或数组,以及一个 STL 容器。通过比较递增的begin迭代器和end迭代器,可以确定何时到达最后一个元素。您还可以对迭代器应用其他操作,但这取决于迭代器的类型,而迭代器的类型又取决于您正在使用的容器的种类。有全局的cbegin()cend()函数返回数组、容器或string对象的const迭代器。记住——const迭代器指向的是常量,你仍然可以修改迭代器本身。在本节的后面,我将介绍返回其他类型迭代器的其他全局函数。

A978-1-4842-0004-9_1_Fig1_HTML.gif

图 1-1。

Operation of iterators

迭代器类别

所有迭代器类型都必须有一个复制构造函数、一个复制赋值操作符和一个析构函数。迭代器指向的对象必须是可交换的;我将在下一章进一步解释这意味着什么。有五类迭代器反映了不同级别的能力。不同的算法可能需要不同级别的迭代器能力来识别它们要操作的元素范围。类别不是新的迭代器模板类型;迭代器类型支持的类别由iterator模板的类型参数的实参值标识。我将在这一节的稍后部分对此进行更多的解释。

容器中迭代器的类别取决于容器的类型。类别使算法能够确定传递给它的迭代器的能力。算法可以以两种方式使用迭代器参数的类别:首先,它可以确定操作的最低功能要求得到满足;第二,如果超过迭代器的最低要求,算法可以使用扩展能力来更有效地执行操作。当然,算法只能应用于为迭代器提供所需功能级别的容器中的元素。

迭代器类别如下,从最简单到最复杂排序:

Input iterators have read access to objects. If iter is an input iterator, it must support the expression *iter to produce a reference to the value to which iter points. Input iterators are single use only, which means that once an iterator has been incremented, to access the previous element that it pointed to you need a new iterator. Each time you want to read a sequence, you must create a new iterator. The operations that you can apply to input iterators are: ++iter or iter++; iter1==iter2 and iter1!=iter2; and *iter Note the absence of the decrement operator. You can use the expression iter->member for input iterators.   Output iterators have write access to objects. If iter is an output iterator, it allows a new value to be assigned so *iter=new_value is supported. Output iterators are single use only. Each time you want to write a sequence, you must create a new iterator. The operations that you can apply to output iterators are: ++iter or iter++; and *iter Note the absence of the decrement operator. You only get write access with output iterators. You cannot use the expression iter->member for output iterators.   Forward iterators combine the capabilities of input and output iterators and add the capability to be used more than once. Therefore you can reuse a forward iterator to read or write an element as many times as necessary. The operation to be performed determines when forward iterators are required. The replace() algorithm that searches a range and replaces elements requires the capability of a forward iterator, for example, because the iterator that points to an element that is to be replaced is reused to overwrite it.   Bidirectional iterators provide the same capabilities as forward iterators but allow traversal through a sequence backward as well as forward. Therefore in addition to incrementing these iterators to move to the next element, you can apply the prefix and postfix decrement operators, --iter and iter--, to move to the previous element.   Random access iterators provide the same capabilities as bidirectional iterators but also allow elements to be accessed at random. In addition to the operations permitted for bidirectional iterators, these support the following operations:

  • 递增和递减一个整数:iter+niter-niter+=niter-=n
  • 按整数索引:iter[n],相当于*(iter+n)
  • 两个迭代器的区别:iter1-iter2,产生一个整数指定元素个数。
  • 比较迭代器:iter1<iter2iter1>iter2iter1<=iter2iter1>=iter2。对一系列元素进行排序需要随机访问迭代器指定范围。可以在随机访问迭代器中使用下标操作符。给定一个迭代器first,表达式first[3]等价于*(first+3),所以它访问第四个元素。一般来说,在带有迭代器的表达式iter[n]中,itern是一个偏移量,表达式返回从iter到偏移量n的元素的引用。请注意,没有检查应用于迭代器的下标操作符所使用的索引。没有什么可以阻止合法范围之外的索引值的使用。

每个迭代器类别由一个名为迭代器标签类的空类标识,该类用作iterator模板的类型参数。迭代器标签类的唯一目的是指定一个特定的迭代器类型可以做什么,因此它们被用作一个iterator模板类型参数。标准迭代器标记类有:

input_iterator_tag

output_iterator_tag

forward_iterator_tag源自input_iterator_tag

bidirectional_iterator_tag源自forward_iterator_tag

random_access_iterator_tag源自bidirectional_iterator_tag

这些类的继承结构反映了迭代器类别的累积性质。当创建一个iterator模板实例时,第一个模板类型参数将是迭代器标签类之一,它将决定迭代器的能力。在第二章中,我将解释如何定义你自己的迭代器以及如何定义它们的类别。

如果一个算法需要一个给定类别的迭代器,那么你就不能使用一个下级迭代器;然而,你总是可以使用一个高级迭代器。正向、双向和随机访问迭代器也可以是常量或可变的,这取决于对迭代器的解引用是产生一个引用还是一个const引用。显然你不能使用赋值左边的const迭代器的解引用结果。

容器中迭代器的特征取决于容器的类型。比如vectordeque容器提供随机访问迭代器;这反映了这些容器中的元素可以被随机访问的事实。另一方面,listmap容器总是提供双向迭代器;这些容器不支持对元素的随机访问。输入输出迭代器和前向迭代器类型通常用于为算法指定参数,以反映算法所需的最低能力水平。在本书的后面,我将在将算法应用于容器内容的上下文中,用工作示例进一步解释迭代器——在实际环境中,它们更容易理解。同时,这里有一个简单的例子来展示迭代器在数组中的作用:

// Ex1_01.cpp

// Using iterators

#include <numeric>                          // For accumulate() - sums a range of elements

#include <iostream>                         // For standard streams

#include <iterator>                         // For iterators and begin() and end()

int main()

{

double data[] {2.5, 4.5, 6.5, 5.5, 8.5};

std::cout << "The array contains:\n";

for (auto iter = std::begin(data); iter != std::end(data); ++iter)

std::cout << *iter << " ";

auto total = std::accumulate(std::begin(data), std::end(data), 0.0);

std::cout << "\nThe sum of the array elements is " << total << std::endl;

}

您可以看到,全局begin()end()函数返回作为函数参数的数组元素的迭代器。迭代器用在列出元素值的for循环中。表达式*iter解引用迭代器通过引用访问值。当然,你可以在for循环体中增加iter,就像这样:

for (auto iter = std::begin(data); iter != std::end(data);)

std::cout << *iter++ << " ";

包含iterator头的指令可以省略,因为iterator包含在容器的任何头中,并且包含定义accumulate()函数模板的numeric头。accumulate()函数返回由前两个参数定义的范围内的元素之和,这两个参数必须是指定范围内第一个元素和最后一个元素的迭代器。第三个参数是用于求和的初始值。accumulate()函数适用于支持加法的任何类型的元素,因此它也适用于定义operator+()的任何类类型的对象。

Note

当我们开始对容器使用accumulate()时,你会看到,还有另一个版本的函数模板,它允许你指定一个不同的二进制操作来代替默认的+

流迭代器

使用流迭代器在流和源或目的地之间以文本模式传输数据,可以通过迭代器访问源或目的地。因为 STL 算法接收的输入是由一对迭代器指定的范围,所以您可以将算法应用于通过输入流迭代器可访问的任何来源的可用对象。例如,这意味着算法可以应用于流中的对象,也可以应用于容器中的对象。算法也可以应用于任何其他可以提供可接受的迭代器的环境中;稍后我会解释迭代器是如何被接受的。同样,您可以通过使用输出流迭代器将一系列元素转移到输出流。标准迭代器将iterator模板类型作为基类。

创建一个流迭代器对象,它处理流对象中指定类型的数据;数据类型是迭代器模板类型参数,流对象是构造函数参数。一个istream_iterator<T>是一个输入迭代器,它可以从一个istream中读取类型为T的对象,它可以是一个文件流或者标准的输入流 c i n。对象是使用>>操作符读取的,所以要读取的对象类型必须支持它。istream_iterator<T>的无参数构造函数创建了一个结束迭代器对象,当到达一个流的末尾时,这个对象将被匹配。显然,当您想要传输混合类型的数据时,流迭代器不是合适的方法。默认情况下,istream_iterator对象忽略空白;您可以通过对底层输入流应用std::noskipws操纵器来覆盖它。一个istream_iterator只能用一次。如果您想再次从流中输入对象,您必须创建一个新的istream_iterator对象。

一个ostream_iterator补充了istream_iterator,因为它是一个输出迭代器,为对象向一个ostream提供一次性输出能力。使用<<操作符编写对象。当您创建一个ostream_iterator对象时,您可以选择指定一个分隔符字符串,它将被写入每个对象的输出之后。

下面是一个使用输入流迭代器的工作示例:

// Ex1_02.cpp

// Using stream iterators

#include <numeric>                          // For accumulate() - sums a range of elements

#include <iostream>                         // For standard streams

#include <iterator>                         // For istream_iterator

int main()

{

std::cout << "Enter numeric values separated by spaces and enter Ctrl+Z to end:" << std::endl;

std::cout << "\nThe sum of the values you entered is "

<< std::accumulate(std::istream_iterator<double>(std::cin),

std::istream_iterator<double>(), 0.0)

<< std::endl;

}

这将把accumulate()函数应用于由输入流迭代器为cin提供的一系列值。可以输入任意数量的值。第二个参数是流尾迭代器,当 read 设置流尾条件时,它将与第一个参数指定的迭代器匹配(对于文件流,称为EOF);从键盘输入Ctrl-Z会导致这种情况。

迭代器适配器

迭代器适配器是为标准迭代器提供专门行为的类模板,因此它们是从iterator模板中派生出来的。适配器类模板定义了三种迭代器,反向迭代器、插入迭代器和移动迭代器。这些由以下模板类类型定义:reverse_iteratorinsert_iteratormove_iterator

反向迭代器

反向迭代器的工作方式与标准迭代器相反。您可以创建双向或随机访问迭代器的反向迭代器版本。容器的rbegin()rend()函数成员分别返回指向最后一个元素和第一个元素前的反向迭代器;同名的全局函数做同样的事情,如图 1-2 所示。

A978-1-4842-0004-9_1_Fig2_HTML.gif

图 1-2。

Operations with reverse iterators

递增或递减反向迭代器在元素顺序方面与标准迭代器的工作方式相反,因此递增反向 begin 迭代器会导致它指向前面的元素——左边的那个——而递减它会指向下一个元素——右边的那个。图 1-3 显示了与标准迭代器相比,反向迭代器的增量方向。反向迭代器类型的模板是从常规迭代器的模板中派生出来的,它重载操作函数来实现反向操作。在string头中定义的字符串类也使反向迭代器可用,因此您可以调用string对象的rbegin()成员来获得指向最后一个字符的反向迭代器,调用字符串对象的rend()将返回指向第一个字符之前的反向迭代器。全局(和成员)crbegin()crend()函数返回const反向迭代器。

A978-1-4842-0004-9_1_Fig3_HTML.gif

图 1-3。

How iterators and reverse iterators relate to a container

你可以在反向随机访问迭代器中使用下标操作符,就像标准的随机访问迭代器一样,这在相反的意义上也是有效的。对于标准迭代器iter,表达式iter[n]导致n元素位于iter指向的元素之后,因此它相当于*(iter+n)。对于反向迭代器riter,表达式riter[n]等价于*(riter+n),,因此它返回位于riter所指向的元素之前n位置的元素。

图 1-3 显示了容器的反向迭代器和标准迭代器的关系。可以看到容器元素的反向迭代器相对于普通迭代器向左移动了一个位置。每个反向迭代器内部都包含一个标准迭代器,这个迭代器的位置是相似的,所以它不会指向同一个元素。一个reverse_iterator对象有一个返回底层迭代器的base()函数成员,因为它是一个标准迭代器,所以它的工作方式与反向迭代器相反。反向迭代器的基本迭代器riter指向范围末尾的下一个元素,如图 1-3 所示。容器的一些函数成员不接受反向迭代器。当你需要应用一个算法时,在这种情况下,已经使用反向迭代器找到了位置,你可以调用base()来获得反向迭代器对应的标准迭代器。显然,你需要考虑这样一个事实,基本迭代器将指向反向迭代器所标识的元素之后的元素。在下一章你会学到更多。

插入迭代器

虽然插入迭代器是基于标准迭代器的,但是它们的功能有很大的不同。普通迭代器只能访问或更改一个范围内的现有元素。插入迭代器用于在容器中的任何地方添加新元素。插入迭代器不能应用于标准数组或array<T,N>容器,因为这些容器中的元素数量是固定的。有三种插入迭代器:

  • A back_insert_iterator通过调用push_back()函数成员在容器末尾添加新元素;vectorlistdeque容器都有一个push_back()成员。如果容器没有定义push_back(),那么back_insert_iterator就不能使用。全局back_inserter()函数为作为参数传递的容器返回一个back_insert_iterator对象。
  • 一个front_insert_iterator通过调用它的push_front()成员在容器的开头添加新元素;listforward_listdeque容器有一个push_front()成员。不能对没有push_front()成员的容器使用front_insert_iterator。全局front_inserter()函数为容器返回一个作为参数传递的front_inserter_iterator对象;显然,容器必须是一个listforward_listdeque容器。
  • 您可以使用一个insert_iterator在任何具有insert()成员的容器的现有范围内插入新元素。在string头中定义的字符串类有一个insert()成员,所以一个insert_iterator对象处理这些成员。全局inserter()函数返回一个容器的insert_iterator对象,该容器被指定为第一个参数;第二个参数是一个迭代器,它指向容器中要插入元素的位置。

插入迭代器通常用作从指定范围复制元素的算法或生成新元素的算法的参数。你将在下一章看到它们的应用。

移动迭代器

移动迭代器是从指向一个范围内的元素的常规迭代器创建的。您可以使用移动迭代器将一系列类对象移动到目标范围,而不是复制它们。用作输入迭代器的移动迭代器将它指向的对象转换为右值,这允许对象被移动而不是复制。因此,移动迭代器会使源范围中的原始元素处于未定义的状态,所以你不能使用它们。你可以通过传递一个普通的迭代器来获得一个move_iterator,例如由begin()end(),返回给由iterator头中的模板定义的make_move_iterator()函数。因此,通过将容器的begin()end()返回的迭代器传递给make_move_iterator()函数,您可以创建一对迭代器来定义要移动的元素范围。在本书的后面,您将看到展示如何使用移动迭代器的例子。

迭代器的运算

迭代器头定义了四个实现迭代器操作的函数模板:

  • 将您作为第一个参数提供的迭代器递增第二个参数指定的元素数。第一个参数可以是任何具有输入迭代器功能的迭代器。如果迭代器是双向或随机访问迭代器,第二个参数可以为负,以递减迭代器。没有返回值。比如:int data[] {1, 2, 3, 4, 5, 6}; auto iter = std::begin(data); std::advance(iter, 3); std::cout << "Fourth element is " << *iter << std::endl;
  • distance()返回由两个迭代器参数指定的范围内的元素个数。例如:int data[] {1, 2, 3, 4, 5, 6}; std::cout << "The number of elements in data is " << std::distance(std::begin(data), std::end(data)) << std::endl;
  • next()返回迭代器,该迭代器是将作为第一个参数提供的迭代器递增第二个参数指定的元素数而得到的。第一个参数必须具有正向迭代器功能。第二个参数的默认值为 1。比如:int data[] {1, 2, 3, 4, 5, 6}; auto iter = std::begin(data); auto fourth = std::next(iter, 3); std::cout << "1st element is " << *iter << " and the 4th is " << *fourth << std::endl;
  • prev()返回一个迭代器,该迭代器是将作为第一个参数提供的迭代器减去作为第二个参数指定的元素数而得到的,默认值为 1。第一个参数必须具有双向迭代器功能。例如:int data[] {1, 2, 3, 4, 5, 6}; auto iter = std::end(data); std::cout << "Fourth element is " << *std::prev(iter, 3) <<  std::endl;

显然,使用随机访问迭代器,您可以获得这些函数使用算术运算产生的结果,但是使用能力较弱类别的迭代器,您不能。这些函数可以简化除随机访问迭代器之外的操作代码。例如,为了在能力较弱的迭代器上产生与advance()相同的效果,您需要编写一个循环。

智能指针

作为 C++ 语言一部分的指针被称为原始指针,因为这些类型的变量只包含一个地址;原始指针可以包含自动变量、静态变量或在堆上创建的变量的地址。智能指针是一种模板类型的对象,它模仿原始指针,因为它包含一个地址,在某些方面,您可以以相同的方式使用它,但有两个主要区别:

  • 智能指针仅用于存储在空闲存储(堆)中分配的内存地址。
  • 您不能像处理原始指针那样对智能指针执行算术运算,如递增或递减。

对于在免费商店中创建的对象,使用智能指针通常比原始指针好得多。智能指针的巨大优势在于,您不必担心使用delete来释放堆内存,因为为智能指针所指向的对象分配的内存会在不再需要该对象时自动释放。这意味着您消除了内存泄漏的可能性。

您可以将智能指针存储在容器中,这在处理类类型的对象时特别有用。存储指针而不是对象允许您保留多态行为–如果您使用基类类型作为智能指针的模板类型参数,您可以使用它来指向派生类类型的对象。智能指针类型的模板在memory头中定义,因此您必须将它包含到源文件中才能使用它们。在std名称空间中,有三种类型的智能指针由以下模板定义:

  • 一个unique_ptr<T>对象表现为一个指向类型T的指针,并且是唯一的,这意味着不能有一个以上的unique_ptr<T>对象包含相同的地址。一个unique_ptr<T>对象独占它所指向的对象。您不能分配或复制unique_ptr<T>对象。您可以使用utility标题中定义的std::move()函数将一个unique_ptr<T>对象存储的地址移动到另一个对象。操作后,原始对象将无效。当您需要强制一个对象的单一所有权时,可以使用unique_ptr<T>
  • 一个shared_ptr<T>对象表现为一个指向类型T的指针,与unique_ptr<T>相反,可以有任意数量的shared_ptr<T>对象包含相同的地址。因此shared_ptr<T>对象允许共享免费存储中的对象的所有权。记录包含给定地址的shared_ptr<T>对象的数量。每当创建包含给定堆地址的新的shared_ptr<T>对象时,包含该地址的shared_ptr<T>的引用计数就增加;当包含地址的shared_ptr<T>对象被销毁或被指定指向不同的地址时,引用计数递减。当没有包含给定地址的shared_ptr<T>对象时,引用计数将为零,该地址对象的堆内存将自动释放。所有指向同一个地址的shared_ptr<T>对象都可以访问这个地址的数量。
  • 一个weak_ptr<T>链接到一个shared_ptr 对象并从其创建,并且包含相同的地址。创建一个 weak_ptr<T>不会增加被链接的shared_ptr<T>对象的引用计数,所以它不会阻止被指向的对象被销毁。当最后一个shared_ptr<T>引用被销毁或被重新分配指向一个不同的地址时,它的内存将被释放,即使关联的weak_ptr<T>对象可能仍然存在。

拥有weak_ptr<T>对象的主要原因是有可能无意中用shared_ptr<T>对象创建引用循环。从概念上讲,一个参考周期是一个shared_ptr<T>对象pA指向另一个shared_ptr<T>对象pB,而pB指向pA。在这种情况下,两者都不能被摧毁。实际上,这是以一种复杂得多的方式发生的。weak_ptr<T>对象旨在避免参考周期的问题。通过使用weak_ptr<T>对象指向单个shared_ptr<T>对象所指向的对象,可以避免引用循环;稍后我会解释。当最后一个shared_ptr<T>对象被销毁时,所指向的对象也被销毁。任何与shared_ptr<T>相关联的weak_ptr<T>对象将不会指向有效对象。

使用 unique_ptr 指针

一个unique_ptr<T>对象唯一地存储一个地址,因此它所指向的对象被unique_ptr<T>对象独占。当unique_ptr<T>对象被销毁时,它所指向的对象也被销毁。这种类型的智能指针适用于不需要多个智能指针并且希望确保单点所有权的情况。当一个对象为一个unique_ptr<T>所拥有时,您可以通过使一个原始指针可用来提供对该对象的访问。下面是如何使用构造函数创建一个unique_ptr<T>:

std::unique_ptr<std::string> pname {new std::string {"Algernon"}};

在堆上创建的string对象被传递给unique_ptr<string>构造函数。默认构造函数将创建一个 unique_ptr ,用 nullptr作为内部原始指针。

创建unique_ptr<T>对象的一个更好的方法是使用在memory头中定义的make_unique<T>()函数模板:

auto pname = std::make_unique<std::string>("Algernon");

该函数通过将参数传递给类构造函数在堆上创建string对象,并创建和返回指向它的唯一指针。根据T构造函数的要求,你可以向make_unique<T>()函数提供任意多的参数。这里有一个例子:

auto pstr = std::make_unique<std::string>(6, '*');

有两个参数将被传递给string构造函数,因此创建的对象包含"******"

您可以取消引用指针来访问对象,就像原始指针一样:

std::cout << *pname << std::endl;      // Outputs Algernon

您可以创建一个指向数组的unique_ptr<T>。例如:

size_t len{10};

std::unique_ptr<int[]> pnumbers {new int[len]};th

这创建了一个指向在自由存储中创建的len元素数组的unique_ptr对象。您可以通过调用make_unique<T>()获得相同的结果:

auto pnumbers = std::make_unique<int[]>(len);

这也创建了一个指向在堆上创建的len元素数组的指针。您可以使用带有unique_ptr变量的索引来访问数组元素。以下是更改这些值的方法:

for(size_t i{} ; i < len ; ++i)

pnumbers[i] = i*i;

这会将数组元素的值设置为其索引位置的平方。当然,您可以使用下标操作符来输出值:

for(size_t i{} ; i < len ; ++i)

std::cout << pnumbers[i] << std::endl;

不能通过值将unique_ptr<T>对象传递给函数,因为它不能被复制。您必须在函数中使用引用参数,以允许将unique_ptr<T>对象作为参数。你可以从一个函数中返回一个unique_ptr<T>,因为它不会被复制,但是会被一个隐式的移动操作返回。

您只能通过将unique_ptr<T>对象移动到容器中或就地创建来将它们存储在容器中,因为unique_ptr<T>对象不能被复制。永远不会有两个包含相同地址的unique_ptr<T>对象。shared_ptr<T>对象没有这个特性,所以每当你需要多个指针指向一个对象,或者需要复制存储智能指针的容器的内容时,你就使用这些;否则使用unique_ptr<T>物体。对于具有unique_ptr<T>元素的容器,您可能需要使指向一个对象的原始指针可用。下面是如何从一个unique_ptr<T>中获得一个原始指针:

auto unique_p = std::make_unique<std::string>(6, '*');

std::string pstr {unique_p.get()};

get()函数成员返回unique_ptr<T>包含的原始指针。这样做的典型情况是,当指向对象的智能指针封装在类对象中时,提供对该对象的访问。你不能归还unique_ptr<T>,因为它不能被复制。

重置 unique_ptr 对象

当智能指针被销毁时,unique_ptr<T>对象所指向的对象也被销毁。为没有参数的unique_ptr<T>对象调用reset()会销毁所指向的对象,并用nullptr替换unique_ptr<T>对象中的原始指针;这使您能够随时销毁所指向的对象。例如:

auto pname = std::make_unique<std::string>("Algernon");

...

pname.reset();                                   // Release memory for string object

您可以将空闲存储中一个新的T对象的地址传递给reset(),,之前被指向的对象将被销毁,其地址将被新对象的地址替换:

pname.reset(new std::string{"Fred"});

这将释放由pname指向的原始字符串的内存,在空闲存储中创建一个新的string对象"Fred",并将其地址存储在pname中。

Caution

您不能将一个空闲存储对象的地址传递给另一个unique_ptr<T>对象包含的reset(),或者使用已经包含在另一个unique_ptr<T>中的地址创建一个新的unique_ptr<T>。这样的代码可能会编译,但是你的程序肯定会崩溃。第一个unique_ptr<T>的销毁将释放它所指向的对象的内存。第二个的销毁将导致尝试释放已经释放的内存。

你可以通过调用智能指针的release()来释放一个unique_ptr<T>所指向的对象。这将包含在unique_ptr<T>中的原始指针设置为nullptr,而不释放原始对象的内存。例如:

auto up_name = std::make_unique<std::string>("Algernon");

std::unique_ptr<std::string> up_new_name{up_name.release()};

up_namerelease()成员返回包含"Algernon"的字符串对象的原始指针,因此在执行第二条语句后,up_name将包含nullptr,而up_new_name将指向原始的"Algernon" string对象。其效果是将自由存储中对象的所有权从一个唯一指针转移到另一个唯一指针。

您可以交换两个unique_ptr<T>指针所拥有的对象:

auto pn1 = std::make_unique<std::string>("Jack");

auto pn2 = std::make_unique<std::string>("Jill");

pn1.swap(pn2);

执行完这里的第二条语句后,pn1将指向字符串"Jill"pn2将指向"Jack."

比较和检查 unique_ptr 对象

有一些非成员函数模板定义了一整套比较操作符,用来比较两个unique_ptr<T>对象或者比较一个unique_ptr<T>对象和nullptr。比较两个unique_ptr<T>对象比较它们的get()成员返回的地址。将unique_ptr<T>nullptr进行比较,将智能指针的get()成员返回的地址与nullptr进行比较。

unique_ptr<T>对象可以隐式转换为类型bool。如果对象包含nullptr,转换的结果是false;否则结果就是true。这意味着您可以使用一个if语句来检查一个非空的unique_ptr<T>对象:

auto up_name = std::make_unique<std::string>("Algernon");

std::unique_ptr<std::string> up_new{up_name.release()};

if(up_new)                                       // true if not nullptr

std::cout << "The name is " << *up_new << std::endl;

if(!up_name)                                     // true if nullptr

std::cout << "The unique pointer is nullptr" << std::endl;

当您为唯一指针对象调用reset()release()时,这种检查是可取的,因为您需要在取消引用之前确定unique_ptr<T>不是nullptr

使用共享指针指针

您可以像这样定义一个shared_ptr<T>对象:

std::shared_ptr<double> pdata {new double{999.0}};

您还可以取消对共享指针的引用,以访问它所指向的内容或更改存储在以下地址的值:

*pdata = 8888.0;

std::cout << *pdata << std::endl;      // Outputs 8888.0

*pdata = 8889.0;

std::cout << *pdata << std::endl;      // Outputs 8889.0

pdata的定义包括为double变量分配一个堆内存,另一个分配与控制块的智能指针对象相关,用于记录智能指针的副本数量。分配堆内存在时间上相对昂贵。通过使用在memory头文件中定义的make_shared<T>()函数创建一个shared_ptr<T>类型的智能指针,可以使这个过程更加有效:

auto pdata = std::make_shared<double>(999.0);  // Points to a double variable

要在自由存储中创建的变量类型在尖括号之间指定。函数名后面括号中的参数用于初始化它创建的double变量。一般来说,make_shared<T>()函数可以有任意数量的参数,实际数量取决于被创建对象的类型。当您使用make_shared<T>()在自由存储中创建对象时,如果T构造函数需要的话,可以有两个或更多由逗号分隔的参数。auto关键字导致pdata的类型从make_shared<T>()返回的对象中自动推导出来,所以它将是shared_ptr<double>。但是不要忘记——当你指定一个类型为auto时,你不应该使用初始化列表,因为这个类型将被推断为std::initializer_list

定义shared_ptr<T>时,可以用另一个初始化它:

std::shared_ptr<double> pdata2 {pdata};

pdata2指向与pdata相同的变量,这将导致引用计数递增。您也可以将一个shared_ptr<T>分配给另一个:

std::shared_ptr<double> pdata{ new double{ 999.0 } };

std::shared_ptr<double> pdata2;        // Pointer contains nullptr

pdata2 = pdata;                        // Copy pointer - both point to the same variable

std::cout << *pdata << std::endl;      // Outputs 999.0

当然,复制pdata会增加引用数。为了释放由变量double占用的内存,两个指针都必须被重置或销毁。默认情况下,不能使用shared_ptr<T>来存储在自由存储中创建的数组的地址。但是,您可以存储您在免费存储中创建的array<T>vector<T>容器对象的地址。

Note

可以创建一个指向数组的shared_ptr<T>对象。这包括为删除函数提供一个定义,智能指针将使用该函数来释放数组的堆内存。如何做到这一点的细节超出了本书的范围。

与对unique_ptr<T>类似,通过调用shared_ptr<T>对象的get()成员,可以获得一个指向该对象的原始指针。对于上一节定义的pdata,您可以写:

auto pvalue = pdata.get();             // pvalue is type double* and points to 999.0

只有在必须使用原始指针时,才需要这样做。

Caution

一个shared_ptr<T>对象的副本只能由复制构造函数或复制赋值操作符创建。使用由get()返回的原始指针为不同的指针创建一个shared_ptr<T>将导致未定义的行为,这在大多数情况下意味着程序崩溃。

重置 shared_ptr 对象

如果您将nullptr分配给一个shared_ptr<T>对象,存储的地址将被替换为nullptr,其效果是将指向该对象的指针的引用计数减少 1。例如:

auto pname = std::make_shared<std::string>("Charles Dickens");  // Points to a string object

// ... lots of other stuff happening...

pname = nullptr;                                                // Reset pname to nullptr

这将在自由存储中创建一个用"Charles Dickens"初始化的string对象,并创建一个包含其地址的共享指针。最终,将nullptr分配给pname会替换用nullptr存储的地址。当然,持有string对象地址的任何其他shared_ptr<T>对象都将继续存在——只是引用计数会减少。

您可以通过调用不带参数值的shared_ptr<T>对象的reset()来获得相同的结果:

pname.reset();                                                  // Reset to nullptr

您还可以将一个原始指针传递给reset()来改变共享指针所指向的内容。例如:

pname.reset(new std::string{"Jane Austen"});                    // pname points to new string

reset()的参数必须是与最初存储在智能指针中的地址类型相同的地址,或者必须可以隐式转换为该类型。

比较和检查 shared_ptr 对象

您可以使用任何比较运算符将一个shared_ptr<T>对象中包含的地址与另一个对象进行比较,或者与nullptr进行比较。最有用的是相等或不相等的比较,它告诉你两个指针是否指向同一个对象。给定两个指向同一类型Tshared_ptr<T>对象pApB,可以这样比较它们:

if((pA == pB) && (pA != nullptr))

std::cout << " Both pointers point to the same object.\n";

指针可能都是nullptr并且相等,因此简单的比较不足以确定它们都指向同一个对象。像unique_ptr<T>一样,shared_ptr<T>对象可以隐式转换为类型bool,因此您可以将语句写成:

if(pA && (pA == pB))

std::cout << " Both pointers point to the same object.\n";

您还可以检查 shared_ptr 对象是否有重复:

auto pname = std::make_shared<std::string>("Charles Dickens");

if(pname.unique())

std::cout << there is only one..." << std::endl;

else

std::cout << there is more than one..." << std::endl;

如果记录的对象实例数为 1,函数成员unique()返回true,否则返回false。您还可以确定有多少实例:

if(pname.unique())

std::cout << there is only one..." << std::endl;

else

std::cout << there are " << pname.use_count() << " instances." << std::endl;

成员返回调用它的对象的实例数。如果share_ptr<T>对象包含nullptr,则返回 0。

弱 _ 指针指针

只能从shared_ptr<T>对象创建weak_ptr<T>对象。weak_ptr<T>当类的对象在自由存储器中被创建时,指针通常被用作存储同一类的另一个实例的地址的类成员。在这种情况下,使用一个shared_ptr<T>成员指向同类型的另一个对象可能会产生一个引用循环,这将阻止类类型的对象被自动从空闲存储中删除。这种情况并不常见,但也有可能,如图 1-4 所示。

A978-1-4842-0004-9_1_Fig4_HTML.gif

图 1-4。

How a reference cycle prevents objects from being deleted

删除图 1-4 中数组中的所有智能指针或将它们重置为nullptr不会删除它们所指向对象的内存。仍然有一个shared_ptr<X>对象包含每个对象的地址。没有剩余的外部指针可以访问这些对象,因此不能删除它们。如果对象使用weak_ptr<X>成员来引用其他对象,就可以避免这个问题。当数组中的外部指针被销毁或重置时,这些不会阻止对象被销毁。

您可以像这样创建一个weak_ptr<T>对象:

auto pData = std::make_shared<X>();     // Create a shared pointer to an object of type X

std::weak_ptr<X> pwData {pData};        // Create a weak pointer from shared pointer

std::weak_ptr<X> pwData2 {pwData};      // Create a weak pointer from another

因此,你可以从一个shared_ptr<T>或者一个现有的weak_ptr<T>中创建一个weak_ptr<T>。你不能用弱指针做很多事情——例如,你不能去引用它来访问它所指向的对象。您可以用一个weak_ptr<T>对象做两件事:

  • 您可以测试它指向的对象是否仍然存在,这意味着仍然有一个shared_ptr<T>指向它。
  • 您可以从一个weak_ptr<T>对象创建一个shared_ptr<T>对象。

下面是测试弱指针引用的对象是否存在的方法:

if(pwData.expired())

std::cout << "Object no longer exists.\n";

如果对象不再存在,pwData对象的expired()函数返回true。您可以从弱指针创建共享指针,如下所示:

std::shared_ptr<X> pNew {pwData.lock()};

如果对象存在,lock()函数通过返回一个初始化pNew的新的shared_ptr<X>对象来锁定该对象。如果对象不存在,lock()函数将返回一个包含nullptrshared_ptr<X>对象。您可以在if语句中测试结果:

if(pNew)

std::cout << "Shared pointer to object created.\n";

else

std::cout << "Object no longer exists.\n";

使用weak_ptr<T>指针超出了本书的范围,所以我不会再深入研究。我将在第三章中探讨在容器中存储智能指针的含义和优点。

算法

算法提供了计算和分析功能,这些功能主要应用于由一对迭代器指定的一系列对象——begin 迭代器指向第一个元素,end 迭代器指向最后一个元素之后的一个元素。因为它们通过迭代器访问数据元素,所以算法不关心数据在哪里。您可以将算法应用于可以通过算法所需类型的迭代器访问的任何序列,因此您可以将算法应用于容器中的元素、string对象中的字符、标准数组元素、流以及存储在您定义的类类型的容器中的序列,只要您的类支持迭代器。

算法是 STL 中最大的工具集合。其中许多都与大量的应用程序相关,尽管有些应用程序在使用上非常专业。您可以将算法分为三大类:

Non-mutating sequence operations don’t change the sequence to which they are applied in any way. An algorithm that finds an element that matches a given value obvious doesn’t change the original data. Numerical algorithms such as inner_product() and accumulate() that process a sequence or sequences without changing them to produce a result also fall into this category. Algorithms in this category include find(), count(), mismatch(), search(), and equal().   Mutating sequence operations do change the elements in a sequence. Algorithms in this category include swap(), copy(), transform(), replace(), remove(), reverse(), rotate(), fill(), and shuffle(). Heap operations also fall into this category.   Sorting, merging, and related operations in many instances will change the order of the sequences to which they are applied. Algorithms in this category include sort(), stable_sort(), binary_search(), merge(), min(),and max().

当然,我在这些类别中识别的算法的例子决不是可用的详尽列表;您将在后续章节中了解更多内容,以及如何应用它们。有些算法,比如transform(),需要一个函数作为参数传递,应用于一个范围内的元素。对元素重新排序的其他方法经常提供选项来为比较元素提供谓词。接下来让我们看看将一个函数作为参数传递给另一个函数的可能性。

将函数作为参数传递

可接受作为另一个函数的参数的函数的签名由函数参数的规范决定。参数说明取决于函数参数的性质。有三种方法可以将函数作为参数传递给另一个函数:

  • 您可以使用一个函数指针,其中您使用函数名作为参数值。我不会对此做进一步的阐述,因为我假设你已经熟悉了函数指针,并且下两种可能性是更好的。
  • 您可以传递一个函数对象作为参数。
  • 您可以使用 lambda 表达式作为参数。

在接下来的章节中,你会看到很多使用最后两个选项的例子,所以我会提醒你这两个选项的细节,以防你对它们有点生疏。

功能对象

函数对象——也称为仿函数——是重载函数调用操作符operator()()的类的对象;它们提供了一种比使用原始函数指针更有效的方式来将一个函数作为参数传递给另一个函数。让我们看一个简单的例子。假设我这样定义了一个Volume类:

class Volume

{

public:

double operator()(double x, double y, double z) {return x*y*z; }

};

我可以创建一个Volume对象,我可以像使用函数一样计算体积:

Volume volume;                              // Create a functor

double room { volume(16, 12, 8.5) };        // Room volume in cubic feet

room的初始化列表中的值是为volume对象调用operator()()的结果,所以表达式等价于volume.operator()(16, 12, 8.5)。当函数接收一个函数对象作为参数时,它可以像函数一样使用。当然,你可以在一个类中定义多个版本的operator()()函数,这允许一个对象以不同的方式应用。假设我们已经定义了一个Box类,它的成员定义了一个对象的长度、宽度和高度,访问器函数成员返回这些值;我们可以扩展Volume类来容纳Box对象,如下所示:

class Volume

{

public:

double operator()(double x, double y, double z) {return x*y*z; }

double operator()(const Box& box)

{ return box.getLength()*box.getWidth()*box.getHeight(); }

};

现在可以用一个Volume物体来计算一个Box物体的体积:

Box box{1.0, 2.0, 3.0};

std::cout << “The volume of the box is “ << volume(box) << std::endl;

为了允许将一个Volume对象作为一个函数的参数传递,您可以将函数参数指定为类型Volume&。STL 算法通常对参数使用更一般化的规范,该规范要求通过具有标识类型的函数模板参数来表示函数的自变量。

λ表达式

lambda 表达式定义了一个匿名函数。由 lambda 表达式定义的函数不同于常规函数,因为它可以捕获存在于 lambda 范围内的变量并访问它们。Lambda 表达式经常与 STL 算法一起使用。让我们举一个 lambda 表达式的例子。假设您想将计算类型为double的数值的立方(x 3 )的能力传递给一个函数。这里有一个 lambda 表达式来实现这一点:

[] (double value) { return value*value*value; }

开始的方括号被称为λ导入器。它们标志着 lambda 表达式的开始。lambda 介绍器的内容比这里多得多——括号并不总是空的。lambda 引入程序后面是圆括号中的 lambda 参数列表。这就像一个常规的函数参数列表。在这种情况下,只有一个参数,value,但是可能有多个,用逗号分隔。还可以为 lambda 表达式中的参数指定默认值。

lambda 表达式的主体出现在参数列表后面的大括号中,也和普通函数一样。这个 lambda 的主体只包含一个语句,一个也计算返回值的return语句。一般来说,lambda 的主体可以包含任意数量的语句。注意,在上面的例子中没有返回类型规范。返回类型默认为返回值的类型。如果没有返回任何内容,则返回类型为void。您可以使用尾随返回类型语法来指定返回类型。你可以像这样为上面的 lambda 提供它:

[] (double value) -> double { return value*value*value; }

命名 Lambda 表达式

尽管 lambda 表达式是一个匿名对象,但您仍然可以将其地址存储在一个变量中。你不知道它的类型是什么,但是编译器知道:

auto cube = [] (double value) { return value*value*value; };

auto关键字告诉编译器从出现在赋值右边的任何内容中找出变量cube应该具有的类型,因此它将具有存储 lambda 表达式地址所必需的类型。如果方括号之间没有任何东西 lambda 导入器,您总是可以这样做。有时候方括号之间的东西会阻止你以这种方式使用auto。你可以像使用函数指针一样使用cube,例如:

double x{2.5};

std::cout << x << " cubed is " << cube(x) << std::endl;

output 语句产生 2.5 的立方。

将 Lambda 表达式传递给函数

一般来说,你不知道 lambda 表达式的类型。没有通用的“lambda 表达式类型”我已经说过,通常使用 lambda 表达式将一个函数作为参数传递给另一个函数,这立即引发了一个问题,当参数是 lambda 表达式时,如何指定参数类型。有不止一种可能性。一个简单的答案是为函数定义一个模板,其中类型参数是 lambda 表达式的类型。

编译器总是知道 lambda 表达式的类型,所以它可以用一个参数实例化一个函数模板,该参数将接受给定的 lambda 表达式作为参数。通过一个例子很容易看出这是如何工作的。假设您在一个容器中存储了许多double值,您希望能够以任意方式转换这些值;有时,您希望用它们的平方、平方根或一些更复杂的变换来替换这些值,这取决于这些值是否在特定的范围内。您可以定义一个模板,允许 lambda 表达式指定元素的转换。模板如下所示:

template <typename ForwardIter, typename F>

void change(ForwardIter first, ForwardIter last, F fun)

{

for(auto iter = first; iter != last; ++iter)   // For each element in the range...

*iter = fun(*iter);                          // ...apply the function to the object

}

fun参数将接受任何合适的 lambda 表达式,以及函数对象或普通函数指针。你可能想知道编译器是如何处理这个模板的,记住没有关于fun做什么的信息。答案是编译器不处理。编译器不会以任何方式处理模板,直到它需要实例化。对于上面的模板,当你使用它时,所有关于 lambda 的信息对编译器都是可用的。下面是一个使用模板的示例:

int data[] {1, 2, 3, 4};

change(std::begin(data), std::end(data), [] (int value){ return value*value; });

第二条语句将用原始值的平方替换data数组中每个元素的值。

标准库中的functional头文件定义了一个模板类型std::function<>,它是一个包装器,用于包装指向一个函数的任何类型的指针,该函数具有一组给定的返回和参数类型;当然,这包括 lambda 表达式。std::function模板的类型参数的形式是Return_Type(Param_Types). Return_Type是 lambda 表达式(或指向的函数)返回的值的类型。Param_Types是用逗号分隔的 lambda 表达式(或指向的函数)的参数类型列表。代表上一节中 lambda 表达式的变量的定义可以指定为:

std::function<double(double)> op {  [] (double value) { return value*value*value; } };

op变量可以作为参数传递给任何接受具有相同签名的函数参数的函数。当然,您可以重新定义op来表示其他东西,只要“其他东西”具有相同的返回类型以及参数的数量和类型:

op = [] (double value) { return value*value; };

op现在表示返回自变量平方的函数。您可以使用std::function类型模板来指定任何可调用内容的类型,包括任何 lambda 表达式或函数对象。

捕获条款

λ引入器[]不一定是空的;它可以包含一个 capture 子句,该子句指定如何从 lambda 的主体中访问封闭范围内的变量。方括号之间没有任何内容的 lambda 表达式体只能处理在 lambda 中本地定义的参数和变量。没有 capture 子句的 lambda 被称为无状态 lambda 表达式,因为它不能访问其封闭范围内的任何内容。图 1-5 显示了 lambda 表达式的语法。

A978-1-4842-0004-9_1_Fig5_HTML.gif

图 1-5。

Components of a lambda expression

默认的 capture 子句适用于包含 lambda 定义的范围内的所有变量。如果将=放在方括号之间,lambda 的主体可以通过值访问封闭范围内的所有自动变量——也就是说,变量的值在 lambda 表达式中是可用的,但是存储在原始变量中的值不能改变。如果您将mutable关键字添加到参数列表括号后面的 lambda 定义中,那么您可以从 lambda 内部修改封闭范围中的变量副本。从 lambda 的一次执行到下一次执行,lambda 会记住由 value 捕获的变量副本的本地值,因此副本实际上是static

如果将&放在方括号之间,封闭范围内的所有变量都可以通过引用来访问,因此它们的值可以通过 lambda 主体中的代码来更改。在这种情况下,mutable关键字是不必要的。为了便于访问,变量必须在 lambda 表达式定义之前定义。

你不能使用auto来指定一个变量的类型来存储一个 lambda 的地址,这个 lambda 访问包含它的地址的变量。这意味着您试图用使用该变量的表达式来初始化该变量。不能将auto与引用正在定义的变量的任何 lambda 一起使用——自引用不允许与auto一起使用。

您可以在封闭范围内捕获特定的变量。要捕获希望通过值访问的特定变量,只需在 capture 子句中列出它们的名称。要通过引用捕获特定的变量,需要在每个名称前加上前缀&。capture 子句中的两个或更多变量必须用逗号分隔。您可以在 capture 子句中包含=以及要通过引用捕获的特定变量名。capture 子句[=, &factor]将允许通过引用访问factor,通过值访问封闭范围内的任何其他变量。捕获子句[&, factor]将通过值捕获factor,通过引用捕获所有其他变量。您还需要指定mutable关键字来修改factor的副本。

Warning

通过 lambda 表达式中的值来捕获封闭范围内的所有变量会增加很多开销,因为它们每个都会创建一个副本——无论您是否引用它们。更明智的做法是只捕捉那些你需要的东西。

transform()算法将您作为参数提供的函数应用于一系列元素。transform()的前两个参数是迭代器,指定函数参数应用的范围;第三个参数是一个迭代器,指定结果的起始位置;第四个参数是应用于输入范围的函数。这里有一个例子演示了仿函数、lambda 表达式和std::function模板类型在转换算法中的使用:

// Ex1_03.cpp

// Passing functions to an algorithm

#include <iostream>                         // For standard streams

#include <algorithm>                        // For transform()

#include <iterator>                         // For iterators

#include <functional>                       // For function

class Root

{

public:

double operator()(double x) { return std::sqrt(x); };

};

int main()

{

double data[] { 1.5, 2.5, 3.5, 4.5, 5.5};

// Passing a function object

Root root;                                // Function object

std::cout << "Square roots are:" << std::endl;

std::transform(std::begin(data), std::end(data),

std::ostream_iterator<double>(std::cout, " "), root);

// Using an lambda expression as an argument

std::cout << "\n\nCubes are:" << std::endl;

std::transform(std::begin(data), std::end(data),

std::ostream_iterator<double>(std::cout, " "), [](double x){return x*x*x; });

// Using a variable of type std::function<> as argument

std::function<double(double)> op {[](double x){ return x*x; }};

std::cout << "\n\nSquares are:" << std::endl;

std::transform(std::begin(data), std::end(data),

std::ostream_iterator<double>(std::cout, " "), op);

// Using a lambda expression that calls another lambda expression as argument

std::cout << "\n\n4th powers are:" << std::endl;

std::transform(std::begin(data), std::end(data),

std::ostream_iterator<double>(std::cout, " "), &op{return op(x)*op(x); });

std::cout << std::endl;

}

输出应该是:

Square roots are:

1.22474 1.58114 1.87083 2.12132 2.34521

Cubes are:

3.375 15.625 42.875 91.125 166.375

Squares are:

3.375 15.625 42.875 91.125 166.375

4th powers are:

11.3906 244.141 1838.27 8303.77 27680.6

如果您已经理解了前面的部分,那么使用这个示例应该不会有太大的困难。transform()算法要处理的输入数据包含在data数组中。每次调用中transform()的前两个参数是数组的开始和结束迭代器。输出的目的地由输出流迭代器指定,它将数据写入标准输出流。ostream_iterator构造函数的第二个参数是写在每个值后面的分隔符。

第一个transform()调用传递一个Root对象作为最后一个参数。Root类定义了operator()()成员来返回参数的平方根。第二个transform()调用显示,您可以编写一个 lambda 表达式作为参数,在这种情况下,它计算输入值的立方体。第三个transform()调用表明std::function类型模板在这里也可以工作。最后一个调用表明一个 lambda 可以调用另一个 lambda。因此,当您需要将函数作为参数传递给算法时,您可以应用这些技术中的任何一种。

摘要

本章介绍了 STL 背后的基本思想。我在本章中介绍的 STL 的所有方面将在本书的后面进行更深入的演示和解释。本章还概述了一些理解起来很重要的 C++ 功能,因为它们是应用 STL 的基础,我将在后续章节中广泛使用它们。本章中最重要的内容如下:

  • STL 定义了类模板,这些模板是其他对象的容器。
  • STL 定义了迭代器,迭代器是行为类似指针的对象。一对迭代器用于定义连续的元素范围;开始迭代器指向范围中的第一个元素,结束迭代器指向范围中最后一个迭代器之后的一个元素。
  • 反向开始迭代器指向一个范围中的最后一个元素,反向结束迭代器指向第一个元素之前的一个元素。反向迭代器的工作方式与普通迭代器相反。
  • iterator头定义了全局函数,这些函数返回容器、数组或任何其他支持迭代器的对象的迭代器。全局函数begin()cbegin()end(),cend()返回普通迭代器。函数rbegin()crbegin()rend(),crend()返回反向迭代器。这个集合中以'c'开头的函数名返回const迭代器。
  • 您可以使用流迭代器在流之间来回传输给定类型的数据。
  • STL 定义了函数模板,这些模板定义了应用于迭代器指定的一系列元素的算法。
  • 智能指针是行为类似指针的对象,以及在自由存储中创建的对象的地址。当智能指针不存在时,由智能指针管理的对象将被自动删除。智能指针不能递增或递减。
  • lambda 表达式定义了一个匿名函数。Lambda 表达式经常用于将函数作为参数传递给 STL 算法。
  • 您可以使用在functional头中定义的std::function<>模板类型来为任何类型的具有给定签名的可调用实体指定类型。

Exercises

这里有几个练习来测试你对本章主题的记忆程度。如果你卡住了,回头看看这一章寻求帮助。如果之后你仍然停滞不前,你可以从 Apress 网站( http://www.apress.com/9781484200056 )下载解决方案,但这真的应该是最后的手段。

Write a program that defines an array of std::string objects that is initialized with a set of words of your choice and lists the contents of the array, one to a line, using iterators.   Write a program that applies the transform() algorithm to the elements of the array in the previous exercise to replace all lowercase vowels in the words by '*' and write the results to the standard output stream one to a line. Define the function to replace vowels in a string as a lambda expression that uses iterators.   Write a program that applies the transform() algorithm to the array from the first exercise to convert the strings to uppercase and output the results. The function to convert the strings should be passed to transform() as a lambda expression that calls transform() to apply the std::toupper() library function to characters in a string.

二、使用序列容器

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​2) contains supplementary material, which is available to authorized users.

本章介绍了你可能最常用的基本容器——序列容器。在里面,你会学到以下内容:

  • 序列容器的特征
  • 如何在序列容器中获取和使用迭代器
  • 如何使用array容器
  • 一个vector容器的能力是什么
  • 一个deque容器的特征和能力以及它与一个vector容器的不同之处
  • 一个list容器如何构造它所包含的数据元素,它的优点和缺点是什么
  • 一个forward_list容器和一个list容器有什么不同,以及你什么时候会使用它
  • 如何定义自己的迭代器

序列容器

序列容器以线性顺序存储元素。元素之间没有秩序。这些元素按照你存储它们的顺序排列。有五种标准序列容器,每种都有不同的特征:

  • 一个array<T,N>容器是一系列固定长度NT类型的对象。您不能添加或删除元素。
  • 一个vector<T>容器是一个可变长度的类型为T的对象序列,它会在需要时自动增长。您只能在序列的末尾有效地添加或删除元素。
  • 一个deque<T>容器是一个自动增长的可变长度序列。您可以在序列的两端有效地添加或删除元素。
  • 一个list<T>容器是一个由类型为T的对象组成的变量序列,组织成一个双向链表。您可以在序列中的任何位置有效地添加或删除元素。与前三个容器相比,访问序列内部的任意元素相对较慢,因为必须从第一个元素或最后一个元素开始,并在列表中移动,直到到达所需的元素。
  • 一个forward_list<T>是一个可变长度的类型为T的对象序列,被组织成一个单链表。这比列表容器更快,需要的内存更少,但是序列内部的元素只能从第一个元素开始访问。

图 2-1 显示了可用的序列容器以及它们之间的差异。

A978-1-4842-0004-9_2_Fig1_HTML.gif

图 2-1。

The standard sequence containers

图 2-1 中所示的每种集装箱的操作都是可以有效执行的操作。正如您将看到的,在某些情况下,其他操作也是可能的,但这些操作会慢得多。

容器之间通用的函数成员

我将在本章的剩余部分详细解释如何使用每个序列容器。序列容器有许多共同的函数成员,它们在每个容器中的行为是相同的。我将在一个容器的上下文中描述每个成员,而不是重复详细描述这些成员在每种类型的容器中做什么。表 2-1 显示了arrayvectordeque容器的函数成员,以及两个或更多容器实现同一个函数成员的情况。

表 2-1。

Function members of array, vector, and deque containers

| 功能成员 | 数组 | 向量 | deqo | | --- | --- | --- | --- | | `begin()`–返回开始迭代器。 | 是 | 是 | 是 | | `end() –`返回结束迭代器。 | 是 | 是 | 是 | | `rbegin()`–返回反向开始迭代器。 | 是 | 是 | 是 | | `rend() –`返回反向结束迭代器。 | 是 | 是 | 是 | | `cbegin()`–返回`const` begin 迭代器。 | 是 | 是 | 是 | | `cend() –`返回`const`结束迭代器。 | 是 | 是 | 是 | | `crbegin()`–返回`const`反向开始迭代器。 | 是 | 是 | 是 | | `crend()`–返回`const`反向结束迭代器。 | 是 | 是 | 是 | | `assign()`–用一组新的元素替换内容。 | - | 是 | 是 | | `operator=()`–用相同类型的另一个容器的副本,或者从初始化列表中替换元素。 | 是 | 是 | 是 | | `size()`–返回元素的实际数量。 | 是 | 是 | 是 | | `max_size()`–返回元素的最大数量。 | 是 | 是 | 是 | | `capacity()`–返回分配内存的元素数量。 | - | 是 | - | | `empty()`–如果没有元素,则返回`true`。 | 是 | 是 | 是 | | `resize()`–改变元素的实际数量。 | - | 是 | 是 | | `shrink_to_fit()`–将内存减少到实际元素数量所需的内存。 | - | 是 | 是 | | `front()`–返回对第一个元素的引用。 | 是 | 是 | 是 | | `back()`–返回对最后一个元素的引用。 | 是 | 是 | 是 | | `operator[]() –`访问索引处的元素。 | 是 | 是 | 是 | | `at()`–通过边界检查访问索引参数处的元素。 | 是 | 是 | 是 | | `push_back() –`在序列末尾追加一个元素。 | - | 是 | 是 | | `insert() -`在指定位置插入一个或多个元素。 | - | 是 | 是 | | `emplace() –`在指定位置创建一个元素。 | - | 是 | 是 | | `emplace_back() –`在序列的末尾创建一个元素。 | - | 是 | 是 | | `pop_back() –`删除序列末尾的元素。 | - | 是 | 是 | | `erase() –`删除一个元素或一系列元素。 | - | 是 | 是 | | `clear() –`删除所有元素,因此大小为 0。 | - | 是 | 是 | | `swap() –`交换两个容器中的所有元素。 | 是 | 是 | 是 | | 返回包含元素的内部数组的指针。 | 是 | 是 | - |

列中没有'Yes'意味着没有为该容器定义函数。你不需要记住这张桌子。这里仅供参考。当您进一步了解容器如何构造元素时,您会本能地知道哪些功能对于给定的容器是不可用的。

将元素组织成链表的容器在内部组织上与表 2-1 中的容器有很大不同。尽管listforward_list容器彼此非常相似。一个forward_list拥有一个list容器拥有的大部分功能成员。forward_list中缺少的基本上是那些需要向后遍历序列的,所以没有反向迭代器。作为参考,表 2-2 显示了listforward_list容器的功能成员。

表 2-2。

Function members of list and forward_list containers

| 功能成员 | 列表 | 转发 _ 列表 | | --- | --- | --- | | `begin()`–返回开始迭代器。 | 是 | 是 | | `end() –`返回结束迭代器。 | 是 | 是 | | `rbegin()`–返回反向开始迭代器。 | 是 | – | | `rend() –`返回反向结束迭代器。 | 是 | -– | | `cbegin()`–返回`const` begin 迭代器。 | 是 | 是 | | `before_begin() –`返回指向第一个元素之前的迭代器。 | – | 是 | | `cbefore_begin()`–返回指向第一个元素之前的`const`迭代器。 | - | 是 | | `cend()`–返回`const`结束迭代器。 | 是 | 是 | | `crbegin()`–返回`const`反向开始迭代器。 | 是 | - | | `crend()`–返回`const`反向结束迭代器。 | 是 | - | | `assign()`–用一组新的元素替换内容。 | 是 | 是 | | `operator=()`–用相同类型的另一个容器的副本,或者从初始化列表中替换元素。 | 是 | 是 | | `size()`–返回元素的实际数量。 | 是 | - | | `max_size()`–返回元素的最大数量。 | 是 | 是 | | `resize()`–改变元素的数量。 | 是 | 是 | | `empty()`–如果没有元素,则返回`true`。 | 是 | 是 | | `front()`–返回对第一个元素的引用。 | 是 | 是 | | `back()`–返回对最后一个元素的引用。 | 是 | – | | `push_back()`–在序列末尾追加一个元素。 | 是 | – | | `push_front()`–在序列的开头插入一个元素。 | 是 | 是 | | `emplace()`–在指定位置之前创建一个元素。 | 是 | – | | `emplace_after()`–在指定位置后创建一个元素。 | – | 是 | | `emplace_back()`–在序列的末尾创建一个元素。 | 是 | – | | `emplace_front()`–在序列的开头创建一个元素。 | 是 | 是 | | `insert() –`在指定位置前插入一个或多个元素。 | 是 | – | | `insert_after()`–在指定位置后插入一个或多个元素。 | – | 是 | | `pop_back()`–删除序列末尾的元素。 | 是 | – | | `pop_front()`–删除序列开头的元素。 | 是 | 是 | | `reverse()`–反转元素的顺序。 | 是 | 是 | | `erase()`–删除指定位置的元素或删除一系列元素。 | 是 | – | | `erase_after()`–删除指定位置后的元素或删除一系列元素。 | – | 是 | | `remove() -`删除与参数匹配的元素。 | 是 | 是 | | `remove_if()`–删除一元谓词参数返回 true 的元素。 | 是 | 是 | | `unique()`–删除连续的重复项。 | 是 | 是 | | `clear()`–删除所有元素,因此大小为 0。 | 是 | 是 | | `swap()`–交换两个容器中的所有元素。 | 是 | 是 | | `sort()`–对元素进行排序。 | 是 | 是 | | `merge()`–将此容器与另一个容器合并–两者都必须排序。 | 是 | 是 | | 将另一个相同类型列表中的元素移动到指定位置之前。 | 是 | – | | `splice_after()`–将另一个相同类型列表中的元素移动到指定位置之后。 | - | 是 |

所有序列容器拥有的max_size()函数成员返回可以存储的元素的最大可能数量;这通常是一个非常大的数字,通常是 232–1,所以很少需要调用这个函数。

使用数组容器

array<T,N>模板定义了等同于标准数组的容器类型。这是一个由T类型的N元素组成的固定序列,所以除了指定元素的类型和数量略有不同之外,它就像一个常规数组。显然,您不能添加或删除元素。模板实例中的元素存储在标准数组中。array与标准数组相比,容器的开销很小,但提供了两个优势:如果使用at(),可以检测到试图访问索引超出合法范围的元素,并且容器知道它有多少个元素,这意味着数组容器可以作为参数传递给函数,而不需要单独指定元素的数量。您必须在源文件中包含array头才能使用容器类型。它非常容易使用——下面是如何创建由 100 个类型为double的元素组成的array<>:

std::array<double, 100> data;

当您定义一个array容器而没有为元素指定初始值时,元素不会被初始化,但是您可以将元素类型默认初始化为零或其等价物,如下所示:

std::array<double, 100> data {};

使用这个语句,data容器中的所有元素都将是0.0。参数N的规范必须是一个常量表达式,并且容器中的元素数量不能改变。当然,您可以在创建一个array容器的实例时初始化元素,就像普通数组一样:

std::array<double, 10> values {0.5, 1.0, 1.5, 2.0};   // 5th and subsequent elements are 0.0

初始化列表中的四个值用于初始化前四个元素;后续元素将为零。这如图 2-2 所示。

A978-1-4842-0004-9_2_Fig2_HTML.gif

图 2-2。

Creating an array<T,N> container

您可以通过调用array对象的fill()函数成员将所有元素设置为某个给定值。例如:

values.fill(3.1415926);                               // Set all elements to pi

fill()函数将所有元素设置为您作为参数传递的值。

访问元素

您可以像访问标准数组一样,使用方括号中的索引在表达式中访问和使用array容器中的元素,例如:

values[4] = values[3] + 2.0*values[1];

第五个元素被设置为表达式的值,该值是赋值的右操作数。像这样使用索引不会进行边界检查;使用超出范围的索引值来访问或存储数据将不会被检测到。要检查超出范围的索引值,可以使用at()函数成员:

values.at(4) = values.at(3) + 2.0*values.at(1);

这与前面的语句相同,只是如果at()的参数表示超出范围的索引值,将抛出std::out_of_range异常。总是使用at(),除非你确定索引不可能超出范围。问题显然是为什么operator[]()实现不做边界检查。答案是性能;每次访问元素时验证索引值会带来开销;当不可能出现超出范围的索引值时,您希望避免开销。

一个array对象的size()函数返回类型为size_t的元素数量,因此您可以像这样对values数组中的元素求和:

double total {};

for(size_t i {} ; i < values.size() ; ++i)

{

total += values[i];

}

与标准数组相比,size()函数的存在提供了一个优势,即array容器知道它包含多少元素;接收数组容器作为参数的函数可以调用size()成员来获得元素的数量。您不必调用size()成员来决定一个array容器是否没有元素。如果容器没有元素,empty()成员返回true:

if(values.empty())

std::cout << "The container has no elements.\n";

else

std::cout << "The container has " << values.size() <<  " elements.\n";

然而,很难想象一个array容器怎么会没有元素,因为元素的数量在创建时是固定的,不能改变。创建空数组容器实例的唯一方法是将第二个模板参数指定为零——这种情况不太可能发生。然而,调用empty()的相同机制适用于其他容器,其中元素的数量可以变化,元素可以被删除,因此它提供了一致的操作。

您可以对任何使迭代器可用的容器使用基于范围的for循环,这样您可以更简单地对values容器中的元素求和:

double total {};

for(auto&#x0026;& value : values)

total += value;

当然,这也可以用作为参数传递给函数的容器来完成。

一个array容器的front()back()函数成员分别返回对第一个和最后一个元素的引用。还有返回&front()data()函数成员,它是存储元素的底层标准数组的地址。你不太可能经常需要这个设施。

有一个函数模板供get<n>()辅助函数从数组容器中访问第n个元素;例如,模板参数的参数必须是可以在编译时计算的常量表达式,因此它不能是循环变量。被访问的元素是模板参数,它在编译时被检查。get<n>()模板提供了一种无需运行时检查就能访问具有确定索引值的元素的方法。你可以这样使用它:

std::array<std::string, 5> words {"one", "two", "three", "four", "five"};

std::cout << std::get<3>(words) << std::endl;  // Output words[3]

std::cout << std::get<6>(words) << std::endl;  // Compiler error message!

这里有一个例子,用你到目前为止学到的一些东西演示了array容器的作用:

// Ex2_01.cpp

/*

Using array<T,N> to create a Body Mass Index (BMI) table

BMI = weight/(height*height)

weight is in kilograms, height is in meters

*/

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <array>                                 // For array<T,N>

int main()

{

const unsigned int min_wt {100U};              // Minimum weight in table in lbs

const unsigned int max_wt {250U};              // Maximum weight in table in lbs

const unsigned int wt_step {10U};              // Weight increment

const size_t wt_count {1 + (max_wt - min_wt) / wt_step};

const unsigned int min_ht {48U};               // Minimum height in table in inches

const unsigned int max_ht {84U};               // Maximum height in table in inches

const unsigned int ht_step {2U};               // Height increment

const size_t ht_count { 1 + (max_ht - min_ht) / ht_step };

const double lbs_per_kg {2.20462};             // Conversion factor lbs to kg

const double ins_per_m {39.3701};              // Conversion factor ins to m

std::array<unsigned int, wt_count> weight_lbs;

std::array<unsigned int, ht_count> height_ins;

// Create weights from 100lbs in steps of 10lbs

for (size_t i{}, w{ min_wt } ; i < wt_count ; w += wt_step, ++i)

{

weight_lbs.at(i) = w;

}

// Create heights from 48 inches in steps of 2 inches

unsigned int h{ min_ht };

for(auto& height : height_ins)

{

height = h;

h += ht_step;

}

// Output table headings

std::cout << std::setw(7) << " |";

for (const auto& w : weight_lbs)

std::cout << std::setw(5) << w << "  |";

std::cout << std::endl;

// Output line below headings

for (size_t i{1} ; i < wt_count ; ++i)

std::cout << "---------";

std::cout << std::endl;

double bmi {};                                 // Stores BMI

unsigned int feet {};                          // Whole feet for output

unsigned int inches {};                        // Whole inches for output

const unsigned int inches_per_foot {12U};

for (const auto& h : height_ins)

{

feet = h / inches_per_foot;

inches = h % inches_per_foot;

std::cout <<  std::setw(2) << feet << "'" << std::setw(2) << inches << "\"" << "|";

std::cout << std::fixed << std::setprecision(1);

for (const auto& w : weight_lbs)

{

bmi = h / ins_per_m;

bmi = (w / lbs_per_kg) / (bmi*bmi);

std::cout << std::setw(2) << " " << bmi << " |";

}

std::cout << std::endl;

}

// Output line below table

for (size_t i {1} ; i < wt_count ; ++i)

std::cout << "---------";

std::cout << "\nBMI from 18.5 to 24.9 is normal" << std::endl;

}

我没有展示书中示例的输出,因为它占据了相当大的空间。定义了两组四个const变量,它们与身体质量指数表中包含的体重和身高范围相关。重量和高度存储在具有类型为unsigned int的元素的array容器中,因为根据定义,所有的重量和高度都是整数且非负的。容器在for循环中用适当的值初始化。第一个循环演示了at()函数,但是您可以在这里安全地使用weight_lbs[i]。接下来的两个for循环输出表格的列标题和一行将标题与表格的其余部分分开。表格是使用嵌套的基于范围的for循环创建的。外部循环遍历高度,并以英尺和英寸为单位输出最左边一列的高度。内部循环迭代权重,并输出当前高度的一行身体质量指数值。

对数组容器使用迭代器

array模板定义了begin()end()成员,它们分别返回指向第一个元素和最后一个元素之后的随机访问迭代器。正如你在第一章中看到的,随机访问迭代器是最有能力的,所以所有的操作都可以用它们来完成。您可以使用显式迭代器对设置height_ins容器中的值的循环进行编码:

unsigned int h {min_ht};

auto first = height_ins.begin();      // Iterator pointing to 1st element

auto last = height_ins.end();         // Iterator pointing to 1 past last element

while (first != last)

{

*first++ = h;                       // Store h in current element and increment iterator

h += ht_step;

}

迭代器对象由array对象的begin()end()成员函数返回。使用auto省去了担心迭代器的实际类型——但是如果你想知道的话——在这种情况下它们是std::array<unsigned int,19>::iterator类型,这意味着iterator类型是在array<T,N>类型中定义的。您可以看到迭代器对象的使用方式与常规指针相同——在元素中存储了值之后,后缀++操作符增加了first。当first等于end时,所有元素已被设置,循环结束。

正如我在第一章中所说,最好使用全局begin()end()函数来获取容器的迭代器,因为它们是通用的,所以firstlast可以这样定义:

auto first = std::begin(height_ins);  // Iterator pointing to 1st element

auto last = std::end(height_ins);     // Iterator pointing to 1 past last element

请记住,虽然迭代器指向容器中的特定元素,但它们不保留容器本身的任何信息;没有办法从迭代器判断它是指向array容器还是vector容器中的元素。让迭代器标识容器中的一系列元素引入了对它们应用算法的可能性,那么有没有什么算法可以用在Ex2_01.cpp中呢?在algorithm头中定义的generate()函数模板提供了一种用函数对象计算的值初始化范围的方法,这是一种可能性。我们可以像这样重写前面初始化height_ins容器的代码片段:

unsigned int height {};               // Stores the current height initializing value

std::generate(std::begin(height_ins), std::end(height_ins),

[height, &min_ht, &ht_step]()mutable

{ return height += height == 0 ? min_ht : ht_step; });

设置容器元素的值现在减少到两条语句,并且不需要显式循环。第一条语句定义了一个变量来保存元素的初始化器。generate()函数的前两个参数是 begin 和 end 迭代器,它们定义了由作为第三个参数传递的函数设置值的范围。这是一个 lambda 表达式。min_htht_step变量通过引用在 lambda 中被捕获,而mutable关键字使 lambda 能够更新通过值捕获的height的本地副本的值。在 return 语句中,lambda 第一次执行时,本地height副本被设置为min_ht,并在后续调用中递增ht_step。lambda 表达式中由 value 捕获的变量的本地副本的值从 lambda 的一次执行保留到下一次执行,这使得这种机制能够按照我们想要的方式工作。

假设您想用连续递增的值初始化一个数组容器。在numeric头中有一个iota()函数模板。下面是它的使用方法:

std::array<double, 10> values;

std::iota(std::begin(values), std::end(values), 10.0);  // Set values elements to 10.0 to 19.0

前两个参数是定义要设置的元素范围的迭代器。第三个参数是范围中第一个元素的值。后续元素值通过应用增量运算符生成。iota()函数不限于处理数值。要设置的元素范围可以是支持operator++()的任何类型。

Note

不要忘记算法是独立于容器类型的。它们处理任何具有所需类型迭代器的容器中的元素。generate()iota()函数模板只需要前向迭代器,所以定义任何容器范围的迭代器都可以工作。

一个array容器定义了返回const迭代器的cbegin()cend()函数成员;当你只想访问元素而不想修改它们时,你应该使用const迭代器。与非const迭代器一样,最好使用全局cbegin()cend()函数来获得它们。rbegin()rend()函数——全局和成员——返回反向迭代器,分别指向最后一个元素和第一个元素之前的一个元素;返回const反向迭代器的函数有crbegin()crend()。使用反向迭代器以逆序处理元素。例如:

std::array<double, 5> these {1.0, 2.0, 3.0, 4.0, 5.0};

double sum {};

auto start = std::rbegin(these);

auto finish = std::rend(these);

while(start != finish)

sum += *(start++);

std::cout << "The sum of elements in reverse order is " << sum << std::endl;

元素在循环中求和,从最后一个元素开始。finish迭代器指向第一个元素之前的 1,所以循环在第一个元素被添加到sum之后结束。对反向迭代器应用 increment 操作符会将它指向的内容向常规正向迭代器的相反方向移动。你可以在这里使用一个for循环:

for(auto iter = std::rbegin(these); iter != std::rend(these); ++iter)

sum += *iter;

因为数组容器实例有固定数量的元素,所以插入迭代器不适用;插入迭代器用于向容器中添加新元素。

比较数组容器

您可以使用任何比较操作符来比较两个完整的array<T,N>容器,只要容器大小相同,存储的元素类型相同,并且该类型支持比较操作。例如:

std::array<double,4> these {1.0, 2.0, 3.0, 4.0};

std::array<double,4> those {1.0, 2.0, 3.0, 4.0};

std::array<double,4> them  {1.0, 3.0, 3.0, 2.0};

if (these == those) std::cout << "these and those are equal."    << std::endl;

if (those != them)  std::cout << "those and them are not equal." << std::endl;

if (those < them)   std::cout << "those are less than them."     << std::endl;

if (them > those)   std::cout << "them are greater than those."  << std::endl;

容器是逐元素比较的。对于==true结果,所有对应元素对必须相等。对于不等式,对于一个true结果,至少有一对相应的元素必须是不同的。对于所有其他比较,第一对不同的元素产生结果。这基本上是字典中单词排序的方式,其中两个单词中不同的第一对对应字母决定了它们的顺序。代码片段中的所有比较都是true,所以当它执行时,所有四个消息都将被输出。

与标准数组不同,您可以将一个array容器分配给另一个容器,只要它们都存储相同数量的相同类型的元素。例如:

them = those;       // Copy all elements of those to them

赋值左边的数组容器中的元素被赋值右边的容器中的元素覆盖。

使用向量容器

vector<T>容器是类型为T的元素的序列容器。它就像一个array<T,N>容器,只是它的大小可以自动增长以容纳任意数量的元素;因此只需要类型参数T——不需要带有vectorN模板参数。一旦超过vector的当前容量,就会自动为更多元件分配额外空间。只有在容器的末尾才能有效地添加或删除元素。容器是数组的一个非常有用和灵活的替代品。大多数情况下,您可以将vector用作存储序列而不是数组的标准工具。只要您意识到扩展vector的容量或者从序列内部添加或删除元素的开销,您的代码在大多数情况下不会明显变慢。要使用vector容器模板,您必须在源文件中包含vector头。

创建向量容器

下面是创建一个vector容器来存储类型double的值的例子:

std::vector<double> values;

它没有元素,也没有为元素分配空间,所以当您添加第一个数据项时,内存将被动态分配。您可以通过调用容器对象的reserve()来增加容量:

values.reserve(20);                    // Memory for up to 20 elements

这将容器中分配的内存设置为至少容纳 20 个元素。如果当前容量已经大于或等于 20,则该语句不起任何作用。注意,调用reserve()不会创建任何元素。此时,values容器仍然没有元素,但是在分配更多内存之前,最多可以添加 20 个元素。调用reserve()不会影响任何现有元素。但是,如果调用增加了内存,任何现有的迭代器,比如 begin 和 end 迭代器,都将失效,因此您必须重新创建它们。这是因为随着容量的增加,元素可能会被复制或移动到新的存储位置。

创建vector的另一个选项是使用初始化列表来指定初始值以及元素的数量:

std::vector<unsigned int> primes {2u, 3u, 5u, 7u, 11u, 13u, 17u, 19u};

primes vector容器将由八个元素创建,初始值在初始化列表中。

分配内存在时间上是相对昂贵的,所以你不希望它发生得比必要的更频繁。一个vector将使用一种算法来增加容量,该算法依赖于通常是对数的实现。这可能会在早期导致一些非常小的内存分配,但是随着vector的扩展,内存分配会增加。您可以创建一个定义了初始数量元素的vector,如下所示:

std::vector<double> values(20);    // Capacity is 20 double values and there are 20 elements

这个容器从创建的 20 个元素开始,默认情况下用零初始化。创建一个vector容器是一个好主意,它的元素数量将最小化需要分配额外空间的次数。

Caution

20 左右的括号,即元素的数量,在上面的语句中是必不可少的。这里不能用大括号。如果您编写以下定义,结果会大不相同:

std::vector<double> values {20};        // There is one element initialized to 20

这个vector没有 20 个元素;它包含一个初始化为 20 的元素。添加更多元素将导致分配额外的内存。

如果不喜欢将零作为元素的默认值,可以指定一个适用于所有元素的值:

std::vector<long> numbers(20, 99L);    // Size is 20 long values - all initialized with 99

第二个参数指定所有元素的初始值,因此所有 20 个元素都是99L。指定vector中元素数量的第一个参数不必是常量表达式。它可能是运行时执行的表达式的结果,也可能是从键盘读入的结果。

当您用另一个容器中类型为T的元素创建一个vector<T>容器时,您可以初始化它。使用一对迭代器来指定要用作初始值的元素范围。这里有一个例子:

std::array<std::string, 5> words {"one", "two", "three", "four", "five"};

std::vector<std::string> words_copy {std::begin(words), std::end(words)};

将使用来自words数组容器的元素初始化words_copy向量。如果您使用移动迭代器来指定words_copy的初始化范围,那么元素将从words数组中移出。你可以这样做:

std::vector<std::string> words_copy {std::make_move_iterator(std::begin(words)),

std::make_move_iterator(std::end(words))};

words_copy向量将像以前一样被初始化。因为元素是被移动而不是复制的,words数组现在将包含代表空字符串""string对象。

向量的容量和大小

一个vector的容量是它在不分配更多内存的情况下可以存储的元素数量;这些元素可能存在,也可能不存在。一个vector的大小是它实际包含的元素的数量,也就是存储了值的元素的数量。图 2-3 说明了这一点。

A978-1-4842-0004-9_2_Fig3_HTML.gif

图 2-3。

The size and capacity of a vector

显然一个vector容器的大小不能超过它的容量。当大小等于容量时,添加一个元素将导致分配更多的内存。您可以通过调用vector对象的size()capacity()函数来获得vector的大小和容量。这些值作为由您的实现定义的无符号整数类型的整数返回。例如:

std::vector<size_t> primes { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47 };

std::cout << "The size is " << primes.size() << std::endl;

std::cout << "The capacity is " << primes.capacity() << std::endl;

输出语句将给出大小和容量的值 14,如初始化列表所确定的。但是,如果您使用push_back()函数添加一个元素,并再次输出大小和容量,则大小将为15,容量将为28。当大小等于容量时,增加容量的增量由依赖于实现的算法确定;一些实施方案将现有容量翻倍。

您可能想在一个变量中存储一个vector的大小或容量。一个vector<T>对象的大小或容量类型是vector<T>::size_type,这意味着size_type是在编译器从类模板生成的vector<T>类中定义的。因此,对于primes向量,大小值将是类型vector<size_t>::size_type。在大多数情况下,通过在定义变量时使用auto关键字,可以避免担心这些细节,例如:

auto nElements = primes.size();        // Store the number of elements

记住,你必须将=auto一起使用——而不是一个初始化列表;否则将无法正确确定类型。存储大小的一个常见原因是使用索引迭代vector中的元素。您也可以使用基于范围的for循环和vector:

for(auto& prime : primes)

prime *= 2;                          // Double each element value

您在前面看到,您可以为一个vector调用reserve()来增加它的容量;元素的数量不变。调用resize()函数成员会改变vector的大小,这也可能增加容量。resize()有几个品种,例如:

std::vector<int> values {1,2,3};       // 1 2 3           : size is 3

values.resize(5);                      // 1 2 3 0 0       : size is 5

values.resize(7, 99);                  // 1 2 3 0 0 99 99 : size is 7

values.resize(6);                      // 1 2 3 0 0 99    : size is 6

第一个resize()调用将大小更改为参数指定的元素数,因此它附加了两个用类型的默认值初始化的元素。如果添加元素导致超出当前容量,容量将自动增加。第二个resize()调用将大小增加到第一个参数指定的值,并用第二个参数指定的值初始化新元素。第三次调用将values容器的大小更改为 6,这小于当前的大小。当需要减小尺寸时,多余的元素被移除,就好像容器的pop_back()被重复调用一样,我将在本章稍后解释这一点。缩小一艘vector号的大小并不影响它目前的容量。

访问元素

您可以使用方括号之间的索引来设置现有元素的值,或者只是在表达式中使用其当前值。例如:

std::vector<double> values(20);             // Container with 20 elements created

values[0] = 3.14159;                        // Pi

values[1] = 5.0;                            // Radius of a circle

values[2] = 2.0*values[0]*values[1];        // Circumference of a circle

就像标准数组一样,vector的索引值从 0 开始。您总是可以使用方括号之间的索引来引用现有的元素,但是您不能通过这种方式创建新元素——您必须使用push_back()insert()emplace()emplace_back()。像这样索引 a vector时,不会检查索引值。您可以访问数组范围之外的内存,并使用方括号之间的索引将值存储在这些位置。vector对象提供了对参数进行边界检查的at()函数,就像一个array容器一样,所以只要索引有可能超出合法范围,就使用at()函数来引用元素。

vectorfront()back()函数成员返回对序列中第一个和最后一个元素的引用。例如:

std::cout << values.front() << std::endl;        // Outputs 3.14159

因为front()back()函数成员返回引用,所以它们可以出现在赋值的左边:

values.front() = 2.71828;                        // 1st element changed to 2.71828

data()函数成员返回一个指向数组的指针,该数组在vector内部用来存储元素。例如:

auto pData = values.data();

pData将是类型double*,通常data()vector<T>容器返回类型T*的值。您需要有一个非常好的理由来使用这个功能。

对向量容器使用迭代器

如您所料,vector容器有完整的函数成员,这些成员返回其元素的迭代器,包括const和非const迭代器以及反向迭代器。vector的迭代器是随机访问迭代器,当然,您也可以使用全局函数来获得它们。一个vector有一个push_back()函数成员,所以你可以使用一个back_insert_iterator向它添加新元素。你会从第一章的中想起,你可以通过调用全局back_inserter()函数来获得一个反向插入迭代器。一个front_insert_iterator不会与一个vector容器一起工作,因为它需要一个push_front()函数成员,而一个vector容器没有定义它。

我可以通过展示如何使用copy()算法添加元素来演示如何将反向插入器迭代器应用于vectorcopy()算法将元素从迭代器指定的范围(作为前两个参数提供)复制到迭代器指定的目的地(作为第三个参数)。前两个参数只需要是输入迭代器,所以任何类型的迭代器都可以接受;显然,第三个参数必须是输出迭代器。这里有一个例子:

std::vector<double> data {32.5, 30.1, 36.3, 40.0, 39.2};

std::cout << "Enter additional data values separated by spaces or Ctrl+Z to end:"

<< std::endl;

std::copy(std::istream_iterator<double>(std::cin), std::istream_iterator<double>(),

std::back_inserter(data));

std::copy(std::begin(data), std::end(data), std::ostream_iterator<double>(std::cout, " "));

创建data容器时,元素被设置为初始化列表中的值。对copy()的第一次调用有一个istream_iterator对象作为第一个参数,它从标准输入流中读取类型double的值。第二个参数实际上是流迭代器的结束迭代器,当识别出流的结尾时,istream_iterator将拥有这个值;当您从键盘输入Ctrl+Z时,这与cin一起发生。copy()的第三个参数是读取值的目的地,这是由back_inserter()函数返回的databack_insert_iterator。因此,从cin读取的值作为新元素附加到data的末尾。最后一条语句调用copy()data中的所有元素复制到cout;这是通过将目的地指定为ostream_iterator对象来实现的。让我们尝试一个使用迭代器和vector容器的完整例子:

// Ex2_02.cpp

// Sorting strings in a vector container

#include <iostream>                         // For standard streams

#include <string>                           // For string types

#include <algorithm>                        // For swap() and copy() functions

#include <vector>                           // For vector (and iterators)

using std::string;

using std::vector;

int main()

{

vector<string> words;                     // Stores words to be sorted

words.reserve(10);                        // Allocate some space for elements

std::cout << "Enter words separated by spaces. Enter Ctrl+Z on a separate line to end:"

<< std::endl;

std::copy(std::istream_iterator<string> {std::cin}, std::istream_iterator<string> {},

std::back_inserter(words));

std::cout << "Starting sort." << std::endl;

bool out_of_order {false};                // true when values are not in order

auto last = std::end(words);

while (true)

{

for (auto first = std::begin(words) + 1; first != last; ++first)

{

if (*(first - 1) > *first)

{ // Out of order so swap them

std::swap(*first, *(first - 1));

out_of_order = true;

}

}

if (!out_of_order)                      // If they are in order (no swaps necessary)...

break;                                // ...we are done...

out_of_order = false;                   // ...otherwise, go round again.

}

// Output the sorted vector

std::cout << "your words in ascending sequence:" << std::endl;

std::copy(std::begin(words), std::end(words),

std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

// Create a new vector by moving elements from words vector

vector<string> words_copy {std::make_move_iterator(std::begin(words)),

std::make_move_iterator(std::end(words)) };

std::cout << "\nAfter moving elements from words, words_copy contains:" << std::endl;

std::copy(std::begin(words_copy), std::end(words_copy),

std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

// See what’s happened to elements in words vector...

std::cout << "\nwords vector has " << words.size() << " elements\n";

if (words.front().empty())

std::cout << "First element is empty string object." << std::endl;

std::cout << "First element is \"" << words.front() <<  "\"" << std::endl;

}

以下是一些输出示例:

Enter words separated by spaces. Enter Ctrl+Z on a separate line to end:

one two three four five six seven eight

^Z

Starting sort.

your words in ascending sequence:

eight five four one seven six three two

After moving elements from words, words_copy contains:

eight five four one seven six three two

words vector has 8 elements

First element is empty string object.

First element is ""

这个程序使用流迭代器将标准输入流中的单词作为string对象读入到vector容器中。可以输入任意数量的单词。必要时容器会自动膨胀。调用容器的reserve()为这里的 10 个元素分配内存。总是粗略地分配内存或可能需要的元素数量是一个好主意;这将最小化小增量分配空间的开销。back_inserter()创建一个back_insert_iterator,它调用容器的push_back()成员来添加每个string对象作为一个新元素。

copy()算法的前两个参数是输入流迭代器,第二个是流尾迭代器。当从键盘输入Ctrl+Z时,流迭代器将与此匹配,这对应于文件流的文件尾(EOF)。

vector中的元素进行排序的代码演示了迭代器的使用。你将在本书的后面看到有一个sort()算法可以用一条语句完成这项工作。这里的排序方法是一个简单的冒泡排序,它重复遍历要排序的元素。在每一遍中,如果相邻的元素没有按顺序排列,它们将被交换。由algorithm头中的模板定义的swap()函数将有效地交换任何类型的元素。如果完整地遍历了所有不需要交换的元素,则这些元素按升序排列。外部循环是一个由迭代器控制的for循环。first的初始值是begin(words)+1,它是一个迭代器,指向vector中的第二个元素。从第二个元素开始确保在比较连续的元素时使用first-1总是合法的。当first递增以匹配对应于end(words)的迭代器时,每次循环结束。

通过使用copy()算法将元素传输到输出流迭代器来显示words向量的排序内容。要传输的范围由begin()end()返回的迭代器指定,因此所有的元素都将被输出。ostream_iterator构造函数的参数是数据要去的流和每个输出值后面要写的分隔符字符串。

main()中的最后一段代码演示了如何使用移动迭代器以及对被移动的源元素的影响。您可以从输出中看到,在操作之后,words中的元素以包含空字符串的string对象结束;移动元素留下了对应于无参数string构造函数创建的对象。不过一般来说,移动一个类对象元素会使该元素处于不确定状态,所以不应该使用它。

main()中进行排序的代码并不真正依赖于存储元素的容器。它只要求要排序的数据由支持排序方法所用操作的迭代器指定。如果我暂时忽略 STL 有一个比我能想到的任何东西都好得多的sort()函数模板,我可以定义我们自己的函数模板来对满足排序要求的任何类型的元素进行排序:

template<typename RandomIter>

void bubble_sort(RandomIter start, RandomIter last)

{

std::cout << "Starting sort." << std::endl;

bool out_of_order {false};                // true when values are not in order

while (true)

{

for (auto first = start + 1; first != last; ++first)

{

if (*(first - 1) > *first)

{ // Out of order so swap them

std::swap(*first, *(first - 1));

out_of_order = true;

}

}

if (!out_of_order)                      // If they are in order (no swaps necessary)...

break;                                // ...we are done...

out_of_order = false;                   // ...otherwise, go round again.

}

}

模板类型参数是迭代器类型。由于for循环中迭代器上的算术运算,bubble_sort()算法需要随机访问迭代器。这个算法将对任何可以提供随机访问迭代器的容器的内容进行排序;这包括标准数组和string对象。如果在前面的例子中在main()之前插入模板的代码,可以用下面的语句替换main()中对words向量进行排序的代码:

bubble_sort(std::begin(words), std::end(words));  // Sort the words array

为只使用迭代器就能实现的操作定义函数模板,使得它们的使用非常灵活。您可以定义在某个范围上操作的任何算法都可以类似地创建。使用bubble_sort()模板的完整程序在代码下载中被命名为Ex2_02A

向向量容器中添加新元素

请记住,向容器添加元素的唯一方法是调用函数成员。非成员函数不能在不调用容器的函数成员的情况下添加或删除元素;这意味着容器对象必须以某种方式可以从函数中访问,以允许这样做——迭代器是不够的。

追加元素

您可以使用容器对象的push_back()函数将元素添加到序列的末尾。例如:

std::vector<double> values;

values.push_back(3.1415926);           // Add an element to the end of the vector

push_back()函数在现有元素的末尾添加一个新元素,该元素的值作为参数传递——在本例中为3.1415926。由于这里没有现有的元素,这将是第一个;如果没有为容器调用reserve(),这将导致内存被分配。有第二个版本的push_back()带有一个右值引用参数。这为添加元素提供了移动操作。例如:

std::vector<std::string> words;

words.push_back(string("adiabatic"));  // Move string("adiabatic") into the vector

push_back()的参数在这里是一个临时对象,所以它将调用带有右值引用参数的版本。当然,您可以将操作写成:

words.push_back("adiabatic");          // Move string("adiabatic") into the vector

编译器将安排用"adiabatic"作为初始值创建string对象参数,该对象将像以前一样被移入vector

有一种更好的方法来添加新元素。emplace_back()成员比push_back()更有效率。这个片段说明了为什么:

std::vector<std::string> words;

words.push_back(std::string("facetious"));      // Calls string constructor & moves the string object

words.emplace_back("abstemious");               // Calls string constructor to create element in place

emplace_back()函数的参数是构造函数将对象追加到容器中所需的参数。通过使用您提供的一个或多个参数调用对象类型的构造函数,emplace_back()成员在容器中就地创建对象,从而消除了在这种情况下push_back()将执行的对象移动操作。您可以在emplace_back()成员调用中指定对象构造函数所需的任意多个参数。这里有一个关于emplace_back()的例子:

std::string str {"alleged"};

words.emplace_back(str, 2, 3);       // Create string object corresponding to "leg" in place

emplace()函数将调用string构造函数,该构造函数接受指定的三个参数,以创建追加到words序列中的对象。这个构造函数创建一个string对象,包含以索引 2 处的字符开始的str的三个字符的子串。

插入元素

您可以使用emplace()函数成员在vector序列的内部插入一个新元素。对象是就地创建的,而不是在单独的步骤中创建对象,然后将其作为参数传递。emplace()的第一个参数是一个迭代器,它指定了创建对象的位置。对象将被插入到迭代器指定的元素之前。第二个和任何后续参数被传递给要插入的元素的构造函数。例如:

std::vector<std::string> words {"first", "second"};

// Inserts string(5, 'A') as 2nd element

auto iter = words.emplace(++std::begin(words), 5, 'A');

// Inserts string("&&&&") as 3rd element

words.emplace(++iter, "&&&&");

执行这些语句后,vector元素将成为以下对象的string:

"first" "AAAAA" "&&&&" "second"

对于创建要插入的对象的构造函数调用,您可以根据需要向第一个参数后面的emplace()提供任意多的参数。上面代码片段中对emplace()成员的第一次调用创建了从string(5, 'A')构造函数调用得到的string对象。emplace()函数返回一个指向被插入元素的迭代器,在下面的语句中使用这个迭代器来插入另一个对象,该对象位于被插入的前一个对象之后。

insert()函数成员可以在vector中插入一个或多个元素。第一个参数总是指向插入点的const或非const迭代器。一个或多个元素被直接插入到第一个参数指向的元素之前,除非它是一个反向迭代器,在这种情况下,元素被直接插入到插入点之后。你被insert()成员的选择宠坏了,所以我会用单独的语句来说明每一种可能性。我将首先定义一个vector,随后的insert()函数调用列表将依次应用到这个函数:

std::vector<std::string> words {"one", "three", "eight"};          // Vector with 3 elements

对于wordsinsert()成员,您可以选择的选项有:

Insert a single element that is specified by the second argument: auto iter = words.insert(++std::begin(words), "two");

在这个例子中,通过递增由begin()返回的迭代器来指定插入点。这对应于第二个元素,因此新元素将作为新的第二个元素插入到它之前。从第二个到最后一个的原始元素都将被移动一个位置,以便为新元素腾出空间。有两个插入单个对象的insert()重载,一个带有类型为const T&的第二个参数,另一个带有类型为T&&的第二个参数——一个右值引用。因为上面的第二个参数是一个临时对象,所以将调用这些重载中的第二个,并将移动临时对象,而不是复制它。

执行该语句后,words向量包含代表以下内容的string元素:

"one" "two" "three" "eight"

返回的迭代器指向插入的元素string("two")。注意,这个insert()调用不如使用相同参数调用emplace()有效。通过insert()调用,执行构造函数调用string("two")来创建对象,然后将该对象作为第二个参数传递。对于emplace(),第二个参数用于在容器中构建string对象。

Insert a sequence of elements that is specified by iterators that are the second and third arguments: std::string more[] {"five", "six", "seven"};      // Array elements to be inserted iter = words.insert(--std::end(words), std::begin(more), std::end(more));

第二条语句中的插入点是通过递减end()返回的迭代器获得的。这对应于最后一个元素,因此新元素将在此之前插入。执行这条语句后,words向量包含的string对象为:

"one" "two" "three" "five" "six" "seven" "eight"

返回的迭代器指向插入的第一个元素"five"

Insert an element at the end of the vector: iter = words.insert(std::end(words), "ten");

插入点在最后一个元素之后,因此新元素将追加到最后一个元素之后。执行此语句后,words向量包含用于以下目的的string对象:

"one" "two" "three" "five" "six" "seven" "eight" "ten"

返回的迭代器指向插入的元素"ten"。这与上面第 1 点的过载相同;这只是说明了当第一个参数不是指向一个元素,而是指向最后一个元素之后的一个元素时,它是有效的。

Insert multiples of a single element at the insertion point. The second argument is the number of times the object specified by the third argument is to be inserted: iter = words.insert(std::cend(words)-1, 2, "nine");

插入点是最后一个元素,因此新元素的两个副本string("nine")将被插入到最后一个元素之前。执行这条语句后,words向量包含了用于以下目的的string对象:

"one" "two" "three" "five" "six" "seven" "eight" "nine" "nine" "ten"

返回的迭代器指向插入的第一个元素"nine"。注意,例子中的第一个参数是一个const迭代器,只是为了说明一个const迭代器也可以工作。

Insert elements specified by an initializer list at the insertion point. The second argument is the initializer list of elements to be inserted: iter = words.insert(std::end(words), {std::string {"twelve"},                                       std::string {"thirteen"}});

插入点在最后一个元素之后,所以初始化列表中的元素被追加。执行此语句后,words向量包含用于以下目的的string对象:

"one" "two" "three" "five" "six" "seven" "eight" "nine" "nine" "ten" "twelve" "thirteen"

返回的迭代器指向插入的第一个元素,"twelve."初始化列表中的值必须匹配容器元素的类型。类型为T的初始值设定项列表属于类型std::initializer_list<T>,所以这里的列表属于类型std::initializer_list<std::string>。在前面的insert()调用中,文字作为参数出现,参数类型是std::string,因此文字将被用作初始化传递给函数的string对象的值。

请记住,除了在vector末尾的所有插入都会产生开销。插入点之后的所有元素都必须被混洗,以便为新元素腾出空间。当然,如果插入后的元素数量大于容量,就需要分配更多的内存,这会进一步增加开销。

vectorinsert()成员希望您使用标准迭代器来指定插入点;反向迭代器不会被接受——它不会被编译。当你想找到一个给定对象在一个序列中最后一次出现的位置,并在它旁边插入一个新元素时,就需要使用反向迭代器。反向迭代器的base()函数成员会有所帮助。这里有一个例子:

std::vector<std::string> str {"one", "two", "one", "three"};

auto riter = std::find(std::rbegin(str), std::rend(str), "one");

str.insert(riter.base(), "five");

find()算法在前两个参数指定的范围内搜索与第三个参数匹配的第一个元素,所以它在寻找string("one")。它返回一个你用来指定范围的迭代器。结果将是一个反向迭代器,指向找到的元素,如果没有找到,则指向第一个元素rend(str)之前的元素。使用反向迭代器意味着搜索找到最后一个匹配的元素;使用标准迭代器可以找到第一个匹配项,如果没有找到,则返回end(str)。调用riterbase()函数成员,反向返回对应于iter之前位置的标准迭代器,即朝向序列的末尾。由于riter将指向包含"one", riter.base()的第三个元素,将指向包含"three"的第四个元素。使用riter.base()作为insert()的第一个参数会导致"five"被插入到该位置之前,也就是在riter指向的元素之后。执行这些语句后,str将包含这五个string元素:

"one", "two", "one", "five", "three"

如果您希望插入在由find()返回的位置之前,您可以将insert()的第一个参数指定为iter.base()-1

删除元素

正如我所说的,您只能通过调用容器对象的函数成员来从容器中删除元素。您可以通过调用clear()成员来移除vector对象中的所有元素。例如:

std::vector<int> data(100, 99);        // Contains 100 elements initialized to 99

data.clear();                          // Remove all elements

第一条语句创建了一个包含 100 个类型为int的元素的vector对象,因此大小为100,容量为100;所有的元素都被初始化为99。第二条语句删除了所有元素,因此大小为0;容量未因操作而改变,因此仍为100

您可以通过调用pop_back()函数来删除vector对象中的最后一个元素。例如:

std::vector<int> data(100, 99);        // Contains 100 elements initialized to 99

data.pop_back();                       // Remove the last element

第二条语句删除了最后一个元素,因此data的大小将是99,容量保留为100

只要您不关心元素的顺序,能够删除最后一个元素就提供了一种移除任何元素的方法,而无需将元素混在一起。假设您想从 vector 中删除第二个元素data。你可以这样做:

std::swap(std::begin(data)+1, std::end(data)-1); // Interchange 2nd element with the last

data.pop_back();                                 // Remove the last element

第一条语句调用在algorithm头和utility头中定义的swap()模板函数。这交换了第二个和最后一个元素。然后调用pop_back()移除最后一个元素,也就是第二个元素,从而将其从容器中删除。

Note

一个vector容器有一个swap()函数成员。这将把调用该函数的容器中的元素与作为参数传递的vector容器中的元素进行交换。显然,两个vector容器必须存储相同类型的元素。全局swap()函数也将交换作为参数传递的两个vector容器的元素。

如果您不需要容器中的额外容量——例如,因为您不会添加更多的元素,您可以通过调用shrink_to_fit()成员来消除它:

data.shrink_to_fit();                     // Reduce the capacity to that needed for elements

这是否有效取决于你的 STL 的实现。如果它真的工作了,任何还在的vector的迭代器可能会失效,所以最好在这个操作之后获得新的迭代器。

您可以调用vectorerase()函数成员来删除一个或多个元素。要删除单个元素,需要提供单个迭代器参数,例如:

auto iter = data.erase(std::begin(data)+1);      // Delete the second element

删除元素后,vector的大小将减少 1;产能不变。返回的迭代器指向被移除元素后面的元素。这里的值将对应于表达式std::begin(data)+1;如果它是被移除的最后一个元素,那么返回的迭代器将是std::end(data)

要消除一个元素序列,需要提供两个迭代器参数来定义要删除的元素范围。例如:

// Delete the 2nd and 3rd elements

auto iter = data.erase(std::begin(data)+1, std::begin(data)+3);

不要忘记——范围规范中的第二个迭代器指向范围中最后一个元素之后的一个迭代器。这将擦除位置std::begin(data)+1std::begin(data)+2处的元素。返回的迭代器指向被删除元素之后的元素,所以如果最后一个元素被删除,它将是std::begin(data)+1std::end(data)

由在algorithm头中定义的模板产生的remove()算法从匹配特定值的范围中删除元素。这里有一个例子:

std::vector<std::string> words { "one", "none", "some", "all", "none", "most", "many"};

auto iter = std::remove(std::begin(words), std::end(words), "none");

第二条语句从前两个参数指定的范围中删除第三个参数remove()的所有出现,该参数将是string("none")。移除元素有点误导。remove()是一个全局函数,所以它不能从容器中删除元素。remove()删除元素的方式类似于从字符串中删除空格的过程——通过从右边复制元素来覆盖匹配第三个参数的元素。图 2-4 说明了这是如何工作的。

A978-1-4842-0004-9_2_Fig4_HTML.gif

图 2-4。

How the remove( ) algorithm works

如果您在remove()操作之后输出words中的元素,那么只会显示前五个元素。然而,通过调用size()vector返回的值仍然是 7,所以最后两个元素仍然存在,但是被空的string对象所取代。为了去掉多余的元素,你必须调用vectorerase()成员,而remove()返回的迭代器可以用来做这件事:

words.erase(iter, std::end(words));              // Remove surplus elements

这被称为删除习惯用法。迭代器iter指向删除后最后一个元素之后的一个元素,因此它标识了要删除的范围中的第一个元素。要删除的范围的终点由std::end(words)给出。当然,您可以删除这些元素,然后在一条语句中删除末尾不需要的元素:

words.erase(std::remove(std::begin(words), std::end(words), "none"), std::end(words));

remove()算法返回的迭代器成为erase()的第一个参数;erase()的第二个参数是指向容器中最后一个元素之外的迭代器。

了解如何为一个vector容器分配额外的容量,将会让您了解产生开销的频率,以及可以分配的内存量。这里有一个例子可以让你深入了解这一点:

// Ex2_03.cpp

// Understanding how capacity is increased in a vector container

#include <iostream>                             // For standard streams

#include <vector>                               // For vector container

int main()

{

std::vector <size_t> sizes;                    // Record numbers of elements

std::vector <size_t> capacities;               // and corresponding capacities

size_t el_incr {10};                           // Increment to initial element count

size_t incr_count {4 * el_incr};               // Number of increments to element count

for (size_t n_elements {}; n_elements < incr_count; n_elements += el_incr)

{

std::vector<int> values(n_elements);

std::cout << "\nAppending to a vector with " << n_elements << " initial elements:\n";

sizes.push_back(values.size());

size_t space {values.capacity()};

capacities.push_back(space);

// Append elements to obtain capacity increases

size_t count {};                             // Counts capacity increases

size_t n_increases {10};

while (count < n_increases)

{

values.push_back(22);                      // Append a new element

if (space < values.capacity())             // Capacity increased...

{                                          // ...so record size and capacity

space = values.capacity();

capacities.push_back(space);

sizes.push_back(values.size());

++count;

}

}

// Show sizes & capacities when increments occur

std::cout << "Size/Capacity: ";

for (size_t i {}; i < sizes.size(); ++i)

std::cout << sizes.at(i) << "/" << capacities.at(i) << "  ";

std::cout << std::endl;

sizes.clear();                               // Remove all elements

capacities.clear();                          // Remove all elements

}

}

本例中的操作非常简单。元素被追加到一个vector容器中,直到容量必须增加,在这种情况下,该点的大小和容量被记录在sizescapacities容器中。对于具有不同初始元素计数的容器,重复这一过程。用我的编译器,我得到了这样的输出:

Appending to a vector with 0 initial elements:

Size/Capacity: 0/0  1/1  2/2  3/3  4/4  5/6  7/9  10/13  14/19  20/28  29/42

Appending to a vector with 10 initial elements:

Size/Capacity: 10/10  11/15  16/22  23/33  34/49  50/73  74/109  110/163  164/244  245/366  367/549

Appending to a vector with 20 initial elements:

Size/Capacity: 20/20  21/30  31/45  46/67  68/100  101/150  151/225  226/337  338/505  506/757  758/1135

Appending to a vector with 30 initial elements:

Size/Capacity: 30/30  31/45  46/67  68/100  101/150  151/225  226/337  338/505  506/757  758/1135  1136/1702

编译器的输出可能会有所不同,这取决于用来增加vector容量的算法。您可以从第一组输出中看到,当您从空的vector开始时,分配更多内存的需求非常频繁,因为容量增量很小——开始时只有一个元素的内存。另一组输出显示容量增量与容器的大小有关。每次分配都是针对当前元素数量的额外 50%。这意味着在选择初始大小时,如果可以的话,需要多加小心。

假设您最初创建了一个容量为 1000 个元素的vector,实际上您存储了 1001 个元素。您将拥有 499 个元素的过剩容量。如果元素是数值或不占用太多空间的对象,这并不重要。另一方面,如果对象很大,比如说每个对象 10 千字节,那么你的程序将会分配几乎 5 兆字节的内存,而这些内存并没有被使用。因此,您可以推断,稍微高估一点vector的初始大小总是更好,而不是低估它。

当然,您可以自己管理额外内存的分配。如果您比较大小和容量,您可以在必要时通过调用容器的reserve()来增加内存。例如:

std::vector <size_t> junk {1, 2, 3};

for(size_t i {} ; i<1000 ; ++i)

{

if(junk.size() == junk.capacity())            // When the size has reached the capacity...

junk.reserve(junk.size()*13/10);            // ...increase the capacity

junk.push_back(i);

}

这里的容量增加最多为 30%,而不是默认的 50%。容量增量不必是当前大小的百分比。例如,您可以将reserve()的参数指定为junk.capacity()+10,以使容量增加 10 个元素,而不管当前大小。不要忘记,当reserve()增加容量时,容器的现有迭代器不再有效。

向量容器

vector<bool>vector<T>模板的专门化,它为bool类型的元素提供了更有效的内存使用。这是如何实现的由实现定义,但是通常bool元素被存储为单个比特。如果没有专门化,vector中的bool元素通常每个占用一个字节,但也可能更多——这是一个实现选择。bool值的序列不一定存储在连续的内存位置,所以没有data()函数成员。vector<bool>专门化的一些函数成员的操作与通用模板实例略有不同。bool当值被打包为一个字中的位时,它们是不可直接寻址的,因此来自front()back()的返回值不是bool&引用,而是对代表序列中第一个和最后一个值的代理对象的引用。

当您想要处理bool值并知道您想要存储多少值时,在bitset头中定义的bitset<N>类模板是一个比vector<bool>更好的选择。模板参数是位数。这不是一个容器——例如,没有迭代器,但是一个bitset实例提供了一系列vector<bool>没有的位集操作。因为他们的应用程序更专业,所以我不会进一步讨论vector<bool>bitset<N>

使用 deque 容器

一个deque<T>是在deque头中定义的容器模板,它创建了被组织为类型为T的元素的双端队列的容器。与vector容器相比,它的优势在于您可以在序列的开头和结尾有效地添加或删除对象,因此当您需要这种能力时,您可以选择这种类型的容器。每当应用程序涉及先进先出事务处理时,您都会使用deque容器。诸如处理数据库事务或模拟超市结账队列的应用程序可以使用deque容器。

创建队列容器

如果使用默认构造函数创建一个deque容器,该容器没有元素,因此添加第一个元素将导致分配内存:

std::deque<int> a_deque;         // A deque container with no elements

您创建一个具有给定数量元素的deque容器,其方式基本上与vector相同:

std::deque<int> my_deque(10);    // A deque container with 10 elements

一个名为my_dequedeque容器存储了int类型的元素,如图 2-5 所示;在这个容器中,奇整数被存储在元素中。

A978-1-4842-0004-9_2_Fig5_HTML.gif

图 2-5。

An example of a deque container

当您创建一个具有指定数量元素的deque时,每个元素都将是所存储类型的默认值,因此之前对my_deque的定义最初将包含所有元素 0。如果创建一个具有给定数量的string元素的deque,每个元素将通过调用string()构造函数来初始化。

你也可以创建一个deque并使用初始化列表初始化它:

std::deque<std::string> words { "one", "none", "some", "all", "none", "most", "many" };

这个容器将有七个使用初始化列表中的文字创建的字符串元素。当然,您可以将初始化列表中的对象指定为string("one")string("none")等。

还有一个用于deque容器的复制构造函数,它创建一个现有容器的副本:

std::deque<std::string> words_copy { words };         // Makes a copy of the words container

当你创建一个deque时,你也可以使用一个由两个迭代器标识的范围来初始化它:

std::deque<std::string> words_part { std::begin(words), std::begin(words) + 5 };

这个容器将有五个元素与words容器的前五个元素相同。当然,初始值的范围可以来自任何种类的容器——不一定是deque。一个deque提供了随机访问迭代器,你可以像获得一个vector容器一样获得一个deque容器的const和非const迭代器以及反向迭代器。

访问元素

您可以使用下标操作符访问deque容器中的元素。这个操作类似于vector,所以没有对下标的边界检查。一个deque容器中的元素是一个序列,但是以不同于vector的方式存储在内部。元素的组织方式导致一个deque容器的大小总是等于它的容量。因此,没有定义capacity()函数成员——一个deque只有一个size()成员,它将当前大小作为成员类型size_type的无符号整数返回。与vector相比,较慢的操作也是deque容器不同内部组织的结果。

您可以使用下标操作符访问元素,但是索引不进行边界检查。要使用经过边界检查的索引来访问元素,可以使用at()成员函数,就像使用vector一样:

std::cout << words.at(2) << std::endl;           // Output the third element in words

该参数必须是类型为size_t的值,因此不能小于 0。如果at()的参数不在范围内,当它大于words.size()-1时,就会抛出std::out_of_range异常。

front()back()函数成员也以与vector相同的方式工作,然而,deque没有data()成员函数,因为元素不是作为数组存储的。一个deque容器提供了与vector相同的三种resize()函数成员,它们的操作完全相同。

添加和删除元素

一个deque容器提供了与一个vector容器相同的push_back()pop_back()成员,用于在序列末尾添加和删除单个元素,它们以相同的方式工作。一个deque容器也有push_front()pop_front()功能成员,用于序列开始时的类似操作。例如:

std::deque<int> numbers {2, 3, 4};

numbers.push_front(11);            // numbers contains 11 2 3 4

numbers.push_back(12);             // numbers contains 11 2 3 4 12

numbers.pop_front();               // numbers contains 2 3 4 12

除了一个vector提供的emplace_back()成员外,deque还有一个emplace_front()成员,用于在序列开始时创建一个新元素。与vector一样,你可以使用emplace()insert()deque的内部添加或删除元素;该过程相对较慢,因为它总是需要移动现有的元素。

我为一个vector描述的所有insert()函数成员也可用于一个deque容器。在deque的任何地方插入元素都会使deque的所有现有迭代器失效,因此您必须重新创建它们。dequeerase()成员也以与vector相同的方式工作。为deque容器调用clear()会移除所有元素。

替换队列容器的内容

dequeassign()函数成员替换所有现有元素。有三个版本;新内容可以由初始化列表指定,新内容可以是由迭代器指定的范围,或者新内容可以是指定对象的多个副本。下面是如何用初始化列表指定的元素替换deque容器的内容:

std::deque<std::string> words {"one", "two", "three", "four"};

auto init_list = {std::string{"seven"}, std::string{"eight"}, std::string{"nine"}};

words.assign(init_list);

最后一条语句用init_list中的string对象替换了words中的元素。注意,在这里你不能把文字放在初始化列表中。如果这样做,那么init_list的类型将被推导为initializer_list<const char*>,而assign()需要一个类型为initializer_list<string>的参数,所以代码不会被编译。当然,不必单独定义init_list。你可以调用assign()并在参数中定义初始化列表,就像这样:

words.assign({"seven", "eight", "nine"});

因为wordsassign()成员需要一个initializer_list<string>类型的参数,编译器将安排使用文字创建一个这种类型的初始化列表。要将一系列元素分配给一个deque容器,需要提供两个迭代器参数:

std::vector<std::string> wordset {"this", "that", "these", "those"};

words.assign(std::begin(wordset)+1, --std::end(wordset));  // Assigns "that" and "these"

函数只需要输入迭代器,所以任何种类的迭代器都可以。最后一种可能性是用对象的重复来替换内容:

words.assign(8, "eight");                       // Assign eight instances of string("eight")

第一个参数是第二个参数的实例数,用于替换容器的当前内容。

vector容器提供了相同的一组assign()函数成员,因此您可以用一组新的成员替换vector中的元素。

您也可以使用赋值操作符来替换赋值左边的deque容器的内容。赋值的右操作数必须是相同类型的容器,或者是初始值设定项列表。这个操作也由vector容器支持。下面的例子演示了如何给一个deque分配一组新的元素;

std::deque<std::string> words {"one", "two", "three", "four"};

std::deque<std::string> other_words;

other_words = words;                             // other_words same contents as words

words = {"seven", "eight", "nine"};              // words contents replaced

在执行这些语句后,other_words将包含与words中的原始序列相同的元素,而words将包含从初始化列表中的文字创建的string对象。分配后,容器的大小将反映分配的元素数量。将一组新的元素分配给vector(来自相同类型的vector或初始化列表)将导致vector的容量与新的大小相同。

下面是一个使用deque容器的完整例子:

// Ex2_04.cpp

// Using a deque container

#include <iostream>                              // For standard streams

#include <algorithm>                             // For copy()

#include <deque>                                 // For deque container

#include <string>                                // For string classes

#include <iterator>                              // For front_insert_iterator & stream iterators

using std::string;

int main()

{

std::deque<string> names;

std::cout << "Enter first names separated by spaces. Enter Ctrl+Z on a new line to end:\n";

std::copy(std::istream_iterator<string> {std::cin}, std::istream_iterator<string> {},

std::front_inserter(names));

std::cout << "\nIn reverse order, the names you entered are:\n";

std::copy(std::begin(names), std::end(names), std::ostream_iterator<string>{std::cout, "  "});

std::cout << std::endl;

}

以下是一些示例输出:

Enter first names separated by spaces. Enter Ctrl+Z on a new line to end:

Fred Jack Jim George Mary Zoe Rosie

^Z

In reverse order, the names you entered are:

Rosie  Zoe  Mary  George  Jim  Jack  Fred

这个程序读取一系列任意长度的字符串,并将它们存储在names容器中。对于front_inserter()函数返回的names容器,copy()算法通过将istream_iterator<string>迭代器获得的序列复制到front_insert_iterator来执行输入。copy()的第一个参数是输入的开始迭代器,第二个参数是相应的结束迭代器。当你在键盘上输入Ctrl+Z时,输入迭代器将对应结束迭代器;如果数据是从文件流中读取的,那么当到达EOF时就会产生结束迭代器。我们可以使用一个front_insert_iterator,因为一个deque容器有一个push_front()成员,它将一个元素添加到序列的开头;front_insert_iterator的工作方式是调用容器的push_front()来添加每个元素,因此它适用于任何有push_front()成员的容器。

输出也是通过调用copy()算法产生的。前两个参数是迭代器,标识要复制到第三个参数标识的目的地的元素范围。前两个参数是deque容器的开始和结束迭代器,因此所有元素都被复制。目的地是一个接受string对象并将它们写入标准输出流的ostream_iterator

使用列表容器

list头中定义的list<T>容器模板实现了一个类型为T的对象的双向链表。这比vectordeque容器有优势,你可以在固定时间内在序列中的已知位置插入或删除元素。这个优势是使用list容器而不是vectordeque容器的主要动机。缺点是你不能通过元素在序列中的位置直接访问它——换句话说,没有元素的索引。要访问列表中的内部元素,必须从一个元素遍历到下一个元素,通常从第一个或最后一个元素开始。图 2-6 显示了列表容器中的元素在概念上是如何构成的。

A978-1-4842-0004-9_2_Fig6_HTML.gif

图 2-6。

Organization of elements in a list<T> container

一个list<T>容器中的每个T对象通常封装在一个内部节点对象中,该对象维护指向list中的前一个节点和下一个节点的指针。这些指针将一个链中的节点连接在一起,并允许通过简单地跟随指针从任何位置以任何方向遍历元素链。指向第一个元素的前一个元素的指针将是nullptr,因为没有元素,而指向最后一个元素的下一个指针也将是nullptr。这些使得链的末端能够被检测到。一个list<T>实例记录指向第一个和最后一个节点的指针。这使得可以访问任一端的对象,并允许从任一端开始按顺序检索整个元素列表。

用与其他序列容器相同的方式获得一个list容器的迭代器。因为不能随机访问list中的元素,所以得到的迭代器是双向迭代器。用一个list参数调用begin()会返回一个指向第一个元素的迭代器;通过调用end(),你得到的迭代器指向最后一个元素之后的一个元素,因此整个元素范围的指定方式与其他序列容器完全相同。您还可以通过调用rbegin()rend()crbegin()crend()cbegin(),cend()来获得反向迭代器和const迭代器,就像您在其他容器中看到的那样。

创建列表容器

一个list容器的构造函数的范围类似于一个vector或者一个deque容器。该语句创建一个空列表:

std::list<std::string> words;

您还可以使用给定数量的默认元素创建一个列表:

std::list<std::string> sayings {20};             // A list of 20 empty strings

元素的数量由构造函数的参数指定,每个元素都是通过调用存储的元素类型的默认构造函数来创建的,所以元素是通过在这里调用string()来创建的。

下面是如何创建包含给定数量的相同元素的列表:

std::list<double> values(50, 3.14149265);

这会创建一个包含 50 个类型为double的值的列表,每个值都等于π的值。注意这里的括号;你不能使用初始化列表——如果你使用{50, 3.14159265},list将只包含两个元素。

列表容器有一个复制构造函数,因此您可以创建现有列表容器的副本:

std::list<double> save_values {values};          // Duplicate of values

您还可以构造一个列表,用您以通常方式指定的另一个序列中的元素初始化——通过 begin 和 end 迭代器:

std::list<double> samples {++cbegin(values), -cend(values)};

这从values列表的内容中创建了一个list,省略了第一个和最后一个元素。因为由listbegin()end()函数返回的迭代器是双向的,所以不能加减整数值。修改双向迭代器的唯一方法是使用递增或递减运算符。当然,上面语句中初始化列表中的迭代器可以代表任何容器中的一个范围,而不仅仅是另一个list

您可以通过调用size()成员来获得list容器中的元素数量。你也可以通过调用它的resize()函数来改变元素的数量。如果resize()的参数小于元素数,元素将从末尾删除;如果参数更大,将使用存储的元素类型的默认构造函数添加元素。

添加元素

通过调用push_front()成员,可以将元素添加到list的开头。为一个list对象调用push_back()会在list的末尾添加一个元素。在这两种情况下,参数都是要添加的对象。例如:

std::list<std::string> names {"Jane", "Jim", "Jules", "Janet"};

names.push_front("Ian");               // Add string("Ian") to the front of the list

names.push_back("Kitty");              // Append string("Kitty") to the end of the list

这两个函数都有右值引用参数版本,它们将移动实参,而不是将其复制到新元素中。这些显然比带有左值引用参数的版本更有效。然而,emplace_front()emplace_back()成员做得更好:

names.emplace_front("Ian");        // Create string("Ian") in place at the front of the list

names.emplace_back("Kitty");       // Create string("Kitty") in place at the end of the list

这些成员的参数是构造函数的参数,该构造函数将被调用以就地创建元素。这些消除了右值版本的push_front()push_back()必须执行的移动操作的需要。

您可以使用insert()函数成员向list的内部添加元素,它有三个版本,就像其他序列容器一样。第一个版本在迭代器指定的位置插入一个新元素:

std::list<int> data(10, 55);                     // List of 10 elements with value 55

data.insert(++begin(data), 66);                  // Insert 66 as the second element

insert()的第一个参数是指定插入点的迭代器,第二个参数是要插入的元素。递增由begin()返回的双向迭代器使其指向第二个元素。执行此操作后,list的内容将会是:

55 66 55 55 55 55 55 55 55 55 55

list现在包含 11 个元素。插入元素不需要移动任何现有的元素。在创建新元素之后,这个过程只需要适当地设置 4 个指针。第一个元素的下一个指针被更改为指向新元素;来自原始第二元素的先前指针被改变为指向新元素;新元素的前一个指针将被设置为指向第一个元素,其下一个指针将指向序列中第二个原始元素。与在vectordeque中插入相比,这个过程非常快,并且无论新元素插入到哪里,都将花费相同的时间。

您可以在给定位置插入同一元素的多个副本:

auto iter = begin(data);

std::advance(iter, 9);                           // Increase iter by 9

data.insert(iter, 3, 88);                        // Insert 3 copies of 88 starting at the 10th

iter将属于list<int>::iterator类型。insert()函数的第一个参数是指定插入位置的迭代器,第二个参数是要插入的元素数量,第三个参数是要重复插入的元素。为了到达第十个元素,使用在iterator头中定义的全局advance()函数将迭代器递增 9。只能递增或递减双向迭代器;不能只加 9,所以advance()函数会在循环中递增迭代器。在执行了前面的片段之后,list的内容将会是:

55 66 55 55 55 55 55 55 55 88 88 88 55 55

现在list包含了 14 个元素。下面是如何将一系列元素插入到data列表中的方法:

std::vector<int> numbers(10, 5);         // Vector of 10 elements with value 5

data.insert(--(--end(data)), cbegin(numbers), cend(numbers));

insert()的第一个参数是指向data中倒数第二个元素位置的迭代器。要插入的来自numbers的序列是由insert()函数的第二个和第三个参数指定的,所以这将把来自vector的所有元素插入到list,从data中的倒数第二个元素位置开始。执行此命令后,data将包含:

55 66 55 55 55 55 55 55 55 88 88 88 5 5 5 5 5 5 5 5 5 5 55 55

list现在包含 24 个元素。在倒数第二个元素位置插入来自numbers的元素会将list中的最后两个元素向右移动。尽管如此,任何指向最后两个元素的迭代器,或者结束迭代器,都不会失效。指向list中元素的迭代器只有在删除它所指向的元素时才会失效。

有三个函数将在list容器中就地构造元素:emplace()在迭代器指定的位置构造元素;emplace_front()在第一个元素之前的list开头构造一个元素;而emplace_back()在最后构造一个元素,跟在最后一个元素后面。下面是一些使用它们的例子:

std::list<std::string> names {"Jane", "Jim", "Jules", "Janet"};

names.emplace_back("Ann");

std::string name("Alan");

names.emplace_back(std::move(name));

names.emplace_front("Hugo");

names.emplace(++begin(names), "Hannah");

第四行代码使用std::move()函数将对name的右值引用传递给emplace_back()函数。执行该操作后,name将为空,因为内容将被移动到list。执行这些语句后,names将包含元素:

"Hugo" "Hannah" "Jane" "Jim" "Jules" "Janet" "Ann" "Alan"

移除元素

一个list容器的clear()erase()函数成员以相同的方式工作,并具有与前一个序列容器相同的效果。一个list容器的remove()成员删除匹配参数的元素。例如:

std::list<int> numbers { 2, 5, 2, 3, 6, 7, 8, 2, 9};

numbers.remove(2);                                    // List is now 5 3 6 7 8 9

第二条语句从numbers中删除所有出现的值 2。

remove_if()函数成员希望您将一元谓词作为参数传递。一元谓词接受元素类型的单个参数或对元素类型的const引用,并返回一个bool值。谓词返回true的所有元素都将被删除。例如:

numbers.remove_if([](int n){return n%2 == 0;});       // Remove even numbers. Result 5 3 7 9

这里的参数是一个 lambda 表达式,但也可以是一个函数对象。

unique()函数成员很有趣。它删除连续的重复元素,只留下两个或多个重复元素中的第一个。例如:

std::list<std::string> words {"one", "two", "two", "two", "three", "four", "four"};

words.unique();                                   // Now contains "one" "two" "three" "four"

这个版本的unique()使用==操作符来比较连续的元素。您可以在对元素排序后应用unique(),以确保从序列中移除所有重复的元素。

重载unique()接受二元谓词作为参数,谓词返回true的元素被视为相等。这提供了一个非常灵活的平等概念。您可以将具有相同长度的字符串视为相等,或者可以将具有相同首字母的字符串视为相等。谓词可以有不同类型的参数,只要对list的迭代器解引用的结果可以隐式地转换为这两种类型。

排序和合并元素

algorithm头中定义的sort()函数模板需要随机访问迭代器。一个list容器不提供随机访问迭代器,只提供双向迭代器,所以你不能对一个list中的元素应用sort()算法。然而,并没有全部丢失,因为list模板定义了它自己的sort()函数成员。它有两个版本:不带参数的sort()成员将list元素按升序排序,第二个版本接受 function 对象或 lambda 表达式作为参数,定义用于比较两个list元素的predicate。一个predicate只是一个接受一个或多个参数并返回一个bool值的函数。有参数的list容器的sort()成员需要一个二元谓词作为参数,这意味着谓词有两个参数。一些算法期望一元谓词,它有一个参数。

下面是一个使用谓词作为参数调用列表的sort()成员的示例:

names.sort(std::greater<std::string>());        // Names in descending sequence

这使用了在functional头中定义的greater<T>模板。该模板定义了一个函数对象,用于比较T类型的对象;如果第一个参数大于第二个参数,operator()()函数成员返回truefunctional头定义了大量定义谓词的模板,您将在本书的后面看到更多。执行排序后,list元素将是:

"Jules" "Jim" "Janet" "Jane" "Hugo" "Hannah" "Ann" "Alan"

所以list中的名字现在是降序排列的。有一个透明版本的greater<T>谓词,您可以这样使用:

names.sort(std::greater<>());                    // Function object uses perfect forwarding

透明函数对象接受任何类型的参数,并使用完全转发来避免不必要的复制。因此,它会更快,因为要比较的参数将被移动,而不是复制。

当然,必要时可以传递自己的函数对象来定义对一个list进行排序的谓词。但是对于自定义对象来说,这并不总是必要的。如果你只是为你的类定义了operator>(),那么你可以继续使用std::greater<>。当您想要类型的默认比较之外的东西时,可能会需要 function 对象。例如,假设您想要对names列表中的元素进行排序,但是不是对string对象进行标准的大于号比较,而是想要具有相同初始字符的字符串按长度排序。您可以将该类定义为:

// Order strings by length when the initial letters are the same

class my_greater

{

public:

bool operator()(const std::string& s1, const std::string& s2)

{

if (s1[0] == s2[0])

return s1.length() > s2.length();

else

return s1 > s2;

}

};

您可以用它来排序names容器的原始内容:

names.sort(my_greater());                       // Sort using my_greater

执行完这个之后,list将包含:

"Jules" "Janet" "Jane" "Jim" "Hannah" "Hugo" "Alan" "Ann"

这与之前对string对象使用标准比较的结果明显不同。首字母相同的名字现在按长度降序排列。当然,如果不需要重用my_greater谓词,可以使用 lambda 表达式来获得相同的结果。下面是实现这一点的语句:

names.sort([](const std::string& s1, const std::string& s2)

{ if (s1[0] == s2[0])

return s1.length() > s2.length();

else

return s1 > s2;

});

这与前面的语句完全相同。

listmerge()函数成员期望另一个list容器作为具有相同类型元素的参数。两个容器中的元素必须按升序排列。参数list中的元素与当前list中的元素合并。例如:

std::list<int> my_values {2, 4, 6, 14};

std::list<int> your_values{ -2, 1, 7, 10};

my_values.merge(your_values);                    // my_values contains: -2 1 2 4 6 7 10 14

your_values.empty();                             // Returns true

元素从your_values转移到my_values,没有复制,所以操作后your_values将不包含任何元素。元素的转移是通过改变list中每个节点的指针来实现的,这是将它们链接到当前容器中适当位置的元素的参数。list节点完全停留在它们在内存中的位置;只是链接它们的指针发生了变化。在合并过程中,使用operator<()比较两个容器中的元素。merge()函数的重载有第二个参数,您可以为其提供一个将在合并操作中使用的比较函数。例如:

std::list<std::string> my_words {"three", "six", "eight"};

std::list<std::string> your_words {"seven", "four", "nine"};

auto comp_str = [](const std::string& s1, const std::string& s2){ return s1[0]<s2[0]; };

my_words.sort(comp_str);                      // "eight" "six" "three"

your_words.sort(comp_str);                    // "four" "nine" "seven"

my_words.merge(your_words, comp_str);         // "eight" "four" "nine" "six" "seven" "three"

这里的string对象的比较函数是由一个只考虑第一个字符的 lambda 表达式定义的。效果是在合并的list中,"six""seven"之前。如果在上面的代码中没有参数就调用了merge(),那么"seven"将在"six"之前,这是正常的排序顺序。

list容器的splice()成员有几个重载。这个函数从当前容器中的一个特定位置转移参数list中的元素。您可以从源容器中转移单个元素、一系列元素或所有元素。以下是如何拼接单个元素的示例:

std::list<std::string> my_words {"three", "six", "eight"};

std::list<std::string> your_words {"seven", "four", "nine"};

my_words.splice(++std::begin(my_words), your_words, ++std::begin(your_words));

第一个参数是一个迭代器,指向目标容器中的一个位置。第二个参数是元素的源,第三个参数是指向源list中的元素的指针,该元素将在第一个参数指示的位置之前拼接到目标中。执行此操作后,容器的内容将是:

your_words: "seven," "nine"

my_words: "three," "four," "six," "eight"

当您想要从源list拼接一个范围时,第三和第四个参数定义它。例如:

your_words.splice(++std::begin(your_words), my_words, ++std::begin(my_words,

std::end(my_words));

这将从第二个元素拼接到your_words中第二个元素之前的my_words的末端。给定两个列表的状态如上,结果将是:

your_words : "seven""four""six""eight", "nine"

my_words : "three"

现在,您可以使用以下语句将所有元素从your_words拼接到my_words:

my_words.splice(std::begin(my_words), your_words);

your_words中的所有元素都被转移到第一个元素"three"之前的my_words。在此之后,your_words将成为empty()。即使your_words为空,您仍然可以将元素拼接到其中:

your_words.splice(std::end(your_words), my_words);

现在my_words是空的,而your_words拥有所有的元素。第一个参数可以是std::begin(your_words),因为当容器为空时,它也返回结束迭代器。

访问元素

listfront()back()函数成员分别返回对第一个或最后一个元素的引用;为空的list调用这两个函数的效果是不确定的,所以不要这样做。要访问list内部的元素,您可以使用迭代器并递增或递减它,以获得您想要的元素。如您所见,begin()end()分别返回指向第一个元素或最后一个元素之后的双向迭代器。rbegin()rend()函数返回双向迭代器,允许您以相反的顺序遍历元素。您可以使用基于范围的for循环和list,这样当您想要处理所有元素时就不必使用迭代器:

std::list<std::string> names {"Jane", "Jim", "Jules", "Janet"};

names.emplace_back("Ann");

std::string name("Alan");

names.emplace_back(std::move(name));

names.emplace_front("Hugo");

names.emplace(++begin(names), "Hannah");

for(const auto& name : names)

std::cout << name << std::endl;

循环变量name是一个引用,它将依次引用每个list元素,因此循环将输出字符串,每个字符串在单独的一行上。

让我们尝试一下我们在示例中看到的一些内容。这个例子从键盘上读取谚语并将它们存储在一个list容器中:

// Ex2_05.cpp

// Working with a list

#include <iostream>

#include <list>

#include <string>

#include <functional>

using std::list;

using std::string;

// List a range of elements

template<typename Iter>

void list_elements(Iter begin, Iter end)

{

while (begin != end)

std::cout << *begin++ << std::endl;

}

int main()

{

std::list<string> proverbs;

// Read the proverbs

std::cout << "Enter a few proverbs and enter an empty line to end:" << std::endl;

string proverb;

while (getline(std::cin, proverb, '\n'), !proverb.empty())

proverbs.push_front(proverb);

std::cout << "Go on, just one more:" << std::endl;

getline(std::cin, proverb, '\n');

proverbs.emplace_back(proverb);

std::cout << "The elements in the list in reverse order are:" << std::endl;

list_elements(std::rbegin(proverbs), std::rend(proverbs));

proverbs.sort();                               // Sort the proverbs in ascending sequence

std::cout << "\nYour proverbs in ascending sequence are:" << std::endl;

list_elements(std::begin(proverbs), std::end(proverbs));

proverbs.sort(std::greater<>());               // Sort the proverbs in descending sequence

std::cout << "\nYour proverbs in descending sequence:" << std::endl;

list_elements(std::begin(proverbs), std::end(proverbs));

}

以下是一些输出的示例:

Enter a few proverbs and enter an empty line to end:

A nod is a good as a wink to a blind horse.

Many a mickle makes a muckle.

A wise man stands on the hole in his carpet.

Least said, soonest mended.

Go on, just one more:

A rolling stone gathers no moss.

The elements in the list in reverse order are:

A rolling stone gathers no moss.

A nod is a good as a wink to a blind horse.

Many a mickle makes a muckle.

A wise man stands on the hole in his carpet.

Least said, soonest mended.

Your proverbs in ascending sequence are:

A nod is a good as a wink to a blind horse.

A rolling stone gathers no moss.

A wise man stands on the hole in his carpet.

Least said, soonest mended.

Many a mickle makes a muckle.

Your proverbs in descending sequence:

Many a mickle makes a muckle.

Least said, soonest mended.

A wise man stands on the hole in his carpet.

A rolling stone gathers no moss.

A nod is a good as a wink to a blind horse.

输入是一系列包含空格的谚语,因此我们使用了getline()函数。每个谚语都被读为一行,并通过调用proverbs容器的push_front()来添加为一个新的list元素。额外要求一句谚语只是为了锻炼一下emplace_back()成员。输出由位于main()定义之前的list_elements()函数模板产生。该模板将从任何支持输出迭代器的容器中输出支持流插入操作符的任何类型的元素。代码展示了使用正向迭代器和反向迭代器的函数模板。

proverbssort()成员的第一次调用没有参数,所以默认情况下元素按升序排序。第二个sort()调用将greater谓词作为参数传递;这个模板在functional头中定义,还有其他几个标准谓词,您将在本书后面遇到。greater<>()表达式定义了一个使用operator>()比较对象的函数对象,并推导出模板类型参数。其效果是list元素按降序排序。其他定义谓词的对象包括greater_equal<>()less<>()less_equal<>(),这些对象可能对使用sort()很有帮助;这些名称表明了比较的内容。示例的输出显示一切都按预期运行。

使用 forward_list 容器

一个forward_list容器在一个单向链表中存储对象。在forward_list标题中定义了forward_list的模板。一个forward_list和一个list容器之间的主要区别是你不能向后遍历前者中的元素;你只能从第一个走到最后一个。一个forward_list的单链性质暗示了其他的结果。首先,没有反向迭代器可用。您只能为一个forward_list获得const或非const前向迭代器,并且这些迭代器不能递减,只能递增。第二,没有back()成员返回对最后一个元素的引用;正好有一个front()成员。第三,由于到达序列末尾的唯一方法是递增指向前一个元素的迭代器,因此操作push_back()pop_back(),emplace_back()不可用。假设您对应用程序中的这些限制感到满意,forward_list在操作上将比list容器更快,并且需要更少的内存。

一个forward_list容器的构造函数的范围与一个list容器的相同。forward_list的迭代器是前向迭代器。没有size()成员,不能从一个前向迭代器中减去另一个,但是可以使用iterator头中定义的distance()函数获得前向列表中的元素数量。例如:

std::forward_list<std::string> my_words {"three", "six", "eight"};

auto count = std::distance(std::begin(my_words), std::end(my_words)); // Result is 3

distance()的参数指定了一个范围,所以第一个参数是 begin 迭代器,第二个参数是 end 迭代器。当你需要增加一个以上的前向迭代器时,iterator头文件中的advance()函数会很有用。这里有一个例子:

std::forward_list<int> data {10, 21, 43, 87, 175, 351};

auto iter = std::begin(data);

size_t n {3};

std::advance(iter, n);

std::cout << "The " << n+1 << "th element is " << *iter << std::endl;  // Outputs 87

这里没有魔法。advance()函数将递增一个前向迭代器所需的次数。这省去了你编写循环代码的麻烦。你需要记住,这个函数增加了作为第一个参数的迭代器,但没有返回它——返回类型为void

因为forward_list中的链接只是向前的,所以插入新元素和拼接来自另一个容器的元素必须发生在一个元素之后,而不像在list容器中,这些操作在一个元素之前应用。因此,forward_list容器有splice_after()insert_after()成员,代替了list容器的splice()insert()成员;顾名思义,元素被拼接或插入到列表中的指定位置之后。当您需要在forward_list的开头插入或拼接元素时,这些操作仍然存在问题;不能在任何元素之前插入或拼接元素,这适用于第一个元素。这个困难通过使用返回指向第一个元素前一个元素的const和非const迭代器的cbefore_begin()before_begin()函数成员解决了。您可以使用这些迭代器在开头插入或拼接元素,例如:

std::forward_list<std::string> my_words {"three", "six", "eight"};

std::forward_list<std::string> your_words {"seven", "four", "nine"};

my_words.splice_after(my_words.before_begin(), your_words, ++std::begin(your_words));

该操作的效果是将your_words的最后一个元素拼接到my_words的开头,这样my_words将包含string对象:"nine", "three", "six", "eight",your_words将剩下两个string元素,"seven""four"

还有另一个版本的splice_after()将一系列元素从一个forward_list<T>容器拼接到另一个容器:

my_words.splice_after(my_words.before_begin(), your_words,

++std::begin(your_words), std::end(your_words));

最后两个参数是迭代器,指定了第二个参数指定的forward_list<T>容器中的元素范围。范围中的元素(不包括第一个)被移动到当前容器中由第一个参数指定的位置。因此,在假设初始容器状态之后,my_words将包含"four", "nine", "three", "six", "eight",,而your_words将只包含"seven"

另一个版本的splice_after()将把所有元素从一个forward_list<T>容器拼接到另一个容器:

my_words.splice_after(my_words.before_begin(), your_words);

这会将所有元素从your_words移动到my_words中第一个参数指定的位置。

forward_list容器具有与list相同的sort()merge()成员。它们还有remove()remove_if()unique()操作,所有这些操作也与list操作相同。我们可以尝试一个例子来展示一个正在运行的forward_list容器。这一次,容器将存储代表矩形框的类型为Box的对象。下面是Box类的头文件内容:

// Box.h

// Defines the Box class for Ex2_06

#ifndef BOX_H

#define BOX_H

#include <iostream>                              // For standard streams

#include <utility>                               // For comparison operator templates

using namespace std::rel_ops;                    // Comparison operator template namespace

class Box

{

private:

size_t length {};

size_t width {};

size_t height {};

public:

explicit Box(size_t l=1, size_t w=1, size_t h=1) : length {l}, width {w}, height {h} {}

double volume() const { return length*width*height; }

bool operator<(const Box& box) { return volume() < box.volume(); }

bool operator==(const Box& box) { return length == box.length && width == box.width

&& height == box.height; }

friend std::istream& operator>>(std::istream& in, Box& box);

friend std::ostream& operator<<(std::ostream& out, const Box& box);

};

inline std::istream&#x0026; operator>>(std::istream& in, Box& box)

{

std::cout << "Enter box length, width, & height separated by spaces - Ctrl+Z to end: ";

size_t value;

in >> value;

if (in.eof()) return in;

box.length = value;

in >> value;

box.width = value;

in >> value;

box.height = value;

return in;

}

inline std::ostream&#x0026; operator<<(std::ostream&#x0026; out, const Box& box)

{

out << "Box(" << box.length << "," << box.width << "," << box.height << ")  ";

return out;

}

#endif

utility头中的std::relops名称空间包含比较运算符的模板。如果您为一个类定义了operator<()operator==(),模板将在需要时创建其余的。Box类有三个private成员定义了完整的盒子尺寸。构造函数参数的默认值提供了一个无参数的构造函数,这是在容器中存储Box对象所必需的;未初始化的元素是通过调用存储的元素类型的默认构造函数创建的。两个内嵌的friend函数重载了流的提取和插入操作符,这显然包括标准的 I/O 流。在读取每组三个输入值中的第一个值后,operator>>()函数通过调用流对象的eof()成员来检查是否到达了EOF。当您输入Ctrl+Z时,或者通过从文件输入流中读取文件结束标记,为标准输入流设置EOF。当这种情况发生时,输入结束,流对象返回,EOF状态将保持设置,因此可以被调用程序检测到。

下面是将Box对象存储在forward_list容器中的程序:

// Ex2_06.cpp

// Working with a forward list

#include <algorithm>                             // For copy()

#include <iostream>                              // For standard streams

#include <forward_list>                          // For forward_list container

#include <iterator>                              // For stream iterators

#include "Box.h"

// List a range of elements

template<typename Iter>

void list_elements(Iter begin, Iter end)

{

size_t perline {6};                            // Maximum items per line

size_t count {};                               // Item count

while (begin != end)

{

std::cout << *begin++;

if (++count % perline == 0)

{

std::cout << "\n";

}

}

std::cout << std::endl;

}

int main()

{

std::forward_list<Box> boxes;

std::copy(std::istream_iterator<Box>(std::cin), std::istream_iterator<Box>(),

std::front_inserter(boxes));

boxes.sort();                                    // Sort the boxes

std::cout << "\nAfter sorting the sequence is:\n";

// Just to show that we can with Box objects - use an ostream iterator

std::copy(std::begin(boxes), std::end(boxes), std::ostream_iterator<Box>(std::cout, " "));

std::cout << std::endl;

// Insert more boxes

std::forward_list<Box> more_boxes {Box {3, 3, 3}, Box {5, 5, 5}, Box {4, 4, 4}, Box {2, 2, 2}};

boxes.insert_after(boxes.before_begin(), std::begin(more_boxes), std::end(more_boxes));

std::cout << "After inserting more boxes the sequence is:\n";

list_elements(std::begin(boxes), std::end(boxes));

boxes.sort();                                    // Sort the boxes

std::cout << std::endl;

std::cout << "The sorted sequence is now:\n";

list_elements(std::begin(boxes), std::end(boxes));

more_boxes.sort();

boxes.merge(more_boxes);                         // Merge more_boxes

std::cout << "After merging more_boxes the sequence is:\n";

list_elements(std::begin(boxes), std::end(boxes));

boxes.unique();

std::cout << "After removing successive duplicates the sequence is:\n";

list_elements(std::begin(boxes), std::end(boxes));

// Eliminate the small ones

const double max_v {30.0};

boxes.remove_if(max_v{ return box.volume() < max_v; });

std::cout << "After removing those with volume less than 30 the sorted sequence is:\n";

list_elements(std::begin(boxes), std::end(boxes));

}

下面是一个输出示例:

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 4 4 5

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 6 5 7

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 2 2 3

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 1 2 3

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 3 3 4

Enter box length, width, & height separated by spaces - Ctrl+Z to end: 3 3 3

Enter box length, width, & height separated by spaces - Ctrl+Z to end: ^Z

After sorting the sequence is:

Box(1,2,3)   Box(2,2,3)   Box(3,3,3)   Box(3,3,4)   Box(4,4,5)   Box(6,5,7)

After inserting more boxes the sequence is:

Box(3,3,3)  Box(5,5,5)  Box(4,4,4)  Box(2,2,2)  Box(1,2,3)  Box(2,2,3)

Box(3,3,3)  Box(3,3,4)  Box(4,4,5)  Box(6,5,7)

The sorted sequence is now:

Box(1,2,3)  Box(2,2,2)  Box(2,2,3)  Box(3,3,3)  Box(3,3,3)  Box(3,3,4)

Box(4,4,4)  Box(4,4,5)  Box(5,5,5)  Box(6,5,7)

After merging more_boxes the sequence is:

Box(1,2,3)  Box(2,2,2)  Box(2,2,2)  Box(2,2,3)  Box(3,3,3)  Box(3,3,3)

Box(3,3,3)  Box(3,3,4)  Box(4,4,4)  Box(4,4,4)  Box(4,4,5)  Box(5,5,5)

Box(5,5,5)  Box(6,5,7)

After removing successive duplicates the sequence is:

Box(1,2,3)  Box(2,2,2)  Box(2,2,3)  Box(3,3,3)  Box(3,3,4)  Box(4,4,4)

Box(4,4,5)  Box(5,5,5)  Box(6,5,7)

After removing those with volume less than 30 the sorted sequence is:

Box(3,3,4)  Box(4,4,4)  Box(4,4,5)  Box(5,5,5)  Box(6,5,7)

list_elements()函数模板输出由开始和结束迭代器指定的一系列对象,每行六个。这在main()中用于输出forward_list的内容。main()中的第一个动作是从cin中读取一系列Box物体的尺寸。这是通过调用copy()算法来完成的,使用一个istream_iterator<Box>对象作为数据源,使用forward_list对象的front_inserter作为数据的目的地。istream_iterator将调用Box.h中定义的operator>>()函数来读取Box对象。一个front_inserter调用一个容器的push_front()成员,所以这对一个forward_list有效。

在对boxes容器中的元素进行排序后,我们使用copy()算法输出Box对象,将元素转移到ostream_iterator<Box>对象,只是为了说明我们可以这样做。这个迭代器将调用在Box.h中定义的operator<<()函数。这里的限制是我们无法控制每行的输出数量。在剩下的代码中,list_elements()模板的一个实例用于输出。

接下来,more_boxes容器的内容——也就是另一个forward_list——被插入到boxes容器的开头。这是通过调用boxesinsert_after()成员来实现的,插入位置由before_begin()成员返回的迭代器指定。

接下来的操作是对盒子进行排序,然后将more_boxes的内容合并到boxes中。在调用merge()之前,必须对两个容器进行排序,因为只有当两个容器的内容都是升序时,该操作才有效。这显然会导致more_boxes元素的副本出现在boxes中,因为副本已经被插入。调用boxesunique()成员可以消除一个元素的连续重复。最后一个操作是调用boxesremove_if()成员从容器中删除元素。要删除的元素由作为参数传递的一元谓词决定。这里是一个 lambda 表达式,为体积小于max_v的元素返回truemax_v是通过值从外部范围捕获的,因此可以在外部范围中设置不同的值。输出显示所有操作都按预期运行。

定义你自己的迭代器

你不需要理解本书其余部分的内容,所以不要陷入其中——如果你觉得很难,跳过它,继续阅读第三章。然而,本节将提供对 STL 迭代器架构的深入了解,以及对模板能力的评价。迭代器是对任何定义序列的类类型的强大补充。它们允许将算法应用于类实例包含的对象。可能会出现这样的情况,没有一个标准的 STL 容器是您真正需要的,在这种情况下,您需要定义自己的容器类型。你的容器类可能需要迭代器。理解是什么让一个定义迭代器的类被 STL 接受,也会让你对 STL 的幕后工作有所了解。

STL 迭代器要求

STL 对定义迭代器的类类型提出了特殊的要求。这是为了确保所有接受迭代器的算法都能按预期工作。这些算法既不知道也不关心哪种容器存放了要处理的数据,但是它们关心传递给它们的迭代器的特征,以识别要处理的数据。不同的算法需要不同能力的迭代器。你在第一章中看到了这些迭代器类别:输入、输出、正向、双向和随机访问迭代器。在需要能力较弱的迭代器的地方,你总是可以使用能力较强的迭代器。

定义算法的模板需要确定传递给它的迭代器类型的类别,以使算法能够验证迭代器的能力是否足够。知道它的迭代器参数的类别也为算法提供了利用任何超过最小值的功能的可能性,以使操作更有效。一般来说,要做到这一点,迭代器的能力必须以一种标准化的方式来识别。不同的迭代器类别意味着迭代器类必须定义不同的函数成员集。您已经看到迭代器类别在功能上是累积的,这显然会反映在每个类别的函数成员集中。在此之前,让我们看看函数模板如何使用迭代器。

使用 STL 迭代器的一个问题

定义具有迭代器参数的函数模板时出现的一个问题是,您并不总是知道模板定义中需要使用的所有类型。考虑以下带有迭代器参数的交换函数模板;模板类型参数指定迭代器类型:

template <typename Iter> void my_swap(Iter a, Iter b)

{

tmp = *a;                                 // error -- variable tmp undeclared

*a = *b;

*b = tmp;

}

这个函数模板的实例旨在交换迭代器参数ab所指向的对象。tmp应该是什么类型?你无法知道——你知道迭代器指向的是哪种类型的对象,但是你不知道那是什么,因为直到创建了模板的一个实例才确定。在不知道变量类型的情况下,如何定义变量?当然,你可以在这里使用auto,但是有些情况下你也想知道迭代器的值类型和差类型。

还有其他机制可以确定迭代器参数所指向的值的类型。一种可能是坚持每个可被my_swap()使用的迭代器类型都应该包含一个类型别名的public定义,value_ type,比如迭代器指向的对象类型。在这种情况下,您可以使用迭代器类中的value_type别名来指定my_swap()函数模板中tmp的类型——如下所示:

template <typename Iter> void my_swap(Iter a, Iter b)

{

typename Iter::value_type tmp = *a;       // Better - but has limitations...

*a = *b;

*b = tmp;

}

因为value_type别名是在Iter类中定义的,所以可以通过用类名限定value_type来引用它。这对于定义了value_type别名的类类型的迭代器来说很好。然而,STL 算法处理指针和迭代器;如果Iter是一个普通的指针类型,比如int*,或者甚至是Box*,其中Box是一个类类型——那么这种方法就不起作用。你不能写int*::value_type或者Box*::value_type,因为指针类型不是可以包含类型别名定义的类。STL 非常优雅地解决了这个问题和其他相关问题——使用模板——还有什么!

STL 方法

iterator标题中定义了iterator_traits模板类型。该模板为迭代器类型的特征定义了一组标准的类型别名。这是解决前一节所述困难的关键,并使算法能够处理迭代器和普通指针。iterator_traits模板定义如下:

template<class Iterator>

struct iterator_traits

{

typedef typename Iterator::difference_type   difference_type;

typedef typename Iterator::value_type        value_type;

typedef typename Iterator::pointer           pointer;

typedef typename Iterator::reference         reference;

typedef typename Iterator::iterator_category iterator_category;

};

我相信你记得一个struct本质上和一个类是一样的,除了默认情况下它的成员是public。在这个struct模板中没有数据成员或函数成员。iterator_traits模板的主体只包含类型别名的定义。这些别名是以Iterator为类型参数的模板。它定义了模板中类型别名之间的映射—difference_typevalue_type等等——以及用于创建迭代器模板实例的类型——对应于Iterator的类型参数。因此对于一个具体的类Boggleiterator_traits<Boggle>实例将difference_type定义为Boggle::difference_type的别名,value_type定义为Boggle::value_type的别名,以此类推。

那么这对于解决不知道模板定义中的类型是什么的问题有什么帮助呢?首先,假设您定义了一个迭代器类型MyIterator,它包含了以下类型别名的定义:

  • difference_type–类型为MyIterator的两个迭代器之间的差异所产生的值的类型。
  • value_type–类型为MyIterator的迭代器所指向的值类型。
  • pointer–类型为MyIterator的迭代器所代表的指针类型。
  • reference–由* MyIterator产生的参考类型。
  • iterator_category–你在第一章中看到的迭代器类别标签类类型之一:即必须是input_iterator_tagoutput_iterator_tagforward_iterator_tagbidirectional_iterator_tagrandom_access_iterator_tag类型之一。

符合 STL 要求的迭代器类必须定义所有这些类型别名,尽管对于输出迭代器,除了iterator_category别名之外的所有别名都可以定义为void。这是因为输出迭代器指向对象的目的地,而不是对象。这组别名提供了您可能想知道的关于迭代器的一切。

当您使用迭代器作为参数定义函数模板时,您可以使用标准的类型别名,这些别名是iterator_traits模板在您的模板中定义的类型。因此,MyIterator类型的迭代器所代表的指针类型总是可以被称为std::iterator_traits<MyIterator>::pointer,因为它等价于MyIterator::pointer。当您需要指定一个MyIterator迭代器指向的value的类型时,您编写std::iterator_traits<MyIterator>::value_type,它将映射到MyIterator::value_type。我们可以在my_swap()模板中应用iterator_traits模板类型别名来指定tmp的类型,如下所示:

template <typename Iter>

void my_swap(Iter a, Iter b)

{

typename std::iterator_traits<Iter>::value_type tmp = *a;

*a = *b;

*b = tmp;

}

这将tmp的类型指定为来自iterator_traits模板的value_type别名。当用Iter模板参数实例化my_swap()模板时,tmp的类型将是迭代器指向的类型,Iter::value_type

为了弄清楚发生了什么以及这是如何解决问题的,让我们考虑一个my_swap()模板实例的具体情况。假设一个程序包含以下代码:

std::vector<std::string> words {"one", "two", "three"};

my_swap(std::begin(words), std::begin(words)+1); // Swap first two elements

当编译器遇到my_swap()调用时,它会根据调用中的参数创建一个函数模板的实例。这些是模板类型的迭代器。在my_swap()模板的主体中,编译器必须处理tmp的定义。编译器知道my_swap()模板的类型参数是iterator<std::string>,所以将它插入模板后,tmp的定义将是:

typename std::iterator_traits< iterator<std::string> >::value_type tmp = *a;

tmp的类型现在是iterator_traits模板实例的成员。为了弄清楚这到底意味着什么,编译器必须使用出现在my_swap()函数中tmp的类型规范中的类型参数来实例化iterator_traits模板。下面是编译器将创建的iterator_traits模板的实例:

struct iterator_traits

{

typedef typename iterator<std::string>::difference_type   difference_type;

typedef typename iterator<std::string>::value_type        value_type;

typedef typename iterator<std::string>::pointer           pointer;

typedef typename iterator<std::string>::reference         reference;

typedef typename iterator<std::string>::iterator_category iterator_category;

};

由此,编译器确定tmp的类型iterator_traits<iterator<std::string>>::value_type是另一个别名,即iterator<std::string>::value_type的别名。就像所有的 STL 迭代器类型一样,从迭代器的模板创建的iterator<std::string>类型的定义将包含一个value_type的定义,如下所示:

typedef std::string     value_type;

编译器现在从iterator_traits实例中知道iterator_traits<iterator<std::string>>::value_typeiterator<std::string>::value_type的别名,并且从iterator<std::string>类定义中知道iterator<std::string>::value_typestd::string的别名。通过遍历实际类型的别名,编译器将推断出my_swap()函数中tmp的定义是:

std::string tmp = *a;

很简单,不是吗!

不断提醒自己模板不是代码是很重要的——它是编译器用来创建代码的配方。iterator_traits模板只包含类型别名,因此不会产生可执行代码。编译器在创建最终将被编译的 C++ 代码的过程中使用它。被编译的代码将不包含任何iterator_traits模板的痕迹;它唯一的用途是在创建 C++ 代码的过程中。

这仍然留下了指针的问题。iterator_traits模板如何解决允许算法接受指针和迭代器的问题?iterator_traits模板具有为类型T*const T*定义的专门化。例如,当模板类型参数是指针类型T*时,专门化被定义为:

template<class T>

struct iterator_traits<T*>

{

typedef ptrdiff_t                  difference_type;

typedef T                          value_type;

typedef T*                         pointer;

typedef T&&#x00A0;                        reference;

typedef random_access_iterator_tag iterator_category;

};

当模板类型参数是指针类型时,它定义了对应于别名的类型。对于类型为T*的指针,value_type别名总是T;如果你使用一个类型为Box*的指针作为my_swap()的参数,那么value_type的别名就是Box,所以tmp也将是那个类型。随机访问迭代器类别所需的所有操作都适用于指针,因此对于指针来说,iterator_category别名总是等同于类型std::random_access_iterator_tag。因此,iterators_traits模板的工作方式取决于模板类型参数是指针还是迭代器类类型。当模板类型参数是指针时,将选择指针的iterators_traits模板的专门化;否则它将是标准模板定义。

使用迭代器模板

STL 定义了iterator模板来帮助您在自己的迭代器类中包含所需的类型别名。iterator是一个struct的模板,它定义了来自iterator_traits模板的五个类型别名:

template<class Category, class T, class Difference = ptrdiff_t, class Pointer = T*,

class Reference = T&>

struct iterator

{

typedef T          value_type;

typedef Difference difference_type;

typedef Pointer    pointer;

typedef Reference  reference;

typedef Category   iterator_category

};

这个模板定义了 STL 需要迭代器的所有类型。例如,如果你有一个未知的模板参数Iter,当你需要声明一个指针指向迭代器解引用时提供的类型时,你可以写Iter::pointeriterator_category的值必须是我在第一章中介绍的类别标签类的固定集合之一。当你定义一个表示迭代器的类时,你可以使用iterator模板的一个实例作为基类,这将添加你的类需要的类型别名。例如:

class My_Iterator : public std::iterator<std::random_access_iterator_tag, int>

{

// Members of the iterator class...

};

它负责定义 STL 对迭代器要求的所有类型。模板的第一个参数将这个迭代器的类型指定为完全随机访问迭代器。第二个参数是迭代器指向的对象类型。iterator的最后三个模板参数将是默认值,所以第三个参数是两个迭代器之间差异的类型,将是ptrdiff_t。第四个参数是指向一个对象的指针的类型,所以这将是int*。最后,最后一个模板参数指定了引用的类型,它将是int&。当然,迭代器类型不做任何事情;所有成员仍然需要定义。

STL 迭代器成员函数要求

STL 定义了一组迭代器类型必须支持的函数成员,这取决于它的类别。如果你把他们分成小组会有帮助。第一组是所有迭代器都需要的,包括一些所有迭代器类都需要的重要函数:默认构造函数、复制构造函数和赋值操作符。根据经验,如果您需要为迭代器类编写这些函数中的任何一个,那么您也应该编写一个显式析构函数。该组中类型Iterator的全套功能为:

Iterator();                              // Default constructor

Iterator(const Iterator& y);             // Copy constructor

∼Iterator();                             // Destructor

Iterator& operator=(const Iterator& y);  // Assignment operator

STL 需要一个随机访问迭代器类的一整套等式和关系操作符。事实上,通过使用由utility标准库头文件提供的一些函数模板,您可以只定义两个:

bool operator==(const Iterator& y) const;

bool operator<(const Iterator& y)  const;

这里假设您有一个用于utility头的#include指令和一个用于std :: relops名称空间的using指令:

#include <utility>

using namespace std::rel_ops;

如果您为一个类定义了operator==()operator<(),那么在std命名空间中声明的rel_ops命名空间包含函数模板,这些模板使用您的操作符函数在必要时为!=>>=and <=生成操作符函数。所以用using指令激活std::rel_ops可以省去显式定义这四个操作符的工作。如果您定义了将由std::rel_ops名称空间中的模板生成的任何操作符函数,那么您的实现将优先于名称空间中的模板可能创建的那些函数。operator<()功能特殊。这就是所谓的排序关系。它在搜索和比较算法中很重要。

函数测试两个容器或对象是否有相同的内容。这有一个有趣的方面。你可能认为对于任何一对操作数,xy,表达式(x<y || y<x || x==y)必须总是计算为true,因为三个组成表达式中必须有一个是true。事实上,并不一定要这样。很清楚,如果x==ytrue,那么x<yy<x都不可能是true。你可以确定的一件事是,相同的元素不能不同。然而,如果x!=y你一定不能假设x<yy<x中的一个是true。当表达式(!(x<y))&&(!(y<x))true时,元素xy被说成是不等价的,简单来说就是你在排序时没有偏好。一个常见的例子是在对字符串排序时,忽略了大小写。在不区分大小写的基础上,字符串"A123""a123"是不等价的(都不属于第一个),但是它们不相同,也不相等。

迭代器类必须定义的其他操作由其类别决定。你在第一章中看到了每个类别特有的操作,当然它们是累积的,随机访问迭代器支持完整的集合。

让我们看看一个工作示例中迭代器类型的简单定义。我们将定义一个类模板,它表示一个数值类型的值的范围,并且可以创建指定该范围的开始和结束迭代器。迭代器也将是模板类型,两个模板将在同一个头文件Numeric_Range.h中定义。下面是Numeric_Range<T>模板的定义:

template <typename T> class Numeric_Iterator;  // Template type declaration

// Defines a numeric range

template<typename T>

class Numeric_Range

{

static_assert(std::is_integral<T>::value || std::is_floating_point<T>::value,

"Numeric_Range type argument must be numeric.");

friend class Numeric_Iterator <T>;

private:

T start;                                     // First value in the range

T step;                                      // Increment between successive values

size_t count;                                // Number of values in the range

public:

explicit Numeric_Range(T first=0, T incr=1, size_t n=2) :

start {first}, step {incr}, count {n}{}

// Return the begin iterator for the range

Numeric_Iterator<T> begin(){ return Numeric_Iterator<T>(*this); }

// Return the end iterator for the range

Numeric_Iterator<T> end()

{

Numeric_Iterator<T> end_iter(*this);

end_iter.value = start + count*step;       // End iterator value is one step over the last

return end_iter;

}

};

T的类型参数是范围中值的类型,因此它必须是数值类型。如果第一个参数是false,模板主体中的static_assert()将产生一个编译时错误消息,包括作为第二个参数的字符串,这将在T不是整数或浮点类型时发生。我在这里使用的谓词模板是在type_traits头中定义的,还有大量用于模板类型参数的编译时类型检查的其他谓词。这个构造函数有三个参数的默认值,所以它也是默认的无参数构造函数。这些参数是初始值、从一个值到下一个值的增量以及范围内值的数量。因此,默认值用两个值定义了一个范围:0 和 1。当需要时,编译器提供的复制构造函数就足够了。

另外两个函数成员创建并返回范围的开始和结束迭代器。结束迭代器的value成员比范围中的最后一个值多一个增量。end 迭代器是通过修改 begin 迭代器使其具有 end 迭代器的适当值来创建的。模板定义之前的Numeric_Iterator<T>模板类型的声明是必要的,因为迭代器类型的模板尚未定义。将Numeric_Iterator<T>模板指定为该模板的friend,以允许迭代器模板的实例访问Numeric_Range<T>private成员。Numeric_Range<T>模板也需要成为Numeric_Iterator<T>模板的朋友,因为定义范围的模板的end()成员访问迭代器模板的private成员,

迭代器的模板类型定义如下:

// Iterator class template - it’s a forward iterator

template<typename T>

class Numeric_Iterator : public std::iterator <std::forward_iterator_tag, T>

{

friend class Numeric_Range <T>;

private:

Numeric_Range<T>& range;                       // Reference to the range for this iterator

T value;                                       // Value pointed to

public:

explicit Numeric_Iterator(Numeric_Range<T>& a_range) :

range {a_range}, value {a_range.start} {}

// Assignment operator

Numeric_Iterator& operator=(const Numeric_Iterator& src)

{

range = src.range;

value = src.value;

}

// Dereference an iterator

T& operator*()

{

// When the value is one step more than the last, it’s an end iterator

if (value == static_cast<T>(range.start + range.count*range.step))

{

throw std::logic_error("Cannot dereference an end iterator.");

}

return value;

}

// Prefix increment operator

Numeric_Iterator& operator++()

{

// When the value is one step more than the last, it’s an end iterator

if (value == static_cast<T>(range.start + range.count*range.step))

{

throw std::logic_error("Cannot increment an end iterator.");

}

value += range.step;                         // Increment the value by the range step

return *this;

}

// Postfix increment operator

Numeric_Iterator operator++(int)

{

// When the value is one step more than the last, it’s an end iterator

if (value == static_cast<T>(range.start + range.count*range.step))

{

throw std::logic_error("Cannot increment an end iterator.");

}

auto temp = *this;

value += range.step;                         // Increment the value by the range step

return temp;                                 // The iterator before it’s incremented

}

// Comparisons

bool operator<(const Numeric_Iterator& iter) const { return value < iter.value; }

bool operator==(const Numeric_Iterator& iter) const { return value == iter.value; }

bool operator!=(const Numeric_Iterator& iter) const { return value != iter.value; }

bool operator>(const Numeric_Iterator& iter) const { return value > iter.value; }

bool operator<=(const Numeric_Iterator& iter) const { *this < iter || *this == iter; }

bool operator>=(const Numeric_Iterator& iter) const { *this > iter || *this == iter; }

};

它看起来有很多代码,但是非常简单。一个迭代器有一个成员,它存储了一个对与之相关的Numeric_Range对象的引用。它还存储它所指向的范围内的值。迭代器的构造函数的参数是对 range 对象的引用。构造函数用参数初始化range引用成员,并将value成员设置为范围的start值。其他成员定义解引用操作符、前缀和后缀增量操作符以及一组比较操作符。取消引用或递增一个范围的结束迭代器是非法的,因此如果操作数是结束迭代器,递增运算符函数和取消引用运算符函数将引发异常;这由比范围中的最后一个值多一个增量的value成员来指示。为了简单起见,我选择抛出一个标准的异常对象。

Numeric_Range.h标题的完整内容将是:

// Numeric_Range.h for Ex2_07

// Defines class templates for a range and iterators for the range

#ifndef NUMERIC_RANGE_H

#define NUMERIC_RANGE_H

#include <exception>                             // For standard exception types

#include <Iterator>                              // For iterator type

#include <type_traits>                           // For compile-time type checking

template <typename T> class Numeric_Iterator;    // Template type declaration

// Template to define a numeric range, as above...

// Template to define a numeric range iterator, as above...

#endif

以下程序将试用Numeric_Range模板:

// Ex2_07.cpp

// Exercising the Numeric_Range template

#include <algorithm>                             // For copy()

#include <numeric>                               // For accumulate()

#include <iostream>                              // For standard streams

#include <vector>                                // For vector container

#include "Numeric_Range.h"                       // For Numeric_Range<T> & Numeric_Iterator<T>

int main()

{

Numeric_Range<double> range {1.5, 0.5, 5};

auto first = range.begin();

auto last = range.end();

std::copy(first, last, std::ostream_Iterator<double>(std::cout, "  "));

std::cout << "\nSum = " << std::accumulate(std::begin(range), std::end(range), 0.0) << std::endl;

// Initializing a container from a Numeric_Range

Numeric_Range<long> numbers {15L, 4L, 10};

std::vector<long> data {std::begin(numbers), std::end(numbers)};

std::cout << "\nValues in vector are:\n";

std::copy(std::begin(data), std::end(data), std::ostream_Iterator<long>(std::cout, "  "));

std::cout << std::endl;

// List the values in a range

std::cout << "\nThe values in the numbers range are:\n";

for (auto n : numbers)

std::cout << n << " ";

std::cout << std::endl;

}

该示例的输出是:

1.5  2  2.5  3  3.5

Sum = 12.5

Values in vector are:

15  19  23  27  31  35  39  43  47  51

The values in the numbers range are:

15 19 23 27 31 35 39 43 47 51

这首先创建了一个Numeric_Range实例,它有五个类型为double的值,从1.5开始,递增0.5。在copy()算法中使用范围迭代器将值复制到ostream_iterator。这表明迭代器是算法可以接受的。第二个Numeric_Range实例有 10 个类型为long的值。这个范围的开始和结束迭代器在向量容器的初始化列表中使用。然后使用copy()算法输出矢量元素。最后,为了展示它的工作原理,在一个基于范围的for循环中输出范围内的值。输出证实了Numeric_Range模板成功地创建了整数和浮点范围,并且我们确实成功地定义了一个适用于 STL 的迭代器类型。

摘要

这一章是关于序列容器的,因为它们是最灵活的,所以你可能会用得最多。它们没有对包含的数据项进行基本的排序,但是允许你以任何你想要的方式进行排序。您在本章中学到的更重要的内容包括:

  • 一个array<T,N>容器存储固定数量的T类型的N元素。它可以像常规数组一样使用,但与常规数组相比,它的优势在于数组容器知道它的大小,因此它可以作为参数传递给函数,而不需要第二个参数来指定数组元素的数量。它还提供了通过调用元素的at()函数成员来检查用于访问元素的索引的可能性。与常规数组相比,使用数组容器增加的开销很少。
  • 一个vector<T>容器存储任意数量的T类型的元素。一个vector容器会自动增长以容纳你想要的任意多的元素。
  • 您可以在一个vector的末尾有效地添加或删除元素;添加或删除序列内部的元素会比较慢,因为需要移动元素。
  • 您可以使用索引访问vector容器中的任何元素,就像数组一样,或者您可以调用它的at()函数成员来检查所使用的索引。尽管与常规数组相比,vector的开销很小,但在大多数情况下,您不会注意到这一点。
  • 一个deque<T>容器存储任意数量的类型为T的元素作为一个双端队列。您可以像访问vector一样访问deque容器中的元素。
  • 您可以有效地在deque容器的前面或后面添加或删除元素;在序列内部添加或删除元素会很慢。
  • arrayvector,deque容器提供了const和非const随机访问迭代器和反向迭代器。
  • 一个list<T>容器将类型为T的元素存储为一个双向链表。可以在列表容器中的任何地方有效地添加或删除元素。
  • 只有从序列的开头或结尾开始遍历列表,才能访问list容器内部的元素。
  • 一个list容器提供双向迭代器。
  • 一个forward_list<T>容器将类型为T的元素存储在一个单向链表中,该链表只能从第一个元素开始向前遍历。一个forward_list集装箱比一个list集装箱更快更紧凑。
  • 一个forward_list容器提供了前向迭代器。
  • algorithm头中的模板定义的copy()算法将一系列元素复制到另一个迭代器指定的目的地。
  • 您可以使用流迭代器和copy()算法从输入流中读取数据并将其复制到容器中,或者将数据从容器写入输出流。
  • algorithm头中定义的sort()函数模板对随机访问迭代器指定的一系列元素进行排序。默认情况下,元素可以按升序排序,或者按二元谓词确定的顺序排序,该谓词作为参数提供给sort()
  • listforward_list容器为排序元素提供了一个sort()函数成员。

Exercises

这里有几个练习来测试你对本章主题的记忆程度。如果你卡住了,回头看看这一章寻求帮助。如果之后你仍然停滞不前,你可以从 Apress 网站( http://www.apress.com/9781484200056 )下载解决方案,但这真的应该是最后的手段。

The Fibonacci series is the sequence of integers 0, 1, 1, 2, 3, 5, 8, 13, 21 … where each integer after the first two is the sum of the two preceding integers. Write a program that uses a lambda expression to initialize an array<T,N> container with 50 values from the Fibonacci series. Use a global function in the program to output the elements in the container 8 to a line.   Write a program to read an arbitrary number of city names from the keyboard and store them as std::string objects in a vector<T> container. Sort the city names in ascending sequence and list them several to each line, each in a fixed field width that will accommodate the longest city name. Output the names grouped by their initial letter with each group separated from the next by an empty line..   Repeat the previous exercise using a list<T> container, and devise a way to use an input stream iterator to read the city names, even when they consist of two or more names such as "New York" and are stored as such. (Obviously, the input must use an alternative to a space character in a name.)   Extend the previous example to transfer the contents of the list container to a deque<T> container using a front inserter. Sort the contents of the deque container, and output the city names using an output stream iterator.

三、容器适配器

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​3) contains supplementary material, which is available to authorized users.

本章解释了你在前一章看到的 STL 提供的容器的一些变体。这些为您在特定环境中使用的序列容器定义了更简单的接口。您还将看到更多关于如何在容器中存储指针的内容。在本章中,您将学习:

  • 什么是容器适配器。
  • 如何定义一个堆栈,何时以及如何使用它。
  • 如何定义和使用队列?
  • 如何创建和使用优先级队列,以及它与队列有何不同。
  • 什么是堆,如何创建和使用堆,以及堆与优先级队列的关系。
  • 在容器中存储指针的好处,尤其是智能指针。

什么是容器适配器?

容器适配器是一个类模板,它包装了您在上一章中学习的一个序列容器,以定义另一个提供不同功能的序列容器。它们被称为适配器类,因为它们调整容器的现有接口以提供不同的功能。有三种容器适配器:

  • 一个stack<T>容器是一个适配器类模板,默认包装一个deque<T>容器来实现一个下推栈,这是一种后进先出(LIFO)的存储机制。在stack头中定义了stack<T>模板。
  • 一个queue<T>容器是一个适配器类模板,默认包装一个deque<T>容器实现一个队列,这是先进先出(FIFO)的存储机制。如果满足某些条件,您可以指定替代的基础容器。在queue头中定义了queue<T>模板。
  • 一个priority_queue<T>容器是一个适配器类模板,它包装了一个vector<T>容器来实现一个队列,这个队列对元素进行排序,使得最大的元素总是在最前面。在queue头中也定义了priority_queue<T>模板。

适配器类根据底层序列容器上的操作来实现它们的操作,这显然可以由您自己来完成。它们提供的优势是其公共接口的简单性和使用它们的代码的可读性。我们将更详细地探索这些容器适配器可以做什么。

创建和使用堆栈容器适配器

stack<T>容器适配器中的数据是以 LIFO 为基础组织的,类似于自助餐厅中的下沉板栈或箱子中的一堆书;只有栈顶的对象是可访问的。图 3-1 显示了一个概念性的stack容器及其基本操作。只有顶部元素是可访问的;堆栈中较低位置的元素只能通过移除其上的元素来访问。

A978-1-4842-0004-9_3_Fig1_HTML.gif

图 3-1。

Basic stack container operations

stack容器的应用范围很广。例如,你的编辑器中的撤销机制很可能使用堆栈来记录连续的更改;撤消操作反转最后一个操作,该操作将是堆栈顶部的操作。编译器使用堆栈来解析算术表达式,当然,编译后的 C++ 代码在堆栈中记录函数调用。下面是如何定义一个存储字符串对象的stack容器:

std::stack<std::string> words;

stack容器适配器的模板有两个参数。第一个是存储的对象的类型,第二个是底层容器的类型。默认情况下,stack<T>的底层序列容器是一个deque<T>容器,所以模板类型实际上是stack<typename T, typename Container=deque<T>>。通过指定第二个模板类型参数,可以为支持操作back()push_back()pop_back()empty()size()的下属容器使用任何容器类型。下面是如何定义使用list<T>容器的stack:

std::stack<std::string, std::list<std::string>> fruit;

当你创建一个stack时,你不能用初始化列表中的对象初始化它,但是你可以创建它,使它包含另一个容器中元素的副本,只要另一个容器与底层容器的类型相同。例如:

std::list<double> values {1.414, 3.14159265, 2.71828};

std::stack<double, std::list<double>> my_stack (values);

第二条语句创建了my_stack,因此它包含了来自values的元素的副本。你不能在这里使用带有stack构造函数的初始化列表;您必须使用括号。如果您没有在第二个stack模板类型参数中将底层容器类型指定为列表,它将是一个deque,因此您将不能使用list的内容来初始化堆栈;只接受一个deque

stack<T>模板定义了一个复制构造函数,因此您可以复制一个现有的stack容器:

std::stack<double, std::list<double>> copy_stack {my_stack};

copy_stack将是my_stack的副本。如你所见,当你调用复制构造函数时,你可以使用初始化列表;当然也可以用括号。

堆栈操作

stack是一种简单的存储机制,与其他序列容器相比,它提供的操作相对较少。下面是一个stack容器提供的一整套操作:

  • top()返回一个类型为T&的引用到栈顶的元素。如果堆栈为空,则返回值是未定义的。
  • push(const T& obj)obj的副本推到堆栈顶部。这是通过调用底层容器的push_back()成员来完成的。
  • push(T&& obj)通过移动将obj推到堆栈顶部。这是通过调用具有右值引用参数的底层容器的push_back()成员来完成的。
  • 删除堆栈顶部的元素。
  • size()返回堆栈中元素的数量。
  • 如果堆栈中没有元素,则返回 true。
  • emplace()使用传递给emplace()的参数调用T构造函数,在stack<T>的顶部创建一个对象。
  • swap(stack<T> & other_stack)将当前stack的元素与自变量的元素交换。该参数必须包含与当前stack相同类型的元素。还有一个针对stack对象的全局swap()函数模板的专门化,它做同样的事情。

stack<T>模板还定义了operator=()的复制和移动版本,这样你就可以将一个stack对象分配给另一个。对于stack对象有一整套比较操作符。通过按字典顺序比较底层容器的相应元素来执行比较。词典式比较是一种用于在词典中对单词进行排序的比较。比较相应的元素,直到一个元素与另一个不相同。比较这些第一非匹配元素的结果是字典式比较的结果。如果一个堆栈包含的元素比另一个多,并且匹配元素对相等,则包含更多元素的堆栈较大。

我们可以在一个实现简单计算器的程序中尝试使用stack容器的操作。该计划将支持基本的操作,加,减,乘,除,加上指数运算;相应的运算符有+、-、*、/和^.幂运算由在cmath头中定义的pow()函数提供。表达式将被读取为单行上的字符串,可以包含空格。在分析字符串和执行它包含的操作之前,我们将使用remove()算法消除输入表达式中的空格。

我们将定义下面的函数来提供一个表示运算符优先级的值:

inline size_t precedence(const char op)

{

if (op == '+' || op == '-')

return 1;

if (op == '*' || op == '/')

return 2;

if (op == '^')

return 3;

throw std::runtime_error {string{"invalid operator: "} + op};

}

+-为最低优先级,其次是*/为次高优先级,^为最高优先级。运算符优先级将决定包含两个或更多运算符的表达式的执行顺序。如果参数不是支持的操作符之一,抛出一个runtime_error异常对象。异常对象的构造函数的string参数可以通过调用对象的what()catch块中检索。

程序将通过从左到右扫描来分析输入表达式,并将操作符存储在一个stack容器operators中,将相应的操作数存储在另一个stack容器operands中。所有的操作符都需要两个操作数,所以执行一个操作需要访问位于operators堆栈顶部的操作符,然后从operands堆栈中检索顶部的两个元素作为操作数。执行操作将由以下函数执行:

double execute(std::stack<char>& ops, std::stack<double>& operands)

{

double result {};

double rhs {operands.top()};                          // Get rhs...

operands.pop();                                       // ...and delete from stack

double lhs {operands.top()};                          // Get lhs...

operands.pop();                                       // ...and delete from stack

switch (ops.top())                                    // Execute current op

{

case '+':

result = lhs + rhs;

break;

case '-':

result = lhs - rhs;

break;

case '*':

result = lhs * rhs;

break;

case '/':

result = lhs / rhs;

break;

case '^':

result = std::pow(lhs, rhs);

break;

default:

throw std::runtime_error {string{"invalid operator: "} + ops.top()};

}

ops.pop();                                     // Delete op just executed

operands.push(result);

return result;

}

参数是对两个stack容器的引用。通过调用操作数容器的top()来获得操作数。top()函数只访问顶层元素;要访问下一个元素,您必须调用pop()来删除顶部的元素。注意在stack中操作数的顺序是相反的,所以第一个被访问的操作数是操作的右操作数。来自operators容器顶部的元素在选择操作的switch中使用。如果没有一个 case 语句适用,则会引发一个异常,指示该运算符无效。

以下是完整程序的代码:

// Ex3_01.cpp

// A simple calculator using stack containers

#include <cmath>                                 // For pow() function

#include <iostream>                              // For standard streams

#include <stack>                                 // For stack<T> container

#include <algorithm>                             // For remove()

#include <stdexcepT>                             // For runtime_error exception

#include <string>                                // For string class

using std::string;

// Code for the precedence() function goes here...

// Code for the execute() function goes here...

int main()

{

std::stack<double> operands;                   // Push-down stack of operands

std::stack<char> operators;                    // Push-down stack of operators

string exp;                                    // Expression to be evaluated

std::cout << " An arithmetic expression can include the operators +, -, *, /,"

< < "and ^ for exponentiation."

<< std::endl;

try

{

while (true)

{

std::cout << "Enter an arithmetic expression and press Enter"

<

< < STD::end;

std::getline(std::cin, exp, '\n');

if (exp.empty()) break;

// Remove spaces

exp.erase(std::remove(std::begin(exp), std::end(exp), ' '), std::end(exp));

size_t index {};                              // Index to expression string

// Every expression must start with a numerical operand

operands.push(std::stod(exp, &index));            // Push the first (lhs) operand on the stack

while (true)

{

operators.push(exp[index++]);                 // Push the operator on to the stack

// Get rhs operand

size_t i {};                                // Index to substring

operands.push(std::stod(exp.substr(index), &i)); // Push rhs operand

index += i;                                 // Increment expression index

if (index == exp.length())                  // If we are at end of exp...

{

while (!operators.empty())                // ...execute outstanding ops

execute(operators, operands);

break;

}

// If we reach here, there’s another op...

// If there’s a previous op of equal or higher precedence execute it

while (!operators.empty() && precedence(exp[index]) <= precedence(operators.top()))

execute(operators, operands);             //  Execute previous op.

}

std::cout << "result = " << operands.top() << std::endl;

}

}

catch (const std::exception& e)

{

std::cerr << e.what() << std::endl;

}

std::cout << "Calculator ending..." << std::endl;

}

while循环位于try块中,以捕捉任何可能抛出的异常。catch块将异常对象的what()成员返回的字符串写入标准错误流。所有的动作都发生在一个无限的while循环中,该循环通过输入一个空字符串来终止。使用remove()算法从非空输入字符串中删除空格。remove()不删除元素,因为它不能;它只是将元素混在一起,覆盖那些要删除的元素。为了去掉留在exp字符串中的多余元素,用两个迭代器作为参数调用它的erase()成员。第一个是remove()返回的迭代器,它将指向字符串中最后一个有效字符之后的字符;第二个迭代器是原始状态下字符串的结束迭代器。这两个迭代器指定的范围内的元素将被删除。

每个操作数通过调用在string头中定义的stod()函数以浮点值的形式获得。这将把字符序列从作为第一个参数的string转换成类型为double的值。该函数采用最大长度的字符序列,从字符串中的第一个字符开始,表示有效的浮点值。第二个参数是一个整型变量的指针,在这个变量中,stod()将存储第一个字符的索引,这个字符不是字符串中数字的一部分。string头还定义了返回一个float值的stof()和返回一个long double值的stold()

因为所有运算符都需要两个操作数,所以有效的输入字符串将始终采用操作数 op 操作数 op 操作数等形式,序列中的第一个和最后一个操作数以及每对操作数之间的运算符。因为有效的输入表达式总是以操作数开始,所以在执行嵌套的不定while循环分析输入之前,提取第一个操作数。在循环中,输入字符串后面的操作符被推送到operators堆栈中。在验证没有到达字符串末尾之后,从exp中提取第二个操作数。这一次,stod()的第一个参数是从index开始的exp的子串,它对应于被压入operators堆栈的操作符后面的字符。不在值字符串中的第一个字符的索引存储在i中。因为i是相对于index的,所以我们把它的值加到index上来设置index指向操作数后面的下一个操作符(或者如果操作数是exp中的最后一个,则指向字符串末尾以外的一个)。

index的值比exp中的最后一个字符大 1 时,执行operators容器中剩余的所有操作符。如果还没有到达字符串的末尾,并且operators容器不为空,我们将比较exp中下一个操作符和operators栈顶操作符的优先级。如果位于堆栈顶部的运算符优先级高于或等于下一个运算符,则必须首先执行堆栈中的运算符。否则,当前位于堆栈顶部的操作符不会被执行,字符串中的下一个操作符将在下一个循环迭代器开始时被推送到堆栈上。通过这种方式,表达式的计算适当考虑了运算符的优先级。

以下是一些输出示例:

An arithmetic expression can include the operators +, -, *, /, and ^ for exponentiation.

Enter an arithmetic expression and press Enter - enter an empty line to end:

2⁰.5

result = 1.41421

Enter an arithmetic expression and press Enter - enter an empty line to end:

2.5e2 + 1.5e1*4 - 1000

result = -690

Enter an arithmetic expression and press Enter - enter an empty line to end:

3*4*5 + 4*5*6 + 5*6*7

result = 390

Enter an arithmetic expression and press Enter - enter an empty line to end:

1/2 + 1/3 +1/4

result = 1.08333

Enter an arithmetic expression and press Enter - enter an empty line to end:

Calculator ending...

输出显示计算器工作正常。它还显示了stod()函数可以转换用各种符号表示数字的字符串。当然,如果示例支持包含括号的表达式就更好了,但是我将把它留给您来完成。你知道会这样,是吗?

创建和使用队列容器适配器

只有queue<T>容器适配器中的第一个和最后一个元素是可访问的。您只能在queue的后面添加新元素,并且只能从前面删除元素。许多应用程序可以使用queue。一个queue容器可以用来表示超市收银台的队列,或者一系列等待服务器处理的数据库事务。任何需要在 FIFO 基础上处理的序列都是queue容器适配器的候选对象。图 3-2 显示了一个队列及其基本操作。

A978-1-4842-0004-9_3_Fig2_HTML.gif

图 3-2。

A queue container

创建queue的选项类似于创建stack的选项。下面是如何创建一个存储string对象的queue:

std::queue<std::string> words;

还有一个复制构造函数:

std::queue<std::string> copy_words {words};      // A duplicate of words

stack<T>一样,queue<T>适配器类默认包装一个deque<T>容器,但是您可以指定另一个容器作为第二个模板类型参数:

std::queue<std::string, std::lisT<std::string>> words;

底层容器类型必须提供操作front()back()push_back()pop_front()empty()size()

队列操作

queue的一些函数成员与stack的相似,但在某些情况下工作方式略有不同:

  • front()返回对queue中第一个元素的引用。如果队列是const,则返回一个const引用。如果queue为空,则返回值是未定义的。
  • back()返回对queue中最后一个元素的引用。如果队列是const,则返回一个const引用。如果queue为空,则返回值是未定义的。
  • push(const T& obj)obj的副本附加到queue的末尾。这是通过调用底层容器的push_back()成员来完成的。
  • push(T&& obj)通过移动将obj附加到queue的末尾。这是通过调用具有右值引用参数的底层容器的push_back()成员来完成的。
  • pop()删除queue中的第一个元素。
  • size()返回queue中元素的个数。
  • 如果queue中没有元素,则empty()返回 true。
  • 在调用T构造函数时,emplace()使用传递给emplace()的参数在queue后面创建一个对象。
  • swap(queue<T> &other_q)将当前queue的元素与自变量的元素交换。该参数必须包含与当前queue相同类型的元素。还有一个针对queue对象的全局swap()函数模板的专门化,它也做同样的事情。

queue<T>模板定义了operator=()的复制和移动版本。有一整套用于queue对象的比较操作符,这些操作符存储相同类型的元素,其工作方式与用于stack对象的操作符相同。

就像stack一样,queue没有迭代器。访问元素的唯一方法是遍历内容,在遍历的过程中删除第一个元素。例如:

std::deque<double> values {1.5, 2.5, 3.5, 4.5};

std::queue<double> numbers(values);

while (!numbers.empty())                        // List the queue contents...

{

std ::cout << numbers.front() << " ";         // Output the 1st element

numbers.pop();                                // Delete the 1st element

}

std::cout << std::endl;

列出数字内容的循环必须由empty()返回的值控制。调用empty()确保我们不会为空队列调用front()。如这个代码片段所示,要访问一个queue中的所有元素,您必须删除它们。如果需要保留这些元素,必须将它们复制到另一个容器中。如果这种操作是必要的,很可能你应该使用其他东西而不是queue

队列容器的实际应用

让我们一起来看一个使用queue容器的例子。这个程序将通过模拟一个超市的运作,向 a queue的实际应用倾斜一点。结账队伍的长度是超市运营中的一个关键因素。它会影响商店能够接待的顾客数量,尤其是因为长长的队伍会让顾客望而却步。同样的排队问题出现在许多不同的情况下——例如,医院的床位数量会严重影响急诊设施的运作。我们的超市模拟将是一个简单的模型,具有有限的灵活性。

我们可以在一个标题中定义一个类,Customer.h,它代表一个足以满足模拟需求的客户:

// Defines a customer by their time to checkout

#ifndef CUSTOMER_H

#define CUSTOMER_H

class Customer

{

private:

size_t service_t {};                           // Time to checkout

public:

explicit Customer(size_t st = 10) :service_t {st}{}

// Decrement time remaining to checkout

Customer& time_decrement()

{

if(service_t > 0)

--service_t;

return *this;

}

bool done() const { return service_t == 0; }

};

#endif

唯一的数据成员service_t记录结账顾客购物所需的分钟时间。这将因客户而异。每当过了一分钟,就会调用time_decrement()函数,因此该函数将递减service_t值,以便它反映直到客户被处理的时间。当service_t值为零时,done()成员返回true

每个超市的收银台都会有一群等待服务的顾客。在Checkout.h中定义结账位置的类是:

// Supermarket checkout - maintains and processes customers in a queue

#ifndef CHECKOUT_H

#define CHECKOUT_H

#include <queue>                                 // For queue container

#include "Customer.h"

class Checkout

{

private:

std::queue<Customer> customers;                // The queue waiting to checkout

public:

void add(const Customer& customer) { customers.push(customer); }

size_t qlength() const { return customers.size(); }

// Increment the time by one minute

void time_increment()

{ // There are customers waiting...

if (!customers.empty())

{ // There are customers waiting...

if (customers.front().time_decrement().done()) // If the customer is done...

customers.pop();                             // ...remove from the queue

}

};

bool operator<(const CheckouT& other) const { return qlength() < other.qlength(); }

bool operator>(const CheckouT& other) const { return qlength() > other.qlength(); }

};

#endif

这应该是不言自明的。唯一的成员是等待签出的Customer对象的queue容器。add()成员向queue添加一个新客户。只处理第一个queue元素。每过一分钟,Checkout对象的time_increment()成员将被调用,它将调用第一个Customer对象的time_decrement()成员来递减剩余服务时间,然后调用其done()成员。如果Customer对象的done()返回true,则该客户已经被处理,因此从队列中删除。Checkout对象的比较运算符比较队列长度。

我们将需要一个随机数生成能力来进行模拟,所以我将从random头中使用一个非常简单的工具,而不详细解释它;我将在本书的后面详细介绍random头所提供的功能。该程序将使用uniform_int_distribution<>类型的实例。顾名思义,它定义了一个均匀分布,整数值均匀分布在最小值和最大值之间。在均匀分布中,该范围内的所有值都具有相同的可能性。您可以使用以下语句定义 10 到 100 之间的分布:

std::uniform_int_distribution<> d {10, 100};

这只是定义了一个分布对象d,它指定了值如何在范围内分布。为了获得该范围内的随机数,我们需要一个随机数引擎,我们可以将它传递给函数调用操作符d,它将返回随机整数。在random头中定义了几个随机数引擎。这里我们将使用最简单的,我们可以这样定义:

std::random_device random_number_engine;

为了在分布定义的范围内产生一个随机值,d,我们可以写出:

auto value = d(random_number_engine);            // Calls operator()() for d

存储在value中的值将是由d定义的分布中的一个整数。

包含模拟器完整程序的源文件如下所示:

// Ex3_02.cpp

// Simulating a supermarket with multiple checkouts

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <vector>                                // For vector container

#include <string>                                // For string class

#include <numeric>                               // For accumulate()

#include <algorithm>                             // For min_element & max_element

#include <random>                                // For random number generation

#include "Customer.h"

#include "Checkout.h"

using std::string;

using distribution = std::uniform_int_distribution<>;

// Output histogram of service times

void histogram(const std::vector<inT>& v, int min)

{

string bar (60, '*');                          // Row of asterisks for bar

for (size_t i {}; i < v.size(); ++i)

{

std::cout << std::setw(3) << i+min << " "    // Service time is index + min

<< std::setw(4) << v[i] << " "             // Output no. of occurrences

<< bar.substr(0, v[i])                     // ...and that no. of asterisks

<< (v[i] > static_casT<inT>(bar.size()) ? "..." : "")

<< std::endl;

}

}

int main()

{

std::random_device random_n;

// Setup minimum & maximum checkout periods - times in minutes

int service_t_min {2}, service_t_max {15};

distribution service_t_d {service_t_min, service_t_max};

// Setup minimum & maximum number of customers at store opening

int min_customers {15}, max_customers {20};

distribution n_1st_customers_d {min_customers, max_customers};

// Setup minimum & maximum intervals between customer arrivals

int min_arr_interval {1}, max_arr_interval {5};

distribution arrival_interval_d {min_arr_interval, max_arr_interval};

size_t n_checkouts {};

std::cout << "Enter the number of checkouts in the supermarket: ";

std::cin >> n_checkouts;

if(!n_checkouts)

{

std::cout << "Number of checkouts must be greater than 0\. Setting to 1." << std::endl;

n_checkouts = 1;

}

std::vector<CheckouT> checkouts {n_checkouts};

std::vector<inT> service_times(service_t_max-service_t_min+1);

// Add customers waiting when store opens

int count {n_1st_customers_d(random_n)};

std::cout << "Customers waiting at store opening: " << count << std::endl;

int added {};

int service_t {};

while (added++ < count)

{

service_t = service_t_d(random_n);

std::min_element(std::begin(checkouts), std::end(checkouts))->add(Customer(service_t));

++service_times[service_t - service_t_min];

}

size_t time {};                                  // Stores time elapsed

const size_t total_time {600};                   // Duration of simulation - minutes

size_t longest_q {};                             // Stores longest checkout queue length

// Period until next customer arrives

int new_cust_interval {arrival_interval_d(random_n)};

// Run store simulation for period of total_time minutes

while (time < total_time)                        // Simulation loops over time

{

++time;                                        // Increment by 1 minute

// New customer arrives when arrival interval is zero

if (--new_cust_interval == 0)

{

service_t = service_t_d(random_n);           // Random customer service time

std::min_element(std::begin(checkouts), std::end(checkouts))->add(Customer(service_t));

++service_times[service_t - service_t_min];  // Record service time

// Update record of the longest queue occurring

for (auto & checkout : checkouts)

longest_q = std::max(longest_q, checkout.qlength());

new_cust_interval = arrival_interval_d(random_n);

}

// Update the time in the checkouts - serving the 1st customer in each queue

for (auto & checkout : checkouts)

checkout.time_increment();

}

std::cout << "Maximum queue length = " << longest_q << std::endl;

std::cout << "\nHistogram of service times:\n";

histogram(service_times, service_t_min);

std::cout << "\nTotal number of customers today: "

<< std::accumulate(std::begin(service_times), std::end(service_times), 0)

<< std::endl;

}

using指令是为了节省打字和简化代码。客户服务时间的发生率记录在一个vector容器中。服务时间值用于索引vector元素,每次通过减去服务时间范围内的最小值来递增,这使得最低服务时间的出现次数被记录在vector的第一个元素中。histogram()函数以水平条形图的形式生成该范围内每个服务时间出现次数的直方图。

唯一的输入是结帐数量。我选择 600 分钟作为模拟的持续时间,但是你也可以安排输入这个,以及其他参数。main()函数为客户服务时间、商店开门时在门口等待的客户数量以及客户到达的时间间隔创建分布对象。言下之意,客户一次到一个;您可以很容易地扩展该程序,使每次到达的顾客数量在一个范围内随机变化。

顾客总是被分配到排队最短的收银台。通过调用返回一个范围内最小元素的min_element()算法,找到具有最短队列的Checkout对象。它使用<操作符来比较元素,但是该算法的另一个版本有第三个参数来指定比较函数。在时间模拟开始之前,商店开门时在门口等候的顾客的初始序列被添加到Checkout对象中,并且服务时间记录被更新。

模拟都在一个while循环中。每次迭代时,time增加一分钟。下一个客户new_cust_interval到达的时间周期在每次迭代中递减,当它达到零时,一个新的客户以新的随机服务时间被创建并附加到具有最短队列的Checkout对象上。此时longest_q变量也被更新,因为新的最长队列只能作为添加新客户的结果出现。

接下来,调用每个Checkout对象的time_increment()函数,以推进每个队列中第一个客户的处理。循环继续,直到时间达到存储在total_time中的值。

以下是一些输出示例:

Enter the number of checkouts in the supermarket: 3

Customers waiting at store opening: 18

Maximum queue length = 7

Histogram of service times:

2   16 ****************

3   20 ********************

4   13 *************

5   16 ****************

6   16 ****************

7   12 ************

8   11 ***********

9   14 **************

10   10 **********

11   20 ********************

12   15 ***************

13   15 ***************

14   14 **************

15   14 **************

Total number of customers today: 206

这有三个结账处。当我在两个收银台运行时,最长的队列长度增加到了 42——长到足以失去顾客。为了让模拟更加真实,你还可以做更多的事情。例如,均匀分布并不典型,顾客经常成群结队地到来。您还可以添加员工喝咖啡休息的效果,或者一名员工因感冒而咳嗽和打喷嚏的效果,这可能会鼓励一些客户避免结账。

使用 priority_queue 容器适配器

不出所料,priority_queue容器适配器定义了一个队列,其中的元素是有序的;优先级最高的元素(默认情况下是最大的元素)将位于队列的最前面。因为它是一个队列,所以只有第一个元素是可访问的,这意味着具有最高优先级的元素总是首先被处理。如何定义“优先”完全取决于你。如果priority_queue正在记录到达医院的事故和紧急设施的病人,病人情况的严重性将是优先的。如果要素是银行中的经常账户交易,借方很可能优先于贷方。

一个priority_queue的模板有三个参数,其中两个有默认实参;第一个是存储的对象的类型,第二个是用于存储元素的底层容器,第三个是 function 对象类型,它定义了确定元素顺序的谓词。因此,模板类型为:

template

<typename T, typename Container=std::vector<T>, typename Compare=std::less<T>> class priority_queue

如您所见,默认情况下,priority_queue实例包装了vector容器。在functional头中定义的less<T>函数对象类型是默认的排序谓词,这决定了容器中最大的对象将在最前面。functional header 还定义了greater<T>,您可以将它指定为最后一个模板参数,以便对元素进行排序,使最小的元素位于最前面。当然,如果指定最后一个模板参数,则必须提供另外两个模板类型参数。

图 3-3 中显示元素的方式反映了它们将被检索的顺序,这不一定是它们在vector中的排序方式,尽管它可能是。当我讨论堆的时候,我会解释为什么会这样。

A978-1-4842-0004-9_3_Fig3_HTML.gif

图 3-3。

A priority_queue container

创建优先级队列

您可以像这样创建一个空的优先级队列:

std::priority_queue<std::string> words;

还可以用一系列适当类型的对象初始化优先级队列。例如:

std::string wrds[] {"one", "two", "three", "four"};

std::priority_queue<std::string> words {std::begin(wrds), std::end(wrds)}; // "two" "three" "one "four"

该范围可以是来自任何源的序列,并且元素不需要排序。范围内的元素将在优先级队列中适当排序。

复制构造函数创建一个相同类型的现有priority_queue对象的副本——特别是,一个具有相同模板类型参数集的对象。例如:

std::priority_queue<std::string> copy_words {words};           // copy of words

还有一个带有右值引用参数的复制构造函数,它将移动 argument 对象。

当您希望以相反的方式对内容进行排序,将最小的对象放在优先级队列的前面时,您必须提供所有三个模板类型参数:

std::string wrds[] {"one", "two", "three", "four"};

std::priority_queue<std::string, std::vector<std::string>, std::greater<std::string>>

words1 {std::begin(wrds), std::end(wrds)};     // "four" "one" "three "two"

这创建了一个优先级队列,使用operator>()函数来比较范围内的string对象,因此它们的顺序与上面的words优先级队列中的顺序相反。

优先级队列可以使用任何容器来存储具有函数成员front()push_back()pop_back()size()empty()的元素。这包括一个deque容器,因此您可以使用它作为替代:

std::string wrds[] {"one", "two", "three", "four"};

std::priority_queue<std::string, std::deque<std::string>> words {std::begin(wrds), std::end(wrds)};

words优先级队列将来自wrds数组的string对象存储在一个带有默认比较谓词的deque容器中,因此它们的顺序与上面的words1相同。priority_queue构造函数将创建一个由第二个类型参数指定的类型的序列容器来保存元素,这将是priority_queue对象的内部。

您还可以创建一个vectordeque容器,并将其指定为初始化priority_queue的元素源。下面是如何创建一个使用来自vector容器的元素副本作为初始值集的priority_queue:

std::vector<inT> values{21, 22, 12, 3, 24, 54, 56};

std::priority_queue<inT> numbers {std::less<inT>(), values};

priority_queue构造函数的参数是用于排序元素的函数对象和提供初始元素集的容器。向量中元素的副本将使用函数对象在优先级队列中排序。values中的元素将按照它们原来的顺序,但是优先级队列中的元素将按照顺序:56 54  24  22  21  12  3。优先级队列用来存储元素的容器对于优先级队列来说是私有的,所以与内容交互的唯一方式是调用priority_queue对象的函数成员。作为第一个构造函数参数的函数对象的类型必须与指定为Compare模板类型参数的类型相同,默认情况下是less<T>。如果要使用不同类型的函数对象,必须指定所有模板类型参数。例如:

std::priority_queue<int, std::vector<inT>, std::greater<inT>>

numbers1 {std::greater<inT>(), values};

第三个类型参数是比较对象的类型。如果您需要指定这一点,您还必须指定前两个——元素类型和基础容器的类型。

优先级队列的操作

操作priority_queue需要的操作范围有限:

  • push(const T& obj)obj的一个副本按顺序推入容器中的适当位置,这通常涉及到一个排序操作。
  • push(T&& obj)obj移动到容器中序列中适当的位置,这通常涉及到一个排序操作。
  • emplace(T constructor args...)通过调用带有传递参数的构造函数,在序列中的适当位置构造一个T对象。这通常需要排序操作来保持优先级顺序。
  • 返回优先级队列中第一个对象的引用。
  • pop()删除第一个元素。
  • size()返回队列中对象的数量。
  • 如果队列为空,则返回 true。
  • 将此队列的元素与变元的元素互换,变元必须包含相同类型的对象。

priority_queue实现赋值操作符,以将作为右操作数的对象的元素赋给作为左操作数的对象;定义了赋值操作符的复制和移动版本。注意,没有为priority_queue容器定义比较操作符。显然,添加新元素通常会非常慢,因为排序可能是维持顺序所必需的。在后面关于堆的章节中,我会多说一点关于priority_queue的内部操作。

以下是如何记录从键盘输入到priority_queue中的数据的示例:

std::priority_queue<std::string> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

break;

words.push(word);

}

输入Ctrl+Z设置输入流中文件状态的结束,因此这用于结束输入循环。一个istream对象的operator>>()成员返回该对象,因此我们可以调用eof()来使用if条件中的表达式测试cin的状态。输入的单词将被排序,最大的单词在words队列的最前面——输入被自动排序。

priority_queue没有迭代器。如果您想要访问所有的元素——例如列出或复制它们——您将清空队列;一个queue和一个priority_queue有相同的限制。如果您想在这样的操作之后保留它,您将需要首先复制它,但是如果是这种情况,您可能应该使用不同类型的容器。你可以这样列出上面单词优先级队列的内容:

std::priority_queue<std::string> words_copy {words};  // A copy for output

while (!words_copy.empty())

{

std::cout << words_copy.top() << " ";

words_copy.pop();

}

std::cout << std::endl;

这首先制作了一个words的副本,因为仅仅输出words就会删除内容。在输出了top()返回的元素后,我们通过调用pop()移除它,使下一个元素可访问。在循环条件中调用empty()会在所有元素都被移除后结束循环。您可以使用表达式words_copy.size()来控制循环何时结束,因为返回值将被隐式转换为bool,所以当size()返回0时,结果是false

如果words的输入为:

one two three four five six seven

^Z

输出将是:

two three six seven one four five

当然,如果你需要不止一次地输出一个priority_queue的内容,在函数中输出会更好。你可以说得更一般一点,就像这样:

template<typename T>

void list_pq(std::priority_queue<T> pq, size_t count = 5)

{

size_t n{count};

while (!pq.empty())

{

std::cout << pq.top() << " ";

pq.pop();

if (--n) continue;

std::cout << std::endl;

n = count;

}

std::cout << std::endl;

}

参数是通过值传递的,因此这将处理优先级队列的一个副本。它是一个模板,适用于任何实现了输出到ostream对象的operator<<()的类型。如果省略第二个参数,默认情况下输出值为每行 5 个。当然,您可以定义一个类似的函数模板来处理queue容器适配器对象。

您可以像这样使用priority_queueemplace()函数成员:

words.emplace("nine");

作为参数的string文字将被用作调用string类构造函数的参数,以在容器中就地创建对象。这比这个语句要有效得多:

words.push("nine");

在这里,编译器将插入一个string构造函数调用来创建从string文字到push()的参数,然后用这个临时的string对象作为参数调用push()。然后,push()函数将调用string类复制构造函数来创建添加到容器中的对象。

我们可以将一些代码片段组合成一个完整的程序:

// Ex3_03.cpp

// Exercising a priority queue container adapter

#include <iostream>                              // For standard streams

#include <queue>                                 // For priority_queue<T>

#include <string>                                // For string class

using std::string;

// List contents of a priority queue

template<typename T>

void list_pq(std::priority_queue<T> pq, size_t count = 5)

{

size_t n {count};

while (!pq.empty())

{

std::cout << pq.top() << " ";

pq.pop();

if (--n) continue;

std::cout << std::endl;

n = count;

}

std::cout << std::endl;

}

int main()

{

std::priority_queue<std::string> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

break;

words.push(word);

}

std::cout << "You entered " << words.size() << " words." << std::endl;

list_pq(words);

}

以下是一些示例输出:

Enter words separated by spaces, enter Ctrl+Z on a separate line to end:

one two three four five six seven eight nine ten eleven twelve

^Z

You entered 12 words:

two twelve three ten six

seven one nine four five

eleven eight

list_pq<T>()函数模板实例的输出显示输入被优先级队列排序。

堆不是容器,但它是数据的一种特殊组织。堆通常存储在序列容器中。堆很重要,因为它们会出现在许多不同的计算机进程中。为了理解什么是堆,你首先需要有一个什么是树的概念,所以我将首先解释什么是树数据结构。

树是元素或节点的分层排列。每个节点都有一个键,它是存储在节点中的对象——就像链表中的一个节点。父节点是具有一个或多个子节点的节点。一般来说,父节点可以有任意数量的子节点,树中的父节点不必有相同数量的子节点。没有子节点的节点称为叶节点。父节点的键与其子节点的键之间通常存在某种关系。一棵树总是有一个根节点,这个根节点是树的基础,所有的子节点都可以从这个根节点到达。

图 3-4 显示了 2014 年世界杯足球赛决赛结果的树。德国总体赢了,所以是根节点;它在决赛中击败了巴西,所以它的子节点是它自己和巴西。每个父节点最多有两个子节点的树称为二叉树。图 3-4 中的树是一个完整的二叉树,因为每个父节点都有两个子节点。任意树中的父节点需要指针来标识子节点。一个完整的二叉树可以存储为一个数组或其他序列,如vector,而不需要指向子节点的指针,因为每一层的节点数是已知的。如果将树中的每一层编号为n,从根节点开始为 0 层,每一层包含2 n 节点。图 3-4 显示了世界杯比赛树中的节点如何存储在一个数组中;每个节点上面的整数是索引值。根节点存储为第一个数组元素,后面是它的两个子节点。这些子节点的子节点对按顺序出现在下一个节点,依此类推,直到叶节点。存储在数组中索引n处的节点的父节点的索引总是(n-1)/2的整数结果。如果数组元素的索引从 1 开始,那么索引为n的节点的父节点的索引的表达式甚至更简单,即n/2的整数值。

A978-1-4842-0004-9_3_Fig4_HTML.gif

图 3-4。

Example of a binary tree

我现在可以定义一个堆:堆是一个完整的二叉树,其中每个节点相对于其子节点是有序的。父节点要么总是大于或等于其子节点,这种情况称为最大堆,要么总是小于或等于其子节点,这种情况称为最小堆。注意,给定父节点的子节点在堆中不一定相对于彼此排序。

创建堆

您需要处理堆的函数是由algorithm头中的模板定义的。max_heap()函数将在随机访问迭代器定义的范围内重新排列元素,使它们形成一个堆。为此,它默认使用<操作符,这会产生一个最大堆。这里有一个例子:

std::vector<double> numbers { 2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0 };

std::make_heap(std::begin(numbers), std::end(numbers)); // Result: 12 10 3.5 6.5 8 2.5 1.5 6

在执行了make_heap()调用后,vector中的元素将如注释所示。这意味着如图 3-5 所示的结构。

A978-1-4842-0004-9_3_Fig5_HTML.gif

图 3-5。

The tree that a heap represents

根节点是 12,10 和 3.5 是子节点。值为 10 的元素的子节点是 6.5 和 8,值为 3.5 的元素的子节点是 2.5 和 1.5。值为 6.5 的元素有一个叶节点作为值为 6 的子节点。

一个priority_queue就是一堆!在幕后,priority_queue的一个实例创建了一个堆。这就是为什么图 3-3 没有反映元素在底层容器中的排列方式。在堆中,在所有连续的元素对之间不一定应用相同的比较关系。图 3-5 所示堆中的前三个元素是降序排列的,但是第四个元素比第三个元素大。那么,为什么 STL 既有用于堆的priority_queue的功能,又有创建堆的能力,尤其是因为你可以使用堆作为优先级队列?

嗯,priority_queue比堆更有优势;要素顺序是自动维护的。你不能打乱一个priority_queue的有序状态,因为除了第一个元素,你不能直接访问任何其他元素。如果您想要的只是一个优先队列,这是一个很大的优势。

另一方面,使用make_heap()创建的堆比使用priority_queue有一些优势:

  • 您可以访问堆中的任何元素,不仅仅是最大的元素,因为这些元素存储在一个容器中,比如您拥有的一个vector中。这确实有可能意外打乱元素的顺序,但是您总是可以通过调用make_heap()来恢复它。
  • 您可以在任何具有随机访问迭代器的序列容器中创建一个堆。这包括您定义的普通数组、string对象或容器。这意味着您可以在任何需要的时候将这样一个序列容器中的元素排列成一个堆,并且如果需要的话可以重复排列。您甚至可以将元素的子集安排成一个堆。
  • 如果使用维护堆顺序的堆函数,可以将堆用作优先级队列。

还有另一个版本的make_heap(),它有第三个参数,您可以为它提供一个比较函数,用于对堆进行排序。通过指定一个定义大于号操作符的函数,您将创建一个最小堆。为此,您可以使用来自functional头的谓词。例如:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 1.5 6 2.5 6.5 8 12 3.5 10

您可以为greater指定模板类型参数。这里带有空尖括号的版本推导出了类型参数和返回类型。使用make_heap()函数在容器中创建了一个堆之后,您可以对其应用一系列操作,所以接下来让我们来看看这些操作。

堆操作

因为堆不是容器,而是容器中元素的特定组织,所以只能通过 begin 迭代器和 end 迭代器将堆标识为一个范围。这意味着您可以在一个容器中创建一个元素子序列堆。创建了一个堆之后,你肯定会想要添加元素。algorithm中的push_heap()模板函数可以做到这一点,但是乍一看,它的方式似乎有点奇怪。要将一个元素添加到一个堆中,首先要通过对序列有效的任何方法将该元素添加到序列中。然后,调用push_heap()将最后一个元素——您添加的元素——插入序列中的正确位置,以保持堆的排列。例如:

std::vector<double> numbers { 2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers)); // Result: 12 10 3.5 6.5 8 2.5 1.5 6

numbers.push_back(11);                                  // Result: 12 10 3.5 6.5 8 2.5 1.5 6 11

std::push_heap(std::begin(numbers), std::end(numbers)); // Result: 12 11 3.5 10 8 2.5 1.5 6 6.5

注释显示了每个操作对numbers中元素的影响。向堆中添加元素的过程必须这样进行。只能通过调用函数成员向容器中添加新元素——接收迭代器指定范围的函数没有添加元素的机制。是push_back()将元素添加到序列的末尾,push_heap()恢复堆的顺序。通过调用push_heap(),您发出信号表明您已经将一个元素添加到一个堆中,这可能打乱了堆的顺序。因此,push_heap()函数假定最后一个元素是新的,并重新排列序列中的元素以维护堆。从结果中可以看出,在这种情况下,重要的重排是必要的。您还会注意到,虽然序列是一个堆,但元素并不完全是降序排列的。这清楚地表明,虽然优先级队列是一个堆,但是堆中元素的顺序不一定与优先级队列中的顺序相同。

当然,如果您使用自己的比较函数创建堆,那么您必须使用与push_heap()相同的比较函数:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 1.5 6 2.5 6.5 8 12 3.5 10

numbers.push_back(1.2);                                  // Result: 1.5 6 2.5 6.5 8 12 3.5 10 1.2

std::push_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 1.2 1.5 2.5 6 8 12 3.5 10 6.5

如果不将第三个参数指定为与第三个参数相同,代码将无法正常工作。评论中显示的结果与 6.5 last 看起来有点奇怪,但图 3-6 中显示的堆树会澄清这一点。

A978-1-4842-0004-9_3_Fig6_HTML.gif

图 3-6。

A heap of floating-point values

从树中可以明显看出,6.5 是 6 的子节点,而不是 10 的子节点,所以堆的组织是应该的。

移除最大的元素有点类似于向堆中添加元素,但是事情发生的方式正好相反。首先调用pop_heap(),然后从容器中移除最大的元素,就像这样:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers)); // Result: 12 10 3.5 6.5 8 2.5 1.5 6

std::pop_heap(std::begin(numbers), std::end(numbers));  // Result: 10 8 3.5 6.5 6 2.5 1.5 12

numbers.pop_back();                                     // Result: 10 8 3.5 6.5 6 2.5 1.5

pop_heap()函数将第一个元素放在最后,然后确保其余的元素仍然表示一个堆。然后调用vectorpop_back()成员来删除最后一个元素。

如果您使用自己的比较函数和make_heap()创建了堆,那么您还必须将该函数作为第三个参数提供给pop_heap():

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 1.5 6 2.5 6.5 8 12 3.5 10

std::pop_heap(std::begin(numbers), std::end(numbers),

std::greater<>());  // Result: 2.5 6 3.5 6.5 8 12 10 1.5

numbers.pop_back();                                     // Result: 2.5 6 3.5 6.5 8 12 10

从注释中显示的操作结果来看,您需要为pop_heap()提供比较器的原因显而易见。该函数不只是将第一个元素与最后一个元素交换;它还对从begin(numbers)end(numbers)-1范围内的元素进行重新排序,以保持堆的顺序。为了正确地做到这一点,pop_heap()必须使用与make_heap()相同的比较函数。

因为您可以对存储堆的容器做一些事情来打乱堆,所以 STL 提供了一种检查序列是否仍然是堆的方法:

if (std::is_heap(std::begin(numbers), std::end(numbers)))

std::cout << "Great! We still have a heap.\n";

else

std::cout << "Oh bother! We messed up the heap.\n";

如果范围是堆,is_heap()函数返回true。当然,这里是使用默认比较谓词less<>的实例来检查顺序。如果堆是使用greater<>的实例创建的,结果将是错误的。在这种情况下,为了得到正确的结果,你必须使用表达式std::is_heap(std::begin(numbers),std::end(numbers),std::greater<>())

另一个检查工具是检查一个部分是堆的范围。例如:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers),

std::greater<>());  // Result: 1.5 6 2.5 6.5 8 12 3.5 10

std::pop_heap(std::begin(numbers),  std::end(numbers),

std::greater<>());  // Result: 2.5 6 3.5 6.5 8 12 10 1.5

auto iter = std::is_heap_until(std::begin(numbers), std::end(numbers), std::greater<>());

if(iter != std::end(numbers))

std::cout << "numbers is a heap up to " << *iter << std::endl;

is_heap_until()函数返回一个迭代器,该迭代器指向范围中不按堆顺序排列的第一个元素。这个片段将输出最后一个元素1.5的值,因为它不在pop_heap()调用之后的堆序列中。如果整个范围是一个堆,那么函数返回结束迭代器,因此if语句是必要的,以确保您不会试图解引用结束迭代器。如果范围包含的元素少于两个,也将返回结束迭代器。还有第二个版本的is_heap_until(),带有两个参数,将使用默认谓词less<>

STL 提供的最后一个操作是sort_heap(),它对一个假定为堆的范围进行排序。如果它不是堆,那么在运行时会崩溃。带有两个参数(对应于定义范围的迭代器)的函数版本假设该范围是一个最大堆(即,使用一个less<>的实例进行排列),并将元素按升序排序。结果当然不会是最大堆。下面是一个使用它的例子:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers));  // Result: 12 10 3.5 6.5 8 2.5 1.5 6

std::sort_heap(std::begin(numbers), std::end(numbers));  // Result: 1.5 2.5 3.5 6 6.5 8 10 12

排序操作的结果显然不可能是最大堆,但它是最小堆,如图 3-7 中的树所示。虽然堆不一定是完全有序的,但是任何完全有序的序列都是堆。

A978-1-4842-0004-9_3_Fig7_HTML.gif

图 3-7。

A min heap resulting from sorting a max heap

第二个版本的sort_heap()有第三个参数,您可以为其提供用于创建堆的谓词。如果您用来创建堆的谓词是greater<>,那么它是一个 min 堆,结果将是元素按降序排序。结果不能是最小堆。这里有一个片段可以说明这一点:

std::vector<double> numbers {2.5, 10.0, 3.5, 6.5, 8.0, 12.0, 1.5, 6.0};

std::make_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 1.5 6 2.5 6.5 8 12 3.5 10

std::sort_heap(std::begin(numbers), std::end(numbers),

std::greater<>()); // Result: 12 10 8 6.5 6 3.5 2.5 1.5

正如最后一行中的注释所示,sort_heap()在最小堆上的结果是最大堆。

您知道algorithm头定义了一个sort()函数的模板,您可以用它来对堆进行排序,那么为什么会有一个sort_heap()函数呢?sort_heap()函数使用一种特殊的排序算法,不可思议的巧合是这种算法被称为堆排序。这首先创建一个堆,然后利用数据是部分有序的这一事实对数据进行排序;sort_heap()假设堆存在,所以它只做第二位。利用堆的部分排序可能会使排序更快,尽管情况并不总是如此。

在一个例子中,我们可以使用堆作为优先级队列,它是Ex3_03.cpp的变体:

// Ex3_04.cpp

// Using a heap as a priority queue

#include <iostream>                              // For standard streams

#include <iomanip>                               // For  stream manipulators

#include <algorithm>                             // For heap support functions

#include <string>                                // For string class

#include <deque>                                 // For deque container

using std::string;

// List a deque of words

void show(const std::deque<string>& words, size_t count = 5)

{

if(words.empty()) return;                      // Ensure deque has elements

// Find length of longest string

auto max_len = std::max_element(std::begin(words), std::end(words),

[](const string& s1, const string& s2)

{return s1.size() < s2.size(); })->size();

// Output the words

size_t n {count};

for(const auto& word : words)

{

std::cout << std::setw(max_len + 1) << word << " ";

if(--n) continue;

std::cout << std::endl;

n = count;

}

std::cout << std::endl;

}

int main()

{

std::deque<string> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

{

std::cin.clear();

break;

}

words.push_back(word);

}

std::cout << "The words in the list are:" << std::endl;

show(words);

std::make_heap(std::begin(words), std::end(words));

std::cout << "\nAfter making a heap, the words in the list are:" << std::endl;

show(words);

std::cout << "\nYou entered " << words.size() << " words. Enter some more:" << std::endl;

while (true)

{

if ((std::cin >> word).eof())

{

std::cin.clear();

break;

}

words.push_back(word);

std::push_heap(std::begin(words), std::end(words));

}

std::cout << "\nThe words in the list are now:" << std::endl;

show(words);

}

以下是一些输出示例:

Enter words separated by spaces, enter Ctrl+Z on a separate line to end:

one two three four five six seven

^Z

The words in the list are:

one    two  three   four   five

six  seven

After making a heap, the words in the list are:

two    one  three   four   five

six  seven

You entered 7 words. Enter some more:

eight nine ten twelve fifteen ninety forty fifty-three

^ Z

The words in the list are now:

two       twelve        three         nine          ten

six        seven        eighT&#x00A0;        four         five

one      fifteen       ninety        forty  fifty-three

这个例子在一个deque容器中创建了一个堆,只是为了与众不同;一辆vector也可以。show()函数是一个助手,它列出了deque<string>容器中的单词。对于简洁的输出,单词以比最大单词长度大 1 的固定字段宽度呈现。计算最大长度的语句使用在algorithm头中定义的max_element()函数模板的一个实例。该函数返回一个迭代器,该迭代器指向使用您提供的比较函数获得的范围中的最大元素。前两个参数是指定范围的迭代器。第三个参数是要使用的比较,在本例中是一个 lambda 表达式。

注意,max_element()函数需要定义一个小于操作来寻找最大元素,而不是大于。比较函数的形式应该是:

bool comp(const T1& a, const T2& b);

在大多数情况下,第一个参数的类型与第二个参数的类型相同,但通常类型可以不同。唯一的附带条件是范围中的元素必须可以隐式转换为类型T1T2。参数不需要指定为const,但是这样做是个好主意。在任何情况下,比较函数都不能改变传递给它的参数。

lambda 表达式只返回字符串参数的size()成员的值的比较结果。max_element()返回的迭代器将指向最长的字符串,因此它被用来调用size()成员以在max_len中记录它的长度。

按照您之前看到的方式从cin开始朗读单词。这里调用cinclear()成员来清除进入Ctrl+Z时设置的EOF状态。如果您不调用clear(),EOF状态将保持不变,因此您将无法在main()中以同样的方式从标准输入流中获得输入。

在读取一个单词序列后,通过调用make_heap()deque容器的内容排列成一个堆。然后我们读取更多的单词,但是这一次通过在每个单词被添加到容器后调用push_heap()来保持堆的顺序。push_heap()期望新元素被添加到容器的末尾;如果你使用push_front(),程序会崩溃,因为堆是无效的。输出显示,它一切正常。

当然,您可以在每个输入单词后使用push_heap(),在这种情况下,您不需要调用make_heap()。这个例子说明了在您的控制下底层容器如何使您能够访问整个序列并保留它,而不必像使用priority_queue容器适配器那样复制它。

在容器中存储指针

通常情况下,将指针存储在容器中比存储在对象中更好,而且大多数时候智能指针比原始指针更好。这有几个原因:

  • 在容器中存储指针意味着指针被复制,而不是它们指向的对象。复制指针通常比复制对象快得多。
  • 通过在容器中存储指针,可以获得多态行为。一个定义为存储指向基本类型元素的指针的容器也可以存储指向派生类型对象的指针。当您需要处理具有公共基类的任意对象序列时,这是一个非常有用的功能。这种应用的一个常见示例是处理要显示的一系列对象,如直线、曲线和几何形状。
  • 对指针容器的内容进行排序将比对对象进行排序更快;只需要移动指针,而不需要移动对象。
  • 存储智能指针比存储原始指针更安全,因为空闲存储中的对象在不再被引用时会被自动删除。您不必担心可能的内存泄漏。默认情况下,不指向任何东西的智能指针是nullptr

众所周知,智能指针主要有两种类型:unique_ptr<T>shared_ptr<T>。一个unique_ptr对它所指向的任何对象拥有独占的所有权,而一个shared_ptr允许多个指向同一个对象的指针存在。还有一种weak_ptr<T>类型,这是一种智能指针,总是从shared_ptr<T>创建,用于避免shared_ptr s 可能出现的循环引用所导致的问题。通过将unique_ptr<T>类型的指针移动到容器中,可以将它们存储在容器中。例如,这可以编译:

std::vector<std::unique_ptr<std::string>> words;

words.push_back(std::make_unique<std::string>("one"));

words.push_back(std::make_unique<std::string>("two"));

vector存储类型为unique_ptr<string>的智能指针。make_unique<T>()函数创建对象和智能指针,并返回后者。因为结果是一个临时的unique_ptr<string>对象,调用带有右值引用参数的push_back()函数,所以不需要复制。添加一个unique_ptr对象的另一种方法是创建一个本地unique_ptr变量,并使用std::move()将其移动到容器中。但是,任何需要复制容器元素的后续操作都将失败。只能有一个。如果你必须能够复制元素,那么shared_ptr对象是你的不二选择;否则使用unique_ptr对象。

在序列容器中存储指针

我将首先解释在容器中使用原始指针可能遇到的问题,然后继续使用智能指针——这是推荐的方法。这里有一段代码从标准输入流中读取单词,并将指向string对象的指针存储在vector容器的空闲存储区中:

std::vector<std::string*> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

{

std::cin.clear();

break;

}

words.push_back(new std::string {word});       // Create object and store its address

}

作为push_back()的参数的表达式在自由存储中创建了一个string对象,因此push_back()的参数是该对象的地址。下面是如何列出words向量的内容;

for (auto& w : words)

std::cout << w << " ";

std::cout << std::endl;

如果您想使用迭代器来访问容器元素,输出字符串的代码如下所示:

for (auto iter = std::begin(words); iter != std::end(words); ++iter)

std::cout << **iter << " ";

std::cout << std::endl;

iter是一个迭代器,您必须解引用它才能访问它所指向的元素。该元素是一个指针,因此您必须取消对它的引用才能获得字符串对象;因此有了**iter这个说法。

在擦除指针元素以释放空闲存储内存时,您必须小心。如果不这样做,在指针被删除后,你将无法释放内存,除非你复制了指针。这是容器中原始指针内存泄漏的一个常见来源。下面是使用words向量时可能发生的情况:

for (auto iter = std::begin(words); iter != std::end(words) ; )

{

if (**iter == "one")

words.erase(iter);                         // Memory leak!

else

++iter;

}

这删除了一个指针,但是它所指向的内存仍然存在。每当删除原始指针元素时,首先释放内存:

for (auto iter = std::begin(words); iter != std::end(words) ; )

{

if (**iter == "one")

{

delete *iter;                              // Release the memory...

words.erase(iter);                         // ...then delete the pointer

}

else

++iter;

}

vector超出范围之前,您必须记得从自由存储中删除string对象。有一种方法可以做到这一点:

for (auto& w : words)

delete w;                                    // Delete the string pointed to

words.clear();                                 // Delete all the elements from the vector

使用索引访问指针,所以只需使用删除操作符来删除string对象。当循环结束时,vector中的所有元素都是无效指针,所以不要让vector处于这种状态是很重要的。调用clear()会删除所有元素,所以为向量调用size()现在会返回 0。当然,您可以使用迭代器,在这种情况下,循环将是:

for (auto iter = std::begin(words); iter != std::end(words); ++iter)

delete *iter;

如果你存储智能指针,就不需要担心在免费存储中释放内存。智能指针会解决这个问题。下面是读取字符串并将shared_ptr<string>对象存储在vector中的代码片段:

std::vector<std::shared_ptr<std::string>> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

{

std::cin.clear();

break;

}

words.push_back(std::make_shared<string>(word));    // Create smart pointer to string & store it

}

这和原始指针版本没有太大区别。vector模板的类型参数现在是std::shared_ptr<std::string>,而push_back()的参数调用make_shared(),这在自由存储中创建了string对象和指向它的智能指针;然后,该函数返回智能指针。因为智能指针是由自变量表达式创建的,所以将调用带有右值引用参数的版本push_back(),这将把指针移动到容器中。

模板类型参数可能有点麻烦,但是您总是可以使用 using 指令来简化代码的外观。例如:

using PString = std::shared_ptr<std::string>;

这样,您可以像这样定义vector容器:

std::vector<PString> words;

通过智能指针元素访问字符串与使用原始指针完全相同。输出words内容的两个早期片段与智能指针一样工作良好。当然,没有必要从免费存储中删除字符串对象;智能指针会解决这个问题。执行words.clear()会删除所有元素,因此将调用智能指针的析构函数;这也将释放为它们所指向的对象分配的内存。

为了防止vector容器过于频繁地为元素分配额外的内存,创建vector,然后调用reserve()来分配初始内存量。例如:

std::vector<std::shared_ptr<std::string>> words;

words.reserve(100);                              // Space for 100 smart pointers

这比用预定义数量的元素创建vector要好,因为这样做时,每个元素都将通过调用shared_ptr<string>构造函数来创建。这没什么大不了的,但是没有必要产生不必要的开销,即使开销很小。通常,每个智能指针所需的空间会比它所指向的对象所需的空间少很多,所以你可以用reserve()慷慨地分配空间。

存储shared_ptr<T>对象允许指针的副本存在于容器之外。如果你不需要这个功能,你应该使用unique_ptr<T>对象。下面是如何与words向量一起工作的:

std::vector<std::unique_ptr<std::string>> words;

std::string word;

std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";

while (true)

{

if ((std::cin >> word).eof())

{

std::cin.clear();

break;

}

words.push_back(std::make_unique<string>(word));    // Create smart pointer to string & store it

}

在代码中用“unique”代替“shared ”,本质上是一样的。

从本章前面的Ex3_02中,我们可以看到如何使用智能指针来实现超市结账模拟。在这个版本中,Customer类的定义将是相同的,但是Checkout类的定义可以使用智能指针,所以这将会改变,我们也可以在main()中使用它们。不需要指针的副本,所以我们可以从头到尾使用unique_ptr<T>。下面是Checkout.h头文件的新内容:

// Supermarket checkout - using smart pointers to customers in a queue

#ifndef CHECKOUT_H

#define CHECKOUT_H

#include <queue>                                  // For queue container

#include <memory>                                 // For smart pointers

#include "Customer.h"

using PCustomer = std::unique_ptr<Customer>;

class Checkout

{

private:

std::queue<PCustomer> customers;                // The queue waiting to checkout

public:

void add(PCustomer&& customer) { customers.push(std::move(customer)); }

size_t qlength() const { return customers.size(); }

// Increment the time by one minute

void time_increment()

{

if (customers.front()->time_decrement().done())  // If the customer is done...

customers.pop();                            // ...remove from the queue

};

bool operator<(const CheckouT& other) const { return qlength() < other.qlength(); }

bool operator>(const CheckouT& other) const { return qlength() < other.qlength(); }

};

#endif

我们需要指令包含memory头,以使智能指针类型的模板可用。记录顾客在收银台排队的queue容器存储了PCustomer元素。PCustomer被一个using指令定义为std::unique_ptr<Customer>的别名,这节省了大量的输入。PCustomer对象不能被复制,所以add()的参数是一个右值引用,当函数被调用时,参数被移入容器。对于作为唯一指针的元素,它必须一直移动;当然,参数不能是const。在类定义中不需要其他的改变,所以使用unique_ptr所需要的改变是非常适度的。

下面是使用unique_ptr实现超市模拟的main()程序:

// Ex3_05.cpp

// Using smart pointer to simulate supermarket checkouts

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <vector>                                // For vector container

#include <string>                                // For string class

#include <numeric>                               // For accumulate()

#include <algorithm>                             // For min_element & max_element

#include <random>                                // For random number generation

#include <memory>                                // For smart pointers

#include "Checkout.h"

#include "Customer.h"

using std::string;

using distribution = std::uniform_int_distribution<>;

using PCheckout = std::unique_ptr<CheckouT>;

// Output histogram of service times

void histogram(const std::vector<inT>& v, int min)

{

string bar (60, '*');                          // Row of asterisks for bar

for (size_t i {}; i < v.size(); ++i)

{

std::cout << std::setw(3) << i+min << " "    // Service time is index + min

<< std::setw(4) << v[i] << " "             // Output no. of occurrences

<< bar.substr(0, v[i])                     // ...and that no. of asterisks

<< (v[i] > static_casT<inT>(bar.size()) ? "..." : "")

<< std::endl;

}

}

int main()

{

std::random_device random_n;

// Setup minimum & maximum checkout periods - times in minutes

int service_t_min {2}, service_t_max {15};

std::uniform_int_distribution<> service_t_d {service_t_min, service_t_max};

// Setup minimum & maximum number of customers at store opening

int min_customers {15}, max_customers {20};

distribution n_1st_customers_d {min_customers, max_customers};

// Setup minimum & maximum intervals between customer arrivals

int min_arr_interval {1}, max_arr_interval {5};

distribution arrival_interval_d {min_arr_interval, max_arr_interval};

size_t n_checkouts {};

std::cout << "Enter the number of checkouts in the supermarket: ";

std::cin >> n_checkouts;

if(!n_checkouts)

{

std::cout << "Number of checkouts must be greater than 0\. Setting to 1." << std::endl;

n_checkouts = 1;

}

std::vector<PCheckouT> checkouts;

checkouts.reserve(n_checkouts);                // Reserve memory for pointers

// Create the checkouts

for (size_t i {}; i < n_checkouts; ++i)

checkouts.push_back(std::make_unique<CheckouT>());

std::vector<inT> service_times(service_t_max-service_t_min+1);

// Add customers waiting when store opens

int count {n_1st_customers_d(random_n)};

std::cout << "Customers waiting at store opening: " << count << std::endl;

int added {};

int service_t {};

// Define comparison lambda for pointers to checkouts

auto comp = [](const PCheckouT& pc1, const PCheckouT& pc2){ return *pc1 < *pc2; };

while (added++ < count)

{

service_t = service_t_d(random_n);

auto iter = std::min_element(std::begin(checkouts), std::end(checkouts), comp);

(*iter)->add(std::make_unique<Customer>(service_t));

++service_times[service_t - service_t_min];

}

size_t time {};                                // Stores time elapsed

const size_t total_time {600};                 // Duration of simulation - minutes

size_t longest_q {};                           // Stores longest checkout queue length

// Period until next customer arrives

int new_cust_interval {arrival_interval_d(random_n)};

// Run store simulation for period of total_time minutes

while (time < total_time)                      // Simulation loops over time

{

++time;                                      // Increment by 1 minute

// New customer arrives when arrival interval is zero

if (--new_cust_interval == 0)

{

service_t = service_t_d(random_n);         // Random customer service time

(*std::min_element(std::begin(checkouts),

std::end(checkouts), comp))->add(std::make_unique<Customer>(service_t));

++service_times[service_t - service_t_min];  // Record service time

// Update record of the longest queue length

for (auto& pcheckout : checkouts)

longest_q = std::max(longest_q, pcheckout->qlength());

new_cust_interval = arrival_interval_d(random_n);

}

// Update the time in the checkouts - serving the 1st customer in each queue

for (auto& pcheckout : checkouts)

pcheckout->time_increment();

}

std::cout << "Maximum queue length = " << longest_q << std::endl;

std::cout << "\nHistogram of service times:\n";

histogram(service_times, service_t_min);

std::cout << "\nTotal number of customers today: "

<< std::accumulate(std::begin(service_times), std::end(service_times), 0)

<< std::endl;

}

vector容器现在存储指向Checkout对象的唯一指针。vector的迭代器指向一个指向Checkout对象的指针——一个unique_ptr<CheckouT>对象——所以要调用迭代器识别的Checkout对象的函数成员,您必须解引用迭代器,然后使用间接成员选择操作符来调用函数。您可以在main()中更改的几个语句中看到这一点。默认情况下,min_element()算法对迭代器指向的元素使用<操作符来确定结果。默认情况下会比较智能指针,这不会产生正确的结果。我们需要向min_element()提供第三个参数来指定它应该使用的比较。这是由名为comp的 lambda 表达式定义的;之所以这样命名,是因为我们想不止一次地使用它。这个 lambda 解引用智能指针参数来访问Checkout对象,然后使用Checkout类的operator<()成员来比较它们。所有的CheckoutCustomer对象都是在自由商店中创建的。智能指针负责为它们释放内存。该版本模拟程序的输出与原始版本的输出相同。您可以在示例中使用shared_ptr<T>,但是执行起来会比较慢。unique_ptr<T>就执行时间和内存而言,对象在原始指针上的开销最小。

将指针存储在优先级队列中

我现在将集中讨论智能指针。存储原始指针本质上是一样的,只是您要负责删除它们所指向的对象。当你创建一个priority_queue或者一个堆时,一个排序关系是必要的,它决定了元素的顺序。当存储原始指针或智能指针时,您总是需要提供要使用的比较函数。如果你不这样做,指针将被比较,而不是他们指向的对象,这几乎肯定不是你想要的。让&‘2019;让我们考虑如何定义一个priority_queue来存储指向免费存储中的string对象的指针。我将假设以下指令在后续部分都有效,以保持代码片段中语句的长度合理:

using std::string;

using std::shared_ptr;

using std::unique_ptr;

我们需要定义一个函数对象来比较类型为shared_ptr<string>的指针所指向的对象。我将这样定义比较:

auto comp = [](const shared_ptr<string>& wp1, const shared_ptr<string>& wp2)

{ return *wp1 < *wp2; };

这将comp定义为比较两个智能指针指向的元素的 lambda 表达式。命名 lambda 的原因是我们可以将其类型指定为priority_queue模板的类型参数。下面是优先级队列的定义:

std::priority_queue<shared_ptr<string>, std::vector<shared_ptr<string>>, decltype(comp)>

words1 {comp};

第一个模板类型参数是存储的元素的类型,第二个是用于存储元素的容器的类型,第三个是用于比较元素的函数对象的类型。我们必须指定第三个模板类型参数,因为 lambda 表达式的类型将不同于默认的比较类型std::less<T>

您仍然可以指定一个外部容器来初始化包含指针的priority_queue:

std::vector<shared_ptr<string>> init {std::make_shared<string>("one"),

std::make_shared<string>("two"),

std::make_shared<string>("three"),

std::make_shared<string>("four")};

std::priority_queue<shared_ptr<string>, std::vector<shared_ptr<string>>, decltype(comp)>

words(comp, init);

init向量具有通过调用make_shared<string>()创建的初始值。优先级队列构造函数的参数是定义如何比较元素的对象,以及作为初始化元素源的容器。复制vector中的智能指针并用于初始化words优先级队列。当然,用另一个容器中的元素初始化优先级队列意味着不能使用unique_ptr<string>元素——它们必须是shared_ptr<string>

如果不需要保留初始的元素集,可以调用priority_queue对象的emplace()成员在容器中创建它们:

std::priority_queue<shared_ptr<string>, std::vector<shared_ptr<string>>, decltype(comp)>

words1 {comp};

words1.emplace(new string {"one"});

words1.emplace(new string {"two"});

words1.emplace(new string {"three"});

words1.emplace(new string {"five"});

words1emplace()成员将调用存储对象类型的构造函数,这将是shared_ptr<string>构造函数。这个构造函数的参数是一个string对象的地址,这个对象是在自由存储中创建的,这个自由存储是由作为emplace()参数的表达式产生的。这个片段将存储指向优先级队列中的string对象的四个指针,这些指针指向包含"two," "three," "one," "five"的对象。优先级队列中元素的顺序由本节前面定义的comp决定。

当然,如果您不想保留初始的元素集,您可以将unique_ptr<string>元素存储在优先级队列中。例如:

auto ucomp = [](const std::unique_ptr<string>& wp1, const std::unique_ptr<string>& wp2)

{ return *wp1 < *wp2; };

std::priority_queue<std::unique_ptr<string>, std::vector<std::unique_ptr<string>>,

decltype(ucomp)> words2 {ucomp};

定义比较的 lambda 表达式现在接受对unique_ptr<string>对象的引用。我们必须为优先级队列指定所有三个模板类型参数,因为我们需要指定比较器类型。第二个模板类型参数现在可以是deque<string>,这是使用的默认容器类型。您仍然可以使用emplace()将元素添加到优先级队列中:

words2.emplace(new string{"one"});

words2.emplace(new string {"two"});

words2.emplace(new string {"three"});

words2.emplace(new string {"five"});

或者,您可以使用push():

words2.push(std::make_unique<string>("one"));

words2.push(std::make_unique<string>("two"));

words2.push(std::make_unique<string>("three"));

words2.push(std::make_unique<string>("five"));

make_unique<string>()返回的对象将被移动到容器中,因为带有右值引用参数的push()版本将被自动选择。

指针堆

在创建指针堆时,需要提供一个比较对象的函数指针。这里有一个如何做到这一点的例子:

std::vector<shared_ptr<string>> words

{ std::make_shared<string>("one"), std::make_shared<string>("two"),

std::make_shared<string>("three"), std::make_shared<string>("four") };

std::make_heap(std::begin(words), std::end(words),

[](const shared_ptr<string>& wp1, const shared_ptr<string>& wp2){ return *wp1 < *wp2; });

make_heap()的第三个参数是定义比较函数的 lambda 表达式。它只是通过取消智能指针的引用来比较字符串对象。make_heap()函数的模板有一个函数对象类型的参数。与priority_queue容器适配器的类模板不同,模板参数没有默认实参值,因此编译器将从函数调用中的第三个实参推断出函数对象的类型。如果此函数模板的第三个类型参数指定了默认类型,则必须指定类型参数,这在优先级队列模板的情况下是必要的。

您必须提供与在函数push_heap()pop_heap()is_heap()is_heap_until()的任何调用中使用make_heap()作为最后一个参数相同的比较函数。当然,您可以使用一个命名的 lambda 表达式来实现这一点。如果您调用sort_heap(),您还需要提供一个比较函数。

基类指针的容器

您可以将指向派生类对象的指针存储在用基类类型的元素定义的任何容器或容器适配器中。这将使您能够获得容器元素所指向的对象的多态行为。我们将需要一个基类和一个派生类来探索各种可能性,所以让我们从恢复上一章中的Ex2_06中的Box类开始,并对它做一点修改。下面是类的定义:

class Box

{

protected:

size_t length {};

size_t width {};

size_t height {};

public:

explicit Box(size_t l=1, size_t w=1, size_t h=1) : length {l}, width {w}, height {h} {}

virtual ∼Box() = default;

virtual double volume() const;                 // Volume of a box

// Comparison operators for Box object

bool operator<(const Box& box) const;

bool operator==(const Box& box) const;

// Stream input and output

virtual std::istream& read(std::istream& in);

virtual std::ostream& write(std::ostream& out) const;

};

我们将从Box派生一个Carton类,因此数据成员是protected,析构函数被指定为virtual。这在这里并不重要,但是将基类中的析构函数声明为virtual是一个好习惯;这导致了最小的开销,并且防止了为派生类调用错误的析构函数的可能性。执行流 I/O 的成员volume()和两个成员read()write()是虚拟的,因此必要时可以在派生类中重写它们。

volume()函数可以这样定义为Box.h文件中的inline:

inline double Box::volume() const { return length*width*height; }

比较功能也可以是inline:

// Less-than operator

inline bool Box::operator<(const Box& box) const

{ return volume() < box.volume(); }

//Equality comparion operator

inline bool Box::operator==(const Box& box) const

{

return length == box.length && width == box.width && height == box.height;

}

小于运算符函数比较操作数的大小,而相等运算符函数比较数据成员的值。我们还可以为Box对象的流提取和插入操作符定义重载,如Box.h中的inline:

// Stream extraction operator

inline std::istream& operator>>(std::istream& in, Box& box)

{

return box.read(in);

}

// Stream insertion operator

inline std::ostream& operator<<(std::ostream& out, Box& box)

{

return box.write(out);

}

这些操作符函数各自调用适当的函数成员read()write()来执行操作。成员函数是virtual,所以如果它们在派生类中被重定义,这些操作符函数中的任何一个都将调用作为第二个参数传递的对象类型的版本read()write()

Box.h的完整内容是这样的:

// Box.h

// Defines the Box class that will be a base for the Carton class

#ifndef BOX_H

#define BOX_H

#include <iostream>                              // For standard streams

#include <istream>                               // For stream classes

#include <utility>                               // For comparison operator templates

using namespace std::rel_ops;                    // Comparison operator template namespace

// Definition for the Box class as above...

// Definitions for inline functions as above...

#endif

read()write()函数成员可以放在单独的源文件Box.cpp中,该文件将包含以下内容:

// Box.cpp

// Function members of the Box class

#include <iostream>

#include "Box.h"

// Read a Box object from a stream

std::istream& Box::read(std::istream& in)

{

size_t value {};

if ((in >> value).eof())

return in;

length = value;

in >> width >> height;

return in;

}

// Write a Box object to a stream

std::ostream& Box::write(std::ostream& out) const

{

out << typeid(*this).name() << "(" << length << "," << width << "," << height << ")";

return out;

}

它们以一种简单的方式被定义。请注意,当读取文件结束时,read()会将EOF指示器留在istream对象中;如你所知,这是从键盘读取Ctrl+Z(或从文件读取EOF)的结果,所以你可以用它来检测Box对象尺寸序列输入的结束。write()函数将对象的尺寸写在类型名后面的括号中。当前对象的类型名是通过调用type_info对象的name()成员获得的,该对象是应用typeid运算符的结果。因此,输出中将标识出写出的每个对象的特定类型。

派生类Carton的定义将放在Carton.h头文件中,该头文件将包含以下内容:

// Carton.h

#ifndef CARTON_H

#define CARTON_H

#include "Box.h"

class  Carton :public Box

{

public:

explicit Carton(size_t l = 1, size_t w = 1, size_t h = 1) : Box {l, w, h}{}

double volume() const override {return 0.85*Box::volume(); }

};

#endif

没什么大不了的。唯一的覆盖是针对返回常规Box对象 85%体积的volume()成员。显然Carton有更厚的侧面或者更多的内部包装,但是最重要的是它的体积将不同于相同尺寸的Box,所以我们将知道基类volume()成员的覆盖何时被调用。

下面是一个在容器中存储指向这些类型对象的指针的程序:

// Ex3_06.cpp

// Storing derived class objects in a container of base pointers

#include <iostream>                              // For standard streams

#include <memory>                                // For smart pointers

#include <vector>                                // For the vector container

#include "Box.h"

#include "Carton.h"

using std::unique_ptr;

using std::make_unique;

int main()

{

std::vector<unique_ptr<Box>> boxes;

boxes.push_back(make_unique<Box>(1, 2, 3));

boxes.push_back(make_unique<Carton>(1, 2, 3));

boxes.push_back(make_unique<Carton>(4, 5, 6));

boxes.push_back(make_unique<Box>(4, 5, 6));

for(auto&& ptr : boxes)

std::cout << *ptr << " volume is " << ptr->volume() << std::endl;

}

这个不需要过多解释。vector存储类型为std::unique_ptr<Box>的元素,因此我们可以存储指向Box对象的智能指针,或者指向将Box作为直接或间接基类的任何类型的对象的智能指针。通过调用其push_back()成员——在本例中是右值引用参数版本——,boxes容器中填充了指向BoxCarton对象混合的智能指针。using指令使代码更具可读性。在基于范围的for循环中输出vector中元素指向的每个对象的详细信息。在for循环中为ptr推导出的类型将是一个右值。

输出如下:

class Box(1,2,3) volume is 6

class Carton(1,2,3) volume is 5.1

class Carton(4,5,6) volume is 102

class Box(4,5,6) volume is 120

输出显示我们得到了对volume()函数成员的多态调用。operator<<()函数重载显示了正确的类类型,所以write()函数调用也是多态的。显然,当您需要处理层次结构中各种类型的元素时,智能指针提供了很多优势。通过将指向基类的智能指针存储在容器中,可以自动获得多态行为。您还可以自动释放自由存储内存。这适用于任何类型的容器或容器适配器。

将算法应用于一系列指针

算法处理由范围指定的数据。当范围与存储指针的容器相关时,范围中的迭代器指向指针。因此,您必须定义一个算法可能需要的函数对象来考虑这一点。到目前为止,我们只看到了一些算法,但是让我们用一些例子来看看其中的含义。

默认情况下,numeric头中定义的accumulate()算法使用operator+()对一系列元素求和。第二个版本的accumulate()有四个参数——前两个参数是范围的开始和结束迭代器,后两个参数是操作的初始值,还有一个 function 对象,它定义了在运行累加和每个元素之间依次应用的二元操作。您可以使用这个版本来提供默认操作的替代方案,如果范围包含指针,那么您必须使用这个版本来获得有效的结果。下面是如何使用accumulate()来连接矢量中由shared_ptr<string>对象指向的string对象:

using word_ptr = std::shared_ptr<std::string>;

std::vector<word_ptr> words {std::make_shared<string>("one"), std::make_shared<string>("two"),

std::make_shared<string>("three"), std::make_shared<string>("four")};

auto str = std::accumulate(std::begin(words), std::end(words), string {""},

[](const string& s, const word_ptr& pw)->string {return s + *pw + " "; });

accumulate()的第四个参数是要应用的二元运算,这是一个 lambda 表达式。accumulate()函数将收到的第三个参数作为第一个参数传递给二元函数。它将解引用该范围内的迭代器的结果作为第二个参数传递给函数。因此,lambda 的第一个参数必须是第三个参数的类型,第二个参数必须是范围内迭代器指向的元素的类型。存储在str中的结果将是字符串"one two three four"

您需要考虑到元素是应用谓词的容器的函数成员的指针。listforward_list容器具有的remove_if()函数就是一个例子:

std::lisT<word_ptr> wrds {std::make_shared<string>("one"), std::make_shared<string>("two"),

std::make_shared<string>("three"), std::make_shared<string>("four")};

wrds.remove_if([](const word_ptr& pw){ return (*pw)[0] == 't'; });

当字符串中的第一个字符是't'时,传递给remove_if()的 lambda 表达式将返回true。这将从列表中删除"two""three"。lambda 的参数将是一个指向string对象的智能指针,因此有必要取消对它的引用来访问字符串。

摘要

容器适配器模板提供了标准序列容器的专用接口;用于存储元素的容器只能通过适配器类提供的接口来访问。当您需要这些容器适配器提供的功能时,请选择其中一个,因为它们的接口很简单,并且比您自己用标准容器实现相同的功能更容易使用。使用来自algorithm头的函数模板在标准序列中创建堆的能力提供了与priority_queue容器适配器相同的功能,但是增加了访问存储元素的容器的灵活性。本章涵盖的要点包括:

  • stack<T>容器适配器模板实现了下推堆栈,这是一种 LIFO 检索机制。可以定义一个stack<T>实例,使其使用一个vector<T>deque<T>lisT<T>作为内部存储元素的容器。
  • queue<T>容器适配器模板实现了一个队列,它提供了一个 FIFO 检索机制。可以定义一个queue<T>实例,以便它使用一个deque<T>或一个lisT<T>作为内部存储元素的容器。
  • priority_queue<T>容器适配器模板实现了一个优先级队列,其中优先级最高的元素总是下一个可以被检索的元素。您可以定义一个priority_queue<T>实例,以便它使用一个vector<T>或一个deque<T>作为存储元素的容器。
  • 堆是一个二叉树,其中的节点是部分有序的,并且始终具有相同的顺序;树中的每个父节点要么大于或等于其子节点(最大堆),要么小于或等于其子节点(最小堆)。
  • 函数模板在随机访问迭代器指定的范围内创建一堆元素。默认情况下会创建一个最大堆,但是您可以提供一个 function 对象来定义元素的比较,以确定堆的顺序。堆操作由push_heap()pop_heap()支持,它们在添加一个元素或从范围中删除第一个元素后保持堆的顺序。
  • 您可以将指针存储在容器中。使用智能指针可以确保在不再需要对象时从空闲存储中正确删除它们。
  • 您必须为存储指针的容器提供算法所需的比较或其他操作的函数对象。

ExercisesWrite a program to store an arbitrary number of words entered from the keyboard in a deque<string> container. Copy the words to a lisT<string> container, sort the contents of the list in ascending sequence, and output the result.   Write a program that uses a stack<T> container adapter instance to reverse the sequence of letters in a sentence that is entered from the keyboard on a single line. The program should output the result and indicate whether or not the original sentence is a palindrome. (A palindrome is a sentence that can be read the same when reversed – if you ignore spaces and punctuation. For example: “Are we not drawn onward to a new era?”).   Write a program to use a priority_queue container adapter instance to present an arbitrary number of words entered from the keyboard in descending alphabetical sequence.   Repeat Exercise 1 but with the words created in the free store and referenced by smart pointers in the containers.   Repeat Exercise 3 but with smart pointers to words stored in the priority_queue container adapter.   Write a program to output an arbitrary number of words entered from the keyboard in descending alphabetical sequence. The process should store smart pointers to the words in a vector container in which you create a heap.

四、映射容器

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​4) contains supplementary material, which is available to authorized users.

通过位置从序列容器中检索元素。例如,您可以通过索引位置访问deque的第一个或最后一个元素,或者访问vector中的一个元素。映射容器的工作方式完全不同,这一章将会解释。在本章中,您将了解以下内容:

  • 什么是关联容器?
  • 什么是map容器,它通常是如何组织的。
  • 可用的map容器类型及其功能。
  • map容器提供的功能。
  • a pair是什么,是用来做什么的。
  • 什么是tuple以及如何使用它。

映射容器简介

序列容器是管理数据的有价值的工具,但是对于大量的应用程序来说,它们并不总是提供方便的数据访问机制。使用名称和地址是一个简单的例子,说明序列容器可能不符合您的要求。典型的操作是查找给定名称的地址。如果记录存储在一个序列容器中,你必须搜索。容器提供了一种更有效地存储和访问这些数据的方式。

映射容器是关联容器。在关联容器中,每个对象都是基于与该对象关联的键值来定位的。键可以是基本类型的值,也可以是类类型的对象。字符串经常被用作键,当您想要存储姓名和地址记录时,这很可能适用;名字是一个或多个字符串。一个对象在关联容器中的位置是如何由一个键决定的取决于容器的具体类型,而特定容器类型的内部组织在不同的 STL 实现中会有所不同。

有四种映射容器,每一种都由一个类模板定义。所有映射容器类型都存储键/值对。map 中的元素是类型为pair<const K,T>的对象,这些对象封装了类型为T的对象及其相关的类型为K的键。容器中 pair 元素的键是const,因为允许修改键会破坏容器中元素的顺序。映射容器的类模板各有不同的特征:

  • 一个map<K,T>容器存储了pair<const K,T>类型的元素,这些元素封装了键/对象对,其中键属于K类型,对象属于T类型。密钥必须是唯一的,因此不允许重复的密钥。只要对象的键不同,就可以存储重复的对象。元素是有序的,容器中元素的顺序是通过比较键来确定的。默认情况下,使用一个less<K>对象来比较键。
  • 一个multimap<K,T>容器类似于一个map<K,T>,因为元素是有序的。关键字必须是可比较的,元素的顺序通过比较关键字来确定。不同之处在于允许重复的键。因此一个multimap可以存储多个具有相同键值的pair<const K,T>元素。
  • 一个unordered_map<K,T>容器是一个映射,其中的pair<const K,T>对象不是直接按照键值排序的。使用从元素的键生成的哈希值来定位元素。哈希值是由一个叫做哈希的过程生成的整数,我将在本章后面解释这个过程。不允许重复的密钥。
  • 一个unordered_multimap<K,T>容器也使用从键中产生的哈希值来定位对象,但是重复的键是允许的。

在映射头中定义了mapmultimap的模板,在unordered_map头中定义了unordered_map,unordered_multimap的模板。您可以看到,map模板类型名称的任何前缀都标识了容器的特征:

  • 前缀multi表示键不需要唯一;它的缺失表明键必须是唯一的。
  • unordered_前缀表示使用从键生成的哈希值而不是比较键值来将元素放置在容器中。它的缺失意味着元素是通过比较键来排序的。

让我们先来看看map集装箱。

使用映射容器

map头中定义了map<K,T>类模板,它定义了一个存储类型为T的对象的映射,每个对象都有一个类型为K的相关键。容器中对象的位置是通过比较关键字来确定的。通过提供适当的键值,可以从map容器中检索一个对象。图 4-1 展示了一个map<K,T>容器,其中键是名字,对象是代表年龄值的整数值。

A978-1-4842-0004-9_4_Fig1_HTML.gif

图 4-1。

A conceptual representation of a map<K,T> container

图 4-1 中表示的容器将是类型map<Name, size_t>,其中Name类可以这样定义:

class Name

{

private:

std::string firstname{};

std::string secondname{};

public:

Name(std::string first, std::string second) : firstname{first}, secondname{second}{};

Name()=default;

bool operator<(const Name& name)

{ return secondname < name.secondname ||

((secondname == name.secondname) && (firstname < name.firstname)); }

};

存储在容器中的对象通常需要定义一个默认的构造函数,以允许在必要时创建默认元素。用于Name对象的operator<()函数将名称与secondname成员进行比较,当它们不同时确定顺序。如果secondname成员相等,则firstname成员确定比较结果。string类定义了operator<(),所以默认的less<string>比较将正常工作。

不要被使用less<K>map中的元素进行排序所误导——这些元素并不是简单有序的序列。STL map容器没有特定的组织要求,但是通常元素存储在一个平衡的二叉树中。平衡二叉树中的元素被组织成使得树的高度——根节点和叶节点之间的层数——最小化。如果每个节点的左子树的高度与其右子树的高度相差不超过 1,则称二叉树是平衡的。图 4-2 显示了图 4-1 所示映射的一种可能的平衡树配置。

A978-1-4842-0004-9_4_Fig2_HTML.gif

图 4-2。

Internal organization of a map container

图 4-2 中的树有三层,所以任何元素都可以在距离根最多三步的地方找到。选择根节点以最小化树高度,并且对于每个父节点,左侧子节点的键小于父节点的键,右侧子节点的键大于父节点的键。添加新元素会导致需要不同的根节点来保持平衡的树排列。显然,当元素被添加到容器中时,维护平衡的树组织会产生一些开销。这样做的好处是,与顺序排列或非平衡树相比,检索元素会更快,容器中的元素越多,平衡树组织就越有效。从包含n个元素的平衡二叉树中检索一个随机元素的时间是O(log2n);从序列中检索元素的时间是O(n)

Note

计算机操作上下文中的O(n)符号描述了当参数增加时,执行操作的时间是如何增加的。把O想成是暗示“的顺序”。O(n)表示执行时间随n.线性增加,执行一个O(log 2 n)操作的时间增加比n增加慢得多——因为它与log 2 n成正比。

创建映射容器

map类模板有四个类型参数,但是通常你只需要为前两个指定值。第一个是键的类型,第二个是要存储的对象的类型。第三和第四个模板参数分别定义用于比较键的函数对象的类型和用于在映射中分配内存的对象的类型。最后两个被赋予了默认值。在本节的稍后部分,我将展示如何定义不同类型的函数对象来比较键,但是我不会去定义替代的分配器类型。

一个map<>容器类的默认构造函数创建一个空映射。例如,下面是如何创建一个将年龄值存储为类型size_t并以类型string的名称作为关键字的映射容器:

std::map<std::string, size_t> people;

第一个模板类型参数指定键的类型为string,第二个模板类型参数指定值的类型为size_t。当然,这里的模板类型参数可以是任何类型,唯一的要求是使用less<K>键必须是可比较的,或者一个可选的函数对象类型,如果你指定它的话。

map<K,T>中的每个元素都是一个类型为pair<const K,T>的对象,它封装了一个对象和它的键,其中const K意味着键不能被修改。r the pair<T1,T2>类的模板在utility头中定义,包含在map头中。因此,people容器中的元素将是类型pair<const string, size_t>pair<T1,T2>模板类型并不专门用于这种情况。必要时,您可以自己使用它将两个不同类型的对象打包成一个对象。在这一章的后面我会有更多的话要说。

您可以使用初始化列表来指定映射中的初始值,但是由于映射包含pair<const K,T>元素,初始化列表中的值必须是这种类型。下面是如何为people容器指定初始值:

std::map<std::string, size_t> people{ {"Ann", 25}, {"Bill", 46}, {"Jack", 32}, {"Jill", 32} };

初始化列表中的值是通过将每个嵌套括号对之间的两个值传递给pair构造函数来创建的。因此,该列表将包含四个pair<const string, size_t>对象。

utility头定义了make_pair<T1, T2>()函数模板,它提供了一种组合T1T2类型对象的便捷方式。因此,您可以创建 pair 对象来初始化一个map,如下所示:

std::map<std::string, size_t> people{ std::make_pair("Ann", 25), std::make_pair("Bill", 46),

std::make_pair("Jack", 32), std::make_pair("Jill", 32) };

make_pair<T1, T2>()函数模板从函数参数中推导出类型参数值,因此列表中由make_pair<>()调用返回的对象将是类型pair<char const*, int>。因为这些是people贴图的初始值,这些pair对象将被转换成贴图中元素的类型,即pair<const string, size_t>。一个pair<T1,T2>对象有公共成员firstsecond,分别存储T1T2对象。pair<T1,T2>构造函数的模板提供了pair对象的隐式转换,只要原始pair对象的firstsecond成员可以隐式转换为目标pair对象的相同成员的类型。

map<K,T>模板定义了移动和复制构造函数,因此您可以复制一个现有的容器。例如:

std::map<std::string, size_t> personnel {people};   // Duplicate people map

personnel映射将包含peoplepair元素的副本。

您可以从另一个容器中的一系列pair元素创建一个映射。通过 begin 和 end 迭代器以通常的方式指定元素。显然,迭代器必须指向与容器兼容的类型的pair元素。这里有一个例子:

std::map<std::string, size_t> personnel {std::begin(people), std::end(people)};

这创建了personnel,并用people容器的迭代器标识的元素初始化它。map容器产生双向迭代器,所以你可以增加和减少它们。一个map也提供了反向迭代器,所以你可以从最后一个到第一个访问元素。personnel映射将包含与people映射相同的元素。当然,您可以用另一个容器中的元素子集创建一个容器:

std::map<std::string, size_t> personnel {++std::begin(people), std::end(people)};

在映射中插入元素

有几个版本的map<K,T>容器的insert()函数成员在map中插入一个或多个pair<const K,T>对象。元素只有在映射中不存在时才会被插入。下面的代码片段说明了如何插入单个元素:

std::map<std::string, size_t> people {std::make_pair("Ann", 25), std::make_pair("Bill", 46),

std::make_pair("Jack", 32), std::make_pair("Jill", 32)};

auto pr = std::make_pair("Fred", 22);                             // Create a pair element...

auto ret_pr = people.insert(pr);                                 // ..and insert it

std::cout << ret_pr.first->first << " " << ret_pr.first->second

<< " " << std::boolalpha << ret_pr.second << " \n";    // Fred 22 true

第一条语句创建了map容器,并用初始化列表中的四个值初始化它;在这种情况下,这些将被隐式转换为所需的类型。第二条语句创建了另一个要插入的pair对象。因为make_pair<>()函数模板的类型参数是从参数类型中推导出来的,所以pr对象将属于类型pair<const char*, int>,但是这个对象将在insert()操作中被隐式转换为容器元素类型。当然,如果您不想依赖隐式转换,您可以创建所需类型的pair对象:

auto pr = std::make_pair<std::string, size_t>(std::string {"Fred"}, 22);

make_pair<>()模板的显式模板类型参数决定了返回的pair对象的类型。您可以提供一个字符串作为第一个参数,隐式转换将被应用于创建键所需的string对象。您可以省略make_pair<>()的模板类型参数,让编译器推导它们。假设您将语句写成:

auto pr = std::make_pair("Fred", 22);                            // pair<const char*, int>

pair对象将不同于所需的类型。当您允许编译器推导模板类型参数时,make_pair()的参数会准确地决定pair的模板类型参数。第一个参数是类型为const char*的文字,第二个参数是类型为int的文字。话虽如此,但这在这种情况下并不重要,因为当您插入新元素时,pair对象可以隐式转换为容器所需的类型。需要小心的时候是没有从参数类型到容器中键和对象类型的隐式转换的时候。

insert()函数成员返回一个pair<iterator, bool>对象。对象的first成员是一个迭代器,它或者指向被插入的元素,或者指向阻止插入的元素。如果一个对象已经用相同的键存储在映射中,则是后一种情况。返回的对象的second成员是一个bool值,如果插入成功,该值为true,否则为false。正如您在输出语句中看到的,访问被插入的对的第一个成员的表达式是ret_pr.first->firstret_prfirst成员是一个指向pair对象的迭代器,所以您使用->操作符来访问它所指向的对象的first成员。输出显示该元素已被插入。您可以通过执行以下循环来验证这一点:

for(const auto& p : people)

std::cout << std::setw(10) << std::left << p.first << " " << p.second << " \n";

循环变量p将通过引用依次访问people图中的每个元素。输出将是:

Ann        25

Bill       46

Fred       22

Jack       32

Jill       32

这些元素按照关键字的升序排列,因为默认的less<string>函数对象用于在映射中对它们进行排序。

通过执行以下两条语句,您可以看到插入一个已经存在的元素的效果:

ret_pr = people.insert(std::make_pair("Bill", 48));

std::cout << ret_pr.first->first << " " << ret_pr.first->second

<< " " << std::boolalpha << ret_pr.second << "\n";     // Bill 46 false

这产生的输出显示在注释中。由insert()返回的 pair 对象的first成员指向已经在 map 中的具有匹配键的元素,并且second成员是false以指示不能进行插入。

如果您真的希望当元素存在时键"Bill"的年龄值被修改为48,您可以使用insert()返回的pair对象来实现,如下所示:

if(!ret_pr.second)                                             // If the element is there...

ret_pr.first->second = 48;                                   // ... change the age

当键已经存在于map中时,ret_pr的第二个成员是false,所以这段代码将把值48赋给 map 中元素的second成员。

您可以使用pair构造函数来创建要插入到insert()的参数中的对象:

ret_pr = people.insert(std::pair<const std::string, size_t> {"Bill", 48});

这将调用一个有右值引用参数的版本insert(),所以元素将被移动到容器中,假设它还不存在。

另一个选项允许您提供一个关于元素应该插入到哪里的提示。该提示以迭代器的形式指向map,中的现有元素,并且该提示被用作开始搜索新元素插入位置的地方。好的提示可以加快插入操作;一个不好的暗示会适得其反。例如:

auto ret_pr = people.insert(std::make_pair("Jim", 48));

people.insert(ret_pr.first, std::make_pair("Ian", 38));

第一条语句插入一个元素并返回一个pair对象,如前所述。这个pair对象的第一个成员是一个迭代器,它或者指向被插入的元素,或者指向与被插入的元素具有相同键的现有元素。下一个insert()调用中的第一个参数对应于提示,所以这里的提示是刚刚插入的元素。新元素由insert(),的第二个参数指定,它将被插入到由提示标识的元素之前,并尽可能靠近它。如果提示不能以这种方式使用,它将被忽略。同样,如果要插入的元素在映射中,操作会失败。带有提示的insert()调用返回一个迭代器,指向插入的元素,或者指向阻止插入的元素。因此,您可以使用返回值来确定插入是否成功。因此,当您确定元素不存在时,最好只提供插入提示。如果你不确定,还是想给点提示,映射上的count()成员可以帮忙。它返回给定键的 map 中元素的数量,并且只能是01。因此,您可以写:

if(!people.count("Ian"))

people.insert(ret_pr.first, std::make_pair("Ian", 38));

只有当count()返回 0,表明"Ian"键不在map中时,insert()调用才会发生。当然,你可以在没有提示的情况下插入一个元素,但是insert()的返回值会告诉你。

您可以将一系列来自外部源的元素插入到map中。元素不必来自另一个map容器,但必须与它们所插入的容器中的元素类型相同。下面是演示这一点的一些代码:

std::map<std::string, size_t> crowd {{"May", 55}, {"Pat", 66}, {"Al", 22}, {"Ben", 44}};

auto iter = std::begin(people);

std::advance(iter, 4);                       // begin iterator+ 4

crowd.insert(++std::begin(people), iter);    // Insert 2nd, 3rd, and 4th elements from people

这创建了一个新的映射crowd,最初有四个元素。iter被初始化为people图的开始迭代器。容器的迭代器是双向的,所以你可以增加或减少它们,但不能增加或减少值。你在第一章的中遇到的advance()函数模板的一个实例被用于将iter递增 4,这样它将指向第五个元素,这被用作下一行中crowdinsert()调用的参数中指定的范围的结束迭代器。范围的 begin 迭代器是加 1 的people映射的 begin 迭代器,所以操作从第二个元素开始,将三个元素从people插入到crowd

有一个版本insert()接受一个初始化列表作为参数:

crowd.insert({{"Bert", 44}, {"Ellen", 99}});

这会将初始化列表中的两个元素插入到crowd映射中。由自变量表达式创建的initializer_list<>对象将属于initializer_list<const string,size_t>类型,因为编译器知道这是insert()函数参数的类型。当然,您可以独立创建初始化列表,并将其作为参数传递给insert():

std::initializer_list<std::pair<const std::string, size_t>> init {{"Bert", 44}, {"Ellen", 99}};

crowd.insert(init);

initializer_list模板的第一个类型参数必须是const。没有从initializer_list<string,size_t>initializer_list<const string,size_t>的隐式转换,所以前一种类型的对象不会被接受为insert()的参数。

我们可以在一个完整的例子中看到其中的一些工作。我将使用我们将要定义的一种类型的对象来使它有一点不同。Name类型将代表一个人的名字,并且用于类定义的头文件内容将是:

// Name.h for Ex4_01

// Defines a person’s name

#ifndef NAME_H

#define NAME_H

#include <string>                                // For string class

#include <ostream>                               // For output streams

#include <istream>                               // For input streams

class Name

{

private:

std::string first {};

std::string second {};

public:

Name(const std::string& name1, const std::string& name2) :

first (name1), second (name2) {}

Name() = default;

// Less-than operator

bool operator<(const Name& name) const

{

return second < name.second || (second == name.second && first < name.first);

}

friend std::istream& operator>>(std::istream& in, Name& name);

friend std::ostream& operator<<(std::ostream& out, const Name& box);

};

// Extraction operator overload

inline std::istream& operator>>(std::istream& in, Name& name)

{

in >> name.first >> name.second;

return in;

}

// Insertion operator overload

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{

out << name.first + " " + name.second;

return out;

}

#endif

这个类非常简单,有两个私有的string成员,分别用于名字和名字。有一个构造函数接受string参数或字符串作为参数。我们必须为该类定义operator<(),以允许对象在map容器中用作键。支持流的提取和插入操作符,使得Name对象的输入和输出更加容易。

map中的元素将属于类型std::pair<const Name, size_t>,,但是我们可以使用下面的别名定义使代码不那么冗长:

using Entry = std::pair<const Name, size_t>;

现在,当容器类型为map<Name,size_t>时,我们可以使用Entry作为map元素的类型。我们可以在函数定义中很好地使用这个别名来帮助map元素输入:

Entry get_entry()

{

std::cout << "Enter first and second names followed by the age: ";

Name name {};

size_t age {};

std::cin >> name >> age;

return make_pair(name, age);

}

它读取一个Name对象,后跟一个来自cin的年龄值,并从它们中创建一个pair对象。读取name的输入将调用istream对象的operator>>()重载,该重载在Name.h中定义,并支持读取Name对象。

输出映射中元素的辅助函数将非常有用:

void list_entries(const map<Name, size_t>& people)

{

for(auto& entry : people)

{

std::cout << std::left << std::setw(30) << entry.first

<< std::right << std::setw(4) << entry.second << std::endl;

}

}

这只是使用了一个基于范围的for循环来迭代元素。entry循环变量将依次引用每个map元素。每个元素都是一个pair对象,其中first成员是一个Name对象,而second成员是年龄的一个size_t值。

包含main()的源文件将有以下内容:

// Ex4_01.cpp

// Storing names and ages

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <string>                                // For string class

#include <map>                                   // For map container class

#include <utility>                               // For pair<> & make_pair<>()

#include <cctype>                                // For toupper()

#include "Name.h"

using std::string;

using Entry = std::pair<const Name, size_t>;

using std::make_pair;

using std::map;

// Definition of get_entry() here...

// Definition of list_entries() here...

int main()

{

map<Name, size_t> people { {{"Ann", "Dante"}, 25}, {{"Bill", "Hook"}, 46},

{{"Jim", "Jams"}, 32},  {{"Mark", "Time"}, 32} };

std::cout << "\nThe initial contents of the map is:\n";

list_entries(people);

char answer {'Y'};

std::cout << "\nEnter a Name and age entry.\n";

while(std::toupper(answer) == 'Y')

{

Entry entry {get_entry()};

auto pr = people.insert(entry);

if(!pr.second)

{ // it’s there already - check whether we should update

std::cout << "Key \"" << pr.first->first

<< "\" already present. Do you want to update the age (Y or N)? ";

std::cin >> answer;

if(std::toupper(answer) == 'Y')

pr.first->second = entry.second;

}

// Check whether there are more to be entered

std::cout << "Do you want to enter another entry(Y or N)? ";

std::cin >> answer;

}

std::cout << "\nThe map now contains the following entries:\n";

list_entries(people);

}

定义了一些额外的别名来进一步减少代码的冗长。您可以对std名称空间使用一个using指令,并且完全消除对std名称限定的需要,但是我不喜欢这样做,因为std中的所有名称都被有效地导入了,所以定义名称空间就失去了意义。

容器map的定义有初始值,用于初始化列表中定义的元素。这只是为了说明如何在这种情况下使用嵌套括号。在每个定义一个元素的初始化列表中,Name对象的括号之间有一个初始化列表。每个元素初始化器都是一个Name对象和一个大括号中的年龄值,元素的所有初始值都包含在最外面的一对大括号中。

调用list_entries()助手函数来显示容器的初始状态。在for循环中读取更多条目。循环由answer的值控制,该值在开始时为'Y',因此循环至少执行一次,并且必须从键盘输入至少一个元素。条目对象的类型是Entry,这是一个容器元素的类型。由get_entry()辅助函数返回的对象被用作初始值。通过将 entry 元素作为参数传递给insert()成员,将它插入到容器中。返回的pair对象有一个first成员,该成员指向容器中的元素,该元素的键与entry的键相匹配。这将是原始容器元素,如果它在插入操作之前就存在的话。如果这个键已经在容器中,将不进行插入,并且pr的第二个成员将是false. pr.first是一个指向容器元素的迭代器,所以pr.first->second访问与该键相关的对象,并且如果用户确认更新,它将被更改为entry.second中的值。循环中的最后一个动作是决定是否要输入更多的条目。当不再有条目时,循环结束,通过调用list_entries()输出容器的最终内容。

以下是该示例的一些输出示例:

The initial contents of the map is:

Ann Dante                       25

Bill Hook                       46

Jim Jams                        32

Mark Time                       32

Enter a Name and age entry.

Enter first and second names followed by the age: Emma Nate 42

Do you want to enter another entry(Y or N)? y

Enter first and second names followed by the age: Emma Nate 43

Key "Emma Nate " already present. Do you want to update the age (Y or N)? Y

Do you want to enter another entry(Y or N)? y

Enter first and second names followed by the age: Eamonn Target 56

Do you want to enter another entry(Y or N)? N

The map now contains the following entries:

Ann Dante                       25

Bill Hook                       46

Jim Jams                        32

Emma Nate                       43

Eamonn Target                   56

Mark Time                       32

元素按键以升序输出,因为它们在容器中是用一个less<Name>对象排序的。Name::operator<()成员首先比较姓氏,只有当姓氏相同时才比较名字。这将导致名称的正常排序顺序。

就地构建映射元素

一个map容器有一个emplace()函数成员,它就地构造一个新元素,从而避免复制或移动操作。参数是构造一个元素所必需的,这个元素是一个pair<const K,T>对象。只有当不存在具有相同键的现有元素时,才会构造元素。这里有一个如何使用它的例子:

std::map<Name, size_t> people;

auto pr = people.emplace(Name{"Dan", "Druff"}, 77);

这里的map包含类型Name的键,这是在Ex4_01中定义的类类型。对象是类型size_t,所以map将包含类型pair<const Name,size_t>的元素。emplace()的第一个参数是作为键的Name对象,第二个参数是size_t值,函数将在对pair<const Name,size_t>构造函数的调用中使用这些参数就地创建元素。如果在emplace()的参数中构造pair对象,那么pair<const Name,size_t>类的 move 构造函数将被调用。

emplace()返回的pair对象提供的指示与insert()函数成员返回的指示相同。pairfirst成员是一个迭代器,指向被插入的元素或阻止插入的元素,而second成员是一个bool值,如果元素被插入,则该值为true

mapemplace_hint()成员以与emplace()基本相同的方式就地创建元素,除了您作为第一个参数提供的迭代器被用作搜索创建新元素的位置的起点。例如:

std::map<Name, size_t> people;

auto pr = people.emplace(Name{"Dan", "Druff"}, 77);

auto iter = people.emplace_hint(pr.first, Name{"Cal", "Cutta"}, 62);

emplace_hint()调用使用前面的emplace()调用返回的pair中的迭代器作为提示。如果容器接受了提示,新元素将被放置在这个位置的前面并尽可能靠近它。提示后面的参数用于构造新元素。需要注意的是,返回的值与emplace()函数成员的值完全不同。emplace_hint()成员不返回pair对象——如果插入了新元素,它返回一个指向新元素的迭代器,如果没有插入新元素,则返回具有相同键的现有元素。对于元素是否被创建,您没有直接的指示。然而,并没有全部丢失——一种可能是使用返回map中元素数量的size()成员来检查元素计数的增加。例如:

auto pr = people.emplace(Name{"Dan", "Druff"}, 77);

auto count = people.size();

auto iter = people.emplace_hint(pr.first, Name{"Cal", "Cutta"}, 62);

if(count < people.size()) std::cout << "Success!\n";

仅当元素count因调用emplace_hint()而增加时,才会显示该消息。

访问映射中的元素

您已经知道可以获得开始和结束迭代器以及反向迭代器,这些迭代器提供对map容器中所有元素的访问。mapat()函数成员返回与您作为函数参数提供的键相关联的对象。如果键不存在,抛出一个out_of_range异常。下面是一个如何使用它的例子:

Name key;

try

{

key = Name {"Dan", "Druff"};

auto value = people.at(key);

std::cout << key << " is aged " << value << std::endl;

key = Name {"Don", "Druff"};

value = people.at(key);

std::cout << key << " is aged " << value << std::endl;

}

catch(const std::out_of_range& e)

{

std::cerr << e.what() << '\n'

<< key << " was not found." << std::endl;

}

为一个map调用at()的语句需要在一个try块中——如果异常被抛出并且没有被捕获,程序将被终止。这个片段使用at()来获取与people容器中的两个Name键相关联的对象。如果map的内容是通过执行上一节的代码片段确定的,这将产生输出:

Dan Druff is aged 77

invalid map<K, T> key

Don Druff was not found.

try块中的第一个at()调用成功,产生第一行输出。第二个调用失败并抛出一个out_of_range异常,这个异常被捕获并导致最后两行输出。exception 对象的what()成员返回一个描述异常原因的字符串。当catch块代码执行时,try块中的所有局部变量都被销毁,因此无法访问。在try块之前定义了key变量,因此仍然可以从catch块中访问该变量。

下标操作符由一个map容器实现,接受一个键作为参数,并返回一个对相关对象的引用。这里有一个例子:

auto age = people[Name {"Dan", "Druff"}];

这将检索与Name键相关联的size_t值。注意,使用下标不仅仅是一种检索机制。如果该键不存在,则为该键创建一个新元素,并使用类类型的默认构造函数创建关联的对象,如果关联的对象是基本类型,则创建零的等效项。例如:

auto value = people[Name {"Ned", "Kelly"}];   // Creates a new element if the key is not there

将使用该键创建一个新元素,因为容器中不存在该键。关联的值将为 0,并且将返回该值。当您更新映射中的元素或插入它们(如果它们还不存在)时,您可以使用下标运算符。下标操作符的另一个主要用途是在赋值的左边改变现有的条目:

people[Name {"Ned", "Kelly"}]  =  39;       // Sets the value associated with the key to 39

让我们尝试一个工作示例,它以一种与您目前所看到的稍微不同的方式使用了一个map,并且使用了下标操作符。您可以使用一个map容器来确定每个单词在文本中出现的频率。确定词频可能是有用的——例如,它可以帮助对文档进行分类。下面是计算每个单词在任意文本序列中出现频率的代码:

// Ex4_02.cpp

// Determining word frequency

#include <iostream>                             // For standard streams

#include <iomanip>                              // For stream manipulators

#include <string>                               // For string class

#include <sstream>                              // For istringstream

#include <algorithm>                            // For replace_if() & for_each()

#include <map>                                  // For map container

#include <cctype>                               // For isalpha()

using std::string;

int main()

{

std::cout << "Enter some text and enter * to end:\n";

string text_in {};

std::getline(std::cin, text_in, '*');

// Replace non-alphabetic characters by a space

std::replace_if(std::begin(text_in), std::end(text_in),

[](const char& ch){ return !isalpha(ch); }, ' ');

std::istringstream text(text_in);             // Text input string as a stream

std::istream_iterator<string> begin(text);    // Stream iterator

std::istream_iterator<string> end;            // End stream iterator

std::map<string, size_t> words;               // Map to store words & word counts

size_t max_len {};                            // Maximum word length

// Get the words, store in the map, and find maximum length

std::for_each(begin, end, &max_len, &words

{  words[word]++;

max_len = std::max(max_len, word.length());

});

// Ouput the words and their counts

size_t per_line {4}, count {};

for(const auto& w : words)

{

std::cout << std::left << std::setw(max_len + 1) << w.first

<< std::setw(3) << std::right << w.second << "  ";

if(++count % per_line == 0)  std::cout << std::endl;

}

std::cout << std::endl;

}

使用string对象的getline()函数将文本从标准输入流读入text_inreplace_if()算法用于将输入中的非字母字符替换为空格。replace_if()的前两个参数是定义元素范围的迭代器,在本例中是输入字符串中的字符。下一个参数是一个函数对象,当一个元素要被替换时,它返回true;这是一个λ表达式。最后一个参数是元素的替换,在本例中是空格。该函数将替换所有标点符号,因此我们最终只得到由空格分隔的单词。

我们从text_in创建一个istringstream对象text。一个istringstream对象允许在它封装的字符串上进行流输入操作,因此它充当一个流。这包括获取text的流迭代器的能力,然后我们可以在for_each()算法中使用它来提取单个单词。输入流的迭代器将连续指向每个输入实体。这里的输入是一连串的单词,所以由 begin 和 end 迭代器为text指定的范围定义了所有的单词。for_each()算法将作为第三个参数的 function 对象应用到迭代器所指向的每个元素,该元素在由前两个参数定义的范围内——在本例中是来自text的每个单词。函数对象必须引用迭代器指向的对象类型作为参数,所以这里是const string&。lambda 表达式通过引用捕获max_len变量和map,因此它可以修改这两个变量。lambda 的主体通过将每个单词指定为下标来将其作为一个键存储在容器中,并递增与该单词相关联的值。当单词不存在时,这将创建一个新条目,将单词作为键,值为 1。如果这个单词之前已经被添加到容器中,那么操作只会增加这个值。因此,与每个单词相关联的值是文本中出现的累计次数。lambda 表达式还更新了max_len,因此它记录了最长字符串的长度;该值将在输出过程中使用。

因此,对for_each()算法的调用将输入中的所有单词插入到map中——无论单词有多少——并累计每个单词出现的次数,计算出总的最大单词长度——对于一条语句来说,这还不错!剩下的代码是输出单词和它们的计数,我相信你能明白这是如何工作的。以下是该程序的输出示例:

Enter some text and enter * to end:

How much wood would a wood chuck chuck,

If a woodchuck could chuck wood?

A woodchuck would chuck as much wood as a woodchuck could chuck

if a woodchuck could chuck wood.

*

A           1  How         1  If          1  a           4

as          2  chuck       6  could       3  if          1

much        2  wood        5  woodchuck   4  would       2

这个例子将整数类型的对象存储在一个map中,因此我们可以将 increment 操作符应用于容器的下标操作符返回的值。当下标操作符为map返回的值返回一个类类型的对象时,也可以使用操作符,只要操作符已经为该类实现。为了说明我正在谈论的这类事情,让我们创建另一个工作示例。

假设我们想按名字存储和检索人们的引用。显然,有些人的名字中有很多著名的引语,所以我们需要允许为一个键存储多个引语。我们不能在一个map容器中存储重复的键,但是我们可以将一个键与一个可以封装多个引用的对象相关联。我们可以使用来自Ex4_01Name类的实例作为键,并且我们可以定义一个Quotations类来保存给定名称的所有引用。

我们知道对键使用下标操作符可以访问与键相关的对象,所以我们可以通过在Quotations类中实现operator[]()来扩展符号。我们也可以在类中实现operator<<(),这样它就可以给一个Quotation对象添加一个引用。我们可以方便地将报价存储在一个vector容器中。下面是定义该类的Quotations.h的内容:

#ifndef QUOTATIONS_H

#define QUOTATIONS_H

#include <vector>                                          // For vector container

#include <string>                                          // For string class

#include <exception>                                       // For out_of_range exception

class Quotations

{

private:

std::vector<std::string> quotes;                         // Container for the quotations

public:

// Stores a new quotation that is created from a string literal

Quotations& operator<<(const char* quote)

{

quotes.emplace_back(quote);

return *this;

}

// Copies a new quotation in the vector from a string object

Quotations& operator<<(const std::string& quote)

{

quotes.push_back(quote);

return *this;

}

// Moves a quotation into the vector

Quotations& operator<<(std::string&#x0026;& quote)

{

quotes.push_back(std::move(quote));

return *this;

}

// Returns a quotation for an index

std::string& operator[](size_t index)

{

if(index < quotes.size())

return quotes[index];

else

throw std::out_of_range {"Invalid index to quotations."};

}

size_t size() const {  return quotes.size();  }        // Returns the number of quotations

// Returns the begin iterator for the quotations

std::vector<std::string>::iterator begin()

{

return std::begin(quotes);

}

// Returns the const begin iterator for the quotations

std::vector<std::string>::const_iterator begin() const

{

return std::begin(quotes);

}

// Returns the end iterator for the quotations

std::vector<std::string>::iterator end()

{

return std::end(quotes);

}

// Returns the const end iterator for the quotations

std::vector<std::string>::const_iterator end() const

{

return std::end(quotes);

}

};

#endif

使用<<操作符来添加引用相当符合它在其他上下文中的使用,比如流输入。您可以在这里使用+=操作符。该类定义了三个版本的operator<<(),提供了添加新报价的各种方式。第一个版本接受传递给vectoremplace_back()成员的字符串参数,该参数将调用string构造函数就地创建元素。第二个版本有一个引用字符串的参数,因此参数被传递给vectorpush_back()成员。第三个版本有一个右值引用参数。当您通过名称访问函数体中的右值引用参数时,它将是一个左值,因此您必须使用move()将参数作为右值传递给vectorpush_back()成员。这将使对象能够一直移动,而不需要复制。

该类的operator[]()成员使用索引访问一个vector元素。如果索引不在范围内,该函数将引发异常;这不应该发生,如果发生了,这是程序中的一个错误。

begin()end()成员为vector中的引用返回迭代器。请注意返回类型是如何指定的。提供迭代器的容器一般会定义一个iterator成员,它是它们所支持的迭代器类型的别名,所以你不需要知道详细的类型规范。定义迭代器的类的对象可以与基于范围的for循环结合使用,只要迭代器至少是前向迭代器。

还有begin()end()成员的const版本,它们返回在Quotations类中定义的const迭代器。返回类型也是在vector模板中定义的别名。如果没有定义begin()end()const版本,就不可能使用带有const循环变量的基于范围的for循环,如下所示:

for(const auto& pr : quotations)                 // Requires const iterators

...

我们可以定义几个在main()中使用的inline助手函数。第一个读着一个来自cin的名字:

inline Name get_name()

{

Name name {};

std::cout << "Enter first name and second name: ";

std::cin >> std::ws >> name;

return name;

}

这将一个名字读为第一个和第二个名字。ws操纵器消耗空白,因此通过从cin.读取字符跳过任何留下的空白

第二个助手只是读了一段引语:

inline string get_quote(const Name& name)

{

std::cout << "Enter the quotation for " << name

<< ". Enter * to end:\n";

string quote;

std::getline(std::cin >> std::ws, quote, '*');

return quote;

}

使用星号终止输入允许输入多行。支持存储报价的 Ex4_03.cpp 文件如下所示:

// Ex4_03.cpp

// Stores one or more quotations for a name in a map

#include <iostream>                              // For standard streams

#include <cctype>                                // For toupper()

#include <map>                                   // For map containers

#include <string>                                // For string class

#include "Quotations.h"

#include "Name.h"

using std::string;

// get_name() definition goes here...

// get_quote() definition goes here...

int main()

{

std::map<Name, Quotations> quotations;         // Container for name/quotes pairs

std::cout << "Enter 'A' to add a quote."

"\nEnter 'L' to list all quotes."

"\nEnter 'G' to get a quote."

"\nEnter 'Q' to end.\n";

Name name {};                                  // Stores a name

string quote {};                               // Stores a quotation

char&#x00A0; command {};                              // Stores a command

while(command != 'Q')

{

std::cout << "\nEnter command: ";

std::cin >> command;

command = static_cast<char>(std::toupper(command));

switch(command)

{

case 'Q':

break;                                     // Quit operations

case 'A':

name = get_name();

quote = get_quote(name);

quotations[name] << quote;

break;

case 'G':

{

name = get_name();

const auto& quotes = quotations[name];

size_t count = quotes.size();

if(!count)

{

std::cout << "There are no quotes recorded for "

<< name << std::endl;

continue;

}

size_t index {};

if(count > 1)

{

std::cout << "There are " << count << " quotes for " << name << ".\n"

<< "Enter an index from 0 to " << count - 1 << ": ";

std::cin >> index;

}

std::cout << quotations[name][index] << std::endl;

}

break;

case 'L':

if(quotations.empty())                                      // Test for no pairs

{

std::cout << "\nNo quotations recorded for anyone." << std::endl;

}

// List all quotations

for(const auto& pr : quotations)                            // Iterate over pairs

{

std::cout << '\n' << pr.first << std::endl;

for(const auto& quote : pr.second)                        // Iterate over quotations

{

std::cout << "  " << quote << std::endl;

}

}

break;

default:

std::cout << " Command must be 'A', 'G', 'L', or 'Q'. Try again.\n";

continue;

break;

}

}

}

quotations容器存储类型为pair<const Name, Quotations>的对象。像quotations[name]这样的表达式导致对与Name对象name相关联的对象的引用。如果在对应于name键的映射中没有现存的pair,将会创建一个与之相关联的默认Quotations对象,该对象将为空。为给定的name存储新报价quote的语句是:

quotations[name] << quote;

<<的左操作数相当于返回与名称相关联的Quotations对象的quotations.operator[](name)。因此,该语句相当于:

quotations.operator[](name).operator<<(quote);

你可以在main()中看到,我们可以用表达式quotations[name][index]访问给定索引处名称的引用,这对应于quotations.operator[](name).operator[](index).我想你现在应该能够理解main()中其余代码是如何工作的了。以下是一些输出示例:

Enter 'A' to add a quote.

Enter 'L' to list all quotes.

Enter 'G' to get a quote.

Enter 'Q' to end.

Enter command: a

Enter first name and second name: Winston Churchill

Enter the quotation for Winston Churchill . Enter * to end:

There are a terrible lot of lies going around the world, and the worst of it is half of them are true.*

Enter command: a

Enter first name and second name: Dorothy Parker

Enter the quotation for Dorothy Parker . Enter * to end:

Beauty is only skin deep, but ugly goes clean to the bone.*

Enter command: a

Enter first name and second name: Winston Churchill

Enter the quotation for Winston Churchill . Enter * to end:

Never in the field of human conflict was so much owed by so many to so few.*

Enter command: a

Enter first name and second name: Winston Churchill

Enter the quotation for Winston Churchill . Enter * to end:

Courage is what it takes to stand up and speak, Courage is also what it takes to sit down and listen.*

Enter command: a

Enter first name and second name: Dorothy Parker

Enter the quotation for Dorothy Parker . Enter * to end:

Money cannot buy health, but I’d settle for a diamond-studded wheelchair.*

Enter command: g

Enter first name and second name: Winston Churchill

There are 3 quotes for Winston Churchill .

Enter an index from 0 to 2: 1

Never in the field of human conflict was so much owed by so many to so few.

Enter command: L

Winston Churchill

There are a terrible lot of lies going around the world, and the worst of it is half of them are true.

Never in the field of human conflict was so much owed by so many to so few.

Courage is what it takes to stand up and speak, Courage is also what it takes to sit down and listen.

Dorothy Parker

Beauty is only skin deep, but ugly goes clean to the bone.

Money cannot buy health, but I’d settle for a diamond-studded wheelchair.

Enter command: q

显然,这个程序可以做更多的错误恢复功能,并可能提供比较键无论大小写,但你得到的想法。

一个map容器有一个find()函数成员,该成员返回一个迭代器,该迭代器指向具有与参数匹配的键的元素。例如:

std::map<std::string, size_t>  people {{"Fred", 45}, {"Joan", 33}, {"Jill", 22}};

std::string name{"Joan"};

auto iter = people.find(name);

if(iter == std::end(people))

std::cout <<"Not found.\n";

else

std:: cout << name << " is " << iter->second << std::endl;

如果没有找到匹配的参数,find()返回容器的结束迭代器,所以在尝试使用迭代器之前,必须检查这一点。

为了与multimap兼容,map容器包括equal_range()upper_bound()lower_bound()函数成员,但是因为这些的目的是找到具有相同键的多个元素,我将在本章后面的multi map容器的上下文中讨论这些。

删除元素

maperase()函数成员将删除具有匹配参数的键的元素,并返回被删除元素的数量。例如:

std::map<std::string, size_t>  people {{"Fred", 45}, {"Joan", 33}, {"Jill", 22}};

std::string name{"Joan"};

if(people.erase(name))

std::cout << name << " was removed." << std::endl;

else

std::cout << name << " was not found." << std::endl;

显然,对于一个map容器,返回值只能是 0 或 1,0 表示没有找到该元素。您还可以将指向要删除的元素的迭代器作为参数传递给erase()。在这种情况下,返回一个迭代器,指向被移除的元素后面的元素。参数必须是容器的有效迭代器,并且不能是结束迭代器。如果迭代器参数指向容器中的最后一个元素,将返回结束迭代器。例如:

auto iter = people.erase(std::begin(people));

if(iter == std::end(people))

std::cout << "The last element was removed." << std::endl;

else

std::cout << "The element preceding " << iter->first << " was removed." << std::endl;

当最后一个元素被删除时,这个片段将输出一条消息,或者输出被删除元素后面的元素的键。

erase()还有一个进一步的版本,它接受两个迭代器参数来定义要删除的元素范围。例如:

auto iter = people.erase(++std::begin(people), --std::end(people));  // Erase all except 1st & last

返回的迭代器指向被移除的范围中最后一个元素之后的元素。当你想从一个map中移除所有元素时,你可以调用clear()成员。

使用对<>和元组<>对象

您已经看到了一个pair<const K,T>对象如何封装一个键和一个关联的对象,以及如何表示一个map中的一个元素。一般来说,pair封装的对象可以是任何类型,你可以为任何目的创建pair<T1,T2>对象——例如,你可以创建一个数组或一个pair<T1,T2>对象的vector,或者一个pair封装两个序列容器或两个指向序列容器的指针。在utility头中定义了pair<T1,T2>模板,当你想独立于map使用pair对象时,你需要包含这个模板。

tuple<>模板是pair模板的一般化,允许定义tuple模板实例,它封装了任意数量的不同类型的对象。因此,tuple实例可以有任意数量的模板类型参数。信不信由你,tuple模板是在tuple头中定义的。术语tuple也用于许多其他环境,例如数据库环境,其中元组是由许多不同类型的不同数据项组成的记录,因此概念是相似的。你会发现tuple对象有很多用途。对于将多个对象作为单个对象传递给一个函数或返回多个对象,类型非常有用。显然,定义由几个对象组成的容器元素的能力也将派上用场。我将首先进入pair<T1,T2>模板的细节,然后我们将看看如何创建和使用tuple对象。

成对运算

考虑到它是一个相对简单的模板类型,只有两个公共数据成员,firstsecond,一对<T1, T2>有令人惊讶的构造函数的多样性。您已经看到了如何使用firstsecond的值创建一个对象。有带引用参数和右值引用参数的版本。还有一些版本带有右值引用参数,允许将参数隐式转换为所需的类型。例如,以下是定义同一个pair对象的四种方法:

std::string s1 {"test"}, s2{"that"};

std::pair<std::string, std::string> my_pair{s1, s2};

std::pair<std::string, std::string> your_pair{std::string {"test"}, std::string {"that"}};

std::pair<std::string, std::string> his_pair{"test", std::string {"that"}};

std::pair<std::string, std::string> her_pair{"test", "that"};

第一个pair构造函数调用复制参数值,第二个移动参数值,第三个将第一个参数转发给string构造函数进行隐式转换,最后一个构造函数调用将两个参数隐式转换为string对象,这些对象被移动到pairfirstsecond成员。因为提供了构造函数的右值引用版本,任一或两个pair模板类型参数都可以是unique_ptr<T>

make_pair<T1, T2>()函数模板是一个帮助函数,它创建并返回一个pair<T1,T2>对象。您可以创建前面代码块生成的 pair 对象,如下所示:

auto my_pair = std::make_pair(s1, s2);

auto your_pair = std::make_pair(std::string {"test"}, std::string {"that"});

auto his_pair = std::make_pair<std::string, std::string>("test", std::string {"that"});

auto her_pair = std::make_pair<std::string, std::string>("test", "that");

在前两条语句中,函数模板的类型参数是由编译器推导出来的。在最后两条语句中,它们是明确的。如果在最后两条语句中省略了模板类型参数,那么对象的类型将是pair<const char*, string>pair<const char*, const char*>

一个对象也是复制或移动可构造的,只要它的成员是。例如:

std::pair<std::string, std::string> new_pair{my_pair};     // Copy constructor

std::pair<std::string, std::string> old_pair{std::make_pair(std::string{"his"}, std::string{"hers"})};

old_pair是由pair<string,string>类移动构造函数创建的。

还有另一个 pair 构造函数,它使用了 C++11 中引入的机制,该机制允许通过就地创建firstsecond成员来构造pair<T1, T2>对象。T1T2构造函数的参数作为tuple<>参数传递给pair构造函数;我将在下一节详细解释你可以用tuple对象做什么。下面是一个使用这个pair构造函数的例子:

std::pair<Name, Name> couple{std::piecewise_construct,

std::forward_as_tuple("Jack", "Jones"), std::forward_as_tuple("Jill", "Smith")};

这里,pair构造函数的第一个参数是在utility头中定义的piecewise_construct_t类型的一个实例。这是一个空类型,用作标记或记号。piecewise_construct参数的唯一目的是区分这个构造函数调用和两个tuple参数用作pairfirstsecond成员的值的构造函数调用。这里,构造函数的第二个和第三个参数指定了用于构造firstsecond对象的参数集。forward_as_tuple()是在tuple表头定义的函数模板;在这里,它创建了一个对其参数的引用的tuple,然后可以被转发。您不太可能经常需要这个pair构造函数,但是它提供了创建pair<T1, T2>对象的独特能力,其中类型T1T2不支持复制或移动操作——它们只能就地创建。

注意,如果参数是临时对象,forward_as_tuple()将创建一个右值引用的tuple。例如:

int a {1}, b {2};

const auto& c = std::forward_as_tuple(a,b);

这里,c的类型是tuple<int&, int&#x0026;>,所以成员是引用。但是,假设您编写了以下语句:

const auto& c = std::forward_as_tuple(1,2);

这里的ctuple<int&#x0026;&, int&#x0026;&>,成员为右值引用。

如果成员可以复制或移动,则pair对象支持复制和移动分配。例如:

std::pair<std::string, std::string> old_pair;                      // Default constructor

std::pair<std::string, std::string> new_pair{std::string{"his"}, std::string{"hers"}};

old_pair = new_pair;                                               // Copy assignment

new_pair = pair<std::string, std::string>

{std::string{"these"}, std::string{"those"}};  // Move assignment

old_pair将由默认的pair构造函数创建,其成员为空的string对象。第三条语句将new_pair逐个成员地复制到old_pair。第四条语句将作为赋值的右操作数的pair对象的成员移动到new_pair

pair对象包含不同类型的成员时,也可以将一个pair赋值给另一个,只要右操作数pair的成员类型可以隐式转换为左操作数pair的成员类型。这里有一个例子:

auto pr1 = std::make_pair("these", "those");   // Type pair<const char*, const char*>

std::pair<std::string, std::string> pr2;       // Type pair<string, string>

pr2 = pr1;                                     // OK in this case

pr1firstsecond成员属于const char*类型。这种类型可以隐式地转换为类型string,这是pr2成员的类型,所以赋值是有效的。如果类型不可隐式转换,则赋值不会编译。

对于pair对象、==!=<<=>>=,您已经有了一整套比较操作符。为了使这些工作,作为操作数的pair对象必须是相同的类型,并且它们的成员必须以相同的方式进行比较。如果左右操作数的对应成员相等,则相等运算符返回true:

std::pair<std::string, std::string> new_pair;

new_pair.first = "his";

new_pair.second = "hers";

if(new_pair == std::pair<std::string, std::string> {"his", "hers"})  std::cout << "Equality!\n";

new_pairfirstsecond成员的赋值将它们的值设置为包含右操作数字符串的string对象。因为pair对象是相等的,所以if语句将输出消息。如果pair对象的一个或两个成员不相等,则!=比较将产生true

对于小于或大于比较,pair对象的成员按字典顺序进行比较。如果new_pair.first小于old_pair.first,表达式new_pair < old_pair将为真。如果first成员相等且new_pair.second小于old_pair.second,也将是true。这里有一个例子:

std::pair<int, int> p1 {10, 9};

std::pair<int, int> p2 {10, 11};

std::pair<int, int> p3 {11, 9};

std::cout << std::boolalpha << (p1 < p2) << " "            // Outputs "true"

<< (p1 > p3) << " "            // Outputs "false"

<< (p3 > p2) << std::endl;     // Outputs "true"

第一个比较是true,因为p1的第一个成员等于p2的第一个成员,而p1second成员小于p2的成员。第二个比较是false,因为 p1 的first成员不大于p3的成员。第三个比较是true因为p3的第一个成员大于p2的第一个成员。

一个pair对象的swap()成员将其第一个和第二个成员与作为参数传递的pair成员交换。显然,参数必须是相同的类型。这里有一个例子:

std::pair<int, int> p1 {10, 11};

std::pair<int, int> p2 {11, 9};

p1.swap(p2);                                               // p1={11,9} p2={10,11}

如果你执行同一个swap()调用两次,你将回到你开始的地方。

元组操作

创建一个tuple对象最简单的方法是使用在tuple头中定义的make_tuple()辅助函数。该函数接受任意数量的任意类型的参数,它返回的tuple的类型由参数的类型决定。例如:

auto my_tuple = std::make_tuple(Name{"Peter", "Piper"}, 42, std::string{"914 626 7890"});

因为模板类型参数被推导为make_tuple()的参数,所以my_tuple对象将是类型tuple<Name, int, string>。如果您只提供一个字符串作为第三个参数给make_tuple(),my_tuple的类型将是tuple<Name, int, const char*>,这是不同的。

对象的构造函数提供了你可能需要的每一个选项。以下是一些例子:

std::tuple<std::string, size_t> my_t1;                       // Default initialization

std::tuple<Name, std::string> my_t2{Name{"Andy", "Capp"}, std::string{"Programmer"}};

std::tuple<Name, std::string> copy_my_t2{my_t2};             // Copy constructor

std::tuple<std::string, std::string> my_t3{"this", "that"};  // Implicit conversion

默认构造函数用默认值初始化tuple中的对象。对my_t2的构造函数调用将参数移动到tuple的元素中。下一个语句调用复制构造函数来创建元组,在最后一个构造函数调用中,tuple元素是通过将参数隐式转换为类型string来创建的。

您可以从一个pair构造一个tuple,其中的pair可以是一个左值或一个右值。显然,tuple只能有两个元素。这里有几个例子:

auto the_pair = std::make_pair("these", "those");

std::tuple<std::string, std::string> my_t4 {the_pair};

std::tuple<std::string, std::string> my_t5 {std::pair <std::string, std::string > {"this", "that"}};

第二条语句从the_pair创建一个tuple,它是一个左值。这里,the_pairfirstsecond成员将被隐式转换为tuple中元素的类型。最后一条语句从一个右值的pair对象创建元组。

您可以使用任何比较运算符来比较相同类型的tuple对象。被比较的元组对象中的元素按字典顺序进行比较。这里有一个例子:

std:: cout << std::boolalpha << (my_t4 < my_t5) << std::endl;  // true

tuple对象中的元素被连续比较,在这种情况下,第一个不同的元素决定结果。my_t4中的第一个元素比my_t5中的第一个元素少,所以结果是true。如果比较是为了相等,任何不同的对应元素对都会产生一个false结果。

tuple对象的swap()函数成员将其元素与自变量的元素互换。参数必须是同一类型的tuple对象。例如:

my_t4.swap(my_t5);

通过调用my_t4中每个元素的swap()成员与my_t5中相应的元素进行元素交换。显然,tuple中的所有元素类型都必须是可交换的,tuple头定义了一个全局swap()函数,它将以同样的方式交换两个tuple对象中的元素。

因为一个tuple是一个pair的概括,它必须以不同的方式工作。一个pair中对象的数量是固定的,所以它们有成员名。在一个tuple中可以有任意数量的对象,因此访问它们的机制必须适应这一点。get<>()模板函数从tuple返回一个元素。第一个模板类型参数可以是类型为size_t的值,它是参数tuple中元素的索引,因此 0 选择第一个tuple元素,1 选择第二个,依此类推。get<>()模板的其余类型参数被推断为与作为参数的tuple的类型参数相同。下面是一个使用带有索引值的get<>()来选择元素的例子:

auto my_tuple = std::make_tuple(Name{"Peter", "Piper"}, 42, std::string{"914 626 7890"});

std::cout << std::get<0>(my_tuple)

<< " age = " << std::get<1>(my_tuple)

<< " tel: "  << std::get<2>(my_tuple) << std::endl;

输出语句中对get<>()的第一次调用返回了对my_tuple中第一个元素的引用,这是一个Name对象。第二个get<>()调用返回对下一个元素的引用,是一个整数;第三个调用返回对第三个元素的引用,这是一个string对象。因此,输出将是:

Peter Piper  age = 42 tel: 914 626 7890

您也可以基于类型使用get<>()tuple中获得一个元素,只要该类型只有一个元素。例如:

auto my_tuple = std::make_tuple(Name{"Peter", "Piper"}, 42, std::string{"914 626 7890"});

std::cout << std::get<Name>(my_tuple)

<< " age = " << std::get<int>(my_tuple)

<< " tel: "  << std::get<std::string>(my_tuple) << std::endl;

如果tuple包含多个类型参数值为get<>()的元素,代码将不会编译。在这里,tuple的三个成员都是不同类型的,所以它是有效的。

tuple头中定义的全局tie<>()函数模板提供了另一种访问tuple中元素的方法。这个函数可以将元组中元素的值传递给一组由tie<>()绑定在一起的左值。tie<>()的模板类型参数是从函数参数推导出来的。这里有一个例子:

auto my_tuple = std::make_tuple(Name{"Peter", "Piper"}, 42, std::string{"914 626 7890"});

Name name{};

size_t age{};

std::string phone{};

std::tie(name, age, phone) = my_tuple;

作为最后一条语句中赋值的左操作数的std::tie(name,age,phone)表达式返回对参数的引用的tuple。因此,赋值操作的左右操作数是tuple对象。作为tie()参数的变量将被赋予来自my_tuple的元素值。可能是你不想存储每个元素的值。下面是如何从my_tuple中只存储namephone元素的方法:

std::tie(name, std::ignore, phone) = my_tuple;

ignoretuple头中定义,用于标记tie()函数调用中要忽略的值。对应于ignoretuple元素的值将不会被记录。在示例中,它仅允许复制my_tuple的第一个和第三个元素。

您还可以使用tie()函数来实现一个类的数据成员的字典式比较。例如,您可以在Ex4_01Name类中实现operator<(),如下所示:

bool Name::operator<(const Name& name) const

{

return std::tie(second, first) < std::tie(name.second, name.first);

}

由函数体中的tie()调用产生的tuple对象中的元素按顺序进行比较。使用<运算符比较连续的对应元素对。不同的第一对决定结果;结果是不同元素比较的结果。如果所有元素相等或等价,结果为false

行动中的元组和对

让我们放一个练习tuple s 和pair s 的工作示例;它不一定反映做它所做的事情的最佳方式,但是目标是用tuplepair对象来尝试操作。该示例将利用一个map容器,该容器将一个pair对象作为键,将一个tuple对象作为与该键相关联的对象。每个map元素将记录一个人的数据。关键字将是一个名字,相关的tuple对象将包含这个人的出生日期、身高和职业作为元素。出生日期也将是一个元组,因此我们将创建一个将tuple作为元素的tuple。该示例将使用一组类型别名来使代码不那么冗长:

using std::string;

using Name = std::pair<string, string>;                     // Defines a name

using DOB = std::tuple<size_t, size_t, size_t>;             // Month, day, year

using Details = std::tuple< DOB, size_t, string> ;           // DOB, height(inches), occupation

using Element_type = std::map<Name, Details>::value_type;   // Type of map element

Name是封装两个字符串对象的 pair 类型的别名。DOB 是 tuple 类型的别名,它有三个元素size_t,分别是月、日和年值。Details是与一个键相关联的对象的类型别名,并且是类型为DOBsize_t的年龄值和string的职业的三个元素的tuple。map 中元素的类型是一个pair<const K,T>对象,在这种情况下,这是一个相当混乱的类型。尽管是由map容器的value_type成员指定的类型,但是我们可以很容易地为它定义Element_type别名。如果进行替换,您会看到 map 元素的完整显式类型名称是:

std::pair<std::pair<std::string, std::string>,

std::tuple<std::tuple<size_t, size_t, size_t>, size_t, std::string>>

这说明了类型别名是多么有用。

我们可以像这样使用这些别名来定义容器:

std::map<Name, Details> people;                            // Records of the people

键是类型Name,关联的对象是类型Details——使用别名定义非常简单。我们可以进一步为容器类型定义一个别名:

using People = std::map<Name, Details>;

现在我们可以将容器定义为:

People people;                                             // Records of the people

我们可以将map元素的输入过程打包到一个函数中:

void get_people(Peoples& people)

{

string first {}, second {};                              // Stores name inputs

size_t month {}, day {}, year {};                        // Stores DOB input

size_t height {};                                        // Stores height input

string occupation {};                                    // Stores occupation input

char answer {'Y'};

while(std::toupper(answer) == 'Y')

{

std::cout << "Enter a first name and a second name: ";

std::cin >> std::ws >> first >> second;

std::cout << "Enter date of birth as month day year (integers): ";

std::cin >> month >> day >> year;

DOB dob {month, day, year};                            // Create DOB tuple

std::cout << "Enter height in inches: ";

std::cin >> height;

std::cout << "Enter occupation: ";

std::getline(std::cin >> std::ws, occupation, '\n');

// Create the map element in place- a pair containing a Name pair and a tuple object

people.emplace(std::make_pair(Name {first, second}, std::make_tuple(dob, height, occupation)));

std::cout << "Do you want to enter another(Y or N): ";

std::cin >> answer;

}

}

其中大部分是简单的流输入。使用getline()读取职业,允许输入多个单词描述。getline()的第一个参数消除了前一个输入操作可能留在输入缓冲区中的空白,这里就是这样。如果缓冲区中有换行符,那么getline()会读取一个空行。保存出生日期值的tuple是使用类型tuple的别名DOB从输入中创建的。通过调用emplace()成员,在map中就地创建元素pair对象。元素pairfirst成员是使用Name别名创建的pair<string,string>对象,因此调用构造函数。元素pairsecond成员是一个包含DOB元组、高度和职业的tuple。这是通过调用make_tuple()辅助函数创建的。

在读取了map的输入数据后,程序将按姓名顺序列出这些人以及他们的职业。另一个函数将完成这项工作:

void list_DOB_Job(const People& people)

{

DOB dob;

string occupation {};

std::cout << '\n';

for(auto iter = std::begin(people); iter != std::end(people); ++iter)

{

std::tie(dob, std::ignore, occupation) = iter->second;

std::cout   << std::setw(20) << std::left << (iter->first.first + " " + iter->first.second)

<< "DOB: " << std::right               << std::setw(2) << std::get<0>(dob) << "-"

<< std::setw(2) << std::setfill('0') << std::get<1>(dob) << "-"

<< std::setw(4) << std::get<2>(dob) << std::setfill(' ')

<< "  Occupation: " << occupation << std::endl;

}

}

产生输出的for循环使用迭代器——只是为了说明我们可以。使用基于范围的for循环会更简单,但是下一个函数将演示这一点。循环中的第一条语句是赋值语句。左边的操作数是一个tie()函数调用,它创建一个tuple,函数参数作为左值成员。赋值的右操作数是iter指向的pair对象的second成员,它是Details类型的tuple。赋值操作将右操作数tuple的成员复制到左操作数tuple的成员。因为tie()的第二个参数是ignore,所以只存储赋值右边的tuple的第一个和第三个成员——在变量doboccupation中,变量dob本身就是一个tuple

循环体中的第二条语句输出姓名、出生日期和职业。一个人的名字和名字记录在 p a ir 元素的firstsecond成员中:也就是键。iter指向pair元素,因此iter->first引用关键对象;因此iter->first.first访问作为键的pairfirst成员,而iter->first.second访问second成员。使用get<>()函数模板访问DOB元组的成员。get<>()模板参数自变量选择tuple成员。

我们还可以包含一个函数来输出每个人的所有细节。如果可以选择按照记录中的任何字段对输出中的记录进行排序,那就太好了。实现这一点的一种方法是允许 function 对象作为参数传递,该参数比较与键相关联的Details对象的成员之一。下面是实现这一点的代码:

template<typename Compare>

void list_sorted_people(const People& people, Compare comp)

{

std::vector< Element_type*> folks;

for(const auto& pr : people)

folks.push_back(&pr);

// Lambda to compare elements via pointers

auto ptr_comp =

&comp->bool

{  return comp(*pr1, *pr2);  };

std::sort(std::begin(folks), std::end(folks), ptr_comp); // Sort the pointers to elements

// Output the sorted elements

DOB dob {};

size_t height {};

string occupation {};

std::cout << '\n';

for(const auto& p : folks)

{

std::tie(dob, height, occupation) = p->second;

std::cout << std::setw(20) << std::left << (p->first.first + " " + p->first.second)

<< "DOB: " << std::right << std::setw(2) << std::get<0>(dob) << "-"

<< std::setw(2) << std::setfill('0') << std::get<1>(dob) << "-"

<< std::setw(4) << std::get<2>(dob) << std::setfill(' ')

<< "  Height: " << height

<< "  Occupation: " << occupation << std::endl;

}

}

这是一个函数模板,它使函数对象的类型能够决定在函数调用中推导出的输出顺序。map 中元素的顺序是由键的顺序决定的,所以很明显,重新排序必须发生在map容器之外。我们可以将所有元素复制到另一个容器中,但是更好、更有效的方法是将指向元素的指针存储在另一个容器中,并使用作为第二个参数传递的函数对象对指针进行排序。

存储在vector容器中的指针是类型为const Element_type*的原始指针。在这里使用unique_ptr对象并不是一个好主意,因为unique_ptr<T>拥有它所指向的T对象。如果vector包含了unique_ptr<Element_type>元素,那么map元素就会被复制,这样就违背了使用指针的目的。在这种情况下使用原始指针没有坏处,因为vector及其元素对于函数来说是局部的,只是充当map元素的观察者。

vector元素只是映射中pair对象的地址,这些地址是在for循环中创建和存储的,该循环遍历map中的元素。调用者不一定知道排序是使用指针完成的,所以list_sorted_people()函数模板假设传递给它的函数对象实现了两个map元素的比较。函数体中定义的 lambda 表达式ptr_comp使用comp进行比较,其结果是将指向map元素的指针解引用为参数。因此,在对vector中的指针进行排序的sort()函数调用中使用了ptr_comp lambda 表达式。最后,在基于范围的for循环中产生输出,该循环遍历vector中的指针。tie()函数用于将Details元组中的所有元素提取到局部变量中。然后输出相关联的Details元组的名称和元素。

包含行使这些功能的main()的源文件的内容是:

// Ex4_04.cpp

// Using tuples and pairs

#include <iostream>                                        // For standard streams

#include <iomanip>                                         // For stream manipulators

#include <string>                                          // For string class

#include <cctype>                                          // For toupper()

#include <map>                                             // For map container

#include <vector>                                          // For vector container

#include <tuple>                                           // For tuple template

#include <algorithm>                                       // For sort() template

using std::string;

using Name = std::pair <string, string>;                   // Defines a name pair

using DOB = std::tuple <size_t, size_t, size_t>;           // Month, day, year tuple

using Details = std::tuple < DOB, size_t, string > ;       // DOB, height(inches), occupation

using Element_type = std::map<Name, Details>::value_type; // Type of map element

using People = std::map<Name, Details>;                    // Type of people container

// Code  for get_people() function goes here...

// Code  for list_DOB_Job() function goes here...

// Code  for list_sorted_people() function template goes here...

int main()

{

std::map<Name, Details> people;                     // Records of the people

get_people(people);                                 // Read all the people

std::cout << "\nThe DOB & jobs are: \n";

list_DOB_Job(people);                               // List names, DOB & job

// Define height comparison for people

auto comp = [](const Element_type& pr1, const Element_type& pr2)

{

return std::get<1>(pr1.second) < std::get<1>(pr2.second);

};

std::cout << "\nThe people in height order are : \n";

list_sorted_people(people, comp);

}

有一个令人印象深刻的标准库头文件的#include指令列表,后面是您之前看到的类型别名的定义。main()中的代码相对简单——本质上是三个函数调用。map元素的比较器由 lambda 表达式定义,尽管它可能是一个函数对象。在这种情况下,它比较Details对象中的第二个元素,即一个人的高度。高度值是使用get<>()以你看到的方式提取的。对于映射元素的任何特征,list_sorted_people()模板将与比较器一起工作。以下是一些输出示例:

Enter a first name and a second name: Dan Druff

Enter date of birth as month day year (integers): 2 3 1978

Enter height in inches: 74

Enter occupation: Trichologist

Do you want to enter another(Y or N): y

Enter a first name and a second name: Jane Brudit

Enter date of birth as month day year (integers): 13 11 1990

Enter height in inches: 63

Enter occupation: Barista

Do you want to enter another(Y or N): y

Enter a first name and a second name: Will Derness

Enter date of birth as month day year (integers): 5 5 1981

Enter height in inches: 76

Enter occupation: Explorer

Do you want to enter another(Y or N): N

The DOB & jobs are:

Dan Druff           DOB:  2-03-1978  Occupation: Trichologist

Jane Brudit         DOB: 13-11-1990  Occupation: Barista

Will Derness        DOB:  5-05-1981  Occupation: Explorer

The people in height order are :

Jane Brudit         DOB: 13-11-1990  Height: 63  Occupation: Barista

Dan Druff           DOB:  2-03-1978  Height: 74  Occupation: Trichologist

Will Derness        DOB:  5-05-1981  Height: 76  Occupation: Explorer

使用多映射容器

一个multimap容器是有序的,它存储键/值对,就像一个map一样,但是它允许重复的键。具有相同键的元素将按照它们被添加到容器的顺序出现在一个multimap中。对于multimapmap,你有相同范围的构造函数,用于比较键的默认函数对象是less<K>()multimap的大多数函数成员的工作方式与map的相同。这种差异是因为容器中可能存在重复的键。我将只描述与map不同的multimap的函数成员。

multimap容器的insert()成员插入一个或多个元素,并且总是成功。这个函数有多种版本来insert()单个元素,所有版本都返回指向被插入元素的迭代器。下面是一些假设为std::string使用 using 声明的例子:

std::multimap<string, string> pets;                    // Element is pair{pet_type, pet_name}

auto iter = pets.insert(std::pair<string, string>{string{"dog"}, string{"Fang"}});

iter = pets.insert(iter, std::make_pair("dog", "Spot"));   // Insert Spot before Fang

pets.insert(std::make_pair("dog", "Rover"));               // Inserts Rover after Fang

pets.insert(std::make_pair("cat", "Korky"));               // Inserts Korky before all dogs

pets.insert({{"rat", "Roland"}, {"pig", "Pinky"}, {"pig", "Perky"}}); // Inserts list elements

第三条语句中的第一个参数是一个迭代器,它提示应该将元素放在哪里。该元素直接插入到由iter指向的元素之前,因此这允许您覆盖默认的插入位置,该位置将跟随使用等同于"dog"的键插入的前一个元素。使用默认比较,按键的升序顺序插入元素。具有相同键的元素将按照您插入它们的顺序排列,除非您提供一个提示来改变这个顺序。最后一条语句插入初始化列表中的元素。insert()还有一个进一步的版本,它接受两个迭代器参数来标识要插入的元素范围。

一个multimapemplace()成员以与一个map相同的方式在容器中构建一个新元素。您还可以使用带有multimapemplace_hint(),在这里您可以以迭代器的形式提供一个提示,以控制元素相对于具有相同键的元素的创建位置:

auto iter = pets.emplace("rabbit", "Flopsy");

iter = pets.emplace_hint(iter, "rabbit", "Mopsy");         // Create preceding Flopsy

这两个函数都返回指向插入元素的迭代器。emplace_hint()函数在第一个参数指向的位置之前创建新元素,并尽可能靠近它。只需使用emplace()插入"Mopsy"就可以用"rabbit"键将其定位在所有现有元素之后。

multimap不支持下标操作符,因为键不一定标识唯一元素。类似地,你为一个map容器所拥有的at()函数对于一个multimap来说是不可用的。multimapfind()函数成员返回一个迭代器,该迭代器指向一个元素,该元素的键等同于参数。例如:

std::multimap<std::string, size_t> people{ {"Ann", 25}, {"Bill", 46}, {"Jack", 77},

{"Jack", 32}, {"Jill", 32}, {"Ann", 35} };

std::string name {"Bill"};

auto iter = people.find(name);

if(iter != std::end(people))  std::cout << name << " is " << iter->second << std::endl;

iter = people.find("Ann");

if(iter != std::end(people))  std::cout << iter->first << " is " << iter->second << std::endl;

如果找不到键,则返回容器的结束迭代器,因此您应该经常检查这一点。第一个find()调用有一个 key 对象作为参数,输出语句将会执行,因为这个键存在。第二个find()调用有一个字符串作为参数,这表明参数不必与键的类型相同。使用对容器有效的函数对象,可以将任何值或对象作为可以与键进行比较的参数进行传递。执行最后一个输出语句是因为有一个相当于"Ann."的键,实际上有两个相当于"Ann"的键,在我的系统上,输出对应于 25 岁的 Ann。您可能会得到与第一个人工神经网络相同的输出,但这不能保证。

如果您正在使用一个multimap容器,它几乎肯定会包含具有重复键的元素;否则你会使用一个map。因此,通常情况下,您会希望访问与给定键对应的所有元素。equal_range()函数成员完成了这项工作。具有等价于参数的键的元素范围作为封装在一个pair对象中的一对迭代器返回——还有什么!例如:

auto pr = people.equal_range("Ann");

if(pr.first != std::end(people))

{

for(auto iter = pr.first ; iter != pr.second; ++iter)

std::cout << iter->first << " is " << iter->second << std::endl;

}

equal_range()的参数可以是与键相同类型的对象,也可以是与键不同类型的对象。返回的pair对象的first成员是一个迭代器,指向第一个元素,该元素的键不小于参数;这将是具有等效键的第一个元素(如果存在的话)。如果找不到键,pairfirst成员将是容器的结束迭代器,所以您应该经常检查这种可能性。pairsecond成员是一个迭代器,指向第一个键大于参数的元素;如果没有这样的元素,这将是结束迭代器。代码片段输出来自容器元素的信息,这些元素的键相当于"Ann."

multimaplower_bound()函数成员返回一个迭代器,该迭代器要么指向第一个元素,其键等于或大于函数的参数,要么指向容器的结束迭代器。upper_bound()成员返回一个迭代器,它指向第一个键大于函数参数的元素,如果没有这样的元素,则返回结束迭代器。因此,当存在一个或多个等价键时,这些函数返回容器中匹配该键的元素范围的开始和结束迭代器,这些迭代器与由equal_range()返回的迭代器相同。你可以用这些来重写之前的片段:

auto iter1 = people.lower_bound("Ann");

auto iter2 = people.lower_bound("Ann");

if(iter1 != std::end(people))

{

for(auto iter = iter1 ; iter != iter2; ++iter)

std::cout << iter->first << " is " << iter->second << std::endl;

}

这会产生与前面的代码片段完全相同的输出。通过调用multimapcount()函数成员,可以发现有多少元素的键等同于给定的键:

auto n = people.count("Jack");                             // Returns 2

你可以用不同的方式使用它。一种可能是在find()equal_range()之间选择访问元素。如果您使用 class 作为键将学生存储在一个multimap中,那么您可以使用count()成员来获得班级大小。当然,您也可以通过将您在第一章中遇到的distance()函数模板应用到equal_range()函数成员返回的迭代器或lower_bound()upper_bound()返回的迭代器中,来获得相当于给定键的元素数量:

std::string key{"Jack"};

auto n = std::distance(people.lower_bound(key), people.upper_bound(key));           // No. of elements matching key

Note

有全局equal_range()lower_bound(),upper_bound()函数模板,它们与关联容器中同名函数成员的工作方式略有不同。你将在本书的后面了解到这些。

一个multimaperase()成员有三个版本。一个版本接受指向一个元素的迭代器作为删除该元素的参数;该函数不返回任何内容。第二个版本接受一个键作为参数,并删除所有包含该键的元素;它返回从容器中移除的元素数量。第三个版本接受两个迭代器,它们将容器中的一系列元素定义为参数。该范围内的所有元素都被删除,函数返回一个迭代器,该迭代器指向被删除的最后一个元素之后的元素。

让我们在一个工作示例中尝试一些multimap操作:

// Ex4_05.cpp

// Using a multimap

#include <iostream>                                        // For standard streams

#include <string>                                          // For string class

#include <map>                                             // For multimap container

#include <cctype>                                          // For toupper()

using std::string;

using Pet_type = string;

using Pet_name = string;

int main()

{

std::multimap<Pet_type, Pet_Name> pets;

Pet_type type {};

Pet_name name {};

char more {'Y'};

while(std::toupper(more) == 'Y')

{

std::cout << "Enter the type of your pet and its name: ";

std::cin >> std::ws >> type >> name;

// Add element - duplicates will be LIFO

auto iter = pets.lower_bound(type);

if(iter != std::end(pets))

pets.emplace_hint(iter, type, name);

else

pets.emplace(type, name);

std::cout << "Do you want to enter another(Y or N)? ";

std::cin >> more;

}

// Output all the pets

std::cout << "\nPet list by type:\n";

auto iter = std::begin(pets);

while(iter != std::end(pets))

{

auto pr = pets.equal_range(iter->first);

std::cout << "\nPets of type " << iter->first << " are:\n";

for(auto p = pr.first; start != pr.second; ++p)

std::cout << "  " << p->second;

std::cout << std::endl;

iter = pr.second;

}

}

有类型别名可以使代码中的类型与它们所代表的内容相关联。pets容器存储pair<string,string>对象,这些对象包含作为键的宠物类型和作为对象的宠物名称。第一个循环中的代码将具有给定键的第二个和后续元素插入到具有该键的序列的开头。这使用emplace_hint()来插入元素。如果它是给定类型的第一个元素,则通过调用emplace()就地创建该元素。在第二个while循环中,元素按 pet 类型分组输出。这是通过找到由iter指向的第一个 pet 条目的类型,并使用由equal_range(). iter返回的迭代器列出该 pet 类型的整个序列来完成的,然后将该序列的结束迭代器设置为指向下一个 pet 类型的第一个元素的迭代器,或者是容器的结束迭代器。后者结束了循环。以下是一些输出示例:

Enter the type of your pet  and their name: rabbit Flopsy

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: rabbit Mopsy

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: rabbit Cottontail

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: dog Rover

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: dog Spot

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: snake Slither

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: snake Sammy

Do you want to enter another(Y or N)? y

Enter the type of your pet  and their name: cat Max

Do you want to enter another(Y or N)? n

Pet list by type:

Pets of type cat are:

Max

Pets of type dog are:

Spot  Rover

Pets of type rabbit are:

Cottontail  Mopsy  Flopsy

Pets of type snake are:

Sammy  Slither

输出显示元素按照键的升序排序,具有相同键的元素按照与输入时相反的顺序排序。

更改比较函数

您可能需要更改mapmultimap的比较函数有几个原因:您可能希望元素按降序排序,而不是默认的升序;或者,您的键可能需要一个不同于直接小于或大于运算的比较函数,例如,如果键是指针,这将适用。在我举例说明如何指定一个可选的比较之前,我将首先强调您为比较键定义的任何函数对象的一个非常重要的要求:

Caution

为了相等,map容器的比较函数不能返回true

换句话说,你不能使用<=>=比较。那么这是为什么呢?一个mapmultimap容器使用等价来决定什么时候键是相等的。两个键key1key2是等价的,因此如果表达式key1 < key2key2 < key1都产生false,则认为它们是相等的。换句话说,等价意味着表达式!(key1 < key2) && !(key2 < key1)的计算结果为true。考虑一下如果你的函数对象实现了<=会发生什么。当key1等于key2时,key1 <= key2key2 <= key1都计算为true,所以表达式!(key1 <= key2)&&!(key2 <= key1)计算为false;这意味着从容器的角度来看,这些键根本就不相等。事实上,没有任何情况下键会被确定为相等。这意味着容器不能正常工作。让我们看看如何提供一个替代的比较函数,使容器正确运行。

使用更大的对象

假设我们为本章前面使用的Name类实现了operator>()。在类定义中,operator>()成员的代码将是:

bool operator>(const Name& name) const

{

return second > name.second || (second == name.second && first > name.first);

}

当然,您可以将成员的定义放在类之外:

inline Name::bool operator>(const Name& name) const

{

return second > name.second || (second == name.second && first > name.first);

}

现在我们可以用Name对象作为键定义一个map,并让容器中的pair对象按降序排列:

std::map<Name, size_t, std::greater<Name>> people

{ {Name{"Al", "Bedo"}, 53}, {Name{"Woody", "Leave"}, 33}, {Name{"Noah", "Lot"}, 43} };

比较键的函数对象的类型由第三个模板类型参数指定。一个greater<Name>对象将使用>操作符来比较Name对象,这是可行的,因为Name类实现了operator>()。这三个元素现在将按降序排列。如果您列出这些元素,这一点会很明显,您可以这样做:

for( const auto& p : people)

std::cout << p.first << " " << p.second << " \n";

基于范围的for循环遍历people容器中的元素,输出将是:

Noah Lot  43

Woody Leave  33

Al Bedo  53

定义自己的函数对象来比较元素

如果mapmultimap中的键是指针,那么你需要定义一个函数来比较它们所指向的内容,否则指针所代表的地址就会被比较,而这很少是你想要的。如果键的类型不直接支持小于或大于比较,您必须定义一个函数对象,使键能够被适当地比较,以便在mapmultimap中使用它们。你处理这两种情况的方式基本相同。

假设我们想要使用指向我们在堆上创建的对象的指针作为map容器中的键。我将使用指向string对象的智能指针来说明这一点。键的类型可以是unique_ptr<string>,在这种情况下,我们需要一个比较函数,它将有两个unique_ptr<string>参数,并将比较所指向的string对象。你可以通过一个仿函数来定义它——一个函数对象——我假设一个std::stringusing指令:

// Compares keys that are unique_ptr<string> objects

class Key_compare

{

public:

bool operator()(const std::unique_ptr<string>& p1, const std::unique_ptr<string>& p2) const

{

return *p1 < *p2;

}

};

我们可以使用Key_compare类型作为函数对象的类型,map应该使用它来比较键:

std::map<std::unique_ptr<string>, std::string, Key_compare> phonebook;

第三个map模板参数指定提供元素比较的函数对象的类型。因为这个类型参数有一个指定的默认值less<T>,所以您必须指定您的函数对象的类型。映射中的元素是pair对象,封装了一个指向存储为string的名称的智能指针,以及一个存储为string的电话号码。我们不能用这个map来使用初始化列表,因为初始化列表涉及到复制,而unique_ptr对象不能被复制。我们至少有几种方法可以向容器中添加元素:

phonebook.emplace(std::make_unique<string>("Fred"), "914 626 7897");

phonebook.insert(std::make_pair(std::make_unique<string>("Lily"), "212 896 4337"));

第一条语句创建了pair对象,它是元素的位置。有一个pair构造函数可以移动这里指定的参数,所以不需要复制它们。第二条语句调用容器的insert()成员,这也将把作为参数的元素移动到容器中。

您可以像这样列出phonebook容器中的元素:

for(const auto& p: phonebook)

std::cout << *p.first << " " << p.second << std::endl;

基于范围的for循环遍历map中的元素,这些元素是pair对象。每个pair对象的第一个成员是一个惟一的指针,所以必须取消引用才能访问它所指向的字符串。如果使用迭代器访问元素,语法会有所不同:

for(auto iter = std::begin(phonebook); iter != std::end(phonebook); ++iter)

std::cout << *iter->first << " " << iter->second << std::endl;

这与前面的循环产生相同的输出,但是使用了迭代器。必须使用->操作符来访问pair对象的成员。由于定义Key_compare函子的方式,容器中的元素将按升序排列。

散列法

如果将对象及其相关的键存储在容器中,而键/对象对不是按键排序的,那么必须有一个方案,以某种方式使用键值来定位内存中的元素。例如,作为字符串等对象的键的问题是,可能的变体数量巨大。例如,10 个字符的字母串的可能值的数量是 26 10 ,换句话说就是 2.6×1010–2600 亿。这不是一个有用的索引范围。你需要的是一种机制,将这样的范围缩小到更合理的限度内;理想情况下,该机制应该为每个键产生一个唯一的值。这是哈希做的事情之一。

哈希是从基本类型的数据项或从字符串等对象生成给定范围内的整数值的过程。哈希得到的值称为哈希值或哈希代码,通常在容器中用来定位表中的对象。正如我所说的,理想情况是散列应该在每种情况下产生唯一的值,但这在一般情况下是不可能的。俗话说“你不能把一夸脱放进一品脱的锅里”,这是显而易见的,因为当不同键值的数量大于可能的哈希值的数量时,你迟早会得到重复的键值。重复的哈希值被称为冲突。使用散列来定位元素的容器为处理来自不同键的重复散列值做好了准备,我将在无序 map 容器的上下文中解释它们是如何做到这一点的。

哈希不仅用于在容器中存储对象。它还有许多其他应用,例如在加密和安全系统中,用于将数据转换成不易理解的形式。例如,密码识别有时涉及散列。以原始形式存储系统密码是一个主要的安全风险。存储密码的哈希值而不是原始的密码字符串可以提供一定的安全性来防止黑客攻击。获得哈希值访问权限的黑客需要能够将哈希值转换回原始密码才能使用它们——这是一项困难的任务。因此,STL 为各种类型的散列数据提供的能力不仅适用于关联容器;它们也可以在更广泛的背景下使用。

理解散列如何与使用它的容器一起工作并不重要,但是对它的一些实现方式有一个基本的理解是有用和有趣的。有许多散列算法,但没有普遍适用的方法。为特定的上下文确定合适的散列方法并不总是容易的。计算除法后的余数经常会涉及到。也许最简单的散列算法是将密钥——不管它是什么——视为一个数值,比如说,k然后计算除以给定数字后的余数,比如说,m,并将其用作散列值。因此,散列值将是表达式k%m的结果。显然,这种方法最多允许m个不同的哈希值,值可以从0m-1。很容易看出哪里会出现重复的哈希值。给定k的哈希值将被复制为k+mk+2*m的键值,以此类推,这些值可能会出现。选择m的值对于最小化生成重复哈希值的可能性和确保值均匀分布至关重要。如果m是 2 的幂,比如说 2 n ,哈希值就是k的最低有效位n。这不是一个很好的结果,因为k的最高有效位对哈希值没有影响;理想情况下,密钥中的所有位都会影响哈希的结果。m通常被选为质数,因为这使得散列值更有可能均匀地分布在整个范围内。

另一种更好的计算哈希值的方法是将键值k乘以精心选择的常数a,计算a*k除以整数m后的余数,然后从(a*k)%m结果的中间选择一个给定长度的比特序列n作为哈希值。显然,am的选择很重要。对于 32 位整数的计算机,通常选择m为 2 32 。然后选择乘数a为与m互质的值,这意味着am没有大于 1 的公因数。此外,a的二进制表示不应该有前导零或尾随零,否则会因为键值有前导零和/或尾随零而产生冲突。由于显而易见的原因,这种方法被称为散列的乘法方法。

散列字符串有专门的算法。一种方法是将一个字符串视为多个单词,使用乘法等方法计算第一个单词的哈希值,向其添加下一个单词并对结果进行哈希运算,然后对所有单词以相同的方式继续计算,以产生该字符串的最终哈希值。幸运的是,STL 为散列提供了相当多的帮助,所以这是下一个主题。

生成哈希值的函数

functional头定义了无序关联容器使用的hash<K>模板的专门化。hash<K>模板为从类型K的对象创建哈希值的函数对象定义了类型。一个hash<K>实例的operator()()成员接受一个类型为K的参数,并将哈希值作为类型size_t返回。所有的基本类型和指针类型都有hash<K>模板的专门化。

hash<K>模板专门化使用的算法取决于实现,但是如果要符合 C++14 标准,它们必须满足一些特定的要求。其中包括:

  • 它们不能抛出异常。
  • 它们必须为相等的键生成相等的哈希值。
  • 不相等密钥的冲突概率必须非常小——接近最大值size_t的倒数。

请注意,为相同的键生成相同的哈希值的要求仅适用于单次执行中。特别允许对给定的密钥进行散列可以在不同的场合产生不同的散列值。这允许在哈希算法中使用随机数据,这在将哈希应用于密码术时是理想的——例如,当哈希密码时。还要注意,符合 C++14 的条件并不排除给定类型的关键字的散列值可能与该关键字相同的可能性。在无序关联容器中用于散列整数键的散列函数可能就是这种情况。

下面是一个使用hash<K>从整数生成哈希值的例子:

std::hash<int> hash_int;                                     // Function object to hash int

std::vector<int> n {-5, -2, 2, 5, 10};

std::transform(std::begin(n), std::end(n), std::ostream_iterator<size_t>(std::cout, " "), hash_int);

这使用了transform()算法来散列vector中元素的值。transform()的参数是定义操作范围的迭代器,定义结果目的地的迭代器,这里是ostream迭代器,最后是应用于范围内值的函数—hash<int>对象。在我的系统上,输出是:

554121069 2388331168 3958272823 3132668352 1833987007

对于 C++ 编译器和库,哈希值可能会有所不同,这适用于所有哈希值。下面是一个散列浮点值的例子:

std::hash<double> hash_double;

std::vector<double> x {3.14, -2.71828, 99.0, 1.61803399, 6.62606957E-34};

std::transform(std::begin(x), std::end(x),                std::ostream_iterator<size_t>(std::cout, " "), hash_double);

我的系统上的输出是:

4023697370 332724328 2014146765 3488612130 3968187275

散列指针同样简单:

std::hash<Box*> hash_box;                                  // Box class as in 第二章

Box box{1, 2, 3};

std::cout << "Hash value = " << hash_box(&box) << std::endl;                                    // Hash value = 2916986638 for me

您可以使用相同的函数对象来散列智能指针:

std::hash<Box*> hash_box;                                  // Box class as in 第二章

auto upbox = std::make_unique<Box>(1, 2, 3);

std::cout << "Hash value = " << hash_box(upbox.get()) << std::endl;                                    // Hash value = 1143026886 for me

这只是调用unique_ptr<Box>对象的get()成员来获取原始指针,该指针将是空闲存储中的一个地址,并将其传递给哈希函数。还有一个针对unique_ptr<T>shared_ptr<T>对象的hash<K>模板的专门化。例如,您可以散列unique_ptr<Box>对象,而不是它包含的原始指针:

std::hash<std::unique_ptr<Box>> hash_box;                  // Box class as in 第二章

auto upbox = std::make_unique<Box>(1, 2, 3);

std::cout << "Hash value = " << hash_box(upbox) << std::endl;                                    // Hash value = 4291053140 for me

原始指针和unique_ptr的散列值将是相同的。不要被这误导,认为当键的类型没有特定的散列函数时,散列指针的能力是合适的。你散列的是一个地址,而不是对象本身。指针指向什么都无关紧要。考虑一下,如果使用指向键的指针而不是键将对象存储在无序容器中,会发生什么。指向一个键的指针的哈希值将与原始键的哈希值有很大不同,因为地址不同,所以它对检索对象没有用。需要有一种方法来为您使用的任何类型的密钥生成哈希值。如果键是您已经定义的类型,一种选择是使用 STL 提供的散列函数从您的类的数据成员生成一个散列值。

string头中定义了hash<K>模板的专门化。这些生成的函数对象将从表示字符串的对象中计算哈希值。有四种专门化,对应于字符串类型:stringwstringu16stringu32string。类型为wstring的字符串包含类型为wchar_t的宽字符;类型u16string包含char16_t字符,是 UTF-16 编码的 Unicode 字符。类型u32string包含char32_t字符,这是 UTF-32 编码的 Unicode 字符。当然,字符类型charwchar_tchar16_t,char32_t都是 C++ 14 中的基础类型。下面是散列一个string对象的例子:

std::hash<std::string> hash_str;

std::string food {"corned beef"};

std::cout << "corned beef hash is " << hash_str(food) << std::endl;

这将创建一个函数对象,以与本节前面的示例相同的方式散列string对象。这段代码的输出是:

corned beef hash is 3803755380

对于 C 风格字符串的散列没有具体的规定。使用类型为const char*hash<T>模板将使用指针的专门化。如果您想获得一个 C 风格字符串的哈希值作为字符序列的哈希值,您可以从中创建一个string对象并使用一个hash<string>函数对象。

我所展示的代码片段产生的哈希值都是巨大的数字,对于决定在无序容器中何处存储对象来说,看起来并不十分有用。有多种方法可以使用哈希值在容器中定位对象。一种常见的方法是使用来自哈希值的位的子序列作为索引来识别对象在表或树中的位置。

使用 unordered_map 容器

一个unordered_map容器存储具有唯一键的键/值对元素。元素在容器中不是有序的。元素是使用键的散列值来定位的,因此对于您使用的键的类型,必须有一个散列函数。如果你正在使用一个你已经定义为键的类类型的对象,你需要为它定义一个实现散列函数的函数对象。如果您的键是 STL 中受特殊化hash<T>支持的类型的对象,容器可以使用它来生成键的哈希值。因为键允许无序映射中的对象无需搜索即可访问,所以检索元素的速度比在有序映射容器中更快。在无序映射中迭代一系列元素通常比在有序映射中慢,所以在任何特定应用程序中容器的选择取决于您希望如何访问元素。

一个unordered_map容器中的元素组织方式与一个map容器中的完全不同,元素内部组织的具体方式取决于你的 C++ 实现。通常,元素存储在哈希表中,表中的条目被称为桶,一个桶可以保存几个元素。给定的散列值选择特定的桶,并且因为可能的散列值的数量几乎肯定大于桶的数量,所以两个不同的散列值可以映射到同一个桶。因此,可能由于两个不同的关键字导致相同的散列值而产生冲突,也可能由于两个不同的散列值选择相同的桶而产生冲突。

有许多参数会影响元素存储的管理方式:

  • 容器中的桶数。存储桶的数量是默认的,但是您也可以指定初始数量。
  • 负载系数,即每个存储桶的平均元素数量。这是存储在容器中的元素数除以存储桶数。
  • 负载系数的最大值,默认为 1.0,但是您可以更改它,正如您将看到的。这是负载系数的上限。当达到装载因子的最大值时,容器将为更多的桶分配空间,这通常包括重新散列容器中的元素。

不要将任何给定时刻一个桶中的最大元素数量与最大负载系数相混淆。假设您有一个包含八个桶的容器,进一步假设前两个桶中各有 3 个元素,其余的桶为空。这种情况下的负载系数是6/8,也就是0.75,小于默认的最大负载系数 1.0,所以这是可以的。

一个unordered_map的基本组织如图 4-3 所示。

A978-1-4842-0004-9_4_Fig3_HTML.gif

图 4-3。

Data in an unordered_map

为了简单起见,图 4-4 只显示了每个桶中的一个元素。可以使用从 0 开始的索引来访问存储桶。

有各种方法来组织存储桶。一种可能是简单地将一个桶定义为一个序列,比如一个vector,并将该序列的地址存储在哈希表中。另一种方法是将 bucket 定义为一个链表,并将根节点存储在哈希表中。使用的具体方法取决于您的实现。

一个unordered_map必须能够比较键是否相等。这对于识别要从包含多个元素的桶中检索的特定元素,以及确定容器中何时已经存在相同的键是必要的。默认情况下,容器将使用在functional头中定义的equal_to<K>模板的一个实例。这将使用==操作符来比较键,因此容器在键相等时确定它们是相同的,这不同于使用等价的map容器。如果你使用的键是没有实现operator==()的类类型,你需要提供一个函数对象来比较你的键。

创建和管理无序映射容器

您可以像创建map一样简单地创建unordered_map容器,只要键类型K可以使用hash<K>实例散列,并且键可以使用==操作符比较。例如,下面是如何定义和初始化一个unordered_map:

std::unordered_map<std::string, size_t> people {{"Jan", 44}, {"Jim", 33}, {"Joe", 99}}; // Name,age

这将创建包含pair<string, size_t>元素的容器,并从初始化列表中初始化它。容器将有默认数量的桶,并使用一个equal_to<string>()对象来比较键是否相等。它将使用string头定义的hash<K>模板的hash<string>专门化的一个实例来散列这些键。如果不提供初始值,默认构造函数会创建一个空容器,其中包含默认数量的桶。

当您对要存储在容器中的元素数量有了一个很好的概念时,您可以指定构造函数应该分配的存储桶数量:

std::unordered_map<std::string, size_t> people {{{"Jan", 44}, {"Jim", 33}, {"Joe", 99}}, 10};

这个构造函数有两个参数:初始化列表和要分配的桶的数量。

您还可以创建一个容器,其中包含由迭代器定义的一系列pair对象的内容。显然,只要范围包含所需类型的pair对象,任何对象源都是可接受的。例如:

std::vector<std::pair<string, size_t>> folks {{"Jan", 44}, {"Jim", 33}, {"Joe", 99},

{"Dan", 22}, {"Ann", 55}, {"Don", 77}};

std::unordered_map<string, size_t> neighbors {std::begin(folks), std::end(folks), 500};

neighbors容器由来自folks向量的pair<string,size_t>元素填充。这也为neighbors,分配了 500 个存储桶,但是您可以省略这个参数并获得默认的存储桶计数。

您可以指定一个 function 对象,该对象使用前面两个构造函数中的任何一个来定义哈希函数;function 对象是初始化列表构造函数的第三个参数,是范围构造函数的第四个参数,所以在这样做的时候还必须指定一个桶计数。我将说明如何用接受初始化列表的构造函数来实现这一点。

假设我们想使用Name对象作为键,并在Ex4_01中定义Name类。我们必须为该类定义一个散列函数,以及相等运算符,因此该类定义需要扩展如下:

class Name

{

// Private and public members and friends as in Ex4_01...

public:

size_t hash() const { return std::hash<std::string>()(first+second); }

bool operator==(const Name& name) const { return first == name.first && second== name.second; }

};

在这种情况下,编译器可以提供的默认operator==()函数成员是令人满意的,但是我还是把定义放了进去。hash()成员使用一个hash<string>()函数对象来散列一个Name对象的firstsecond成员的连接。一个unordered_map容器需要的散列函数必须接受一个与键相同类型的参数,并以类型size_t返回散列值。我们可以定义一个函数对象类型,它将调用一个Name对象的hash()成员来满足需求:

class Hash_Name

{

public:

size_t operator()(const Name& name) const { return name.hash(); }

};

我们可以使用一个Hash_Name对象来指定一个unordered_map容器在创建时应该使用的散列函数:

std::unordered_map<Name, size_t, Hash_Name> people

{{{{"Ann", "Ounce"}, 25}, {{"Bill", "Bao"}, 46}, {{"Jack", "Sprat"}, 77}},

500,                                         // Bucket count

Hash_Name()};                                // Hash function for keys

元素将是pair<Name, size_t>对象。初始化列表是容器构造函数的第一个参数,它定义了三个这样的对象。请注意大括号是如何嵌套的。最里面的大括号包含了Name构造函数的参数。下一级大括号括起了pair<Name,size_t>构造函数的参数。unordered_map构造函数的第二个参数是桶计数——我们必须指定这个参数,因为我们想要指定第三个参数,它是散列键的函数对象。函数对象的类型是指定作为容器的第三个模板类型参数为Hash_Name。这是必要的,因为模板类型参数有一个默认值,它将不同于我们的函数对象的类型。创建一个用 range 初始化的unordered_map的构造函数,前两个参数是迭代器,第三和第四个参数分别是桶计数和散列函数。

当您需要指定一个函数对象来比较键对象是否相等时,您还必须指定桶计数,以及将从键生成哈希值的函数对象。如果我们忽略Name类的operator==()成员,并假设我们已经定义了一个定义函数对象的Name_Equal类类型,您可以像这样为构造函数指定一个Name_Equal函数对象:

std::unordered_map<Name, size_t, Hash_Name, Name_Equal> people

{{{{"Ann", "Ounce"}, 25}, {{"Bill", "Bao"}, 46}, {{"Jack", "Sprat"}, 77}},

500,                                         // Bucket count

Hash_Name(),                                 // Hash function for keys

Name_Equal()};                               // Equality comparison for keys

有一个额外的模板类型参数和一个额外的构造函数参数。模板类型参数是必需的,因为该参数有默认值。使用接受一系列元素作为初始值的构造函数来指定键相等比较的函数对象本质上是一样的。

您有一个unordered_map的移动和复制构造函数。显然,您将创建一个与参数容器具有相同桶计数和散列函数的重复容器。

调整存储桶计数

如果在保持当前负载系数的情况下,向容器中插入的元素多于存储桶的数量,容器将不得不增加存储桶的数量。这将导致元素被重新散列,以在新的存储桶集中重新分配它们。这将使容器中现有的迭代器失效。您可以通过调用rehash()函数成员随时更改存储桶的数量:

people.rehash(15);                                         // Make bucket count 15

rehash()的参数可以是比当前更多或更少的桶。只要不会导致超过最大装载系数,此语句就会将存储桶计数更改为 15。整个内容将被重新散列,以便在新的存储桶集合中重新分配元素,这将使周围的所有迭代器无效。如果指定的铲斗数超过了最大装载系数,铲斗将会增加,因此不会超过最大值。

如果您想确保增加存储桶计数,可以使用bucket_count()返回的值:

people.rehash((5*people.bucket_count())/4);                // Increase bucket count by 25%

另一种可能性是增加最大负载系数,从而允许每个桶的平均元件数增加:

people.max_load_factor(1.2*people.max_load_factor());      // Increse max load factor by 20%

要更改最大装载因子,您可以使用新值作为参数来调用容器的max_load_factor();如果在没有参数的情况下调用成员,它将返回当前最大值,您可以在此语句中使用该最大值来指定新值。

您可以通过调用unordered_map对象的load_factor()来发现当前的负载系数,返回值是类型float:

float lf = people.load_factor();

您还可以选择设置存储桶的数量,以便它们可以容纳给定数量的元素,同时将负载系数保持在最大值范围内:

size_t max_element_count {100};

people.reserve(max_element_count);

这将设置存储桶计数,以便它可以容纳 100 个元素,而不会超过当前的加载因子限制。这将导致容器的内容被重新散列,从而使现有的迭代器失效。当然,您可以创建和使用unordered_map容器,而不用担心桶数或装载因素。集装箱会处理好的。对于性能很重要并且容器是其中一个重要因素的实际应用程序,这是值得考虑的。当每个桶不超过一个元素时,将获得最快的访问,但是这在实践中是不现实的,因为这将需要大量的存储器,因为不可避免地会有大量的空桶。增加最大加载因子允许每个存储桶有更多的元素,因此总的存储桶更少,这样可以更有效地使用内存。但是,每个存储桶中的元素越多,访问元素的速度越慢,因此性能会越差。这是在每个应用环境中的判断,在那里你设置条件。也许你能做的最重要的事情是避免重复地重复内容。如果您能够很好地猜测将要存储的元素的数量,那么您可以将存储桶的数量和/或加载因子设置在适当的级别,以最大限度地减少重新散列的可能性。

插入元素

一个unordered_map容器的insert()成员提供了与在map中相同的功能范围。您可以通过复制或移动来插入单个元素,可以提示也可以不提示。你也可以插入几个在初始化列表中标识的元素,或者由两个定义范围的迭代器标识的元素。让我们看一些例子,这是第一个:

std::unordered_map<std::string, size_t> people { {"Jim", 33}, {"Joe", 99}};      // Name,age

std::cout << "people container has " << people.bucket_count() << " buckets.\n";  // 8 buckets for me

auto pr = people.insert(std::pair<string,size_t> {"Jan", 44});                   // Move insert

std::cout << "Element " << (pr.second ? "was" : "was not") << " inserted." << std::endl;

第一条语句创建一个包含两个初始元素和一个默认桶计数的容器。下一条语句调用peoplebucket_count()成员来获取桶计数;在我的系统上执行这段代码会返回注释中显示的值,但是您的系统可能会有所不同。insert()调用将是带有右值引用参数的版本,因此pair对象将被移动到容器中。这个函数返回一个pair对象,其中第一个成员是一个迭代器,指向新元素,或者如果它没有被插入,指向阻止它插入的元素。pairsecond成员是一个bool值,如果插入了对象,则该值为true

看看这些陈述:

std::pair<std::string, size_t> Jim {"Jim", 47};

pr = people.insert(Jim);

std::cout << "\nElement " << (pr.second ? "was" : "was not") << " inserted." << std::endl;

std::cout << pr.first->first << " is " << pr.first->second << std::endl;         // 33

因为参数是左值,所以调用带有const引用参数的insert()版本,如果插入成功,它将复制参数。这个插入将会失败,因为已经有一个键值为string("Jim")的元素,所以最后一个语句将报告年龄为 33。

下面是如何插入一个元素,并给出它应该放在哪里的提示:

auto count = people.size();

std::pair<std::string, size_t> person {"Joan", 33};

auto iter = people.insert(pr.first, person);

std::cout << "\nElement " << (people.size() > count ? "was" : "was not") << " inserted." << std::endl;

这里insert()的第一个参数是迭代器,它是前面的insert()调用返回的pair的第一个成员,这是一个关于元素应该放在哪里的提示;容器可能会也可能不会遵守提示。insert()的第二个参数是要插入的元素。这个版本的insert()函数不返回一个pair对象;它只返回一个迭代器,要么指向被插入的元素,要么指向阻止插入的元素。代码片段使用容器中的元素计数(由它的size()成员返回)来确定插入是否成功。

你也可以插入初始化列表的内容:

people.insert({{"Bill", 21}, {"Ben", 22}});            // Inserts the two elements in the list

此版本的insert()不返回值,插入一系列元素的版本也不返回值:

std::unordered_map<std::string, size_t> folks;       // Empty container

folks.insert(std::begin(people), std::end(people));  // Insert copies of all people elements

这些迭代器定义的范围包含来自与folks相同类型的容器的元素,但是它可以来自任何类型的容器,只要这些元素是folks所要求的类型。

您可以通过调用emplace()emplace_hint()成员在unordered_map容器中就地创建元素。例如:

auto pr = people.emplace("Sue", 64);                    // returns pair<iterator, bool>

auto iter = people.emplace_hint(pr.first, "Sid", 67);   // Returns iterator

people.emplace_hint(iter, std::make_pair("Sam", 59));   // Uses converting pair<string, size_t>

emplace()成员根据您提供的参数在容器中创建对象。它返回一个包含迭代器的pair和一个与insert()意义相同的bool值。emplace_hint()的第一个参数是作为提示的迭代器,后面是用于创建元素的参数。它只返回一个迭代器,指向插入的元素或阻止插入的元素。

一个unordered_map容器实现了赋值操作符,用作为参数的unordered_map对象的内容替换容器的内容;

folks = people;                                 // Replace folks elements by people elements

显然,参数必须包含与当前容器相同类型的元素。

访问元素

您可以使用带键的下标操作符来获取对unordered_map中相应对象的引用。例如:

people["Jim"] = 22;                             // Set Jim’s age to 22;

people["May"] = people["Jim"];                  // Set May’s age to Jim’s

++people["Joe"];                                // Increment Joe’s age

people["Kit"] = people["Joe"];                  // Set Kit’s age to Joe’s

这与在map容器中的工作是一样的。对元素使用不存在下标操作符的键将导致使用该键的元素以关联对象的默认值创建。如果容器中没有"Kit",最后一条语句将创建以"Kit"为关键字、年龄为 0 的元素;与"Joe"相关的对象将被复制到"Kit."

at()成员返回一个对与参数相关的对象的引用,该参数是一个键,但是如果键不存在,则抛出一个out_of_range异常。因此,当你不想用默认对象创建元素时,你可以使用at()而不是下标操作符。您还有find()equal_range()成员,它们的工作方式与我描述的map相同。

迭代器可用于unordered_map,因此您可以访问基于范围的for循环中的元素,例如:

for(const auto& person : people)

std::cout << person.first << " is " << person.second << std::endl;

这列出了people容器中的所有元素。

移除元素

您可以通过调用erase()成员从unordered_map中移除元素。参数可以是标识元素的键,也可以是指向元素的迭代器。当参数是一个键时,erase()成员返回一个整数,即被删除的元素的数量,所以0返回表示没有找到。当参数是迭代器时,返回一个迭代器,该迭代器指向被移除元素之后的元素。以下是它的一些用法示例:

auto n = people.erase("Jim");                      // Returns 0 if key not found

auto iter = people.find("May");                    // Returns end iterator if key not found

if(iter != people.end())

iter = people.erase(iter);                           // Returns iterator for element after "May"

您还可以删除由某个范围标识的一系列元素。例如:

// Remove all except 1st and last

auto iter = people.erase(++std::begin(people), --std::end(people));

这将返回一个迭代器,它指向最后一个被移除的元素之后的元素。

当容器中没有元素时,clear()函数成员移除所有元素,而empty()成员返回true

访问存储桶

您可以访问unordered_map中的单个存储桶及其包含的元素。您可以使用容器的begin()end()成员的重载来实现,这些重载返回容器中元素的迭代器。存储桶从 0 开始索引,您可以通过将它的索引传递给容器的begin()成员来获得一个迭代器,该迭代器指向存储桶中作为给定索引位置的第一个元素。例如:

auto iter = people.begin(1);                         // Returns an iterator for the 2nd bucket

向容器的cbegin()成员传递一个索引会返回一个const迭代器,该迭代器指向桶中该索引位置的第一个元素。容器的end()cend()成员也有接受索引的版本,它们分别返回一个迭代器和一个const迭代器,指向桶中指定索引位置的最后一个元素。因此,您可以用这样的循环输出特定存储桶(换句话说,存储桶列表)中的元素:

size_t index{1};

std::cout << "The elements in bucket[" << index << "] are:\n";

for(auto iter = people.begin(index); iter != people.end(index); ++iter)

std::cout << iter->first << " is " << iter->second << std::endl;

您已经看到了一个unordered_mapbucket_count()成员返回桶的数量。bucket_size()成员返回作为参数的索引所选择的存储桶中的元素数量。bucket()函数成员返回一个桶的索引,该桶包含您作为参数传递的键的元素。您可以以各种方式组合使用这些工具。例如:

string key {"May"};

if(people.find(key) != std::end(people))

std:: cout << "The number of elements in the bucket containing " << key << " is "

<< people.bucket_size(people.bucket(key)) << std::endl;

bucket_size()的参数是由bucket()返回的索引。当key在容器中时,这个片段执行输出语句。输出记录了包含key的桶中的元素数量。

这里有一个例子,可以让你了解当你添加元素时,unordered_map容器在你的系统上是如何工作的:

// Ex4_06.cpp

// Analyzing how and when the number of buckets in an unordered_map container increases

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <string>                                // For string class

#include <unordered_map>                         // For unordered_map container

#include <vector>                                // For vector container

#include <algorithm>                             // For max_element() algorithm

using std::string;

using std::unordered_map;

// Outputs number of elements in each bucket

void list_bucket_counts(const std::vector<size_t>& counts)

{

for(size_t i {}; i < counts.size(); ++i)

{

std::cout << "bucket[" << std::setw(2) << i << "] = " << counts[i] << "  ";

if((i + 1) % 6 == 0) std::cout << '\n';

}

std::cout << std::endl;

}

int main()

{

unordered_map<string, size_t> people;

float mlf {people.max_load_factor()};          // Current maximum load factor

size_t n_buckets {people.bucket_count()};      // Number of buckets in container

std::vector<size_t> bucket_counts (n_buckets);   // Records number of elements per bucket

string name {"Name"};                          // Key - with value appended

size_t value {};                               // Element value

size_t max_count {8192};                       // Maximum number of elements to insert

auto lf = people.load_factor();                // Current load factor

bool rehash {false};                           // Records when rehash occurs

while(mlf <= 1.5f)                                    // Loop until max load factor is 1.5

{

std::cout << "\n\n***************New Container***************"

<< "\nNumber of buckets: " << n_buckets

<< "  Maximum load factor: " << mlf << std::endl;

// Insert max elements in container

for(size_t n_elements {}; n_elements < max_count; ++n_elements)

{

lf = people.load_factor();                        // Record load factor before insert

people.emplace("name" + std::to_string(++value), value);

auto new_count = people.bucket_count();           // Current bucket count

if(new_count > n_buckets)                         // If bucket count increases...

{                                                 // Output info

std::cout << "\nBucket count increased to " << new_count

<< ". Load factor was " << lf << " and is now " << people.load_factor()

<< "\nMaximum elements in a bucket was "

<< *std::max_element(std::begin(bucket_counts), std::end(bucket_counts))

<< std::endl;

if(n_buckets <= 64)

{

std::cout << "Bucket counts before increase were: " << std::endl;

list_bucket_counts(bucket_counts);

}

n_buckets = new_count;                                  // Update bucket count

bucket_counts = std::vector < size_t > (n_buckets);     // New vector for counts

rehash = true;                                          // Record rehash occurred

}

// Record current bucket counts

for(size_t i {}; i < n_buckets; ++i)

bucket_counts[i] = people.bucket_size(i);

if(rehash)                                        // If the container was rehashed...

{                                                 // ...output info

rehash = false;                                 // Reset rehash indicator

std::cout << "\nRehashed container. Bucket count is " << n_buckets

<< ". Element count is " << people.size()

<< "\nMaximum element count in a bucket is now "

<< *std::max_element(std::begin(bucket_counts), std::end(bucket_counts)) << std::endl;

if(n_buckets <= 64)                             // If no more than 64 buckets...

{

std::cout << "\nBucket counts after rehash are:\n";

list_bucket_counts(bucket_counts);

}

}

}

std::cout << "Final state for this container is:\n"

<< "Bucket count: " << people.bucket_count()

<< "  Element count: " << people.size()

<< "  Maximum element count in a bucket: "

<< *std::max_element(std::begin(bucket_counts), std::end(bucket_counts))

<< std::endl;

value = 1;                                              // Reset key suffix

people = unordered_map<string, size_t>();               // New empty container

n_buckets = people.bucket_count();

bucket_counts = std::vector < size_t >(n_buckets);      // New vector for bucket counts

mlf += 0.25f;                                             // Increase max load factor...

people.max_load_factor(mlf);                            // ...and set for container

}

}

这个程序的想法是随着容器中元素数量的增加,跟踪负载系数和桶的数量。这将提供对容器如何增加桶计数以及在什么条件下增加桶计数的洞察。你需要耐心,因为它需要相当长的时间来执行。如果花费的时间太长,减少max变量的值。

该示例从一个空的unordered_map容器开始,并插入新元素,直到达到由max.指定的限制。唯一键是通过将to_string()返回的字符串加上递增value的结果作为"name."的参数来生成的。string头中定义的to_string()函数将任何类型的数值转换为string对象。

每个桶中的元素数量记录在一个vector容器中。只要最大负载系数小于或等于 1.5,外部while循环就会继续。嵌套的for循环在unordered_map容器中插入max_count元素。每当桶的数量发生变化时,就会调用list_bucket_counts()辅助函数来输出每个桶中的元素数量。为了防止已经庞大的输出变得难以管理,只列出了 64 个或更少桶的桶计数。当max_count元素已经被插入时,一个新的unordered_map被创建,具有更大的最大装载因子,并且内部循环对新的容器重复。这是为了显示最大负载系数如何影响存储桶数量增加的级别,从而导致元素被重新散列。

我不会复制我的系统的输出,因为它会占用太多的空间,但我会概述发生了什么。初始默认桶计数是 8。添加 8 个元素后,桶数从 8 增加到 64,这是一个非常大的变化。当任何存储桶中的最大元素数为 2,并且除了一个存储桶之外的所有存储桶都包含元素时,会出现这种情况;元素的总数是 9。输出显示,铲斗的增加是由负载系数达到 1.0 触发的。在我的系统上,桶数的下一次增加也是 8 倍,从 64 增加到 512。桶计数增加的因子在此之后减慢到 2,因此桶计数的顺序是:8、64、512、1024、2048、4096 和 8192。看看空桶的数量是如何随着桶的数量而增加的,这很有意思。在我的系统中,任何 bucket 中的最大元素数是 8,不出所料,这是最高的最大负载系数。我在一个最大负载系数为 1.5 的桶中获得了 7 个元素。存储桶数量的每次增加都会导致容器中的所有元素被重新散列并分配到新的存储桶位置。您可以轻松地调整程序,通过在存储桶计数增加之前和之后输出特定元素,来输出它们是如何移动的。这涉及到相当大的开销,因此随着要存储的元素数量的增加,一开始就正确地计算桶数变得更加重要。

我的系统的输出让我们对容器如何将原始哈希值映射到桶索引有了更多的了解。桶的数量总是 2 的幂。这允许桶的索引是来自原始哈希值的固定长度的位序列——3 位用于 8 个桶,6 位用于 64 个桶,9 位用于 512 个桶,等等。这使得获取桶索引变得简单而快速。这解释了为什么在桶计数增加后需要重新散列元素。从给定的散列值中取出 6 位几乎肯定会表示不同于从相同值中取出 3 位的索引值,因此给定的原始散列值很可能映射到不同的桶。

使用 unordered_multimap 容器

一个unordered_multimap容器是一个允许重复键的无序映射。因此,它支持的操作基本上与unordered_map容器相同,除了处理相同的多个键所必需的更改和添加。我只讨论不同之处。创建一个unordered_multimap是通过与unordered_map相同范围的构造函数选项。这里有一个例子:

std::unordered_multimap<std::string, size_t> people {{"Jim", 33}, {"Joe", 99}};

使用insert()emplace()emplace_hint()添加新元素总是使用unordered_multimap,只要参数与容器中的元素类型一致。每个函数成员返回一个迭代器,指向容器中的新元素;这与insert()emplace()中的unordered_map不同,在insert()emplace()中,返回一个pair对象来提供成功或失败的指示以及一个迭代器。以下是一些例子:

auto iter = people.emplace("Jan", 45);

people.insert({"Jan", 44});

people.emplace_hint(iter, "Jan", 46);

第三条语句使用第一条语句返回的迭代器作为放置元素的提示。容器或者您的实现可能会随意忽略这个提示。

由于潜在的重复键,unordered_map支持的at()operator[]()成员对unordered_multimap不可用。访问元素的唯一选项是find()equal_range()成员。成员总是返回一个迭代器到它找到的第一个元素,或者如果它没有找到键,返回结束迭代器。您可以使用一个键参数调用count()来发现容器中具有给定键的元素的数量。这里有一个实例:

std::string key{"Jan"};

auto n = people.count(key);                      // Number of elements stored with key

if(n == 1)

std::cout << key << " is " << people.find(key)->second << std::endl;

else if(n > 1)

{

auto pr = people.equal_range(key);           // pair of begin & end iterators returned

while(pr.first != pr.second)

{

std::cout << key << " is " << pr.first->second << std::endl;

++pr.first;                                // Increment begin iterator

}

}

如果只有一个带有key的元素,那么使用find()来访问该元素;如果有多个元素,那么使用equal_range()来访问该范围。当然,在任何一种情况下你都可以使用equal_range()

让我们看一个unordered_multimap的工作示例。这个例子还将展示一些定义函数模板来使用容器的方法。该计划将实现一个电话簿,允许查找一个或多个名字的电话号码。我将使用一个pair对象来封装第一个和第二个名字,使用一个tuple来记录区号、交换和号码,作为string对象。我将使用以下指令来简化代码的外观:

using std::string;

using std::unordered_multimap;

using Name = std::pair<string, string>;

using Phone = std::tuple<string, string, string>;

电话号码可以用三个整数来表示,但其组成部分更像代码而不是数字。数字中的每个元素都有固定的位数,并且不允许某些数字组合。使用string对象使得检查正确的数字位数或验证区号变得非常简单,如果你想增加这种能力的话。我没有包括它,因为对于这本书来说代码太多了。

重载提取操作符从一个istream对象中读取电话号码将有助于例子中的输入操作。该函数如下所示:

inline std::istream& operator>>(std::istream& in, Phone& phone)

{

string area_code {}, exchange {}, number {};

in >> std::ws >> area_code >> exchange >> number;

phone = std::make_tuple(area_code, exchange, number);

return in;

}

Phone是一个tuple模板类型。这使用make_tuple()从从in读取的局部变量的值中创建phone对象。

我们可以对Name对象做同样的事情:

inline std::istream& operator>>(std::istream& in, Name& name)

{

in >> std::ws >> name.first >> name.second;

return in;

}

这只是丢弃任何前导空格,并从in中读取两个名称字符串,它们是pair对象的成员name

当然,我们也需要输出能力。下面是为Phone对象提供输出的operator<<()函数定义:

inline std::ostream& operator<<(std::ostream& out, const Phone& phone)

{

std::string area_code {}, exchange {}, number {};

std::tie(area_code, exchange, number) = phone;

out << area_code << " " << exchange << " " << number;

return out;

}

这使用了tie<>()函数模板来创建一个引用三个局部变量的tuple。将phone赋值给由tie<>()产生的tuple,将phone成员的值存储在局部变量中,然后写入 out。或者,您可以使用get<>()函数模板来访问phone成员的值。这将是一个更好的方法,因为它将避免在上面的实现中出现的对string对象的复制,但是这个想法是要展示运行中的tie<>()函数。

Name对象重载<<很简单:

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{

out << name.first << " " << name.second;

return out;

}

所有这些 I/O 函数都是inline,所以我把它们放在一个名为Record_IO.h的头文件中。文件开头的#includeusing指令是:

#include <string>                                // For string class

#include <istream>                               // For istream class

#include <ostream>                               // For ostream class

#include <utility>                               // For pair type

#include <tuple>                                 // For tuple type

using Name = std::pair <std::string, std::string>;

using Phone = std::tuple <std::string, std::string, std::string>;

该程序将使用两个关联容器——一个以姓名作为关键字,另一个以电话号码作为关键字,因此它们都包含相同的基本信息,但访问方式不同。有其他更有效的方法来实现同样的事情,但是这个想法是尝试使用unordered_multimap容器。容器将在main()中定义如下:

unordered_multimap<Name, Phone, NameHash> by_name {8, NameHash()};

unordered_multimap<Phone, Name, PhoneHash> by_number {8, PhoneHash()};

没有为pairtuple对象提供默认的散列功能,所以我们必须定义它们。这里它们是NameHashPhoneHash类型的函数对象。哈希函数对象的构造函数参数前面是桶计数的参数,因此必须指定桶计数。我只是把它作为我系统的默认值。

我将两种散列函数类型的定义放在同一个头文件中,我用下面的指令开始将其称为Hash_Function_Objects.h:

#include <string>                                // For string class

#include <utility>                               // For pair type

#include <tuple>                                 // For tuple type

using Name = std::pair<std::string, std::string>;

using Phone = std::tuple < std::string, std::string, std::string>;

我是这样定义PhoneHash类型的:

class PhoneHash

{

public:

size_t operator()(const Phone& phone) const

{

return std::hash<std::string>()(std::get<0>(phone)+std::get<1>(phone)+std::get<2>(phone));

}

};

哈希值是通过将在string头中定义的hash<string>()模板专门化应用于连接电话号码中三个元素的结果而产生的。我以类似的方式定义了HashName类型:

class NameHash

{

public:

size_t operator()(const Name& name) const

{

return std::hash<std::string>()(name.first + name.second);

}

};

我将输出打包,显示在一个单独的函数中支持的操作:

void show_operations()

{

std::cout << "Operations:\n"

<< "A: Add an element.\n"

<< "D: Delete elements.\n"

<< "F: Find elements.\n"

<< "L: List all elements.\n"

<< "Q: Quit the progr.\n\n";

}

列出所有元素的操作将允许按名称或数字输出。我们可以定义一个函数模板来处理这两种可能性:

template<typename Container>

void list_elements(const Container& container)

{

for(const auto& element : container)

std::cout << element.first << "  " << element.second << std::endl;

}

模板将从调用函数时使用的参数中推断出容器类型。两个容器中的元素都是pair对象。对于by_name容器,元素是pair<Name, Phone>对象,对于by_number容器,元素是pair<Phone, Name>对象。因为我们已经为NamePhone类型重载了operator<<(),所以循环体将自动从pair元素的成员类型中选择适当的输出函数。我将把这个函数模板和随后的模板放在完整示例的My_Templates.h头文件中。

按名称或编号查找元素的过程本质上是相同的,因此我们也可以为此定义一个函数模板:

template<typename Container>

auto find_elements(const Container& container) ->

std::pair<typename Container::const_iterator, typename Container::const_iterator>

{

typename Container::key_type key {};

std::cin >> key;

auto pr = container.equal_range(key);

return pr;

}

模板的这段代码对应于 C++ 11 标准。返回类型是一个依赖于容器类型的pair<>模板类型。这是因为由返回的pair封装的迭代器的类型是特定于容器类型的。这意味着编译器不能处理函数名前面的返回类型规范,因为容器类型是由后面的函数参数决定的。要使 C++ 11 编译器能够确定返回类型,必须使用尾随返回类型语法。这允许编译器在处理完函数参数后处理返回类型。注意,typename关键字在pair模板类型参数的规范中是必不可少的。这在局部变量key的类型规范中也是必不可少的。容器中键的类型由容器类的成员key_type指定,因此key的类型规范自动为容器选择正确的类型。如果您需要与一个键相关联的对象类型,它由Container::mapped_type成员指定,容器的元素类型由Container::value_type给出。

C++ 14 标准引入了编译器推断函数返回类型的能力,因此函数模板可以写成这样:

template<typename Container>

auto find_elements(const Container& container)

{

typename Container::key_type key {};

std::cin >> key;

auto pr = container.equal_range(key);

return pr;

}

不需要尾随的返回类型,因为编译器可以将返回类型推断为返回值的类型,即pr的类型。

查找元素的操作将允许通过名称或编号进行搜索,在每种情况下,结果可以是一个包含迭代器的pair对象,迭代器定义了一系列这种或那种类型的元素。我们可以定义另一个函数模板来输出这样一系列元素:

template<typename T>

void list_range(const T& pr)

{

if(pr.first != pr.second)

{

for(auto iter = pr.first; iter != pr.second; ++iter)

std::cout << "  " << iter->first << "  " << iter->second << std::endl;

}

else

std::cout << "No records found.\n";

}

如果作为参数的pair对象的成员相同,则该范围为空,在这种情况下,我们只输出一条消息。为NamePhone类型实现插入操作符的函数使这个模板能够工作。pair成员的实际类型将自动选择所需的operator<<()功能。请注意,这些模板不会减少编译程序中的代码。它们只是为生成所使用的函数提供了一种方便的机制,并提供了一些如何使用模板的简单示例。

main()函数将在代码下载的Ex4_07.cpp中,它包含以下语句:

// Ex4_07.cpp

#include <iostream>                              // For standard streams

#include <cctype>                                // For toupper()

#include <string>                                // For string class

#include <unordered_map>                         // For unordered_map container

#include "Record_IO.h"

#include "My_Templates.h"

#include "Hash_Function_Objects.h"

using std::string;

using std::unordered_multimap;

using Name = std::pair<string, string>;

using Phone = std::tuple<string, string, string>;

// show_operations() definition goes here...

int main()

{

unordered_multimap<Name, Phone, NameHash> by_name {8, NameHash()};

unordered_multimap<Phone, Name, PhoneHash> by_number {8, PhoneHash()};

show_operations();

char choice {};                                     // Operation selection

Phone number {};                                    // Records a number

Name name {};                                       // Records a name

while(std::toupper(choice) != 'Q')                  // Go until you quit...

{

std::cout << "Enter a command: ";

std::cin >> choice;

switch(std::toupper(choice))

{

case 'A':                                         // Add a record

std::cout << "Enter first & second names, area code, exchange, "

< < "和用空格分隔的数字:\ n ";

std::cin >> name >> number;

by_name.emplace(name, number);                  // Create in place...

by_number.emplace(number, name);                // ...in both containers

break;

case 'D':                                         // Delete records

{

std::cout << "Enter a name: ";                  // Only find by name

auto pr = find_elements(by_name);

auto count = std::distance(pr.first, pr.second);  // Number of elements

if(count == 1)

{                                               // If there’s just the one...

by_number.erase(pr.first->second);            // ...delete from numbers container

by_name.erase(pr.first);                      // ...delete from names container

}

else if(count > 1)

{                                               // there’s more than one

std::cout << "There are " << count << " records for "

<< pr.first->first << ". Delete all(Y or N)? ";

std::cin >> choice;

if(std::toupper(choice) == 'Y')

{

// Erase records from by_number container first

for(auto iter = pr.first; iter != pr.second; ++iter)

{

by_number.erase(iter->second);

}

by_name.erase(pr.first, pr.second);         // Now delete from by_name

}

}

}

break;

case 'F':                                         // Find a record

std::cout << "Find by name(Y or N)? ";

std::cin >> choice;

if(std::toupper(choice) == 'Y')

{

std::cout << "Enter first name and second name: ";

list_range(find_elements(by_name));

}

else

{

std::cout << "Enter area code, exchange, and number separated by spaces: ";

list_range(find_elements(by_number));

}

break;

case 'L':                                         // List all records

std::cout << "List by name(Y or N)? ";

std::cin >> choice;

if(std::toupper(choice) == 'Y')

list_elements(by_name);

else

list_elements(by_number);

break;

case 'Q':

break;

default:

std::cout << "Invalid command - try again.\n";

}

}

}

我想你不会觉得这很难理解。在输出可能的操作之后,所有的事情都在 while 循环中发生,直到进入'q''Q'为止。循环体只是一个选择所需操作的大的switch语句。

添加一个元素只需要在每个容器中就地创建一个元素,将与by_name容器一起使用的键/对象值切换为by_number容器。

删除元素使用带有by_name容器的find_elements()函数模板。首先从by_number容器中删除元素以保持容器内容的同步是很重要的。为了从by_name容器中删除几个元素,用迭代器调用它的erase()函数,迭代器将范围定义为参数。所有元素都有相同的键,所以您可以将范围中第一个元素的键传递给erase()来删除它们,就像这样:

by_name.erase(pr.first->first);                   // Delete elements with the specified key

对于查找操作,由find_elements()模板的实例返回的pair被直接传递给list_range()模板实例的实例。编译器会自动确保进行正确的调用。最后,为了列出元素,通过指定的键调用list_elements()模板的一个实例来输出元素。

以下是一些输出的示例:

Operations :

A: Add an element.

D: Delete elements.

F: Find elements.

L: List all elements.

Q: Quit the program.

Enter a command: a

Enter first & second names, area code, exchange, and number separated by spaces:

Bill Bloggs 112 234 4545

Enter a command: a

Enter first & second names, area code, exchange, and number separated by spaces:

Nell Bloggs

112 234 4545

Enter a command: a

Enter first & second names, area code, exchange, and number separated by spaces:

Bill Bloggs 914 626 7890

Enter a command: a

Enter first & second names, area code, exchange, and number separated by spaces:

Al Capone 312 334 4566

Enter a command: l

List by name(Y or N)? y

Nell Bloggs  112 234 4545

Bill Bloggs  112 234 4545

Bill Bloggs  914 626 7890

Al Capone  312 334 4566

Enter a command: l

List by name(Y or N)? n

112 234 4545  Bill Bloggs

112 234 4545  Nell Bloggs

914 626 7890  Bill Bloggs

312 334 4566  Al Capone

Enter a command: f

Find by name(Y or N)? y

Enter first name and second name: Bill Blogs

No records found.

Enter a command: f

Find by name(Y or N)? y

Enter first name and second name: Bill Bloggs

Bill Bloggs  112 234 4545

Bill Bloggs  914 626 7890

Enter a command: f

Find by name(Y or N)? n

Enter area code, exchange, and number separated by spaces: 112 234 4545

112 234 4545  Bill Bloggs

112 234 4545  Nell Bloggs

Enter a command: q

摘要

你在本章中学到的关联容器是使用键而不是索引值来访问数据的强大工具。这尤其适用于涉及相关数据项的应用程序——姓名和电话号码,例如:人员和地址、组件和子组件;或子组件和零件。无序映射容器通常提供比有序映射容器更快的对象访问,但是这依赖于具有散列函数的键,这些散列函数在大多数时候生成唯一的散列值。一个差的散列函数将会减慢元素检索的速度,因为需要在桶中搜索匹配的关键字的情况会更频繁地出现。如果您对您的键的散列有疑问,您最好使用有序映射容器。

虽然 map 容器为编程提供了极大的便利,但是存储 pair 对象的 sequence 容器是否是一个合理的选择,尤其是当您可以按键对容器进行排序时,这总是值得考虑的。这有时会比关联容器提供更好的解决方案。

您在本章中学到的重要内容包括:

  • 一个pair<T1,T2>对象封装了两个任意类型的对象。
  • 模板类型的一个实例可以封装任意数量的不同类型的对象。
  • 所有的映射容器都将作为键/对象对的元素存储为pair<const K,T>对象。
  • 一个map<K,T>容器存储具有唯一键的元素,默认情况下使用小于操作符按键排序,所以键类型必须支持<操作符,除非您指定一个替代的比较函数。
  • 一个multimap<K,T>容器中的元素以与一个map相同的方式排序,但是允许重复的键。
  • 有序关联容器使用等价来确定两个键何时相同。其结果是键的比较只能小于或大于。当键相等时返回true的比较函数将阻止容器正确工作。
  • 哈希是从对象生成相对唯一的整数(称为哈希值)的过程。哈希用于决定元素在无序关联容器中的存储位置。哈希在密码学中也很重要。
  • 元素存储在一个unordered_map<K,T>容器中,使用从键中生成的哈希值。哈希值选择一个特定的桶,每个桶可以包含几个元素。unordered_map<K,T>中的键必须是唯一的。
  • 一个unordered_multimap容器类似于一个unordered_map,但是允许重复的键。
  • 默认情况下,使用equal_to<K>比较无序映射容器中的键是否相等,因此键类型必须支持键对象的==比较和散列。

ExercisesImplement a program that uses a map<K,T> container to store the names of the students in each of an arbitrary number of classes. The program should support adding and deleting classes and listing all classes. The list of students in a class should be retrieved and displayed by supplying the class name such as “Biology.”   Implement a program that uses a multimap<K,T> container to store the classes that are attended by each student where there may be more than one student with the same name. The elements should be in descending order. Provide for adding and deleting students, listing all students and their classes, and retrieving and displaying the class for a given student name.   Write a program that simulates a supermarket with a number of checkouts entered from the keyboard. Each checkout should be represented by an element in a map container where the key is the checkout ID, and the queue at that checkout is the associated object. Customers should arrive at random intervals with random checkout occupancy times when they are served. The program should report average and maximum queue lengths for each checkout after a given period that is also entered from the keyboard.   Define a class for Person objects that store a name, an address, and a phone number. Use an unordered_multimap container to store Person object with names as keys. Provide for retrieving an address, or a phone number for a name, as well as listing all Person objects in ascending sequence of names. Define a main() program to demonstrate all the functions offered.

五、使用集合

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​5) contains supplementary material, which is available to authorized users.

本章是关于使用集合的。集合是一个简单的数学概念,其含义是直观的——具有某些共同特征的事物的集合。STL 中有两个关于集合的概念,它们都与集合的数学思想有关。集合可以是由两个迭代器定义的范围内的一系列对象。器械包也是一种具有特定特征的容器。集合容器是关联容器,其中的对象是它们自己的关键点。在本章中,您将学习以下内容:

  • STL 提供了哪些set容器。
  • 不同类型的set容器可用的功能。
  • 可以应用于set容器的操作。
  • 如何创建和使用set容器。
  • 可以应用于由范围定义的对象集的操作。

了解集合容器

除了没有单独的键之外,set 容器与 map 容器非常相似。有四个定义集合的模板,其中两个默认存储按less<T>排序的元素,另外两个使用元素的哈希值存储元素。有序集合的模板在set头中定义,无序集合的模板在unordered_set头中定义。因此,存储在有序集容器中的对象必须支持比较,无序集中的对象必须是可以散列的类型。

定义集合容器的模板有:

  • 容器存储类型为T的对象,并且对象必须是唯一的。默认情况下,使用一个less<T>对象对容器中的元素进行排序和比较。等价,而不是相等,用于确定对象何时相同。
  • 容器以与set<T>容器相同的方式存储T类型的对象,但是可以存储重复的对象。
  • 容器存储类型为T的对象,对象在容器中必须是唯一的。使用从对象生成的哈希值在容器中定位元素。默认情况下,使用equal_to<T>对象比较元素是否相等。
  • 容器以与unordered_set<T>容器相同的方式存储T类型的对象,但是可以存储重复的对象。

有序和无序关联容器可以获得的迭代器种类是不同的。对于有序容器,可以获得正向和反向迭代器,但是对于无序容器,只能获得正向迭代器。

如果你以前没有遇到过set容器,你可能想知道容器到底有什么用,在哪里检索一个对象,你提供相同的对象。如果已经有了对象,为什么还需要检索它呢?也许令人惊讶的是,set 容器有许多用途。

集合是在涉及事物集合的应用程序中存储数据的候选者,其中确定特定集合的成员是感兴趣的。在学术机构中处理班级的应用程序就是一个例子。每个类可以由一个单独的set容器来表示,该容器存储学生。一个set容器可能是合适的,因为一个类中不能有重复的学生;显然,两个学生可以有一个共同的名字,但是代表他们的对象应该是唯一的。你可以很容易地发现某个学生是否注册了某个特定的课程。您还可以确定给定学生注册的所有课程。

通常,当存储了大量元素并且存在随机插入和检索操作时,无序集在操作上比有序集更快。从一组有序的n元素中检索元素与log n成比例。从无序集合中检索元素平均来说是恒定的,并且与元素的数量无关,尽管实际的性能会受到哈希操作的有效性和元素内部组织的不利影响。

对象的存储位置取决于有序集合的比较函数和无序集合的散列函数。您可以对存储相同对象的不同集合使用不同的比较函数或不同的散列函数。一个简单的例子是考虑一个代表雇员的Person类类型,该类封装了个人的许多不同方面。该类可以包括人员 ID、部门、姓名、年龄、地址、性别、电话号码、工资级别等等。然后,您可以创建几个集合,以各种方式对个人进行分类。您可以选择在一个集合中比较或散列工作部门,在另一个集合中比较或散列工资等级。这将允许您访问给定工资级别的员工或给定部门的员工。这些集合不一定需要存储Person对象的副本。您可以在免费商店中创建Person对象,并在容器中存储智能指针。在本章的后面,你会看到一些这样做的例子。我假设在这一章中有一个针对std::stringusing指令来最小化语句中的行溢出。

使用集合容器

一个set<T>容器中元素的内部组织通常与一个map<K,T>容器相同——一个平衡的二叉树。考虑这个set容器定义,它用初始化列表指定的内容创建容器:

std::set<int> numbers {8, 7, 6, 5, 4, 3, 2, 1};

因为默认的比较将是less<int>,所以元素将在容器中以升序排列。内部二叉树将类似于图 5-1 所示。通过执行以下语句,您可以看到元素是按升序排列的:

A978-1-4842-0004-9_5_Fig1_HTML.gif

图 5-1。

Balanced binary tree of integers ordered by less

std::copy(std::begin(numbers), std::end(numbers), std::ostream_iterator<int> {std::cout, " "});

copy()算法将前两个参数指定的范围复制到第三个参数指定的目的地,在本例中是一个输出流迭代器。该语句将以升序输出从 1 到 8 的整数。

当然,您可以为元素提供不同的比较函数对象:

std::set<std::string, std::greater<string>> words

{"one", "two", "three", "four", "five", "six", "seven", "eight"};

该容器中的元素将按降序排列,因此容器中的树将类似于图 5-2 。

A978-1-4842-0004-9_5_Fig2_HTML.gif

图 5-2。

Balanced binary tree of strings ordered by greater<string>

您可以使用来自某个范围的元素创建一个set容器,并选择性地指定比较:

std::set<string> words2 {std::begin(words), std::end(words)};

std::set<string, std::greater<string>> words3 {++std::begin(words2), std::end(words2)};

第一条语句定义了words2来包含来自words容器的元素的副本,这些元素按照默认比较排序——一个less<string>实例。第二条语句定义words3包含来自words2的所有元素的副本,除了第一个。这个容器是使用一个greater<string>实例订购的。

set<T>模板还定义了复制和移动构造函数。提供 move 构造函数很重要,因为它允许函数中本地定义的set容器被有效地返回——无需复制。编译器可以识别你何时返回一个本地的set容器,该容器将在函数结束时被销毁,并将使用 move 构造函数返回它。在本章的后面你会看到一个使用它的例子。

添加和删除元素

一个set没有at()成员或者operator[]()实现,但是除此之外set容器提供的操作很大程度上反映了map容器的操作。为了给一个set添加一个元素,您有insert()emplace()emplace_hint()函数成员。下面是一些使用insert()的例子:

std::set<string, std::greater<string>> words {"one", "two", "three"};

auto pr1 = words.insert("four");      // pr1.first points to new element. pr1.second is true

auto pr2 = words.insert("two");       // Element is NOT inserted - pr2.first points to the

// existing element and pr.second is false

auto iter3 = words.insert(pr.first, "seven"); // iter3 points to new element just before "four"

words.insert({"five", "six"});              // Insert list of elements - no return value

string wrds[] {"eight", "nine", "ten"};

words.insert(std::begin(wrds), std::end(wrds)); // Inserts range - no return value

插入单个元素返回一个pair<iterator,bool>对象,插入带提示的单个元素只是返回一个迭代器。在一个范围或初始化列表中插入多个元素不返回任何内容。在语句中,insert()的参数是一个初始化列表,列表中的值是字符串文字,将用于创建类型为string的对象。

这里有几个在set中就地创建元素的例子:

std::set<std::pair<string,string>> names;

auto pr = names.emplace("Lisa", "Carr");  // pr.first points to new element. pr.second is true

auto iter = names.emplace_hint(pr.first, "Joe", "King");

这和你见过的map是一样的。成员emplace()返回一个pair<iterator,bool>对象,成员emplace_hint()返回一个迭代器。前者的参数直接传递给元素的构造函数,以便就地创建元素。emplace_hint()的第一个参数是迭代器,它提示元素可能的位置,随后的参数传递给元素构造函数。

函数成员删除集合中的所有元素。成员可以从迭代器指定的位置删除一个元素或者一个匹配对象的元素。例如:

std::set<int> numbers {2, 4, 6, 8, 10, 12, 14};

auto iter = numbers.erase(++std::begin(numbers));

// Removes the 2nd element - 4\. iter points to 6

auto n = numbers.erase(12);                          // Returns no. of elements removed - 1

n = numbers.erase(13);                               // Returns no. of elements removed - 0

numbers.clear();                                     // Removes all elements

erase()成员还可以删除一系列元素:

std::set<int> numbers {2, 4, 6, 8, 10, 12, 14};

auto iter1 = std::begin(numbers);                         // iter1 points to 1st element

advance(iter1, 5);                                        // Points to 6th element - 12

auto iter = numbers.erase(++std::begin(numbers), iter1);

// Remove 2nd to 5th inclusive. iter points to 12

如果一个集合不包含任何元素,empty()成员返回true,而size()成员返回它包含的元素数量。如果您担心在一个set中不能存储尽可能多的对象,您可以调用返回最大可能数量元素的max_size()成员,通常是很多。

访问元素

set容器的find()成员返回一个迭代器,当参数存在时,它指向与参数匹配的元素;如果对象不在集合中,则返回结束迭代器。例如:

std::set<string> words {"one", "two", "three", "four", "five"};

auto iter = words.find("one");                       // iter points to "one"

iter = words.find(string{"two"});                    // iter points to "two"

iter = words.find("six");                            // iter is std::end(words)

调用setcount()成员会返回给定键的元素数量,该键只能是01,因为元素在set容器中必须是唯一的。set容器模板还定义了equal_range()lower_bound(),upper_bound()成员,很大程度上是为了与multiset容器保持一致,在那里它们更有用。

使用集合

是时候看看set容器的作用了。我们可以组合一个使用vectorsetmap容器的例子,并引入另一个有用的算法。该示例将一组学生分配到多个课程科目中。每个学生都必须学习规定的最少数量的科目。特定科目的学生可以存储在一个set容器中,因为一个学生在给定的课程中不能出现超过一次。这个例子效率不会很高。会有很多学生的拷贝,这在这个例子中并不重要,但是如果代表学生的对象非常大,并且有很多学生,这就很重要了。在本章的后面,您将了解如何消除对象的复制。图 5-3 展示了该示例如何工作的基本概念。

A978-1-4842-0004-9_5_Fig3_HTML.gif

图 5-3。

Operations in the example using set containers to represent course groups

示例中定义类型别名的using指令:

using std::string;

using Distribution = std::uniform_int_distribution<size_t>;

using Subject = string;                           // A course subject

using Subjects = std::vector<Subject>;            // A vector of subjects

using Group = std::set<Student> ;                 // A student group for a subject

using Students = std::vector<Student>;            // All the students

using Course = std::pair<Subject, Group>;         // A pair representing a course

using Courses = std::map<Subject, Group>;         // The container for courses

这些别名并不重要,但是它们使代码不那么混乱。Distribution别名是你在第四章中遇到的类型。这种类型定义了正态统计分布,并将用于生成随机数。

首先,我们可以定义一个代表学生的类。学生是简单的灵魂,所以我们可以用一种简单的方式在Student.h头中定义类,就像这样:

#ifndef STUDENT_H

#define STUDENT_H

#include <string>                                                     // For string class

#include <ostream>                                                    // For output streams

class Student

{

private:

std::string first {};

std::string second {};

public:

Student(const std::string& name1, const std::string& name2) : first (name1), second (name2){}

// Move constructor

Student(Student&& student) : first(std::move(student.first)), second(std::move(student.second)){}

Student(const Student& student) :                    first(student.first), second(student.second){}   // Copy constructor

Student() {}                                                        // Default constructor

// Less-than operator

bool operator<(const Student& student) const

{

return second < student.second || (second == student.second && first < student.first);

}

friend std::ostream& operator<<(std::ostream& out, const Student& student);

};

// Insertion operator overload

inline std::ostream& operator<<(std::ostream& out, const Student& student)

{

out << student.first + " " + student.second;

return out;

}

#endif

只有两个数据成员存储学生的名和姓。Student对象最初存储在一个vector容器中,因此定义了默认的构造函数。有一个复制构造函数和一个移动构造函数,后者是为了避免在适当的时候复制对象。定义小于运算符是因为学生将被存储在代表不同科目课程的set容器中。还有一个friend函数,它重载流插入操作符来帮助输出。

创造学生

该程序将需要合理数量的学生,因此为了避免从键盘上费力地输入他们,我们将通过创建名和名的所有组合来合成一个由Student对象组成的vector:

Students create_students()

{

Students students;

string first_names[] {"Ann", "Jim", "Eve", "Dan", "Ted"};

string second_names[] {"Smith", "Jones", "Howe", "Watt", "Beck"};

for(const auto& first : first_names)

{

for(const auto& second : second_names)

{

students.emplace_back(first, second);

}

}

return students;

}

有一个using指令将Students定义为Student对象的vector的别名。该函数使用两个数组中所有可能的名称组合在本地students容器中创建Student对象。外层循环遍历名字,内层循环将每个第二个名字附加到给定的名字上。因此,我们最终会有 25 名不同的学生。我们可以像这样调用函数来创建学生:

Students students = create_students();

一个vector容器有一个移动构造函数,所以编译器会安排移动返回的本地students对象,而不是复制它。上面的语句将调用vector<Student>的移动赋值操作符来移动create_students()返回的值,因此不会复制vector或其元素。学生的类型在这里是显式的,但是您可以使用auto让编译器从create_students()的返回类型中推导出它。

为一个科目创建一组学生

该示例将随机选择学生,并为他们随机挑选课程,因此将需要一个随机数引擎。为了使它在整个计划中可用,我们可以在全球范围内创建它:

static std::default_random_engine gen_value;

random标题中定义了default_random_engine类型,以及uniform_int_distribution类型。分布对象是一个函数对象,为了用该分布生成一个随机数,您将一个随机引擎对象传递给分布对象的operator()()成员。我们可以在一个函数中利用这一点,为给定的科目创建一组随机的学生:

Group make_group(const Students& students, size_t group_size,                                                          const Distribution& choose_student)

{

Group group;                                             // The group of students for a subject

// Select students for the subject group

// Insert a random student into the group until there are group_size students in it

while(group.size() < group_size)

{

group.insert(students[choose_student(gen_value)]);

}

return group;

}

第一个参数是Student对象的vector,第二个参数是组中需要的学生数量,最后一个参数是用于随机选择学生的索引值的分布。groupStudent对象的set容器。该函数通过调用其insert()成员,将从作为第一个参数传递的vector中随机选择的学生插入到本地set容器group中。不需要检查插入是否成功。如果容器中已经存在一个新对象,则insert()成员不会插入该对象。来自students容器的随机选择可能选择已经存储在group容器中的学生,但是循环将继续下一次迭代,尝试另一个随机选择,直到添加了group_size学生。当函数结束时,返回的本地group对象将被移动,而不是复制,就像前面的函数一样。

创建主题和课程

课程科目的定义如下:

Subjects subjects {"Biology", "Physics",  "Chemistry",  "Mathematics", "Astronomy",

"Drama",   "Politics", "Philosophy", "Economics"};

Subjectsvector<Subject>的别名,Subject是类型string的别名,所以subjectsstring对象的vector。我们将把课程存储在一个map<Subject,Group>容器中,所以每个课程的键将是来自subjects容器的一个唯一的Subject对象。map<Subject,Group>类型定义了别名Courses,因此我们可以像这样定义所有课程的容器:

Courses courses;                                 // All the courses with subject keys

需要确定每个学生需要学习的最少科目数。当我们生成学习某个主题的学生集合时,我们还会对该主题的小组规模设置一些初始约束:

size_t min_subjects {4};                           // Minimum number of Subjects per student

size_t min_group {min_subjects};                   // Minimum no. of students per course

size_t max_group {(students.size()*min_subjects)/subjects.size()};

// Max initial students per course

我选择了一个任意值 4 作为要研究的最少人数,并使一组中最少的学生人数相同。如果所有学生都学习最小数量的科目,并且学生在组中平均分布,则科目组的最大规模也可以任意设置为组中的平均数。您可以试验这些参数,看看它们如何影响学生到科目组的分配。

我们将需要定义分布的函数对象,以便为分配到一个主题组的学生数量选择一个随机值,并随机选择学生:

Distribution group_size {min_group, max_group};       // Distribution for students per course

Distribution choose_student {0, students.size() - 1}; // Random student selector

group_size对象将产生从min_groupmax_group的数字。类似地,choose_student分布将生成有效的索引值,用于从学生的vector中进行选择。

我们将希望随机选择学生参加的课程,因此我们也需要一个Distribution对象:

Distribution choose_course {0, subjects.size() - 1};  // Random course selector

这将生成随机索引值,以便从subjects容器中选择课程。

为学生注册课程

main()中用代表课程的元素填充courses容器的代码只是一个for循环,循环体是一条语句:

for(const auto& subject : subjects)

courses.emplace(subject, make_group(students, group_size(gen_value), choose_student));

诚然,作为循环体的语句做了很多工作。为 courses 容器调用emplace()会在适当的位置创建一个元素。每个元素必须是一个pair<Subject, Group>对象,因此emplace()的参数必须是一个Subject对象和一个Group对象,由emplace()函数传递给pair构造函数。第一个参数由循环变量subject提供,因为循环遍历所有主题。第二个参数是前面看到的make_group()函数返回的Group对象。make_group()的第一个参数是学生的vector;第二个是通过将gen_value引擎传递给group_size函数对象产生的组大小的随机值;第三个参数是用于选择学生的分布。

检查学生的课程

当上面的循环创建了所有的课程后,我们必须检查是否有学生没有履行注册最低数量课程的义务。下面的循环将做到这一点,如果他们没有尽到责任,就为他们注册额外的课程:

for(const auto& student : students)

{ // Verify the minimum number of Subjects has been met

// Count how many Subjects the student is on

size_t course_count = std::count_if(std::begin(courses), std::end(courses),

&student { return course.second.count(student); });

if(course_count >= min_subjects) continue;          // On to the next student

// Minimum no. of Subjects not signed up for

size_t additional {min_subjects - course_count};      // Additional no. of Subjects needed

if(!course_count)                                     // If none have been chosen...

std::cout << student << " is work-shy, having signed up for NO Subjects!\n";

else                                                  // Some have - but E for effort

std::cout << student << " is only signed up for " << course_count << " Subjects!\n";

std::cout << "Registering " << student << " for " << additional

<< " more course" << (additional > 1 ? "s" : "") << ".\n\n";

// Register for additional Subjects up to the minimum

while(course_count < min_subjects)

if((courses.find(subjects[choose_course(gen_value)])->second.insert(student)). second) ++course_count;

}

外部循环遍历vector中的Student对象。count_if()算法用于确定每个学生注册的课程数量。该算法计算由前两个参数指定的范围内的元素数量,函数将这两个参数作为第三个参数传递并返回true。这里的前两个参数指定了courses容器中元素的范围,因此迭代器指向pair<Subject,Group>对象。count_if()的第三个参数必须是返回bool值的一元函数,或者是可以隐式转换为类型bool的值。参数的类型必须是通过取消引用范围内的迭代器而得到的。这里是一个 lambda 表达式,它返回表达式的值course.second.count(student). course是一个类型为pair<Subject,Group>的对象,所以表达式首先选择course对象的second成员。second成员是一个Group对象,类型为set<Student>,因此表达式调用保存学生的 set 容器的count()成员。由于set容器中不允许有重复的元素,所以count()成员只能在student存在时返回1,否则返回0。幸运的是,这些值可以隐式地分别转换为truefalse,因此只要student处于当前过程中,count_if()就会增加计数。

我们为任何没有注册所需科目数量的学生输出一条合适的消息,并执行嵌套的while循环,为学生注册新课程,直到满足最低要求。这是另一个用单个语句体完成大量工作的循环。本质上,这个语句是一个if语句,当前学生每成功注册一门课程,这个语句就会增加course_count。通过调用其find()成员从courses容器中选择一个课程,该成员返回一个迭代器,该迭代器指向与作为参数的键相对应的元素,如果没有找到键,则返回结束迭代器。我们使用所有可能的Subject键创建了课程,所以后一种情况不会发生——如果发生了,我们会知道,因为程序会崩溃。使用由choose_course分布产生的指数值从subjects中随机选择一个新的球场。find()返回的pair元素的second成员是包含课程中的学生的Group,因此用student参数调用其insert()成员会将学生添加到组中,如果他们还不在set中的话。总是有可能学生已经在选择的课程上,在这种情况下,insert()返回的pair对象的second成员将是false,在这种情况下course_count不会递增,循环将继续尝试随机选择的另一门课程。当前学生的课程数量达到最小值min_subjects时,循环结束。

输出课程

为了允许使用另一种 STL 算法输出课程,我们将在一个List_Course.h头文件中定义以下函数对象类型:

// List_Course.h

// Function object to output the students in a group for Ex5_01

#ifndef LIST_COURSE_H

#define LIST_COURSE_H

#include <iostream>                              // For standard streams

#include <string>                                // For string class

#include <set>                                   // For set container

#include <algorithm>                             // For copy()

#include <iterator>                              // For ostream_iterator

#include "Student.h"

using Subject = std::string;                     // A course subject

using Group = std::set<Student>;                 // A student group for a subject

using Course = std::pair<Subject, Group>;        // A pair representing a course

class List_Course

{

public:

void operator()(const Course& course)

{

std::cout << "\n\n" << course.first << "  " << course.second.size() << " students:\n  ";

std::copy(std::begin(course.second), std::end(course.second), std::ostream_iterator<Student>(std::cout, "  "));

}

};

#endif

List_Course类的operator()()成员的参数是对一个Course对象的引用,该对象的类型是pair<string,set<Student>>。该函数输出课程主题,当然是coursefirst成员,以及该课程的学生人数,这是通过调用pairsecond成员的size()成员获得的。课程中的学生由copy()算法列出。当然,前两个参数是由coursesecond成员标识的set<Student>容器的开始和结束迭代器。Student对象的范围被复制到由copy()的第三个参数指定的目的地,它是一个ostream_iterator<Student>对象。该对象将调用范围内每个Student对象的operator<<()成员,将学生输出到cout,后跟几个空格。

我们可以在main()中使用List_Course的一个实例,在一条语句中输出所有的课程,就像这样:

std::for_each(std::begin(courses), std::end(courses), List_Course());

for_each()算法将第三个参数指定的函数对象应用于前两个参数指定的范围内的每个元素。前两个参数定义了对应于所有课程的范围,因此将使用后续课程作为参数来调用List_Courses()()。结果将是每门课程的所有学生都被写到cout

当然,定义List_Course函数对象类型并不重要。您可以使用 lambda 表达式作为for_each()算法的第三个参数:

std::for_each(std::begin(courses), std::end(courses),

[](const Course& course){

std::cout << "\n\n" << course.first << "  " << course.second.size() << " students:\n  ";

std::copy(std::begin(course.second), std::end(course.second),

std::ostream_iterator<Student>(std::cout, "  "));

});

我为这个例子定义了List_Course类型,只是为了演示如何操作,但是除非在一个程序中不止一次需要 function 对象,否则使用 lambda 表达式会更简单、更容易。

完整的程序

包含main()的源文件的内容将是:

// Ex5_01.cpp

// Registering students on Subjects

#include <iostream>                              // For standard streams

#include <string>                                // For string class

#include <map>                                   // For map container

#include <set>                                   // For set container

#include <vector>                                // For vector container

#include <random>                                // For random number generation

#include <algorithm>                             // For for_each(), count_if()

#include "Student.h"

#include "List_Course.h"

using std::string;

using Distribution = std::uniform_int_Distribution<size_t>;

using Subject = string;                          // A course subject

using Subjects = std::vector<Subject>;           // A vector of subjects

using Group = std::set<Student>;                 // A student group for a subject

using Students = std::vector<Student>;           // All the students

using Course = std::pair<Subject, Group>;        // A pair representing a course

using Courses = std::map<Subject, Group>;        // The container for courses

static std::default_random_engine gen_value;

// create_students() helper function definition goes here...

// make_group () helper function definition goes here...

int main()

{

Students students = create_students();

Subjects subjects {"Biology", "Physics", "Chemistry", "Mathematics", "Astronomy",

"Drama", "Politics", "Philosophy", "Economics"};

Courses courses;                                     // All the courses with subject keys

size_t min_subjects {4};                         // Minimum number of Subjects per student

size_t min_group {min_subjects};                 // Minimum no. of students per course

// Maximum initial``students

size_t max_group {(students.size()*min_subjects)/subjects.size()};

// Create groups of students for each subject

Distribution group_size {min_group, max_group};    // Distribution for students per course

Distribution choose_student {0, students.size() - 1}; // Random student selector

for(const auto& subject : subjects)

courses.emplace(subject, make_group(students, group_size(gen_value), choose_student));

Distribution choose_course {0, subjects.size() - 1};  // Random course selector

// Every student must attend a minimum number of Subjects...

// ...but students being students we must check...

for(const auto& student : students)

{ // Verify the minimum number of Subjects has been met

// Count how many Subjects the student is on

size_t course_count = std::count_if(std::begin(courses), std::end(courses),

&student

{  return course.second.count(student); });

if(course_count >= min_subjects) continue;          // On to the next student

// Minimum no. of Subjects not signed up for

size_t additional {min_subjects - course_count};    // Additional no. of Subjects needed

if(!course_count)                                   // If none have been chosen...

std::cout << student << " is work-shy, having signed up for NO Subjects!\n";

else                                                // Some have - but E for effort

std::cout << student << " is only signed up for " << course_count << " Subjects!\n";

std::cout << "Registering " << student << " for " << additional

<< " more course" << (additional > 1 ? "s" : "") << ".\n\n";

// Register for additional Subjects up to the minimum

while(course_count < min_subjects)

if((courses.find(subjects[choose_course(gen_value)])->second.insert(student)).second) ++course_count;

}

// Output the students attending each course

std::for_each(std::begin(courses), std::end(courses), List_Course());

std::cout << std::endl;

}

我不会在书中列出所有的输出,因为这本书篇幅很长,但是下面是我得到的一些输出片段:

Ann Smith is only signed up for 1 Subjects!

Registering Ann Smith for 3 more courses.

Ann Watt is only signed up for 2 Subjects!

Registering Ann Watt for 2 more courses.

Ann Beck is only signed up for 3 Subjects!

Registering Ann Beck for 1 more course.

Jim Smith is work-shy, having signed up for NO Subjects!

Registering Jim Smith for 4 more courses.

...

Ted Beck is work-shy, having signed up for NO Subjects!

Registering Ted Beck for 4 more courses.

Astronomy  9 students:

Dan Beck  Ted Beck  Eve Howe  Ann Jones  Dan Jones  Eve Jones  Ted Smith  Ann Watt  Dan Watt

Biology  14 students:

Ann Beck  Dan Beck  Jim Beck  Ann Howe  Dan Howe  Jim Howe  Dan Jones  Ted Jones  Dan Smith  Eve Smith

Ann Watt  Eve Watt  Jim Watt  Ted Watt

Chemistry  10 students:

Eve Beck  Dan Howe  Ann Jones  Eve Jones  Ted Jones  Dan Smith  Jim Smith  Ann Watt  Dan Watt  Jim Watt

...

Physics  15 students:

Ann Beck  Dan Beck  Eve Beck  Jim Beck  Eve Howe  Ted Howe  Ann Jones  Jim Jones  Ann Smith  Eve Smith

Ted Smith  Dan Watt  Eve Watt  Jim Watt  Ted Watt

Politics  12 students:

Eve Beck  Jim Howe  Ted Howe  Dan Jones  Eve Jones  Jim Jones  Ann Smith  Dan Smith  Eve Smith  Jim Smith

Dan Watt  Ted Watt

集合迭代器

容器成员可以返回的迭代器是双向迭代器。这些迭代器的类型由set<T>模板中的别名定义来定义。您可以从set中获得的迭代器类型的别名是iteratorreverse_iteratorconst_iteratorconst_reverse_iterator,这些名称表明了它们是什么。例如,begin()end()成员返回类型iterator.的迭代器,set容器也有返回类型reverse_iterator迭代器的rbegin()rend()成员,以及返回类型const_iterator迭代器的cbegin()cend()成员。最后,crbegin()crend()成员返回类型const_reverse_iterator的迭代器。

然而,set容器的迭代器类型的别名有些误导。一个set<T>容器的函数成员返回的所有迭代器都指向一个const T元素。因此,iterator迭代器指向一个const元素,就像reverse_iterator迭代器和其他类型一样。这意味着您不能修改元素。如果你想改变一个set容器中的元素,你必须首先删除它,然后插入修改后的版本。

仔细想想,这也不是没有道理。set中的对象是它们自己的键,通过比较来确定对象在容器中的位置。如果要修改元素,可能会使元素的顺序无效,从而中断后续的访问操作。当您必须能够修改对象,并且仍然将它们分组在一个或多个set容器中时,仍然有一种方法可以做到这一点。您将指向对象的指针存储在一个set容器中——最好是智能指针。当你使用set容器时,你通常会存储shared_ptr<T>weak_ptr<T>对象。在set容器中存储unique_ptr<T>对象没有多大意义。您永远无法直接检索元素,因为容器中不存在与unique_ptr<T>对象匹配的独立键。

在集合容器中存储指针

如果您想要对对象进行的更改可能会改变指向您已存储在set中的那些对象的指针的顺序,指针的比较函数不得依赖于对象。大多数时候,您不会关心set中元素的特定顺序,只关心给定的元素是否在容器中。在这种情况下,您可以使用应用于指针的比较函数对象,而不用考虑指针指向的对象。比较容器中智能指针的推荐选项是使用在memory头中定义的owner_less<T>函数对象类型的实例。

owner_less<T>模板为shared_ptr和/或weak_ptr对象的小于比较定义函数对象类型;换句话说,它允许weak_ptr对象与shared_ptr对象进行比较,反之亦然,也允许两个weak_ptrshared_ptr对象进行比较。一个owner_less<T>实例通过调用智能指针的owner_before()函数成员到T来实现比较,这提供了与另一个智能指针的小于比较。shared_ptr<T>weak_ptr<T>模板都定义了这个成员函数。当这个智能指针小于作为参数的智能指针时,owner_before<T>()实例返回true,否则返回false。这种比较基于指针所拥有的对象的地址,并且当两个指针指向同一个对象时,允许确定等价性。

shared_ptr<T>类模板中定义owner_before()实例的函数模板的原型如下所示:

  • template<typename X> bool owner_before(const std::shared_ptr<X>& other) const;
  • template<typename X> bool owner_before(const std::weak_ptr<X>& other) const;

weak_ptr<T>类模板类似地定义了成员。请注意,模板类型参数与类模板的类型参数不同。这意味着,除了比较指向同一类型对象的指针之外,还可以比较指向不同类型对象的指针;换句话说,shared_ptr<T1>对象可以比作shared_ptr<T2>对象或weak_ptr<T2>对象。这意味着指针所指向的可能与它所拥有的不同。

所有权方面对于shared_ptr<T>对象可能很重要。这个话题对于这本书来说有点超前,不过还是危险地生活吧。一个shared_ptr<T>可以共享一个对象的所有权,但是指向一个它不拥有的不同对象;换句话说,共享指针包含的地址不是它所拥有的对象的地址。这种shared_ptr的一个用途是指向它所拥有的对象的成员。如图 5-4 所示。

A978-1-4842-0004-9_5_Fig4_HTML.gif

图 5-4。

A shared pointer that points to a different object from the object it owns

用于创建图 5-4 中pname的构造函数称为别名构造函数。第一个参数是另一个shared_ptr对象,它拥有pname也将拥有的对象。第二个参数是一个原始指针,这个指针将被存储在pname中。第二个参数指向的对象不归pname所有或管理。但是,pname可以用来访问这个对象,它恰好是Person对象的一个数据成员。在这个例子中,pname拥有的对象是Person对象。它指向该对象的一个成员。

销毁图 5-4 中的pperson指针不会导致它所拥有的Person对象被销毁,因为它仍然属于pname指针。已经创建了指针pname来提供对Person对象的name成员的访问。*pperson指的是Person对象,因为pperson包含了它共享所有权的对象的地址。*pname指的是Person对象的name成员,因为pname包含该成员的地址,并且它也拥有Person对象的所有权。这使您能够继续使用pname,即使周围没有包含Person对象地址的shared_ptr<Person>指针。只有当所有拥有该对象所有权的shared_ptr对象都被销毁时,Person对象才会被销毁。如果没有别名构造函数提供的功能,就无法保证存储在pname中的指针的有效性。

在 set 容器中存储智能指针的示例

为了展示如何在set容器中存储智能指针,我将把Ex5_01重构为Ex5_02。在students向量中的元素和在set容器中的元素代表不同学科的学生群体,它们将是智能指针。我可以在set容器中使用shared_ptr<Student>对象,但是我将只为学生的vector使用shared_ptr<Student>元素,并在set容器中使用weak_ptr<Student>对象来展示这涉及到什么。请注意,当您存储weak_ptr<T>对象时,您需要确保只要您在使用weak_ptr<T>对象,它们所依赖的shared_ptr<T>对象就会继续存在。当然,你总是可以通过调用weak_ptr<T>对象的expired()成员来检测与一个weak_ptr<T>关联的shared_ptr<T>对象是否仍然存在;当shared_ptr<T>对象被删除时,返回true

我必须做的第一件事是重新定义在Ex5_02.cpp中使用的类型别名,以适应智能指针:

using std::string;

using Distribution = std::uniform_int_Distribution<size_t>;

using Subject = string;                                               // A course subject

using Subjects = std::vector<Subject>;                                // A vector of subjects

using Group =                                                         // Group for a subject

std::set<std::weak_ptr<Student>, std::owner_less<std::weak_ptr<Student>>>;

using Students = std::vector<std::shared_ptr<Student>>;               // All the students

using Course = std::pair<Subject, Group>;                             // Represents a course

using Courses = std::map<Subject, Group>;                             // The courses

只有GroupStudents别名需要更改。一个Group现在是一个包含weak_ptr<Student>对象的set,这些元素将使用一个owner_less<weak_ptr<Student>>实例进行比较。这将允许集合中的元素与类型为vector<shared_ptr<Student>>Students容器中的元素进行比较。如果两个指针拥有相同的对象,set中的一个元素将匹配vector中的一个元素。

创建 shared_ptr 元素的向量

Student类可以保持在Ex5_01,中的状态,在create_students()函数中只需要修改一条语句:

Students create_students()

{

Students students;

string first_names[] {"Ann", "Jim", "Eve", "Dan", "Ted"};

string second_names[] {"Smith", "Jones", "Howe", "Watt", "Beck"};

for(const auto& first : first_names)

for(const auto& second : second_names)

{

students.emplace_back(std::make_shared<Student>(first, second));

}

return students;

}

现在,emplace_back()的参数是一个由make_shared<Student>()返回的shared_ptr<Student>对象。因为它是临时的,所以这个指针将由emplace_back()转发给shared_ptr<Student>移动构造函数,以在vector中创建元素。

令人惊讶的是,make_group()函数根本不需要改变。所有必要的更改都由类型别名负责。

输出由 weak_ptr 引用的对象

虽然List_Course函数对象类型确实需要改变:

using Subject = std::string;                                 // A course subject

using Group =                                               // A group for a subject

std::set<std::weak_ptr<Student>, std::owner_less<std::weak_ptr<Student>>>;

using Course = std::pair<Subject, Group>;                    // A pair representing a course

class List_Course

{

public:

void operator()(const Course& course)

{

std::cout << "\n\n" << course.first << "  " << course.second.size() << " students:\n  ";

std::copy(std::begin(course.second), std::end(course.second),

std::ostream_iterator<std::weak_ptr<Student>>(std::cout, "  "));

}

};

inline std::ostream& operator<<(std::ostream& out, const std::weak_ptr<Student>& wss)

{

out << *wss.lock();

return out;

}

必须在头文件中复制对Ex5_02.cpp中的Group别名定义的更改。第一个变化是函数调用操作符函数的定义。ostream_iterator模板类型参数必须改为weak_ptr<Student>。这需要为weak_ptr<Student>对象重载插入操作符。要将一个Student对象写到流中,指针需要被解引用。不能解引用 aweak_ptr<T>;您必须首先获得一个拥有相同对象的shared_ptr<T>,然后取消对它的引用。为weak_ptr<Student>对象调用lock()会返回一个shared_ptr<Student>对象,该对象拥有weak_ptr引用的对象,并且可以被解引用。在本例中,我们有理由确信Student对象将仍然存在,但通常情况下可能不是这样。如果由一个weak_ptr引用的对象因为所有拥有的shared_ptr对象都被销毁而被销毁,调用weak_ptrlock()将返回一个包含nullptrshared_ptr。在可能发生这种情况的地方,您必须检查nullptr以避免崩溃。

类型别名已经处理了main(),中所需的大部分更改,但是您还需要取消引用student变量,以便在循环中输出它,该循环迭代students容器中的Student对象,因此:

if(!course_count)                                      // If none have been chosen...

std::cout << *student << " is work-shy, having signed up for NO Subjects!\n";

else                                                   // Some have - but E for effort

std::cout << *student << " is only signed up for " << course_count << " Subjects!\n";

std::cout << "Registering " << *student << " for " << additional

<< " more course" << (additional > 1 ? "s" : "") << ".\n\n";

完整的程序在第五章的Ex5_02文件夹下的代码下载中。如果您运行这个程序,您应该得到类似于Ex5_01的输出。

在映射容器中使用智能指针作为键

Ex5_02还有另一个可以改变的方面——当前所有课程的map容器中的键都是来自subjects向量的string对象的副本。如果键是智能指针,代码需要如何改变?

Subject别名需要更改,为了方便起见,我为std::make_shared添加了一个using声明。Courses别名的定义也将不同:

using std::make_shared;

using Subject = std::shared_ptr<string>;                        // A course subject

using Courses = std::map<Subject, Group, std::owner_less<Subject>>; //  The container for courses

关键字现在是shared_ptr<string>指针,因此用于比较关键字的函数对象类型现在是owner_less<shared_ptr<string>>。显然,main()中主题的vector的定义也将改变:

Subjects subjects { make_shared<string>("Biology"),   make_shared<string>("Physics"),

make_shared<string>("Chemistry"), make_shared<string>("Mathematics"),

make_shared<string>("Astronomy"), make_shared<string>("Drama"),

make_shared<string>("Politics"),  make_shared<string>("Philosophy"),

make_shared<string>("Economics") };

vector中的元素现在是智能指针。

List_Courseoperator()()成员的定义必须考虑到这样一个事实,即map中课程的关键字现在是一个指针:

void operator()(const Course& course)

{

std::cout << "\n\n" << *course.first << "  " << course.second.size() << " students:\n  ";

std::copy(std::begin(course.second), std::end(course.second),

std::ostream_iterator<std::weak_ptr<Student>>(std::cout, "  "));

}

唯一的变化是*去引用course的第一个成员,这是关键。没有必要对main()做更多的修改——它像以前一样工作,产生与以前版本相似的输出,但是有一些微妙的不同。我可以通过扩展一点功能来揭示它。

比较智能指针的问题是

假设不是在main()结束时输出所有课程,而是提供从键盘输入一个主题的能力,然后显示正在学习该主题的学生。首先,我们可以为课程主题提供一个提示,以确保输入了正确的课程:

std::cout << "Course subjects are:\n  ";

for(const auto& p : subjects)

std::cout  << *p << "  ";

std::cout << "\n\n";

现在,我们可以定义一个循环,从键盘上读取一个主题,并逐项列出给定课程的学生:

// Code that doesn’t work!

char answer {'Y'};

string subject {};

while(std::toupper(answer) == 'Y')

{

std::cout << "Enter a course subject to get the list of students: ";

std::cin >> subject;

auto iter = courses.find(make_shared<string>(subject));

if(iter == std::end(courses))

std::cout << subject << " not found!\n";

else

{

List_Course()(*iter);

std::cout << std::endl;

}

std::cout << "Do you want to see another subject(Y or N)? ";

std::cin >> answer;

}

表面上看,这似乎是合理的。我们从输入中创建一个shared_ptr<string>对象,并使用它来搜索键。然而,这段代码没有找到一个主题。该程序可以列出所有的课程,但找不到任何科目的课程。为什么不呢?

答案就在存储课程的map中键的owner_less<T>比较函数对象中。只有当两个指针拥有同一个对象时,它们才匹配;指向相同对象的两个指针根本不是一回事,永远不会相等。基于主题检索课程的唯一方法是访问指向该主题的原始智能指针,或者它的克隆。下面是如何修改代码以适应这种情况:

char answer {'Y'};

string subject {};

while(std::toupper(answer) == 'Y')

{

std::cout << "Enter a course subject to get the list of students: ";

std::cin >> subject;

auto iter = std::find_if(std::begin(subjects),

std::end(subjects),  // Find the pointer in subjects

&subject{ return subject == *psubj; });

if(iter == std::end(subjects))

std::cout << subject << " not found!\n";

else

{

List_Course()(*courses.find(*iter));

std::cout << std::endl;

}

std::cout << "Do you want to see another subject(Y or N)? ";

std::cin >> answer;

}

find_if()算法返回一个迭代器,该迭代器指向由前两个参数指定的范围内的第一个元素,作为最后一个参数的函数对象返回true。如果没有,则返回您为该范围指定的最后一个迭代器。这里的范围包括了subjects向量中的所有元素,最后一个参数是一个 lambda 表达式,当范围中的元素的解引用结果与subject匹配时,该表达式返回true。如果迭代器不是subjects的结束迭代器,它指向vector中的一个shared_ptr<Subject>。解引用迭代器访问 subjects 中的元素,并将其传递给courses容器的find()成员。这将返回一个迭代器,该迭代器指向带有该键的Course对象,因此通过解引用find()返回的迭代器得到的Course对象将被传递给List_Course实例的函数调用操作符,以输出课程中的学生。这个版本的程序在代码下载中是 Ex5_03。

使用多集容器

一个multiset<T>容器就像一个set<T>,但是你可以存储重复的元素。这意味着你可以随时插入一个元素——只要它是可接受的类型。默认情况下,使用less<T>来比较元素,但是您可以指定一个不同的比较器,该比较器不能为相等返回true。例如:

std::multiset<string, std::greater<string>> words{ {"dog", "cat", "mouse"},                                                                     std::greater<string>()};

该语句定义了一个由string元素组成的multiset,这些元素使用一个greater<string>实例进行比较,该实例是构造函数的第二个参数。容器有三个初始元素,由初始化列表指定,它是第一个构造函数参数。就像集合一样,如果两个元素等价,那么它们就是匹配的——在带有比较运算符comp的多重集合中,如果表达式!(a comp b)&&!(b comp a)的计算结果为true,那么元素ab就是等价的。一个multiset容器和一个set,容器有相同的函数成员,但是由于潜在的重复元素,它们中的一些行为不同。与set容器中的功能成员略有不同的功能成员有:

  • insert()总能成功。当插入单个元素时,返回的迭代器指向插入的元素。当插入一系列元素时,迭代器指向插入的最后一个元素。
  • emplace()emplace_hint()总是成功,并且都返回指向新元素的迭代器。
  • 返回一个迭代器,指向第一个与参数匹配的元素,如果没有匹配的元素,返回容器的结束迭代器。
  • equal_range()返回一个包含迭代器的pair对象,迭代器定义了匹配参数的元素范围。如果没有与参数匹配的元素,pairfirst成员将是容器的结束迭代器;在这种情况下,second成员将是第一个大于参数的元素,或者是容器的结束迭代器(如果没有的话)。
  • 返回一个迭代器,它指向第一个匹配参数的成员,如果没有成员,则返回容器的结束迭代器。迭代器与equal_range()返回的pairfirst成员相同。
  • upper_bound()返回与equal_range()返回的pairsecond成员相同的迭代器。
  • count()返回与参数匹配的元素数量。

我们可以使用一个multiset容器而不是一个map来实现你在示例Ex4_02中看到的文本中的词频分析:

// Ex5_04.cpp

// Determining word frequency

#include <iostream>                               // For standard streams

#include <iomanip>                                // For stream manipulators

#include <string>                                 // For string class

#include <sstream>                                // For istringstream

#include <algorithm>                              // For replace_if() & for_each()

#include <set>                                    // For set container

#include <iterator>                               // For advance()

#include <cctype>                                 // For isalpha()

using std::string;

int main()

{

std::cout << "Enter some text and enter * to end:\n";

string text_in {};

std::getline(std::cin, text_in, '*');

// Replace non-alphabetic characters by a space

std::replace_if(std::begin(text_in), std::end(text_in),                                            [](const char& ch){ return !isalpha(ch); }, ' ');

std::istringstream text(text_in);             // Text input string as a stream

std::istream_iterator<string> begin(text);    // Stream iterator

std::istream_iterator<string> end;            // End stream iterator

std::multiset<string> words;                  // Container to store words

size_t max_len {};                            // Maximum word length

// Get the words, store in the container, and find maximum length

std::for_each(begin, end, &max_len, &words

{  words.emplace(word);

max_len = std::max(max_len, word.length());

});

size_t per_line {4},                           // Outputs per line

count {};                               // No. of words output

for(auto iter = std::begin(words); iter != std::end(words); iter = words.upper_bound(*iter))

{

std::cout << std::left << std::setw(max_len + 1) << *iter

<< std::setw(3) << std::right << words.count(*iter) << "  ";

if(++count % per_line == 0)  std::cout << std::endl;

}

std::cout << std::endl;

}

输入过程和从输入中删除非字母字符与Ex4_02中相同。通过for_each()函数从istringstream对象的文本中提取单词,并传递给 lambda 表达式,该表达式是for_each(),的最后一个参数,它在multiset容器中创建元素。文本中的每个单词都将被存储为一个单独的元素,因此容器中通常会有重复的内容。for循环遍历multiset容器words的迭代器,从指向第一个元素的 begin 迭代器开始。元素在容器中是有序的,所以所有等价的元素都在连续的位置。相同元素的数量是通过调用容器的成员count()获得的,其中iter指向的元素作为参数。在每次循环迭代结束时,iter被设置为upper_bound()返回的迭代器,它将是与当前元素不同的元素的迭代器。如果没有,那么upper_bound()将返回容器的结束迭代器,因此循环将结束。

因为元素在multiset中是有序的,所以您可以使用相同的字数来增加for循环中的迭代器,如下所示:

size_t word_count {};                            // Number of identical words

for(auto iter = std::begin(words); iter != std::end(words);)

{

word_count = words.count(*iter);

std::cout << std::left << std::setw(max_len + 1) << *iter

<< std::setw(3) << std::right << word_count << "  ";

if(++count % per_line == 0)  std::cout << std::endl;

std::advance(iter, word_count);

}

这是可行的,但原始循环更好。在这个版本中,循环结束的方式不太明显。在我看来,Ex4_02解决方案比multiset版本更优雅。以下是一些输出的示例:

Enter some text and enter * to end:

He was saying godnight to his horse.

He was saying goodnight to his horse,

And as he was saying goodnight to his horse, he was saying goodnight to his horse.

"Goodnight horse, goodnight horse", he was saying goodnight to his horse.*

And         1  Goodnight   1  He          2  as          1

godnight    1  goodnight   5  he          3  his         5

horse       7  saying      5  to          5  was         5

存储指向派生类对象的指针

您可能希望将指向派生类对象的指针存储在一个setmultiset容器中,您可以通过将元素类型指定为指向基类类型的指针来实现这一点。主要担心的是比较函数——它必须能够比较指向不同派生类类型的对象的基类指针。你通常可以毫无困难地安排这一点,你如何做取决于你是否对元素的顺序有什么特殊的要求。如果你不在乎元素排序的方式,你可以使用一个owner_less<T>实例,但是记住检索一个元素需要你使用一个指向同一个对象的指针,而不是一个等价的对象。让我们考虑一个例子。我将使用一个multiset,尽管不会存储重复的元素。但是,会有不同类型的元素。

假设我们想在一个容器中存储一个人拥有的宠物,其中宠物的类型由一个从基类Pet派生的类类型定义。这个类将在代码下载中的Pet_Classes .h头中这样定义:

ussing std::string;

class Pet

{

protected:

string name {};

public:

virtual ∼Pet(){}                               // Virtual destructor for base class

const string& get_name() const { return name;  }

virtual bool operator<(const Pet& pet) const

{

auto result = std::strcmp(typeid(*this).name(), typeid(pet).name());

return (result < 0) || ((result == 0) && (name < pet.name));

}

friend std::ostream& operator<<(std::ostream& out, const Pet& pet);

};

关于Pet类的operator<()成员的定义有几点需要注意。它被指定为virtual来获得派生类对象的多态行为。它使用了typeid操作符,该操作符产生了一个type_info对象,该对象封装了其操作数的类型。使用typeid需要包含typeinfo头。调用type_info对象的name()成员会返回一个 C 风格的字符串,这将是类型名称的实现定义的表示。在我的系统中,类的类型名以"class "为前缀,所以name()成员为类型为My_Type的对象返回"class My_Type"。在您的系统上可能有所不同。

使用在cstring头中定义的strcmp()来比较类型名字符串。如果第一个参数小于第二个参数,则该函数返回一个负整数,如果两个参数相等,则返回 0,否则返回一个正整数。operator<()函数返回两个表达式或运算的结果。如果第一个表达式是true,函数将总是返回true。当当前对象的类型名小于作为参数的对象的类型名时,就会出现这种情况。因此,对象主要是按类型排序的。当第一个表达式为 false 时,比较的结果将是第二个表达式的结果。只有当类型名字符串相等,并且比较的左操作数的name成员小于右操作数的name成员时,才会返回true

返回表达式中相同类型名称的比较非常重要。您为一个set容器(或一个map)指定的比较必须强加一个严格的弱排序。在其他条件中,这要求如果a < btrue,那么b < a必须是false。如果不比较类型名是否相等,返回值的表达式将不会满足此条件。这可能会导致程序在容器中存储派生类对象时崩溃。很容易看出这是如何发生的。假设您将一个名为"Tiddles"Cat对象cat与一个名为"Rover."Dog对象dog进行比较,由于类型名称的原因,表达式cat < dogtrue。表情dog < cat也是true因为爱称!两个物体同时有一个小于另一个,这肯定是个问题…

当然,除了使用strcmp(),您还可以将type_infoname()成员返回的空终止字符串转换为类型string,然后使用<操作符来比较它们。

输出流的插入操作符将在Pet_Classes.h头中定义,如下所示:

inline std::ostream& operator<<(std::ostream& out, const Pet& pet)

{

return out << "A " <<

string {typeid(pet).name()}.erase(0,6) << " called " << pet.name;

}

这会将类型名和昵称写入输出流。类型名称字符串的表达式首先将 C 样式的字符串转换为类型string,然后从字符串前面删除前六个字符"class,"。如果您的系统使用不同的类型名称表示,您需要修改这一点。

为了简单起见,我将只定义从Pet派生的三个类:CatDogMouse。除了类型之外,它们的定义基本上是相同的。下面以Dog类为例:

class Dog : public Pet

{

public:

Dog() = default;

Dog(const string& dog_name)

{

name = dog_name;

}

};

这只是在构造函数中初始化继承的name成员。所有的派生类都将和 Pet 在同一个头文件中。

定义容器

multiset容器将存储shared_ptr<Pet>对象。我将指定两个using声明来为此定义类型别名:

using Pet_ptr = std::shared_ptr<Pet>;            // A smart pointer to a pet

using Pets = std::multiset<Pet_ptr>;             // A set of smart pointers to pets

Pet_ptr别名简化了multiset容器类型的定义,而Pets别名将简化map容器类型的定义,该容器将存储以人名作为关键字的multiset容器。一个Pets容器可以存储指向Pet对象的指针,以及指向CatDogMouse对象的指针。

Pet_ptr对象的multiset容器需要定义小于比较运算符:

inline bool operator<(const Pet_ptr& p1, const Pet_ptr& p2)

{

return *p1 < *p2;

}

这将解引用作为参数传递的指针,并将结果对象传递给派生类从Pet继承的虚拟operator<()函数成员。该函数将由默认的less<Pet_ptr>函数对象调用,该对象将应用于示例中的multiset容器。

还有两个更进一步的using声明会有所帮助:

using std::string;

using Name = string;

Name别名将使map容器中的键类型更加清晰。我将这样定义main()中的map:

std::map<Name, Pets> peoples_pets;

容器中的元素是pair<Name, Pets>对象,完全是类型pair<string, multiset<shared_ptr<Pet>>。后者对于一个元素所代表的信息要少得多。

为示例定义 main()

从标准输入流中读取人名及其宠物将由一个助手函数完成:

Pets get_pets(const Name& person)

{

Pets pets;

std::cout << "Enter " << person << "'s pets:\n";

char ch {};

Name name {};

while(true)

{

std::cin >> ch;

if(toupper(ch) == 'Q') break;

std::cin >> name;

switch(std::toupper(ch))

{

case 'C':

pets.insert(std::make_shared<Cat>(name));

break;

case 'D':

pets.insert(std::make_shared<Dog>(name));

break;

case 'M':

pets.insert(std::make_shared<Mouse>(name));

break;

default:

std::cout << "Invalid pet ID - try again.\n";

}

}

return pets;

}

看起来代码很多,但是非常简单。首先创建一个类型为Pets的本地multiset容器。这个人的名字作为参数传递给函数,在提示中使用,这个人的宠物在不定的while循环中读取。宠物类型由首字母识别——'C'代表猫,'D'代表狗,依此类推。在main()中将会产生一个提示。键入字符后是宠物的名字,输入'Q'将结束当前人的输入。在switch语句中创建适当类型的shared_ptr<T>对象,并存储在pets容器中。当输入完成时,返回本地pets对象,这将通过移动操作返回。

该程序将在一个Pets容器中输出 pet,因此实现一个流插入操作符将是有用的:

inline std::ostream& operator<<(std::ostream& out, const Pet_ptr& pet_ptr)

{

return out << " " << *pet_ptr;

}

这将取消对智能指针的引用,并使用插入操作符将对象写入输出流out。因此,这将调用属于Pet类的friendoperator<<()函数。我将在另一个函数的定义中使用这个函数,从map容器中输出一个元素:

void list_pets(const std::pair<Name, Pets>& pr)

{

std::cout << "\n" << pr.first << ":\n";

std::copy(std::begin(pr.second), std::end(pr.second),

std::ostream_iterator<Pet_ptr>(std::cout, "\n"));

}

元素是一个pair对象,其中first成员是人的名字,second成员是包含指向他们宠物的指针的multiset容器。在将pair的第一个成员写入标准输出流之后,容器中的元素(即第二个成员)由copy()算法输出。copy()的前两个参数是迭代器,定义了要复制的对象的范围。复制操作的目的地由第三个参数指定,它是一个ostream_iterator<Pet_ptr>对象。这将调用将Pet_ptr作为第二个参数的类型的operator<<()函数,这将调用作为Pet类的friendoperator<<()函数。

main()函数的代码将在下载的Ex5_05.cpp中。以下是文件内容:

// Ex5_05.cpp

// Storing pointers to derived class objects in a multiset container

#include <iostream>                              // For standard streams

#include <string>                                // For string class

#include <algorithm>                             // For copy() algorithm

#include <iterator>                              // For ostream_iterator

#include <map>                                   // For map container

#include <set>                                   // For multiset container

#include <memory>                                // For smart pointers

#include <cctype>                                // For toupper()

#include "Pet_Classes.h"

using std::string;

using Name = string;

using Pet_ptr = std::shared_ptr<Pet>;            // A smart pointer to a pet

using Pets = std::multiset <Pet_ptr>;            // A set of smart pointers to pets

// operator<() function to compare shared pointers to pets goes here...

// Stream insertion operator for pointers to pets goes here...

// get_pets() function to read in all the pets for a person goes here...

// list_pets() function to list the pets in a Pets container goes here...

int main()

{

std::map<Name, Pets> peoples_pets;             // The people and their pets

char answer {'Y'};

string name {};

std::cout << "You’ll enter a person’s name followed by their pets.\n"

<< "Pets can be identified by C for cat, D for dog, or M for mouse.\n"

<< "Enter the character to identify each pet type followed by the Pet&#x2019;s name.\n"

<< "Enter Q to end pet input for a person.\n";

while(std::toupper(answer) == 'Y')

{

std::cout << "Enter a name: ";

std::cin >> name;

peoples_pets.emplace(name, get_pets(name));

std::cout << "Another person(Y or N)? ";

std::cin >> answer;

}

// Output the pets for everyone

std::cout << "\nThe people and their pets are:\n";

for(const auto& pr : peoples_pets)

list_pets(pr);

}

main()中为人和他们的宠物定义了map容器后,会有一个解释输入过程的提示。所有的输入都由while循环管理。通过调用emplace()成员将元素添加到people_pets容器中,这将在适当的位置创建一个元素。第一个参数是名称,第二个参数是get_pets()返回的multiset容器。当输入完成时,人和他们的宠物由基于范围的for循环输出,该循环遍历map中的元素。每个人的输出是通过调用list_pets() helper 函数产生的,使用来自map容器的当前pair元素作为参数。

以下是一些输出的示例:

You’ll enter a person’s name followed by their pets.

Pets can be identified by C for cat, D for dog, or M for mouse.

Enter the character to identify each pet type followed by the Pet&#x2019;s name.

Enter Q to end pet input for a person.

Enter a name: Jack

Enter Jack’s pets:

d Rover c Tom d Fang m Minnie m Jerry c Tiddles q

Another person(Y or N)? y

Enter a name: Jill

Enter Jill’s pets:

m Mickey d Lassie c Korky d Gnasher q

Another person(Y or N)? n

The people and their pets are:

Jack:

A Cat called Tiddles

A Cat called Tom

A Dog called Fang

A Dog called Rover

A Mouse called Jerry

A Mouse called Minnie

Jill:

A Cat called Korky

A Dog called Gnasher

A Dog called Lassie

A Mouse called Mickey

这些宠物是按照宠物类型的升序排列的,这正是我们所希望的。输出还显示,我们成功地将指向各种派生类对象的智能指针存储在一个容器中,该容器具有“指向基的智能指针”类型的元素。

unordered_set 容器

unordered_set头中定义了unordered_set<T>容器类型的模板。一个unordered_set<T>容器提供的功能与一个unordered_map<T>容器类似,但是你存储的对象充当它们自己的键。类型为T的对象使用它们的哈希值定位在容器中,所以必须存在一个Hash<T>()函数。不能在容器中存储重复的对象。元素必须是可以比较是否相等的类型,因为这是确定元素何时相同所必需的。像unordered_map一样,元素存储在散列表的桶中;存储元素的桶是基于其散列值来选择的。图 5-5 显示了unordered_set容器的概念性组织。

A978-1-4842-0004-9_5_Fig5_HTML.gif

图 5-5。

Conceptual data organization in an unordered_set container

图 5-5 说明了两个不同对象"Jack""Jock,"的哈希值可能选择同一个桶的情况。有一个默认的桶数,但是当你创建一个容器时你可以改变它。请记住,正如您在unordered_map中看到的,桶数通常是 2 的幂,因为这样更容易从哈希值的许多位中选择桶。创建一个unordered_set的选项范围与你看到的unordered_map相似。以下是一些例子:

std::unordered_set<string> things {16};                                  // 16 buckets

std::unordered_set<string> words {"one", "two", "three", "four"};        // Initializer list

std::unordered_set<string> some_words {++std::begin(words), std::end(words)};  // Range

std::unordered_set<string> copy_wrds {words};                            // Copy constructor

模板参数有一个默认的类型参数,用于指示散列函数的类型。当存储必须为其提供哈希函数的对象时,需要为此指定模板类型参数以及构造函数中的函数参数。为了存储我为Ex4_01引入的Name类型的对象,unordered_set<Name>容器可以这样定义:

std::unordered_set<Name, Hash_Name> names {8, Hash_Name()};     // 8 buckets & hash function

第二个模板类型参数是散列Name对象的函数对象类型。第二个构造函数参数是 function 对象的一个实例。当你需要指定一个散列函数时,你必须指定一个桶计数,因为它是第一个构造函数参数。如果省略此构造函数的第二个参数,容器将使用您指定的类型的默认实例作为第二个模板类型参数。如果Hash_Name是一个函数对象类型,就不需要指定第二个构造函数参数。

您可以通过调用现有容器的reserve()成员来增加它的桶数。这可能需要一些时间,因为这将导致现有元素被重新散列,以便在新的存储桶集中分配它们。

最大负载系数是每个存储桶的平均元素数量所允许的最大值。默认情况下,最大装载因子是 1.0,就像一个unordered_map容器一样,但是也像map一样,你可以通过向max_load_factor()成员传递一个新的装载因子来改变它。例如:

names.max_load_factor(8.0);                      // Max average no. of elements per bucket

通过增加最大负载系数,您可以减少使用的存储桶数量,但是这会对访问元素的时间产生不利影响,因为您增加了访问元素涉及搜索存储桶的可能性。

没有unordered_set容器的at()函数成员,下标操作符也没有定义。除此之外,函数成员的范围与unordered_map相同。

添加元素

函数成员可以插入一个你作为参数传递的元素。在这种情况下,它返回一个包含迭代器的pair对象,加上一个指示插入是否成功的bool值。如果元素被插入,迭代器指向新元素,如果没有,它指向阻止插入的元素。您可以向insert()提供一个迭代器作为第一个参数,作为第二个参数插入位置的提示。在这种情况下,只返回一个迭代器;可以忽略该提示。还有另一个版本的insert()成员可以插入初始化列表中的元素,在这种情况下不返回任何内容。以下是一些说明性的陈述:

auto pr = words.insert("ninety");             // Returns a pair - an iterator & a bool value

auto iter = words.insert(pr.first, "nine");   // 1st arg is a hint. Returns an iterator

words.insert({"ten", "seven", "six"});        // Inserting an initializer list

当您调用insert()插入一系列元素时,不会返回任何内容:

std::vector<string> more {"twenty", "thirty", "forty"};

words.insert(std::begin(more), std::end(more));  // Insert elements from the vector

一个unordered_setemplace()emplace_hint()函数成员使您能够就地创建元素。正如您在其他set容器中看到的,emplace()的参数是要传递给构造函数来创建元素的参数,emplace_hint()的参数是迭代器,也就是提示,后面是创建元素的构造函数参数。例如:

std::unordered_set<std::pair<string, string>, Hash_pair> names;

auto pr = names.emplace("Jack", "Jones");                    // Returns pair<iterator, bool>

auto iter = names.emplace_hint(pr.first, "John", "Smith");   // Returns an iterator

容器中的元素是代表姓名的pair对象,其中每个姓名由两个代表一个人的名和姓的string对象组成。一个unordered_set<T>元素的默认散列函数是一个hash<T>类模板的实例。该模板具有为基本类型、指针和string对象定义的专门化。因为没有定义hash<pair<string,string>>模板专门化,所以有必要定义一个函数对象来散列元素,并在这里指定它的类型- Hash_pair,作为容器的第二个模板类型参数。对emplace()的调用将名字和名字作为参数传递给pair构造函数。emplace_hint()调用将指向先前插入元素的迭代器作为提示,这可能被忽略。后面的参数是针对pair构造函数的。用于散列存储在names容器中的pair对象的函数对象类型Hash_pair可以定义为:

class Hash_pair

{

public:

size_t operator()(const std::pair<string, string>& pr)

{

return std::hash<string>()(pr.first + pr.second);

}

};

这只是使用了在string头中定义的hash<string>函数对象的一个实例。它对连接一个pair对象的firstsecond成员产生的string进行散列,并将其作为一个pair元素的散列值返回。

检索元素

为一个unordered_set()调用find()返回一个迭代器。这个迭代器指向哈希值与参数值相匹配的元素,如果元素不存在,则指向容器的结束迭代器。例如:

std::pair<string, string> person {"John", "Smith"};

if(names.find(person) != std::end(names))

std::cout << "We found " << person.first << " " << person.second << std::endl;

else

std::cout << "there’s no " << person.first << " " << person.second << std::endl;

给定上一节代码中的容器,该代码片段将报告 John Smith 在场。如果不是这样,find()函数将返回容器的结束迭代器,第二个输出语句将执行。

一个unordered_set容器中的元素没有排序,所以没有upper_bound()lower_bound()函数成员。成员equal_range()返回一个带有迭代器的pair对象,迭代器定义了一个包含所有匹配参数的元素的范围;带unordered_set的只能有一个。如果没有,两个迭代器都将是容器的结束迭代器。调用count()成员返回容器中参数出现的次数。这只能是带unordered_set01。当你需要知道一个容器中总共有多少个元素时,你可以调用它的size()成员。如果容器中没有元素,empty()成员将返回true

删除元素

调用unordered_set容器的clear()函数成员将删除其所有元素。erase()函数成员可以删除散列值与参数值相同的元素。另一个版本的erase()可以删除迭代器参数指向的元素。例如,以下是删除元素的一种不必要的冗长方法:

std::pair<string, string> person {"John", "Smith"};

auto iter = names.find(person);

if(iter != std::end(names))

names.erase(iter);

erase()的迭代器参数必须是指向容器中元素的有效且可取消引用的迭代器,所以确保它不是容器的结束迭代器是很重要的。这个版本的erase()返回一个迭代器,该迭代器指向被删除的元素之后的元素,如果最后一个元素被删除,这个迭代器将是结束迭代器。

删除person对象的简单而明智的方法是这样写:

auto n = names.erase(person);

这个版本的erase()返回被删除的元素的数量作为size_t。在这种情况下,它只能是01,但对于unordered_multiset集装箱,它可以大于1。显然,如果返回了0,那么这个元素就不存在了。

尽管我最初的例子没有用,但调用erase()删除迭代器指向的元素可能非常有用——例如,当您想要删除具有特定特征的元素时。假设您需要删除names容器中第二个名字以'S'开头的所有元素。这个循环可以做到:

while(true)

{

auto iter = std::find_if(std::begin(names), std::end(names),

[](const std::pair<string, string>& pr ){ return pr.second[0] == 'S';});

if(iter == std::end(names))

break;

names.erase(iter);

}

find_if()算法返回一个迭代器,该迭代器指向由前两个参数定义的范围中的第一个元素,作为第三个参数的谓词返回true。谓词的参数必须是从范围中取消引用迭代器所产生的类型的对象。这里的范围是names容器中的所有元素,它们是pair<string,string>对象,谓词是一个 lambda 表达式,当pairsecond成员以'S'作为初始字符时,它返回true。当没有元素导致从 lambda 返回true时,算法将返回该范围的结束迭代器。

还有另一个版本的erase()删除了一系列元素。以下语句将从names中删除除第一个和最后一个元素之外的所有元素:

auto iter = names.erase(++std::begin(names), --std::end(names));

参数是定义要删除的元素范围的迭代器。该函数返回一个迭代器,指向最后一个被删除的元素之后的元素。

制作遗愿清单

您可以使用与您在unordered_map容器中看到的相同的函数来访问unordered_set容器中的桶。您可以通过迭代器访问存储在特定桶中的元素。使用传递给容器的begin()end()成员的桶索引来选择特定的桶;这将返回 bucket 包含的元素范围的开始和结束迭代器。当需要const迭代器时,将桶索引传递给容器的cbegin()cend()成员。bucket_count()成员返回桶的数量,因此您可以使用它来控制遍历容器中所有桶的循环。下面是如何在names容器中列出每个桶中的元素:

for(size_t bucket_index {}; bucket_index < names.bucket_count(); ++bucket_index)

{

std::cout << "Bucket " << bucket_index << ":\n";

for(auto iter = names.begin(bucket_index); iter != names.end(bucket_index); ++iter)

{

std::cout << "  " << iter->first << " " << iter->second;

}

std::cout << std::endl;

}

外部循环迭代桶索引值。内部循环遍历当前桶中的元素范围,并将pair对象的firstsecond成员写入标准输出流。

bucket_size()成员将返回一个桶中由 index 参数标识的元素数量。通过将对象作为参数传递给容器的bucket()成员,可以获得包含特定对象的桶的索引。如果您传递给bucket()的对象不在容器中,如果您将它添加到容器中,该函数将返回存储该对象的桶的索引。因此,bucket()成员无法让您了解对象是否真的存在。下面是如何列出来自names容器的元素以及它们的桶号:

for(const auto& pr : names)

std::cout << pr.first << " " << pr. second << " is in bucket " << names.bucket(pr) << std::endl;

调用bucket()返回参数的桶号。

Note

当然,不带参数调用begin()cbegin()end(),cend()成员会返回容器元素对应的开始和结束迭代器。

使用 unordered_multiset 容器

一个unordered_multiset<T>容器本质上类似于一个unordered_set<T>,除了你可以在容器中存储重复的T对象。我为一个unordered_set容器描述的所有函数成员都可以用于一个unordered_multiset,并且以同样的方式工作,除了重复的元素影响结果。例如,count()成员可以返回一个大于 1 的值,用一个对象作为参数调用erase()函数成员将删除散列值与参数散列值相同的所有元素,而不仅仅是单个元素。让我们来看一个展示unordered_multiset的工作示例。

示例Ex5_06将在unordered_multiset容器中存储我为Ex4_01定义的Name类的变体实例。这个容器将代表我所有朋友的记录,所以我知道在节日给谁寄贺卡。由于现今邮票的成本,为了经济利益,列表必须尽可能的短。下面是这个例子的Name.h头的内容:

// Name.h for Ex5_06

// Defines a person’s name

#ifndef NAME_H

#define NAME_H

#include <string>                                // For string class

#include <ostream>                               // For output streams

#include <istream>                               // For input streams

using std::string;

class Name

{

private:

string first {};

string second {};

public:

Name(const string& name1, const string& name2) : first (name1), second (name2) {}

Name() = default;

const string& get_first() const  { return first; }

const string& get_second() const { return second; }

size_t get_length() const { return first.length() + second.length() + 1; }

// Less-than operator

bool operator<(const Name& name) const

{

return second < name.second || (second == name.second && first < name.first);

}

// Equality operator

bool operator==(const Name& name) const

{

return (second == name.second) && (first == name.first);

}

size_t hash() const { return std::hash<std::string>()(first+second); }

friend std::istream& operator>>(std::istream& in, Name& name);

friend std::ostream& operator<<(std::ostream& out, const Name& name);

};

// Extraction operator overload

inline std::istream& operator>>(std::istream& in, Name& name)

{

in >> name.first >> name.second;

return in;

}

// Insertion operator overload

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{

out << name.first + " " + name.second;

return out;

}

#endif

Ex4_01版本上添加到Name类中的是数据成员的访问函数成员、返回名称总长度的成员、operator==()成员允许比较对象是否相等,以及hash()成员返回一个对象的哈希值作为两个成员名称连接的哈希值。容器类型要求进行相等性比较。get_length()成员只是一个很好地调整输出的使能器。length()成员返回两个名称的长度之和加一,“加一”考虑了输出中名称之间的空格。在operator<<()定义中,通过设置输出字段宽度,名称被连接用于输出,以允许名称在输出中对齐。使用多个<<操作符会使输出更有效,但是会阻止名称在输出中对齐。

容器类型将需要散列函数对象的类型,该散列函数对象散列要被存储的对象以被指定为模板类型参数。该函数对象类型将在Hash_Name.h头中定义如下:

// Hash_Name.h

// Function object type to hash Name objects for Ex5_06

#ifndef HASH_NAME_H

#define HASH_NAME_H

#include "Name.h"

class Hash_Name

{

public:

size_t operator()(const Name& name) {  return name.hash();  }

};

#endif

函数调用操作符函数只调用传递给它的Name对象的hash()成员。

我将从存储在vector容器中的第一个和第二个名字的序列中合成要存储在容器中的Name对象。在另一个助手函数中填充unordered_multiset容器会很方便,该函数将假设下面的类型别名定义有效:

using Names = std::unordered_multiset<Name, Hash_Name>;

第一个unordered_multiset模板类型参数是元素类型,第二个是散列元素的函数对象类型。下面是将在容器中创建元素的助手函数:

void make_friends(Names& names)

{

// Names are duplicated to get duplicate elements

std::vector<string> first_names {"John", "John", "John", "Joan", "Joan", "Jim", "Jim", "Jean"};

std::vector<string> second_names {"Smith", "Jones", "Jones", "Hackenbush", "Szczygiel"};

for(const auto& name1 : first_names)

for(const auto& name2:second_names)

names.emplace(name1,name2);

}

Name对象在嵌套循环中的names容器中就地创建。这将使用名字和姓氏的所有可能组合来创建对象。一些名字和名字是重复的,以确保我们创建和存储一些相同的元素。

该程序将列出容器中桶的内容,以显示哪些朋友共享一个桶,另一个助手函数将有所帮助:

void list_buckets(const Names& names)

{

for(size_t n_bucket {} ; n_bucket < names.bucket_count(); ++n_bucket)

{

std::cout << "Bucket " << n_bucket << ":\n";

std::copy(names.begin(n_bucket), names.end(n_bucket), std::ostream_iterator<Name>(std::cout, "  "));

std::cout << std::endl;

}

}

for循环迭代桶索引值。在每次循环迭代中,一个标识桶的标题行被写出,然后桶中的所有元素被copy()算法写出。copy()算法将每个迭代器指向的Name元素复制到ostream_iterator,后者将对象写入cout。用于将Name对象写入ostream对象的operator<<()函数的重载允许该操作进行。

包含main()的源文件将包含以下代码:

// Ex5_06.cpp

// Using an unordered_multiset container

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <string>                                // For string class

#include <unordered_set>                         // For unordered_multiset containers

#include <algorithm>                             // For copy(), max(), find_if(), for_each()

#include "Name.h"

#include "Hash_Name.h"

using std::string;

using Names = std::unordered_multiset<Name, Hash_Name>;

// Code for make_friends(Names& names) goes here...

// Code for list_buckets() goes here...

int main()

{

Names pals {8};                                // 8 buckets

pals.max_load_factor(8.0);                     // Average no. of elements per bucket max

make_friends(pals);                            // Load up the container with Name objects

list_buckets(pals);                            // List the contents by bucket

// Report the number of John Smith’s that are pals

Name js {"John", "Smith"};

std::cout << "\nThere are " << pals.count(js) << " " << js << "'s.\n" << std::endl;

// Remove all the John Jones’s - we just don’t get on...

pals.erase(Name {"John", "Jones"});

// Get rid of the Hackenbushes - they never invite us...

while(true)

{

auto iter = std::find_if(std::begin(pals), std::end(pals),

[](const Name& name){ return name.get_second() == "Hackenbush"; });

if(iter == std::end(pals))

break;

pals.erase(iter);

}

// List the friends we still have...

size_t max_length {};                            // Stores the maximum name length

std::for_each(std::begin(pals), std::end(pals),  // Find the maximum name length...

&max_length { max_length = std::max(max_length, name.get_length()); });

size_t count {};                                 // No. of names written out

size_t perline {6};                              // No. of names per line

for(const auto& pal : pals)

{

std::cout << std::setw(max_length+2) << std::left << pal;

if((++count % perline) == 0) std::cout << "\n";

}

std::cout << std::endl;

}

容器被构造成最初有8个桶。如果超过最大装载系数,容器中的桶数将自动增加,因此通过max_load_factor()调用将最大装载系数设置为8.0以减少这种可能性。除了展示这个函数的作用之外,在这个例子中增加最大装载因子的想法是为了在列出存储桶时最小化输出的行数。使用默认的最大负载系数1.0,在我的系统上,桶的数量增加到64——这导致了大量的输出。实际上,像这样增加最大装载系数会显著降低操作速度,因为这将增加搜索铲斗的次数。这往往会抵消unordered_multiset相对于multiset的优势。

调用make_friends()使用第一个和第二个名字的所有组合在容器中创建Name元素。调用list_buckets()输出每个桶中的元素。然后程序通过调用容器的count()函数成员输出被称为"John Smith"的朋友数量。

一气之下,我删除了所有名为"John Jones"的朋友,将对应的Name对象传递给容器的erase()成员。想起没有一个哈肯布斯人邀请我喝杯咖啡,我决定他们也要被砍掉。这有点棘手,因为我们必须在容器中找到具有第二个名称的元素。find_if()算法通过返回指向第一个元素的迭代器来解决这个问题,第三个参数 lambda 表达式为第一个元素返回了truefind_if()返回的迭代器存储在iter中,它是在循环中使用auto定义的,因此类型被推导出来。如果需要在循环外引用iter,可以将其定义为类型Name::iterator. iterator是模板内定义的unordered_multiset<Name>容器中元素迭代器的类型别名。循环继续,直到find_if()返回容器的结束迭代器,表明没有元素是 Hackenbushes。最后,我们通过使用基于范围的for循环迭代容器中的元素,输出我们剩下的所有朋友。用作字段宽度的值由for_each()算法决定,用于将输出排列成列。该算法解引用由前两个参数指定的范围内的每个迭代器,并将结果传递给 lambda 表达式。这将最终在max_length中存储最大名称长度,这是通过 lambda 中的引用捕获的。

您可以使用copy()算法输出元素:

std::copy(std::begin(pals), std::end(pals), std::ostream_iterator<Name>{std::cout, "\n"});

这将每行输出一个元素,这需要很多行。唯一的另一种选择是在一行中输出它们,这在写入cout时也不是很令人满意。使用copy()输出对于写文件更有用。

下面是我的系统上的示例的输出:

Bucket 0:

Joan Jones  Joan Jones  Joan Jones  Joan Jones

Bucket 1:

Joan Szczygiel  Joan Szczygiel

Bucket 2:

Jean Jones  Jean Jones  Jean Smith

Bucket 3:

Jim Szczygiel  Jim Szczygiel  Jim Hackenbush  Jim Hackenbush  John Jones  John Jones  John Jones  John Jones  John Jones  John Jones

Bucket 4:

Joan Smith  Joan Smith  John Hackenbush  John Hackenbush  John Hackenbush

Bucket 5:

Joan Hackenbush  Joan Hackenbush

Bucket 6:

Jim Jones  Jim Jones  Jim Jones  Jim Jones  Jim Smith  Jim Smith  John Szczygiel  John Szczygiel  John Szczygiel

Bucket 7:

Jean Szczygiel  Jean Hackenbush  John Smith  John Smith  John Smith

There are 3 John Smith’s.

Jean Szczygiel  John Smith    John Smith     John Smith      Jim Szczygiel   Jim Szczygiel

Joan Smith      Joan Smith    Jim Jones      Jim Jones       Jim Jones       Jim Jones

Jim Smith       Jim Smith     John Szczygiel John Szczygiel  John Szczygiel  Joan Jones

Joan Jones      Joan Jones    Joan Jones     Joan Szczygiel  Joan Szczygiel  Jean Jones

Jean Jones      Jean Smith

在您的系统上,名称在存储桶之间的分布方式可能会有所不同。这取决于如何从选择桶的散列值中选择比特。每个桶包含一些元素,桶 3 包含 10 个元素。桶的数量不会增加,直到每个桶的平均值超过 8。当然,所有同名的朋友最终都在同一个桶中,因为他们有相同的哈希值。

集合运算

集合的数学概念非常类似于set容器——它是在某些方面相似的事物的集合。定义了集合上的二元运算,这些运算以各种方式组合两个集合的内容来产生另一个集合。图 5-6 说明了这些,并显示如下:

A978-1-4842-0004-9_5_Fig6_HTML.gif

图 5-6。

Set operations

  • 包含整数的集合AB的两个例子。
  • 集合AB的并集是属于任一集合或两个集合的元素集合。
  • 集合AB的交集是两者共有的元素集合。
  • 集合AB之间的区别是当你从 A 中移除AB共有的元素时得到的集合。
  • 集合AB之间的集合对称差是不在两个集合中的任一集合的元素集合。

图 5-6 中操作产生的元素以粗体显示。如果你以前没有遇到过这些操作,它们可能看起来有点抽象,但是它们非常有用。在Ex5_01中,我们给学生分配课程,每门课程的学生都存储在set中。学院的工作人员可能有兴趣找出哪些学生选择学习物理而没有报名参加数学课程。应用于这两门课程的差分运算将立即提供答案,稍后您将在一个示例中看到它是如何工作的。

STL 提供了几种算法来实现对对象集合的操作,包括图 5-6 中的四种二元运算。它们由algorithm标题中的模板定义。这些函数不一定涉及set容器,尽管它们可以。一组对象作为一个范围传递给这些算法——换句话说,由两个迭代器指定。代表集合的范围中的元素必须排序。默认情况下采用升序,但必要时可以更改。这部分是因为当对象被排序时,操作的执行性能是线性的。排序不包括在操作中,因为它会涉及在许多情况下不必要的排序。显然,对于来自容器类型(如setmap)的元素范围来说,无论如何都是有序的。集合运算的所有算法都要求两个集合以相同的方式排序,升序或降序。由实现这些操作的任何 STL 算法产生的对象集将是来自原始范围的对象的副本。

集合算法不能应用于无序关联容器中的元素,因为无序容器中的元素不能排序。排序需要对元素的随机访问迭代器,这在无序关联容器中是不可用的。但是,您可以将元素复制到另一种类型的容器中,比如一个vector,对vector元素进行排序,并对其应用集合操作。我将解释集合运算的每一种 STL 算法,然后在一个例子中展示其中的一些算法。

set_union()算法

实现集合联合操作的第一个版本的set_union()函数模板需要五个参数:两个指定左操作数集合范围的迭代器,两个指定右操作数集合范围的迭代器,以及一个指定结果集合目的地的迭代器。这里有一个例子:

std::vector<int> set1 {1, 2, 3, 4, 5, 6};

std::vector<int> set2 {4, 5, 6, 7, 8, 9};

std::vector<int> result;

std::set_union(std::begin(set1), std::end(set1), // Range for set that is left operand

std::begin(set2), std::end(set2), // Range for set that is right operand

std::back_inserter(result)); // Destination for the result: 1 2 3 4 5 6 7 8 9

set1set2中的初始值按升序排列。如果不是,那么在应用set_union()算法之前,有必要对vector容器进行分类。在第一章中您看到了在iterator头中定义的back_inserter()函数模板返回一个back_inserter_iterator对象,该对象调用作为参数传递的容器的push_back()。因此,由set1set2中的元素的并集产生的元素将被存储在result向量中。union 操作产生的元素集将是容器中元素的副本,因此容器的原始内容不会受到操作的影响。

当然,你不需要存储结果;您可以使用流迭代器写出元素:

std::set_union(std::begin(set1), std::end(set1), std::begin(set2), std::end(set2),

std::ostream_iterator<int> {std::cout, " "});

这里,目的地是一个将结果传输到标准输出流的ostream_iterator

set_union()函数模板的第二个版本接受第六个参数,这是一个函数对象,用于比较集合中的元素。这里有一个利用这种可能性的例子:

std::set<int, std::greater<int>> set1 {1, 2, 3, 4, 5, 6}; // Contains 6 5 4 3 2 1

std::set<int, std::greater<int>> set2 {4, 5, 6, 7, 8, 9}; // Contains 9 8 7 6 5 4

std::set<int, std::greater<int>> result;                  // Elements in descending sequence

std::set_union(std::begin(set1), std::end(set1),std::begin(set2), std::end(set2),

std::inserter(result, std::begin(result)),        // Result destination: 9 8 7 6 5 4 3 2 1

std::greater<int>());       // Function object for comparing elements

这次集合是set容器中的元素。使用类型为greater<int>的函数对象对元素进行排序,因此它们将按降序排列。set_union()的最后一个参数是greater<int>类型的一个实例,函数将用它来比较集合元素。结果的目的地是result容器的inserter_iterator,该容器将调用insert()成员来添加元素。您不能将back_insert_iteratorset容器一起使用,因为它没有push_back()函数成员。union 操作的结果将是两个集合中元素的降序副本。

两个版本的set_union()都返回一个迭代器,该迭代器指向被复制到目标的元素范围的末尾。如果目标是包含操作之前的元素的容器,这将非常有用。例如,如果目的地是一个vector容器,如果新元素是由set_union()使用front_insert_iterator插入的,那么由set_union()返回的迭代器将指向原始元素的第一个,或者如果您使用back_inserter_iterator则指向容器的结束迭代器。

set_intersection()算法

除了产生两个集合的交集,而不是并集,set_intersection()算法的工作原理与set_union()算法相同。它有两个版本,都有和《T2》一样的论点。以下是一些说明其用法的陈述:

std::set<string> words1 {"one", "two", "three", "four", "five", "six"};

std::set<string> words2 {"four", "five", "six", "seven", "eight", "nine"};

std::set<string> result;

std::set_intersection(std::begin(words1), std::end(words1),

std::begin(words2), std::end(words2),

std::inserter(result, std::begin(result)));

// Result: "five" "four" "six"

默认情况下,set容器存储使用less<string>实例排序的string对象。来自两个容器的元素的交集将是两者共有的元素,这些元素存储在result容器中。当然,这些将按升序排列。set_intersection()算法返回一个迭代器,该迭代器指向插入到目标中的范围内最后一个元素之外的元素。

set_difference()算法

产生两个集合的差的set_difference()算法也有两个版本,具有与set_union()相同的参数集合。下面是一个应用于来自set容器的元素的例子,这些元素按降序排列:

std::set<string, std::greater<string>> words1 {"one", "two", "three", "four", "five", "six"};

std::set<string, std::greater<string>> words2 {"four", "five", "six", "seven", "eight", "nine"};

std::set<string, std::greater<string>> result;

std::set_difference(std::begin(words1), std::end(words1),

std::begin(words2), std::end(words2),

std::inserter(result, std::begin(result)),  // Result: "two" "three" "one"

std::greater<string>());          // Function object to compare elements

这将调用带有第六个参数的算法版本,用于函数对象比较元素,因为来自set容器的范围是使用该参数排序的。通过从由来自words1的元素组成的第一组中移除words1words2共有的元素来获得差异。产生的元素是来自words1的前三个元素,按降序排列。该算法还返回一个迭代器,该迭代器指向插入目标的范围中最后一个元素之外的元素。

set _ symmetric _ difference()算法

set_symmetric_difference()算法遵循与前一组算法相同的模式。下面的一些陈述显示了它的工作原理:

std::set<string> words1 {"one", "two", "three", "four", "five", "six"};

std::set<string> words2 {"four", "five", "six", "seven", "eight", "nine"};

std::set_symmetric_difference(std::begin(words1), std::end(words1),

std::begin(words2), std::end(words2),

std::ostream_iterator<string> {std::cout, " "});

范围中的元素按默认的升序排列。集合对称差产生两个集合中的元素,不包括两个集合中的元素。定义结果集目的地的最后一个函数参数是一个ostream_iterator,因此元素将被写入cout,输出如下所示:

eight nine one seven three two

自然地,这些是在将<操作符应用到string对象后产生的序列中,因为默认的比较对象将是类型less<string>

includes()算法

includes()算法比较两组元素,如果第一组包含第二组的所有元素,则返回true。如果第二个集合为空,它也返回true。以下是一些例子:

std::set<string> words1 {"one", "two", "three", "four", "five", "six"};

std::set<string> words2 {"four", "two", "seven"};

std::multiset<string> words3;

std::cout << std::boolalpha

<< std::includes(std::begin(words1), std::end(words1), std::begin(words2), std::end(words2))

<< std::endl;                          // Output: false

std::cout << std::boolalpha

<< std::includes(std::begin(words1), std::end(words1), std::begin(words2), std::begin(words2))

<< std::endl;                          // Output: true

std::set_union(std::begin(words1), std::end(words1), std::begin(words2), std::end(words2), std::inserter(words3, std::begin(words3)));

std::cout << std::boolalpha

<< std::includes(std::begin(words3), std::end(words3), std::begin(words2), std::end(words2))

<< std::endl;                          // Output: true

有两个string元素的set容器,words1words2,用初始化列表中的字符串初始化。第一个输出语句显示false,因为words1不包含出现在words2中的string("seven")元素。第二个输出语句显示true,因为指定第二个操作数的范围是空的——范围的开始迭代器和结束迭代器是相同的。set_union()函数调用使用inserter_iterator将集合的并集从words1words2复制到words3words3中的结果将包含words2中的所有元素,因此第三条输出语句将显示true

当容器为multiset s 时,很容易对 union 操作会发生什么感到困惑。尽管words3是一个允许重复元素的multiset,但是words1words2共有的元素在words3中不会重复。该语句将输出words3元素:

std::copy(std::begin(words3), std::end(words3), std::ostream_iterator<string> {std::cout, " "});

输出将是:

five four one seven seven six three two

这是因为 union 操作将只包含每个重复元素的一个副本。当然,如果words1words2是各自具有重复单词的multiset容器,那么结果可能包括重复的元素:

std::multiset<string> words1 {"one", "two", "nine", "nine", "one", "three", "four", "five", "six"};

std::multiset<string> words2 {"four", "two", "seven", "seven", "nine", "nine"};

std::multiset<string> words3;

"one"words1中重复,"seven"words2\. "nine"中重复,在两个容器中都重复。你现在可以执行同样的set_union()调用:

std::set_union(std::begin(words1), std::end(words1),

std::begin(words2), std::end(words2),

std::inserter(words3, std::begin(words3)));

输出words3的内容将产生以下结果:

five four nine nine one one seven seven six three two

对于一个容器或另一个容器是唯一的重复元素会在联合的结果中重复,但是联合操作不会重复在两个容器中单独出现的元素。当然,当重复出现在两者中时,它们在结果中是重复的。

将操作付诸实施

我们可以看到 set 操作应用在代码下载中的扩展Ex5_01中,该扩展将是Ex5_07。新代码将被追加到Ex5_01main()主体的末尾,所以我在这里只展示额外的代码。下面的代码可以发现哪些学生已经开始学习物理,但却因为没有学习数学而苦苦挣扎:

auto physics = courses.find("Physics");

auto maths = courses.find("Mathematics");

if(physics == std::end(courses) || maths == std::end(courses))

throw std::invalid_argument {"Invalid course name."};

std::cout << "\nStudents studying physics but not maths are:\n";

std::set_difference(std::begin(physics->second), std::end(physics->second),

std::begin(maths->second), std::end(maths->second),

std::ostream_iterator < Student > {std::cout, "  "});

std::cout << std::endl;

调用courses容器的find()成员会返回一个迭代器,该迭代器指向键与参数匹配的元素。如果没有匹配的键,函数将返回容器的结束迭代器,所以最好进行检查。这里,我们知道键是存在的,但是参数中的拼写错误会导致代码失败。如果发生这种情况,就会抛出一个标准异常,从而结束程序。set_difference()算法产生我们正在寻找的结果,在这种情况下,Student对象由ostream_iterator对象写入cout。当然,您可以将结果集存储在另一个容器中,如下面的代码所示。

下面的代码标识了正在做正确的事情的学生——学习数学和物理:

std::vector<Student> phys_and_math;

std::cout << "\nStudents studying physics and maths are:\n";

std::set_intersection(std::begin(physics->second), std::end(physics->second),

std::begin(maths->second), std::end(maths->second),

std::back_inserter(phys_and_math));

std::copy(std::begin(phys_and_math), std::end(phys_and_math),

std::ostream_iterator <Student> {std::cout, "  "});

std::cout << std::endl;

set_intersection()算法返回两个原始集合共有的元素集合,这就是我们想要的。产生的元素由back_inserter()函数为phys_and_math容器返回的back_insert_iterator插入到vector容器中。你可以用一个front_insert_iterator和一个vector容器。vector的内容由copy()算法输出。

只要将结果存储在某个地方,您还可以进一步组合集合操作产生的集合。以下是确定报名参加物理、数学和天文学的学生的方法:

auto astronomy = courses.find("Astronomy");

if(astronomy == std::end(courses)) throw std::invalid_argument{"Invalid course name."};

std::cout << "\nStudents studying physics, maths, and astronomy are:\n";

std::set_intersection(std::begin(astronomy->second), std::end(astronomy->second),

std::begin(phys_and_math), std::end(phys_and_math),

std::ostream_iterator<Student>{std::cout, "  "});

std::cout << std::endl;

这将使上一个操作中的集合与代表天文学课程的学生集合相交。这原来只是我系统上的一个学生。

接下来,我们可以发现学生要么学习戏剧,要么学习哲学,但不是两者都学:

auto drama = courses.find("Drama");

auto philosophy = courses.find("Philosophy");

if(drama == std::end(courses) || philosophy == std::end(courses))

throw std::invalid_argument{"Invalid course name."};

Group act_or_think;                              // set container for result

std::cout << "\nStudents studying either drama or philosophy are:\n";

std::set_symmetric_difference(std::begin(drama->second), std::end(drama->second),

std::begin(philosophy->second), std::end(philosophy->second),

std::inserter(act_or_think, std::begin(act_or_think)));

std::copy(std::begin(act_or_think), std::end(act_or_think),

std::ostream_iterator<Student>{std::cout, "  "});

std::cout << std::endl;

结果的容器是使用Group别名定义的,它对应于set<Student>类型。集合操作的结果可以放在任何可以通过迭代器插入元素的地方。set_symmetric_difference()算法完成了这里所要求的工作。因为结果的目的地是一个set容器,所以我们不能使用back_insert_iteratorfront_insert_iterator。这里我们需要的是一个insert_iterator,它是通过调用inserter()函数创建的。inserter()的参数是容器对象和指向插入元素位置的迭代器。

Ex5_01中代码的最后一个增加输出学生学习戏剧或哲学,或两者兼而有之:

act_or_think.clear();                           // Empty the container to reuse it

std::cout << "\nStudents studying drama and/or philosophy are:\n";

std::set_union(std::begin(drama->second), std::end(drama->second),

std::begin(philosophy->second), std::end(philosophy->second),

std::inserter(act_or_think, std::begin(act_or_think)));

std::copy(std::begin(act_or_think), std::end(act_or_think),

std::ostream_iterator<Student>{std::cout, "  "});

std::cout << std::endl;

这重用了act_or_think容器来存储这个操作的结果,因此调用它的clear()成员来删除现有的元素。set_union()算法生成出现在任一原始集合中的元素集合。输出由copy()算法以通常的方式产生。

摘要

set容器与相应的map容器具有相似的操作,但是通常它们的使用非常不同。您使用map容器来存储和检索与键相关联的对象。为名字寻找地址或电话号码就是一个典型的例子。set容器适用于处理对象集合时,其中给定集合的成员关系很重要。例如,当你需要知道一个特定的人是否在某个班级,或者某人是否同时在足球队和篮球队打球,或者某人是否是圣昆廷监狱的囚犯时,你可以使用set

您在本章中学到的要点包括:

  • set容器使用对象本身作为键来存储对象。
  • 一个set<T>容器按顺序存储类型T的唯一对象。默认情况下,使用less<T>对对象进行排序。
  • 一个multiset<T>容器以与一个set容器相同的方式存储对象,但是对象不必是唯一的。
  • 如果两个对象等价,则确定它们在setmultiset容器中是相同的。如果a<bfalseb<afalse,那么对象ab是等价的。
  • 一个unordered_set<T>容器存储类型为T的唯一对象,这些对象使用对象的哈希值来定位。
  • unordered_multiset<T>容器中的对象也使用对象的散列值来定位,但是对象不需要是唯一的。
  • 无序集合容器通过使用==操作符比较两个对象来确定它们是相同的,所以类型T必须支持这个操作。
  • 无序集合容器中的对象通常存储在哈希表的桶中。通常使用哈希值中的特定位序列为对象选择桶。
  • 无序集合容器的加载因子是每个桶的平均元素数。
  • 无序集合容器分配初始数量的桶。当超过集装箱的最大装载系数时,铲斗数量将自动增加。
  • STL 定义了实现集合运算的算法。二元集合运算是并集、交集、差集、对称差集和包含集。这些应用的元素组由范围定义。

ExercisesDefine a Card class to represent playing cards in a standard deck. Create a vector of Card objects that represents a complete deck of fifty-two cards. Deal the cards randomly into four set containers so that each represents a hand of thirteen cards in a game. Output the cards in the four set containers under the headings “North,” “South,” “East,” and “West”. (These are the usual denotations of hands at Bridge.) The output should be in the usual suit and value order – in card value order within suit sequence Clubs, Diamonds, Hearts, Spades.   Use an unordered_multiset container to store words from an arbitrary paragraph of text entered from the keyboard. Output the words and the frequency with which they occur within the text with six words on each output line.   Simulate throwing a pair of dice (with each face value from 1 to 6) and record the sum of the two dice in a multiset container for 1,000 throws of the pair. Obviously, the sum can be from 2 to 12. Output the number of times each of the possible results occurs.   Define a vector container initialized with ten book titles of your choice. Create a multimap container where the keys are names and the objects are set containers with elements that are book titles. Allocate a random selection of four to six books to each person in the multimap. List the people in name order along with the books each has. Determine and record the pairs of names that have two books in common, three books in common, and so on, up to the unlikely six books in common. Output the pairs of names that have two or more books the same, along with the books each pair has in common. This output should be in ascending sequence of the number of shared books.

六、排序、合并、搜索和分区

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​6) contains supplementary material, which is available to authorized users.

本章描述了与排序和合并范围松散相关的算法。其中有两组专门提供排序和合并功能。另一组提供了相对于给定元素值划分范围的机制。另外两个组提供了在一个范围内查找一个或多个元素的方法。在本章中,您将了解:

  • 如何将随机访问迭代器定义的范围按升序或降序排序?
  • 如何防止相等的元素在排序操作中被重新排序?
  • 如何合并有序范围?
  • 如何搜索一个无序的范围来找到一个或多个给定的元素。
  • 划分一个范围意味着什么,以及如何使用 STL 提供的划分算法。
  • 如何使用二分搜索法算法?

对范围进行排序

排序在许多应用程序中是必不可少的,许多 STL 算法只能处理有序的对象范围。默认情况下,algorithm头中定义的sort<Iter>()函数模板将一系列元素按升序排序,这意味着假定要排序的对象类型支持<操作符。这些对象还必须是可交换的,这意味着必须能够使用在utility头中定义的swap()函数模板交换两个对象。这进一步意味着对象的类型必须实现一个移动构造函数和一个移动赋值操作符。sort()函数模板类型参数Iter,是范围内迭代器的类型,它们必须是随机访问迭代器。这意味着只有提供随机访问迭代器的容器中的元素才能被sort()算法排序,这意味着只有arrayvectordeque容器中的元素或标准数组中的元素是可接受的。你在第二章中看到listforward_list容器有sort()功能成员;这些用于排序的特殊成员是必要的,因为list只提供双向迭代器,而forward_list只提供正向迭代器。

sort()的模板类型参数将从函数调用参数中推导出来,这些参数将是定义要排序的对象范围的迭代器。当然,迭代器类型隐式定义了范围内对象的类型。这里有一个使用sort() algorithm:的例子

std::vector<int> numbers {99, 77, 33, 66, 22, 11, 44, 88};

std::sort(std::begin(numbers), std::end(numbers));

std::copy(std::begin(numbers), std::end(numbers),

std::ostream_iterator<int> {std::cout, " "});   // Output: 11 22 33 44 66 77 88 99

sort()调用将numbers容器中的所有元素按升序排序,copy()算法输出结果。您不必对容器中的所有内容进行排序。该语句对numbers中的元素进行排序,不包括第一个和最后一个:

std::sort(++std::begin(numbers), --std::end(numbers));

要按降序排序,您需要将用于比较元素的函数对象作为第三个参数提供给sort():

std::sort(std::begin(numbers), std::end(numbers), std::greater<>());

比较函数必须返回一个bool值,并有两个参数,要么是解引用迭代器产生的类型,要么是解引用迭代器可以隐式转换的类型。参数可以是不同的类型。只要比较函数满足要求,它可以是你喜欢的任何东西,包括一个 lambda 表达式。例如:

std::deque<string> words {"one", "two", "nine", "nine", "one", "three", "four", "five", "six"};

std::sort(std::begin(words), std::end(words),

[](const string& s1, const string& s2){ return s1.back() > s2.back(); });

std::copy(std::begin(words), std::end(words),

std::ostream_iterator<string> {std::cout, " "}); // six four two one nine nine one three five

这个语句序列对deque容器words中的string元素进行排序,并输出结果。这里的比较函数是一个 lambda 表达式,它比较每个单词的最后一个字母来确定排序顺序。结果是元素按其最后一个字母的降序排列。

让我们用一个简单的工作示例来看看sort()的运行,这个示例将从键盘读取Name对象,按升序对它们进行排序,然后输出结果。Name类将在Name.h头中定义,它将包含以下代码:

#ifndef NAME_H

#define NAME_H

#include <string>                                // For string class

class Name

{

private:

std::string first{};

std::string second{};

public:

Name(const std::string& name1, const std::string& name2) : first(name1), second(name2){}

Name()=default;

std::string get_first() const {return first;}

std::string get_second() const { return second; }

friend std::istream& operator>>(std::istream& in, Name& name);

friend std::ostream& operator<<(std::ostream& out, const Name& name);

};

// Stream input for Name objects

inline std::istream& operator>>(std::istream& in, Name& name)

{

return in >> name.first >> name.second;

}

// Stream output for Name objects

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{

return out << name.first << " " << name.second;

}

#endif

流插入和提取操作符为Name对象定义为friend函数。您可以将operator<()定义为一个类成员,但是我省略了它,以显示将比较指定为sort()的一个参数。程序是这样的:

// Ex6_01.cpp

// Sorting class objects

#include <iostream>                              // For standard streams

#include <string>                                // For string class

#include <vector>                                // For vector container

#include <iterator>                              // For stream and back insert iterators

#include <algorithm>                             // For sort() algorithm

#include "Name.h"

int main()

{

std::vector<Name> names;

std::cout << "Enter names as first name followed by second name. Enter Ctrl+Z to end:";

std::copy(std::istream_iterator<Name>(std::cin), std::istream_iterator<Name>(),

std::back_insert_iterator<std::vector<Name>>(names));

std::cout << names.size() << " names read. Sorting in ascending sequence...\n";

std::sort(std::begin(names), std::end(names), [](const Name& name1, const Name& name2)

{return name1.get_second() < name2.get_second(); });

std::cout << "\nThe names in ascending sequence are:\n";

std::copy(std::begin(names), std::end(names), std::ostream_iterator<Name>(std::cout, "\n"));

}

main()中几乎所有的东西都是使用 STL 模板完成的。names容器将存储从cin中读取的名字。输入由copy()算法执行,该算法使用istream_iterator<Name>实例读取Name对象。istream_iterator<Name>的默认构造函数为流创建结束迭代器。copy()函数使用由back_insert_iterator<Name>()函数创建的back_inserter<Name>迭代器将每个输入对象复制到names。重载Name类的流操作符允许流迭代器用于Name对象的输入和输出。

对象的比较函数由 lambda 表达式定义,它是算法的第三个参数。如果要将operator<()定义为Name类的成员,可以省略这个参数。排序后的名称由copy()算法写入标准输出流,该算法将前两个参数指定的元素范围复制到第三个参数ostream_iterator<Name>对象。

以下是一些输出示例:

Enter names as first name followed by second name. Enter Ctrl+Z to end:

Jim Jones

Bill Jones

Jane Smith

John Doe

Janet Jones

Willy Schaferknaker

^Z

6 names read. Sorting in ascending sequence...

The names in ascending sequence are:

John Doe

Jim Jones

Bill Jones

Janet Jones

Willy Schaferknaker

Jane Smith

姓名的排序只考虑第二个姓名。当第二个名字相同时,您可以扩展 lambda 表达式来比较第一个名字。

你可能想知道为什么我没有在这个例子中使用pair<string,string>对象来表示名字;这将比定义一个新类更简单。显然,这是可能的,但却不太清楚。

排序和相等元素的顺序

sort()算法可能会改变相等元素的顺序,这有时不是您想要的。假设您有一个存储某种交易的容器,可能是银行账户。进一步假设您希望在处理交易之前按帐号对交易进行排序,以便可以按顺序更新帐户。如果 equal 元素出现的顺序反映了它们被添加到容器中的时间顺序,您将需要保留该顺序。允许给定账户的交易被重新安排可能导致账户看起来已经透支,而事实并非如此。

在这种情况下,stable_sort()算法提供了您需要的东西。它对一个范围内的元素进行排序,并确保相等的元素保持其原始顺序。有两个版本;一个接受两个指定排序范围的迭代器参数,另一个接受一个用于比较的附加参数。您可以修改对Ex6_01.cpp中的names容器进行排序的语句,以查看stable_sort()的工作情况:

std::stable_sort(std::begin(names), std::end(names),

[](const Name& name1, const Name& name2) { return name1.get_second() < name2.get_second(); });

当然,我为使用了sort()Ex6_01展示的输出没有打乱相同元素的顺序,所以使用stable_sort()不会改变相同输入的输出。不同之处在于,使用stable_sort()可以保证相同元素的顺序不会改变,而使用sort()算法则不是这种情况。当您想确定相同元素的顺序保持不变时,使用stable_sort()

部分排序

通过一个例子最容易理解部分排序是什么意思。假设您有一个容器,其中收集了几百万个值,但您只对其中最低的 100 个值感兴趣。您可以对容器的全部内容进行排序,并选择前 100 个,但这可能会有点耗时。您需要的是部分排序,在这种情况下,一个范围内的大量值中只有最低的n按顺序排列。这里有一个特殊的算法,partial_sort()算法,它期望三个参数是随机访问迭代器。如果功能参数为firstsecondlast,则算法适用于范围[first,last)内的元素。执行算法后,范围[first,second)将包含范围[first,last)中升序排列的最低second-first元素。

Note

如果你以前没有遇到过,我用来表示范围的符号[first,last)来源于数学,它定义了区间,区间定义了数字的范围。这两个值称为端点。在该符号中,方括号表示相邻端点包括在范围内,圆括号表示相邻端点不包括在内。例如,如果(2,5)是一个整数区间,2 和 5 被排除在外,所以它只代表整数3,4;这被称为开放区间,因为两个端点都不包括在内。区间[2,5)包括 2 但不包括 5,所以它代表2,3,4。(2,5)代表 3,4,5。[2,5]代表 2,3,4,5,被称为封闭区间,因为两端点都包括在内。当然,firstlast是迭代器,first,last)表示first指向的被包含,而last指向的不被包含——所以它在 C++ 中精确地表达了一个范围。

这里有一些代码展示了partial_sort()算法是如何工作的:

size_t count {5};                                // Number of elements to be sorted

std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};

std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));

执行上述partial_sort()的效果如图 [6-1 所示。

A978-1-4842-0004-9_6_Fig1_HTML.gif

图 6-1。

Operation of the partial_sort() algorithm

最低的count元素按顺序排列。在范围first,second)中,second指向的元素不包含在内,因为second是最后一个迭代器。图 [6-1 显示了在我的系统上执行语句的结果;在您的系统上可能有所不同。请注意,没有排序的元素的原始顺序没有保持。执行partial_sort()后这些元素的顺序是不确定的,取决于您的实现。

如果您希望partial_sort()算法使用不同于<操作符的比较,您可以提供一个函数对象作为附加参数。例如:

std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers),

std::greater<>());

现在,numbers中最大的count元素将在容器的开头按降序排列。在我的系统上,执行这条语句的结果是:

93 88 56 45 22 7 19 12 8 7 15 10

同样,numbers中未排序的元素的原始顺序也不会保留。

除了将排序后的元素复制到另一个容器中的不同区域之外,partial_sort_copy()算法与partial_sort()基本相同。前两个参数是迭代器,指定部分排序要应用的范围;第三个和第四个参数是迭代器,用于标识存储结果的范围。目标范围中的元素数量决定了输入范围中将要排序的元素数量。这里有一个例子:

std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};

size_t count {5};                             // Number of elements to be sorted

std::vector<int> result(count);                  // Destination for the results - count elements

std::partial_sort_copy(std::begin(numbers), std::end(numbers), std::begin(result), std::end(result));

std::copy(std::begin(numbers), std::end(numbers), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

std::copy(std::begin(result), std::end(result), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

这些语句实现了numbers容器的部分排序。这个想法是将来自numbers的最底层的count元素按顺序放置,并将它们存储在result容器中。您指定为目的地的范围必须存在,因此在目的地容器result中必须至少有count个元素,在本例中,我们分配的正好是所需的数量。执行这些语句的输出是:

22 7 93 45 19 56 88 12 8 7 15 10

7 7 8 10 12

你可以看到numbers中的元素序列没有被打乱,result包含了从numbers开始按升序排列的最低count元素的副本。

当然,您可以通过额外的参数指定不同的比较函数:

std::partial_sort_copy(std::begin(numbers), std::end(numbers), std::begin(result), std::end(result),

std::greater<>());

greater<>的一个实例指定为函数对象将导致最大的count元素按降序被复制到result。如果该语句后面跟有前面代码片段的输出语句,输出将是:

22 7 93 45 19 56 88 12 8 7 15 10

93 88 56 45 22

和以前一样,原始容器中的元素顺序是不受干扰的。

nth_element()算法不同于partial_sort()。它适用的范围由函数的第一个和第三个参数定义,第二个参数是一个指向第n个元素的迭代器。执行nth_element()将导致第 n 个元素被设置为如果该范围被完全排序时应该存在的元素。范围中第n个元素之前的所有元素都将小于第n个元素,之后的所有元素都将大于第n个元素。默认情况下,该算法使用<运算符来产生结果。下面是一些要练习的代码nth_element():

std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};

size_t count {5};                                // Index of nth element

std::nth_element(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));

第 n 个元素是numbers容器中的第 6 个,对应的是numbers[5]。图 6-2 说明了这是如何工作的。

A978-1-4842-0004-9_6_Fig2_HTML.gif

图 6-2。

Operation of the nth_element() algorithm

n个元素之前的元素将少于第n个元素,但不一定是有序的。同样,第n个元素之后的元素会比它大,但不一定是有序的。如果第二个参数与第三个参数相同(范围的结尾),则该算法无效。

与本章前面的算法一样,您可以提供一个函数对象,将比较定义为第四个参数:

std::nth_element(std::begin(numbers), std::begin(numbers) + count, std::end(numbers),

std::greater<>());

这使用了>操作符来比较元素,所以如果元素是降序的,那么第n个元素将是它应该是的。第n个元素前面的元素会更大,后面的元素会更小。使用前面numbers容器中的初始值,结果将是:

45 56 93 88 22 19 10 12 15 7 8 7

在您的系统中,第 n 个元素两边的元素顺序可能不同,但是左边的应该比它大,右边的应该比它小。

排序范围的测试

排序非常耗时,尤其是当您有大量元素时。测试一个范围是否已经排序可以避免不必要的排序操作。如果由两个迭代器参数指定的范围内的元素是升序,那么is_sorted()函数模板返回true。迭代器必须至少是前向迭代器,以允许元素被顺序处理。提醒你一下——前向迭代器支持前缀和后缀增量操作。这里有一个使用is_sorted()的例子:

std::vector<int> numbers {22, 7, 93, 45, 19};

std::vector<double> data {1.5, 2.5, 3.5, 4.5};

std::cout << "numbers is "

<< (std::is_sorted(std::begin(numbers), std::end(numbers)) ? "": "not ")

<< "in ascending sequence.\n";

std::cout << "data is "

<< (std::is_sorted(std::begin(data), std::end(data)) ? "": "not ")

<< "in ascending sequence." << std::endl;

使用的默认比较是<操作符。输出将显示numbers不在升序中,而data在。有一个版本允许你提供一个函数对象来比较元素:

std::cout << "data reversed is "

<< (std::is_sorted(std::rbegin(data), std::rend(data), std::greater<>()) ? "": "not ")

<< "in descending sequence." << std::endl;

该语句的输出将表明data中逆序的元素按降序排列。

您还可以使用is_sorted_until()函数模板来确定某个范围内的元素的顺序。参数是定义测试范围的迭代器。这个函数返回一个迭代器,这个迭代器是这个范围中按升序排列的元素的上限。这里有一个例子:

std::vector<string> pets {"cat", "chicken", "dog", "pig", "llama",  "coati", "goat"};

std::cout << "The pets in ascending sequence are:\n";

std::copy(std::begin(pets), std::is_sorted_until(std::begin(pets), std::end(pets)),

std::ostream_iterator<string>{std::cout, " "});

copy()算法的前两个参数是pets容器的 begin 迭代器和is_sorted_until()应用于 pets 中所有元素时返回的迭代器。is_sorted_until()算法将返回pets中升序排列的元素的上限——这将是一个迭代器,指向小于其前任的第一个元素,或者是序列的结束迭代器(如果排序的话)。该代码的输出将是:

The pets in ascending sequence are:

cat chicken dog pig

"llama"是小于其前身的第一个元素,因此"pig"是升序序列中的最后一个元素。

您可以选择提供一个函数对象来比较元素:

std::vector<string> pets {"dog", "coati", "cat", "chicken", "pig", "llama",  "goat"};

std::cout << "The pets in descending sequence are:\n";

std::copy(std::begin(pets),

std::is_sorted_until(std::begin(pets), std::end(pets), std::greater<>()),

std::ostream_iterator<string>{std::cout, " "});

这次我们寻找一个降序的元素序列,因为string类的operator>()成员将用于比较元素。输出将是:

The pets in descending sequence are:

dog coati cat

"chicken"是第一个大于其前任的元素,所以由is_sorted_until()返回的迭代器将指向这个元素。因此"cat"是降序排列的最后一个元素。

合并范围

合并操作将两个范围中以相同方式排序的元素组合在一起——要么都是升序,要么都是降序。结果是包含来自两个输入范围的元素副本的范围,其排序方式与原始范围相同。图 6-3 说明了这是如何工作的。

A978-1-4842-0004-9_6_Fig3_HTML.gif

图 6-3。

Merging elements from two vector containers

merge()算法合并两个范围,并将结果存储在第三个范围中。它使用<操作符来比较元素。图 6-3 显示了应用于thesethose容器内容的合并操作,其中结果范围存储在both容器中。merge()算法需要五个参数。前两个是迭代器,指定第一个输入范围—these,在本例中,后两个是迭代器,标识第二个输入范围—those,最后一个参数是迭代器,标识第一个合并的元素范围应该放在哪里—both容器。标识要合并的范围的迭代器至少只需要是输入迭代器,用于保存结果的目标范围的迭代器只需要是输出迭代器。

merge()算法没有关于合并元素范围的容器的信息,所以它不能创建元素——它只能使用您作为第五个参数提供的迭代器来存储元素。因此,示例中目标范围内的元素必须已经存在。这在图 6-3 中通过创建both容器来确保,容器中的元素数量指定为每个输入容器的元素总数。目标区域可以在任何地方,甚至可以在一个源区域容器中,但是源区域和目标区域不能重叠。如果他们这样做,后果是不确定的,但你可以肯定的是,效果不会好。当然,如果通过插入迭代器指定目的地,元素将被自动创建。

merge()算法返回一个迭代器,该迭代器指向合并范围中最后一个元素之后的一个元素,因此您可以通过函数调用中使用的第五个迭代器参数加上函数返回的迭代器来标识合并范围。

当比较需要不同于<操作符时,您可以提供一个函数对象作为第六个参数。例如:

std::vector<int> these {2, 15, 4, 11, 6, 7};                     // 1st input to merge

std::vector<int> those {5, 2, 3, 2, 14, 11, 6};                  // 2nd input to merge

std::stable_sort(std::begin(these), std::end(these),             // Sort 1st range in...

std::greater<>());           // ...descending sequence

std::stable_sort(std::begin(those), std::end(those),             // Sort 2nd range

std::greater<>());

std::vector<int> result(these.size() + those.size() + 10);             // Plenty of room for results

auto end_iter = std::merge(std::begin(these), std::end(these),   // Merge 1st range...

std::begin(those), std::end(those),   // ...and 2nd range...

std::begin(result), std::greater<>()); // ...into result

std::copy(std::begin(result), end_iter, std::ostream_iterator<int>{std::cout, " "});

这个语句序列首先使用stable_sort()将两个vector容器的内容按降序排序,这保证了 equal 元素的原始顺序将被保持。合并操作将两个容器的内容合并到第三个容器result中,这个容器比需要的多创建了 10 个元素——只是为了演示merge()返回的迭代器的使用。copy()算法复制由result的 begin 迭代器和merge()返回的end_iter迭代器指定的范围到输出流迭代器。输出将是:

15 14 11 11 7 6 6 5 4 3 2 2 2

inplace_merge()算法就地合并相同范围内的两个连续排序的元素序列。有三个参数,firstsecondlast是双向迭代器。第一个输入序列的范围是first,second),第二个输入序列的范围是[second,last),因此second指向的元素在第二个输入范围内。结果将是范围[first,last)。图 [6-4 显示了该操作。

A978-1-4842-0004-9_6_Fig4_HTML.gif

图 6-4。

inplace_merge() operation

图 6-4 中的data容器包含两个范围,都是升序排列。inplace_merge()操作将这些组合起来,在同一个容器中产生一个升序范围。

我们可以将你在本章中看到的几个算法合并成一个单一的工作示例。这个有些做作的示例将处理从键盘输入的信贷和借记交易,并将它们应用到根据需要创建的一组帐户。我们将始终创建零余额账户。交易将是一个包含账号、金额以及金额是贷方还是借方的对象。为不存在的账户处理交易将导致该账户被创建。帐户对象将包含标识唯一帐号、所有者姓名和当前余额的成员。账户持有人的名字将是一个包含名字和第二个名字的pair对象。帐号将是一个无符号整数。贷记将被表示为一个bool值,账户余额和要借记或贷记的金额将属于double类型。

Transaction类型将在Transaction.h头文件中定义如下:

#ifndef TRANSACTION_H

#define TRANSACTION_H

#include <iostream>                              // For stream class

#include <iomanip>                               // For stream manipulators

#include "Account.h"

class Transaction

{

private:

size_t account_number {};                      // The account number

double amount {};                              // The amount

bool credit {true};                            // credit = true debit=false

public:

Transaction()=default;

Transaction(size_t number, double amnt, bool cr) : account_number {number}, amount {amnt},

credit {cr}{}

size_t get_acc_number() const { return account_number; }

// Less-than operator - compares account numbers

bool operator<(const Transaction& transaction) const { return account_number <                                                               transaction.account_number; }

// Greater-than operator - compares account numbers

bool operator>(const Transaction& transaction) const { return account_number >                                                              transaction.account_number; }

friend std::ostream& operator<<(std::ostream& out, const Transaction& transaction);

friend std::istream& operator>>(std::istream& in, Transaction& transaction);

// Making the Account class a friend allows Account objects

// to access private members of Transaction objects

friend class Account;

};

// Stream insertion operator for Transaction objects

std::ostream& operator<<(std::ostream& out, const Transaction& transaction)

{

return out << std::right << std::setfill('0') << std::setw(5)              << transaction.account_number

<< std::setfill(' ') << std::setw(8) << std::fixed << std::setprecision(2)              << transaction.amount

<< (transaction.credit ? " CR" : " DR");

}

// Stream extraction operator for Transaction objects

std::istream& operator>>(std::istream& in, Transaction& tr)

{

if((in >> std::skipws >> tr.account_number).eof())

return in;

return in >> tr.amount >> std::boolalpha >> tr.credit;

}

#endif

默认构造函数通常是允许在容器中创建默认元素所必需的。在类中同时包含<>操作符允许对Transaction对象进行升序或降序排序,尽管这个例子不会同时使用这两个选项。我们接下来要讨论的Account类是Transaction类的friend,因此Account类的函数成员可以访问传递给它的Transaction对象的private数据成员。定义了重载的流输入和输出操作符后,我们将能够结合 STL 提供的流迭代器使用copy()算法来读写Transaction对象。

Account类将在Account.h头中定义:

#ifndef ACCCOUNT_H

#define ACCCOUNT_H

#include <iostream>                                   // For stream class

#include <iomanip>                                    // For stream manipulators

#include <string>                                     // For string class

#include <utility>                                    // For pair template type

#include "Transaction.h"

using first_name = std::string;

using second_name = std::string;

using Name = std::pair<first_name, second_Name>;

class Account

{

private:

size_t account_number {};                           // 5-digit account number

Name name {"", ""};                                 // A pair containing 1st & 2nd names

double balance {};                                  // The account balance - negative when overdrawn

public:

Account()=default;

Account(size_t number, const Name& nm) : account_number {number}, name {nm}{}

double get_balance() const { return balance; }

void set_balance(double bal) { balance = bal; }

size_t get_acc_number() const {return account_number;}

const Name& get_name() const { return name; }

// Apply a transaction to the account

bool apply_transaction(const Transaction& transaction)

{

if(transaction.credit)                            // For a credit...

balance += transaction.amount;                  // ...add the mount

else                                              // For a debit...

balance -= transaction.amount;                  // ...subtract the amount

return balance < 0.0;                             // Return true when overdrawn

}

// Less-than operator - compares by account number

bool operator<(const Account& acc) const { return account_number < acc.account_number; }

friend std::ostream& operator<<(std::ostream& out, const Account& account);

};

// Stream insertion operator for Account objects

std::ostream& operator<<(std::ostream& out, const Account& acc)

{

return out << std::left << std::setw(20) << acc.name.first + " " + acc.name.second

<< std::right << std::setfill('0') << std::setw(5) << acc.account_number

<< std::setfill(' ') << std::setw(8) << std::fixed << std::setprecision(2) << acc.balance;

}

#endif

除了帐号之外,该类还有一个Name成员来标识帐户的所有者。一个Name只是一个pair<string,string>类型的别名,first_namesecond_name别名仅仅是为了标识每个pair成员的重要性。类型别名通常有助于将特定于应用程序的含义赋予一般类型。

Account对象重载流插入操作符允许使用<<将对象写入输出流。operator<()成员定义允许Account对象在排序或存储到有序容器中时按账号排序。如果你想让Account对象以不同的方式排序——比如通过名字,你可以定义一个提供比较功能的函数对象。该示例将按名称对Account对象进行排序,启用该功能的函数对象将在Compare_Names.h中定义,如下所示:

#ifndef COMPARE_NAMES_H

#define COMPARE_NAMES_H

#include "Account.h"

// Order Account objects in ascending sequence by Name

class Compare_Names

{

public:

bool operator()(const Account& acc1, const Account& acc2)

{

const auto& name1 = acc1.get_name();

const auto& name2 = acc2.get_name();

return (name1.second < name2.second) ||

((name1.second == name2.second) && (name1.first < name2.first));

}

};

#endif

你应该不难理解这是如何工作的。函数调用操作符定义比较两个Account对象的Name成员,首先通过名字,其次通过名字。

使用我们定义的类的main()程序将在Ex6_02.cpp中:

// Ex6_02.cpp

// Sorting and inplace merging

#include <iostream>                              // For standard streams

#include <string>                                // For string class

#include <algorithm>                             // For sort(), inplace_merge()

#include <functional>                            // For greater<T>

#include <vector>                                // For vector container

#include <utility>                               // For pair template type

#include <map>                                   // For map container

#include <iterator>                              // For stream and back insert iterators

#include "Account.h"

#include "Transaction.h"

#include "Compare_Names.h"

using std::string;

using first_name = string;

using second_name = string;

using Name = std::pair<first_name, second_Name>;

using Account_Number = size_t;

// Read the name of an account holder

Name get_holder_name(Account_Number number)

{

std::cout << "Enter the holder’s first and second names for account number " << number << ": ";

string first {};

string second {};

std::cin >> first  >> second;

return std::make_pair(first, second);

}

int main()

{

std::vector<Transaction> transactions;

std::cout << "Enter each transaction as:\n"

<< "   5 digit account number   amount   credit(true or false).\n"

<< "Enter Ctrl+Z to end.\n";

// Read 1st set of transactions

std::copy(std::istream_iterator<Transaction> {std::cin}, std::istream_iterator<Transaction> {},

std::back_inserter(transactions));

std::cin.clear();                              // Clear the EOF flag for the stream

// Sort 1st set``in

std::stable_sort(std::begin(transactions), std::end(transactions), std::greater<>());

// List the transactions

std::cout << "First set of transactions after sorting...\n";

std::copy(std::begin(transactions), std::end(transactions),                                     std::ostream_iterator<Transaction>{std::cout, "\n"});

// Read 2nd set of transactions

std::cout << "\nEnter more transactions:\n";

std::copy(std::istream_iterator<Transaction> {std::cin}, std::istream_iterator<Transaction> {},

std::back_inserter(transactions));

std::cin.clear();                              // Clear the EOF flag for the stream

// List the transactions

std::cout << "\nSorted first set of transactions with second set appended...\n";

std::copy(std::begin(transactions), std::end(transactions),                                       std::ostream_iterator<Transaction>{std::cout, "\n"});

// Sort second set into descending account sequence

auto iter = std::is_sorted_until(std::begin(transactions), std::end(transactions),

std::greater<>());

std::stable_sort(iter, std::end(transactions), std::greater<>());

// List the transactions

std::cout << "\nSorted first set of transactions with sorted second set appended...\n";

std::copy(std::begin(transactions), std::end(transactions),                                     std::ostream_iterator<Transaction>{std::cout, "\n"});

// Merge transactions in place

std::inplace_merge(std::begin(transactions), iter, std::end(transactions), std::greater<>());

// List the transactions

std::cout << "\nMerged sets of transactions...\n";

std::copy(std::begin(transactions), std::end(transactions),                                     std::ostream_iterator<Transaction>{std::cout, "\n"});

// Process transactions creating Account objects when necessary

std::map<Account_Number, Account> accounts;

for(const auto& tr : transactions)

{

Account_Number number = tr.get_acc_number();

auto iter = accounts.find(number);

if(iter == std::end(accounts))

iter = accounts.emplace(number, Account {number, get_holder_name(number)}).first;

if(iter->second.apply_transaction(tr))

{

auto name = iter->second.get_name();

std::cout << "\nAccount number " << number

<< " for " << name.first << " " <<name.second << " is overdrawn!\n"

<< "The concept is that you bank with us - not the other way round, so fix it!\n"

<< std::endl;

}

}

// Copy accounts to a vector container

std::vector<Account> accs;

for(const auto& pr :accounts)

accs.push_back(pr.second);

// List accounts after sorting in name sequence

std::stable_sort(std::begin(accs), std::end(accs), Compare_Names());

std::copy(std::begin(accs), std::end(accs), std::ostream_iterator < Account > {std::cout, "\n"});

}

get_holder_name()是一个助手函数,它从cin中读取给定账号的名称。这是在为一个给定的账号处理一个交易并且没有Account对象时使用的。返回的Name对象将用于创建Account对象。

事务作为Transaction对象被读取并存储在vector<Transaction>容器transactions中。代码读取一个使用stable_sort()按降序排序的事务序列。然后,第二个事务序列被读入同一个容器,并以同样的方式进行排序。通过设法创建一个包含两个排序的事务序列的vector,我们可以利用inplace_merge()创建两个序列的有序组合。

下面是针对五个帐户的七笔交易的输出示例。我选择事务数量来演示排序和合并操作的行为方式。

Enter each transaction as:

5 digit account number   amount   credit(true or false).

Enter Ctrl+Z to end.

12345 40 true

12344 50 true

12346 75.5 true

^Z

First set of transactions after sorting...

12346   75.50 CR

12345   40.00 CR

12344   50.00 CR

Enter more transactions:

12344 25.25 true

12345 75 false

12345 100 true

12346 100 true

^Z

Sorted first set of transactions with second set appended...

12346   75.50 CR

12345   40.00 CR

12344   50.00 CR

12344   25.25 CR

12345   75.00 DR

12345  100.00 CR

12346  100.00 CR

Sorted first set of transactions with sorted second set appended...

12346   75.50 CR

12345   40.00 CR

12344   50.00 CR

12344   25.25 CR

12346  100.00 CR

12345   75.00 DR

12345  100.00 CR

Merged sets of transactions...

12346   75.50 CR

12346  100.00 CR

12345   40.00 CR

12345   75.00 DR

12345  100.00 CR

12344   50.00 CR

12344   25.25 CR

Enter the holder’s first and second names for account number 12346: Stan Dupp

Enter the holder’s first and second names for account number 12345: Ann Ounce

Account number 12345 for Ann Ounce is overdrawn!

The concept is that you bank with us - not the other way round, so fix it!

Enter the holder’s first and second names for account number 12344: Dan Druff

Dan Druff           12344   75.25

Stan Dupp           12346  175.50

Ann Ounce           12345   65.00

在每个阶段都列出了Transaction对象的序列,所以你可以看到stable_sort()inplace_merge()算法像我描述的那样工作。特别是,等价交易的顺序是保持不变的,所以借项和贷项是按照它们产生的顺序来应用的。最后,帐户按名称顺序列出,以表明交易已被正确应用。这是通过将map容器中的Account对象复制到vector<Account>容器中,并将stable_sort()算法应用于vector中的元素,其中Compare_Names函数对象提供比较。您可以将Account对象复制到一个set<Account, Compare_Names>容器而不是vector<Account>容器中,让对象自动排序,但是这样您就会错过使用stable_sort()的机会。

搜索范围

STL 提供了多种多样的算法,用于以各种方式搜索一系列对象。这些方法大多处理无序序列。但是有些,我稍后会讲到,需要对序列进行排序。

查找范围中的元素

有三种算法可以在两个输入迭代器定义的范围内找到单个对象。

  • find()算法在由前两个参数指定的范围内找到第一个等于第三个参数的对象。
  • find_if()算法在前两个参数指定的范围内找到第一个对象,第三个参数指定的谓词为该对象返回true。谓词不能修改传递给它的对象。
  • find_if_not()算法在前两个参数指定的范围内找到第一个对象,第三个参数指定的谓词为该对象返回false。谓词不能修改传递给它的对象。

每个算法返回一个指向找到的对象的迭代器,如果没有找到对象,则返回该范围的结束迭代器。以下是如何使用find()的示例:

std::vector<int> numbers {5, 46, -5, -6, 23, 17, 5, 9, 6, 5};

int value {23};

auto iter = std::find(std::begin(numbers), std::end(numbers), value);

if(iter !=  std::end(numbers)) std::cout << value << " was found.\n";

这段代码将输出消息,表明在numbers向量中确实找到了23。当然,您可以重复使用find()来查找给定元素在一个范围内的所有出现:

size_t count {};

int five {5};

auto start_iter = std::begin(numbers);

auto end_iter = std::end(numbers);

while((start_iter = std::find(start_iter, end_iter, five)) != end_iter)

{

++count;

++start_iter;

}

std::cout << five << " was found " << count << " times." << std::endl;    // 3 times

while循环中递增的count变量计算在numbers向量中找到five的次数。循环表达式调用find()start_iterend_iter定义的范围内寻找fivefind()返回的迭代器存储在start_iter中,覆盖变量之前的值。最初,搜索的范围是numbers中的所有元素,因此find()将返回一个迭代器,指向第一次出现的five。每次找到five时,start_iter在循环中递增,因此它将指向所找到的元素之后的元素。因此,下一次迭代将从该点搜索到序列的末尾。当five不再存在时,find()将返回end_iter,循环结束。

您可以使用find_if()来查找numbers中大于value的第一个元素,如下所示:

int value {5};

auto iter1 = std::find_if(std::begin(numbers), std::end(numbers),                                                      value { return n > value; });

if(iter1 != std::end(numbers)) std::cout << *iter1 << " was found greater than " << value << ".\n";

find_if()的第三个参数是一个由 lambda 表达式定义的谓词。lambda 表达式通过值捕获value,并在 lambda 的参数大于value时返回true。这个片段将找到值为46的元素。您可以在一个循环中使用find_if()来查找所有大于value的数字,就像前面的代码片段一样。

您可以使用find_if_not()算法来查找谓词为false的元素,如下所示:

size_t count {};

int five {5};

auto start_iter = std::begin(numbers);

auto end_iter = std::end(numbers);

while((start_iter = std::find_if_not(start_iter, end_iter,

five {return n > five; })) != end_iter)

{

++count;

++start_iter;

}

std::cout << count << " elements were found that are not greater than "<< five << std::endl;

作为find_if_not()的第三个参数的谓词是一个 lambda 表达式,类似于我之前在find_if()算法中使用的表达式。只有当一个元素大于five时,才会返回true。当谓词返回false时找到一个元素,因此操作实际上是找到小于或等于five的元素。将找到与该范围中的值5-5-655相对应的五个元素。

在一个范围中查找一系列元素中的任何一个

find_first_of()算法在一个范围中搜索第二个范围中任何元素的第一个匹配项。要搜索的范围可以仅由输入迭代器指定,但是标识所搜索内容的范围必须至少是前向迭代器。来自两个范围的元素使用==操作符进行比较,所以如果范围指定了一个类类型的对象,这个类必须实现operator==()。这里有一个使用find_first_of()的例子:

string text {"The world of searching"};

string vowels {"aeiou"};

auto iter = std::find_first_of(std::begin(text), std::end(text), std::begin(vowels),                                                                 std::end(vowels));

if(iter != std::end(text)) std::cout << "We found '" << *iter << "'." << std::endl; // We found 'e'.

这段代码在text中搜索第一次出现的vowels中的任何字符。指向"The"中第三个字母的迭代器在这个实例中被返回。您可以使用一个循环来查找来自vowels的任何字符在text中的所有出现:

string found {};                                   // Records characters that are found

for(auto iter = std::begin(text);

(iter = std::find_first_of(

iter, std::end(text), std::begin(vowels), std::end(vowels))) != std::end(text);  )

found += *(iter++);

std::cout << "The characters \"" << found << "\" were found in text." << std::endl;

这使用了一个for循环——只是为了证明你可以。第一个循环控制表达式用初始值定义了iter,作为text的开始迭代器。第二个控制表达式调用find_first_of()在范围[iter, std::end(text))中搜索从vowels开始的任何字符的第一次出现。find_first_of()返回的迭代器存储在iter中,然后与text的结束迭代器进行比较。如果iter现在是text的结束迭代器,循环结束。如果iter不是text的结束迭代器,则循环体执行将iter指向的字符追加到found字符串,并递增iter指向下一个字符。该字符将用作下一次搜索范围的起始位置。这个片段产生的输出将是:

The characters "eooeai" were found in text.

另一个版本的find_first_of()使您能够从第二个范围中搜索任何元素的第一次出现,其中由第五个参数指定的二元谓词返回true。范围中的元素不需要属于同一类型。当要比较的元素不支持==操作符时,您可以使用这个版本的算法来定义相等比较,但是您也可以以其他方式使用它。例如:

std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};

int factors[] {7, 11, 13};

auto iter = std::find_first_of(std::begin(numbers),                                std::end(numbers),             // The range to be searched

std::begin(factors), std::end(factors),   // Elements sought

[](long v, long d) { return v % d == 0;});  // Predicate - true for a match

if(iter != std::end(numbers)) std::cout << *iter << " was found." << std::endl;

谓词是一个 lambda 表达式,当第一个参数被第二个参数整除时,它返回true。因此这段代码找到了-65,因为这是numbers中第一个能被factors数组中的一个元素整除的元素,在本例中是13。谓词中的参数类型可以不同于范围中的元素类型,只要每个范围中的元素可以隐式转换为相应的参数类型。这里,factors数组中的元素被隐式转换为类型long

当然,您可以使用循环来查找谓词返回true的所有元素:

std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};

int factors[] {7, 11, 13};

std::vector<long> results;                            // Stores elements found

auto iter = std::begin(numbers);

while((iter = std::find_first_of(iter, std::end(numbers),                 // Range searched

std::begin(factors), std::end(factors),   // Elements sought

[](long v, long d) { return v % d == 0; })) // Predicate

!= std::end(numbers))

results.push_back(*iter++);

std::cout << results.size() << " values were found:\n";

std::copy(std::begin(results), std::end(results), std::ostream_iterator < long > {std::cout, " " });

std::cout << std::endl;

这个代码片段在numbers中找到所有将来自factors的元素作为因子的元素。只要find_first_of()返回的迭代器不是numbers的结束迭代器,while循环就会继续。iter变量开始指向numbers中的第一个元素,指向找到的元素的迭代器存储在iter中,覆盖先前的值。在循环体中,iter指向的元素存储在results容器中,然后iter递增指向后面的元素。当循环结束时,results包含所有找到的元素,并使用copy()算法输出。

从一个范围中查找多个元素

adjacent_find()算法在一个范围内搜索两个相同的连续元素。使用==操作符比较连续的元素对,并返回指向前两个相等元素中第一个的迭代器。如果没有相等的元素对,该算法返回该范围的结束迭代器。例如:

string saying {"Children should be seen and not heard."};

auto iter = std::adjacent_find(std::begin(saying), std::end(saying));

if(iter != std::end(saying))

std::cout << "In the following text:\n\"" << saying << "\"\n'"

<< *iter << "' is repeated starting at index position "

<< std::distance(std::begin(saying), iter) << std::endl;

这将在saying字符串中搜索前两个相同的连续字符,因此代码将产生以下输出:

In the following text:

"Children should be seen and not heard."

'e' is repeated starting at index position 20

第二个版本的adjacent_find()算法允许您提供一个应用于连续元素的谓词。下面是如何使用它来查找一个范围内的第一对连续的奇数整数:

std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};

auto iter = std::adjacent_find(std::begin(numbers), std::end(numbers),

[](long n1, long n2){ return n1 % 2 && n2 % 2; });

if(iter != std::end(numbers))

std::cout << "The first pair of odd numbers is "

<< *iter << " and " << *(iter+1) << std::endl;

当两个参数都不是 2 的倍数时,lambda 表达式返回true,因此这段代码将找到数字12117

find end()算法

find_end()算法查找第二个元素范围中的最后一个匹配项。您可以将此想象为在任何类型的元素序列中查找子序列的最后一个匹配项。该算法返回一个指向子序列最后一次出现的第一个元素的迭代器,或者是正在搜索的范围的结束迭代器。这里有一个使用它的例子:

string text  {"Smith, where Jones had had \"had\", had had \"had had\"."

" \"Had had\" had had the examiners\' approval."};

std::cout << text << std::endl;

string phrase {"had had"};

auto iter = std::find_end(std::begin(text), std::end(text), std::begin(phrase), std::end(phrase));

if(iter != std::end(text))

std::cout << "The last \"" << phrase

<< "\" was found at index " << std::distance(std::begin(text), iter) << std::endl;

这将在text中搜索最后一次出现的"had had",并产生以下输出:

Smith, where Jones had had "had", had had "had had". "Had had" had had the examiners' approval.

The last "had had" was found at index 63

您可以在text中搜索所有出现的phrase。这个例子简单地计算了出现的次数:

size_t count {};

auto iter = std::end(text);

auto end_iter = iter;

while((iter = std::find_end(std::begin(text), end_iter, std::begin(phrase),

std::end(phrase))) != end_iter)

{

++count;

end_iter = iter;

}

std::cout << "\n\""<< phrase << "\" was found " << count << " times." << std::endl;

while循环表达式执行搜索。循环表达式在范围std::begin(text), end_iter)中搜索phrase,搜索到的第一个范围是text中的所有元素。为了帮助澄清这里发生了什么,这个过程如图 [6-5 所示。

A978-1-4842-0004-9_6_Fig5_HTML.gif

图 6-5。

Searching repeatedly with find_end()

find_end()返回的迭代器存储在iter中,当它等于end_iteriter的前一个值时——循环结束。因为find_end()找到子序列的最后一个出现,所以下一次要搜索的范围的结束迭代器(end_iter)必须改为算法返回的迭代器。这指向已找到的序列的第一个字符,因此下一次搜索将从text的开头到这一点,忽略已找到的序列。在循环体内递增count后,end_iter被设置为iter。这是必要的,因为如果没有找到phrase,下一次搜索将返回这个迭代器。

第二个版本的find_end()接受一个二元谓词作为第五个参数,用于比较元素。您可以用它来重复前面的搜索忽略情况:

size_t count {};

auto iter = std::end(text);

auto end_iter = iter;

while((iter = std::find_end(std::begin(text), end_iter, std::begin(phrase), std::end(phrase),

[](char ch1, char ch2){ return std::toupper(ch1) == std::toupper(ch2); })) != end_iter)

{

++count;

end_iter = iter;

}

现在,来自两个范围的字符对将在转换成大写字母后进行比较。将在text中找到phrase的五个实例,因为将发现"Had had"等于phrase

search()算法

search()算法与find_end()相似,它在序列中寻找一个子序列,但它寻找的是第一个而不是最后一个。和find_end()算法一样,有两个版本——第二个版本接受第五个参数,该参数是用于比较元素的谓词。您可以使用search()算法通过find_end()执行之前的搜索。主要的区别在于如何在每次迭代中改变要搜索的范围的规格。代码如下:

string text {"Smith, where Jones had had \"had\", had had \"had had\"."

" \"Had had\" had had the examiners\' approval."};

std::cout << text << std::endl;

string phrase {"had had"};

size_t count {};

auto iter = std::begin(text);

auto end_iter = end(text);

while((iter = std::search(iter, end_iter, std::begin(phrase), std::end(phrase),

[](char ch1, char ch2){ return std::toupper(ch1) == std::toupper(ch2); })) != end_iter)

{

++count;

std::advance(iter, phrase.size());              // Move to beyond end of subsequence found

}

std::cout << "\n\""<< phrase << "\" was found " << count << " times." << std::endl;

执行此代码将产生以下输出:

Smith, where Jones had had "had", had had "had had". "Had had" had had the examiners' approval.

"had had" was found 5 times.

我们仍然在搜索"had had"忽略的情况,但是在向前的方向找到第一个出现的情况。search()算法返回的迭代器指向找到的子序列中的第一个元素,因此为了搜索下一个phraseiter必须增加phrase中的元素数,使其指向找到的子序列之后的第一个元素。

search n()算法

search_n()算法在一个范围内搜索一个元素给定的连续出现次数。前两个参数是定义要搜索的范围的前向迭代器,第三个参数是要查找的第四个参数的连续出现次数。这里有一个例子:

std::vector<double> values {2.7, 2.7, 2.7, 3.14, 3.14, 3.14, 2.7, 2.7};

double value {3.14};

int times {3};

auto iter = std::search_n(std::begin(values), std::end(values), times, value);

if(iter != std::end(values))

std::cout << times << " successive instances of " << value

<< " found starting index " << std::distance(std::begin(values), iter) << std::endl;

这段代码在values容器中搜索value的一系列times实例的第一次出现。它在索引位置 3 查找序列。请注意,指定计数的第三个参数不能是无符号整数类型;如果是这样,代码将不会在没有警告的情况下编译。

使用==比较元素,但是您可以提供一个额外的参数来指定要使用的谓词。当然,这不一定需要定义一个相等的比较。下面是一个完整的工作示例,它做了一些不同的事情:

// Ex6_03.cpp

// Searching using search_n() to find freezing months

#include <iostream>                              // For standard streams

#include <vector>                                // For vector container

#include <algorithm>                             // For search_n()

#include <string>                                // For string class

using std::string;

int main()

{

std::vector<int> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};

int max_temp {32};

int times {3};

auto iter = std::search_n(std::begin(temperatures), std::end(temperatures), times, max_temp,

[](double v, double max){return v <= max; } );

std::vector<string> months {"January", "February", "March", "April", "May", "June",

"July", "August", "September", "October", "November", "December"};

if(iter != std::end(temperatures))

std::cout << "It was " << max_temp << " degrees or below for " << times

<< " months starting in " << months[std::distance(std::begin(temperatures), iter)]

<< std::endl;

}

容器存储一年中每个月的平均温度。作为search_n()最后一个参数的谓词是一个 lambda 表达式,当一个元素小于或等于max_temp时,它将返回truemonths容器存储月份的名称。表达式std::distance(std::begin(temperatures), iter)产生temperatures中元素的索引,这是谓词返回truetimes元素序列的第一个。该值用于索引months向量以选择月份名称。因此,该代码将产生以下输出:

It was 32 degrees or below for 3 months starting in May

划分范围

对一个范围内的元素进行分区会重新排列元素,使得给定谓词返回true的所有元素都在谓词返回false的所有元素之前。partition()算法做到了这一点。前两个参数是前向迭代器,用于标识要划分的范围,第三个参数是谓词。下面是如何使用partition()算法来重新排列一个值序列,使所有小于平均值的值排在所有大于平均值的值之前:

std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};

std::copy(std::begin(temperatures), std::end(temperatures),      // List the values

std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

auto average = std::accumulate(std::begin(temperatures),         // Compute the average value

std::end(temperatures), 0.0)/ temperatures.size();

std::cout << "Average temperature: " << average << std::endl;

std::partition(std::begin(temperatures), std::end(temperatures), // Partition the values

average { return t < average; });

std::copy(std::begin(temperatures), std::end(temperatures),   // List the values after   partitioning

std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

这些语句产生以下输出:

65 75 56 48 31 28 32 29 40 41 44 50

Average temperature: 44.9167

44 41 40 29 31 28 32 48 56 75 65 50

使用accumulate()算法生成元素的和,然后除以元素的数量,从而得到temperatures容器中值的平均值。你以前见过accumulate()算法,所以你会记得第三个参数是总和的初始值。您可以看到,在执行partition()算法后,所有小于average的温度值都在大于average的温度值之前。

谓词不一定是顺序关系,可以是任何你喜欢的东西。例如,您可以对代表个人的一系列Person对象进行划分,使所有女性优先于男性,或者所有拥有大学学位的人优先于没有大学学位的人。下面是一个例子,它划分了一系列代表人们并识别他们性别的tuple对象:

using gender = char;

using first = string;

using second= string;

using Name = std::tuple<first, second, gender>;

std::vector<Name> names {std::make_tuple("Dan", "Old", 'm'), std::make_tuple("Ann", "Old", 'f'),

std::make_tuple("Ed", "Old", 'm'),  std::make_tuple("Jan", "Old", 'f'),

std::make_tuple("Edna", "Old", 'f')};

std::partition(std::begin(names), std::end(names),         // Partition the names

[](const Name& name) { return std::get<2>(name) == 'f'; });

for(const auto& name : names)

std::cout << std::get<0>(name) << " " << std::get<1>(name) << std::endl;

using声明解释了tuple对象成员的重要性。当元组的最后一个成员是'f'时,谓词返回 true,因此输出将在 Ed 和 Dan 之前呈现 Edna、Ann 和 Jan。您可以使用表达式std::get<gender>(name)来引用谓词中tuple的第三个成员。这是可能的,因为第三个成员的类型是唯一的,这允许通过其类型来标识该成员。

partition()算法不保证保持范围内原始元素的相对顺序。在上面使用它的例子中,元件 44 和41在原始范围内跟随40,但是在操作之后不再是这种情况。为了保持元素的相对顺序,可以使用stable_partition()算法。论点与partition()相同。您可以用下面的语句替换前面代码中调用partition()来划分温度的语句:

std::stable_partition(std::begin(temperatures), std::end(temperatures),

average { return t < average; });

经过这一更改后,输出将是:

65 75 56 48 31 28 32 29 40 41 44 50

Average temperature: 44.9167

31 28 32 29 40 41 44 65 75 56 48 50

您可以看到,当不需要重新排序来划分范围时,元素的相对顺序得到了保留。所有小于平均值的元素都按其原始顺序排列,所有不小于平均值的元素也是如此。

partition_copy()算法

partition_copy()算法以与stable_partition()相同的方式划分一个范围,但是谓词返回true的元素被复制到一个单独的范围,谓词返回false的元素被复制到第三个范围。该操作保持原始范围不变。源范围由前两个参数标识,它们必须是输入迭代器。谓词为其返回true的元素的目标范围的开始由第三个参数标识,谓词为false的元素的目标的开始是第四个参数;两者都必须是输出迭代器。第五个参数是用于划分元素的谓词。这里有一个完整的程序展示了partition_copy()的作用:

// Ex6_04.cpp

// Using partition_copy() to find values above average and below average

#include <iostream>                              // For standard streams

#include <vector>                                // For vector container

#include <algorithm>                             // For partition_copy(), copy()

#include <numeric>                               // For accumulate()

#include <iterator>                              // For back_inserter, ostream_iterator

int main()

{

std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};

std::vector<double> low_t;                       // Stores below average temperatures

std::vector<double> high_t;                      // Stores average or above temperatures

auto average = std::accumulate(std::begin(temperatures),std::end(temperatures), 0.0) /

temperatures.size();

std::partition_copy(std::begin(temperatures), std::end(temperatures),

std::back_inserter(low_t), std::back_inserter(high_t),

average { return t < average; });

// Output below average temperatures

std::copy(std::begin(low_t), std::end(low_t), std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

// Output average or above temperatures

std::copy(std::begin(high_t), std::end(high_t), std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

}

这段代码与您之前看到的stable_partition()操作相同,但是将temperatures中低于平均值的元素复制到low_t容器,将高于平均值的元素复制到high_t。output 语句验证了这一点,因为它们会产生以下输出:

31 28 32 29 40 41 44

65 75 56 48 50

注意,main()中的代码使用通过back_inserter()助手函数创建的back_insert_iterator对象作为partition_copy()调用中两个目标容器的迭代器。一个back_insert_iterator调用push_back()向容器中添加一个新元素,因此使用这种方法避免了预先知道有多少元素要被存储的必要性。如果对目标范围使用 begin 迭代器,那么在操作之前,目标中必须已经存在足够的元素,以容纳将要复制的元素。请注意,如果输入范围与任一输出范围重叠,该算法将无法正常工作。

partition_point()算法

使用partition_point()算法获得分区范围中第一个分区的结束迭代器。前两个参数是定义要检查的范围的前向迭代器,最后一个参数是用于划分范围的谓词。您通常不知道每个分区中有多少元素,因此该算法使您能够提取或访问任一分区中的元素。例如:

std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};

auto average = std::accumulate(std::begin(temperatures),         // Compute the average value

std::end(temperatures), 0.0)/ temperatures.size();

auto predicate = average { return t < average; };

std::stable_partition(std::begin(temperatures), std::end(temperatures), predicate);

auto iter = std::partition_point(std::begin(temperatures), std::end(temperatures), predicate);

std::cout << "Elements in the first partition:  ";

std::copy(std::begin(temperatures), iter, std::ostream_iterator<double>{std::cout, " "});

std::cout << "\nElements in the second partition: ";

std::copy(iter, std::end(temperatures), std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

这段代码根据平均温度对temperatures容器中的元素进行分区,并通过调用范围的partition_point()找到分区点。这是第一个分区的结束迭代器,存储在iter中。因此,范围std::begin(temperatures), iter)对应于第一分区中的元素,而范围[iter, std::end(temperatures))包含第二分区中的元素。copy()算法的两个应用程序输出分区,输出将是:

Elements in the first partition:  31 28 32 29 40 41 44

Elements in the second partition: 65 75 56 48 50

在应用partition_point()之前,你需要确保范围已经被划分。如果对此有疑问,您可以致电is_partitioned()来确定是否如此。参数是指定范围的输入迭代器和应该用于划分范围的谓词。如果区域被划分,该算法返回 true,否则返回false。在对其应用partition_point()算法之前,您可以使用它来验证temperatures范围是否被划分:

if(std::is_partitioned(std::begin(temperatures), std::end(temperatures),

[average { return t < average; }))

{

auto iter = std::partition_point(std::begin(temperatures), std::end(temperatures),

average { return t < average; });

std::cout << "Elements in the first partition:  ";

std::copy(std::begin(temperatures), iter, std::ostream_iterator<double>{std::cout, " "});

std::cout << "\nElements in the second partition: ";

std::copy(iter, std::end(temperatures), std::ostream_iterator<double>{std::cout, " "});

std::cout << std::endl;

}

else

std::cout << "Range is not partitioned." << std::endl;

这段代码只在is_partitioned()返回true时执行对partition_point()的调用。指向分割点的iter变量位于true结果的if模块。如果您希望iter随后可用,您可以在if语句之前定义它,如下所示:

std::vector<double>::iterator iter;

在所有使迭代器可用的容器类型模板中定义了iterator类型别名,它对应于容器类型的begin()end()成员返回的迭代器类型。

二分搜索法算法

到目前为止,您在本章中看到的搜索算法按顺序搜索一个范围,并且对元素没有预先的排序要求。二分搜索法算法通常比顺序搜索更快,但是要求对应用它们的范围内的元素进行排序。这是因为二分搜索法的工作方式。如图 6-6 所示。

A978-1-4842-0004-9_6_Fig6_HTML.gif

图 6-6。

A binary search

图 6-6 显示了22的二分搜索法从一系列值中按升序排列的事件顺序。因为元素是按升序排列的,所以搜索机制使用小于运算符来查找元素。按降序搜索范围将使用大于号运算符来比较元素。二分搜索法总是从选择范围中间的元素开始,并将其与所寻求的值进行比较。与要查找的元素等价的元素被认为是匹配的,因此当!(x < n) && !(n < x)时,值n将匹配值x。如果被检查的元素不匹配,如果是x < n,则从左分区的中间元素继续搜索,否则从右分区的中间元素继续搜索。当找到一个等价元素时,或者当被检查的分区只包含一个元素时,搜索结束。如果不匹配,则该元素不在范围内。

二进制搜索()算法

正如您无疑会猜到的那样,binary_search()算法实现了一个二分搜索法。它在前两个参数指定的范围内搜索与第三个参数等效的元素。指定范围的迭代器必须是前向迭代器,并且使用<操作符比较元素。该范围内的元素必须按升序排序,或者至少相对于要查找的值进行分区。该算法返回一个bool值,如果找到第三个参数,该值为true,否则为false,所以它只告诉您元素是否存在,而不告诉您它何时在哪里。当然,如果你一定要知道在哪里,可以用你已经看过的 find 算法之一,或者lower_bound()upper_bound()或者equal_range()。这里有一个使用binary_search()的例子:

std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};

values.sort();                                   // Sort into ascending sequence

int wanted {22};                                 // What we are looking for

if(std::binary_search(std::begin(values), std::end(values), wanted))

std::cout << wanted << " is definitely in there - somewhere..." << std::endl;

else

std::cout << wanted << " cannot be found - maybe you got it wrong..." << std::endl;

我使用了一个list来以任意顺序存储一组任意值——只是为了提醒您这个容器。该代码使用binary_search()算法来搜索wanted的值。由于binary_search()只适用于排序范围,我们必须首先确保列表中的元素是有序的。sort()算法不能应用于list容器中的一系列元素,因为它需要随机访问迭代器,而list容器只提供双向迭代器。出于这个原因,list容器定义了一个sort()成员,该成员按升序对所有元素进行排序,因此它用于对values容器进行排序。该代码执行时,将输出确认wantedvalues中的消息。

第二个版本的binary_search()接受一个附加的参数,它是一个用于搜索的函数对象;显然,这必须与用于排序被搜索范围的比较有效地相同。以下是你如何按降序排列values,然后搜索wanted:

std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};

auto predicate = [](int a, int b){ return a > b; };

values.sort(predicate);                                    // Sort into descending sequence

int wanted {22};

if(std::binary_search(std::begin(values), std::end(values), wanted, predicate))

std::cout << wanted << " is definitely in there - somewhere..." << std::endl;

else

std::cout << wanted << " cannot be found - maybe you got it wrong..." << std::endl;

这使用了接受定义比较的函数对象的list容器的sort()成员。这里,它是由λ表达式定义的。同一个 lambda 表达式被用作binary_search()的第四个参数。当然,结果将与前面的代码相同。

下界()算法

lower_bound()算法在由前两个参数指定的范围内查找不小于第三个参数的元素——换句话说,是大于或等于第三个参数的第一个元素。前两个参数必须是前向迭代器。upper_bound()算法在由它的前两个参数定义的范围内找到大于第三个参数的第一个元素。对于这两种算法,范围必须是有序的,并且假定它们是使用小于运算符进行排序的。这里有一个例子:

std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};

values.sort();                                   // Sort into ascending sequence

int wanted {22};                                 // What we are looking for

std::cout << "The lower bound for " << wanted

<< " is " << *std::lower_bound(std::begin(values), std::end(values), wanted) << std::endl;

std::cout << "The upper bound for " << wanted

<< " is " << *std::upper_bound(std::begin(values), std::end(values), wanted) << std::endl;

这会产生以下输出:

The lower bound for 22 is 22

The upper bound for 22 is 36

从列表容器中的整数可以看出,算法正在按照描述的方式工作。这两种算法都有其他版本,它们接受 function 对象作为第四个参数,指定用于排序范围的比较。

equal_range()算法

equal_range()算法在一个排序范围内找到所有与给定元素等价的元素。前两个参数是指定范围的前向迭代器,第三个参数是所需的元素。该算法返回一个带有前向迭代器成员的pair对象,第一个成员指向不小于第三个参数的元素,第二个成员指向大于第三个参数的元素。因此,您可以在一次调用中得到调用lower_bound()upper_bound()的结果。因此,您可以用以下语句替换前面代码片段中的两个输出语句:

auto pr = std::equal_range(std::begin(values), std::end(values), wanted);

std::cout << "the lower bound for " << wanted << " is " << *pr.first << std::endl;

std::cout << "the upper bound for " << wanted << " is " << *pr.second << std::endl;

输出将与前面的代码完全相同。与以前的二分搜索法算法一样,有一个版本的equal_range()带有一个额外的参数,该参数提供了使用小于运算符之外的比较进行排序的范围。

我说过,本节中的算法要求对它们所应用的范围内的元素进行排序,但这并不是全部。所有二分搜索法算法也适用于以特定方式划分的范围。对于给定的wanted值,范围内的元素必须相对于(element < wanted)进行分区,并且相对于!(wanted < element)进行分区。我可以用equal_range()二分搜索法算法证明这是可行的。在对values容器中的元素执行equal_range()之前,我们可以这样划分它:

std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};

// Output the elements in original order

std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

int wanted {22};                                         // What we are looking for

std::partition(std::begin(values), std::end(values),    // Partition the values wrt value < wanted

wanted { return value < wanted; });

std::partition(std::begin(values), std::end(values), // Partition the values wrt !(wanted < value)

wanted { return !(wanted < value); });

// Output the elements after partitioning

std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

这样的输出将是:

17 11 40 36 22 54 48 70 61 82 78 89 99 92 43

17 11 22 36 40 54 48 70 61 82 78 89 99 92 43

第一行包含原始序列中的元素,第二行显示分区后的序列。这两个分区操作改变了顺序,但没有改变太多。我们现在可以使用wanted的值将equal_range()应用于values中的元素:

auto pr = std::equal_range(std::begin(values), std::end(values), wanted);

std::cout << "the lower bound for " << wanted << " is " << *pr.first << std::endl;

std::cout << "the upper bound for " << wanted << " is " << *pr.second << std::endl;

该代码的输出将与前面的代码片段相同,其中使用容器对象的sort()成员对元素进行了完全排序。本节中的所有算法都处理以这种方式划分的范围。显然,如果分区使用>,那么您必须为搜索算法提供一个与此一致的函数对象。

前面的代码片段将equal_range()应用于只包含一个wanted实例的范围。如果范围包含几个实例,pr.first将指向第一次出现的通缉,因此范围pr.first, pr.second)将包含所有实例。这里有一个工作程序来说明这一点:

// Ex 6_05.cpp

// Using partition() and equal_range() to find duplicates of a value in a range

#include <iostream>                              // For standard streams

#include <list>                                  // For list container

#include <algorithm>                             // For copy(), partition()

#include <iterator>                              // For ostream_iterator

int main()

{

std::list<int> values {17, 11, 40, 13, 22, 54, 48, 70, 22, 61, 82, 78, 22, 89, 99, 92, 43};

// Output the elements in their original order

std::cout << "The elements in the original sequence are:\n";

std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

int wanted {22};                                         // What we are looking for

std::partition(std::begin(values), std::end(values),   // Partition the values with (value < wanted)

[wanted { return value < wanted; });

std::partition(std::begin(values), std::end(values),  // Partition the values with !(wanted < value)

wanted { return !(wanted < value); });

// Output the elements``after

std::cout << "The elements after partitioning are:\n";

std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

auto pr = std::equal_range(std::begin(values), std::end(values), wanted);

std::cout << "The lower bound for " << wanted << " is " << *pr.first << std::endl;

std::cout << "The upper bound for " << wanted << " is " << *pr.second << std::endl;

std::cout << "\nThe elements found by equal_range() are:\n";

std::copy(pr.first, pr.second, std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

}

输出将是:

The elements in the original sequence are:

17 11 40 13 22 54 48 70 22 61 82 78 22 89 99 92 43

The elements after partitioning are:

17 11 13 22 22 22 48 70 54 61 82 78 40 89 99 92 43

The lower bound for 22 is 22

The upper bound for 22 is 48

The elements found by equal_range() are:

22 22 22

这里的values容器有几个值为 22 的元素,这是wanted的值。wanted的三个实例都在equal_range()返回的范围内。该范围只进行了分区,没有完全排序,因此当该范围完全排序时,这显然是可行的。

那么,当范围只是像 Ex6_05 中那样划分,而不是完全排序时,equal_range()为什么返回所有出现的wanted?要理解这一点,你需要理解两个partition()呼叫的影响:

  • 第一次分区操作确保所有严格小于wanted的元素都在左分区中;这些元素不一定按顺序排列。这个操作还确保了所有不小于wanted的元素——也就是大于或等于wanted的元素——都在正确的分区中,所以它们按顺序跟随它,也不一定是按顺序。所有出现的wanted将在正确的分区中,但是与大于wanted的元素混合在一起。在前一个片段中的第一个partition()调用之后,values中的元素是:

17 11 13 40 22 54 48 70 22 61 82 78 22 89 99 92 43

171113是唯一小于wanted的值,并且这些值明显在左分区中。分区不以任何特定的方式定位对应于wanted的值。22的所有实例都位于右分区中元素的任意位置。

  • 第二个分区操作应用于第一个分区操作的结果。表达式!(wanted < value)等价于(value <= wanted)。因此,所有小于或等于wanted的元素将位于左分区,所有严格大于wanted的元素将位于右分区。这样做的效果是将wanted的所有实例移动到左分区中,这样它们就像左分区中的最后一个元素一样作为一个序列在一起。第二次partition()调用后,values包含:

17 11 13 22 22 22 48 70 54 61 82 78 40 89 99 92 43

因此,equal_range()找到的下限指向第一次出现的22,上限指向最后一次出现的22之后的元素,即值为48的元素。

摘要

如果你使用 STL 容器,你会希望熟悉我在本章中讨论的算法。排序是非常常见的需求,尤其是在处理事务的应用程序中。当您需要对数据进行排序时,通常也需要合并数据。确保事务与它们所应用的记录顺序相同通常会使更新过程更快。当然,默认情况下,有序的setmap容器以及priority_queue容器适配器提供了它们所包含的元素的排序,这消除了显式排序操作的需要。但是,当您需要在不同的时间以不同的顺序处理相同的数据时,您需要排序操作来在需要时重新排列对象。STL sort()算法的强大之处一方面在于它们的灵活性——你可以对任何你可以比较的东西进行排序,另一方面在于它们的效率——这个实现几乎肯定比你自己实现的排序算法要好。这并不是说 STL 算法总是最好的选择。C++ 标准库中没有实现许多专门的数据排序方法,但是当您使用 STL 容器来管理数据,并且只是想要一个通用的排序功能时,STL 提供了一个即时的解决方案。

STL 查找算法比排序和合并算法应用得更广泛。除了能够比较元素之外,它们对要搜索的范围没有任何要求。同样,定义要搜索的范围的迭代器所需的功能也很少。对范围进行排序时,实现二分搜索法的算法是对查找算法的补充。最后,分区算法在无序和有序范围之间提供了一个中间站。正如您所看到的,您可以将二分搜索法算法应用于分区范围,并允许在不应用完全排序的情况下找到一系列相同的元素。

ExercisesDefine a Card class to represent playing cards in a standard deck. Create a vector of Card objects that represents a complete deck of fifty-two cards. Deal the cards randomly into four vector containers so that each represents a hand of thirteen cards in a game. Output the cards in the four vector containers under the headings “North,” “South,” “East,” and “West” after sorting each hand using the sort( ) algorithm. The cards should be sorted into the usual suit and value order - so in card value order from 2 through to 10, then Jack, Queen, King, Ace within the suit sequence Clubs, Diamonds, Hearts, Spades.   Add code to your solution to Exercise 1 to merge the four hands and output the result.   Define a Person class that identifies a person by at least their name and hair color. The class should implement operator<<() for output streams and function members to compare hair color. Create a vector that contains Person objects in no particular order, including some that have blond, gray, brown, and black hair. Use the partition() algorithm to arrange the objects in the container so that they are ordered by hair color with those that have black hair first, then those that are gray, followed by brown, and blond last. Output the Person objects in hair color groups using the copy() algorithm.   Add function members for comparing names to the Person class type in Exercise 3, and extend your solution to this exercise to use the sort() algorithm to order the Person objects with a given hair color in ascending sequence of their names before outputting them.

公职选举通常随机排列候选人名字在选票上出现的顺序,以避免对名字出现在字母顺序后面的候选人产生偏见,如 Joe Yodel 和 Bob Zippo。定义一个Name类,使用以下字母顺序对名称进行排序:

英语作文网-英语作文网-英语作文网

所以以R开头的名字排在前面,以L开头的名字排在最后。在一个矢量容器中创建各种Name对象,并使用stable_sort()算法根据上面的字母顺序对它们进行升序排序。

七、更多算法

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​7) contains supplementary material, which is available to authorized users.

本章描述了 STL 提供的更多算法。算法通常分为两组:改变应用范围的变异算法和非变异算法。我将在这一章中讨论算法,这些算法根据你能用它们做什么来分组,而不是根据它们是否改变事情。如果你知道一个算法是做什么的,那么它是否改变了它所应用的数据就显而易见了。在本章中,您将了解:

  • 测试某个范围内元素属性的算法
  • 计算给定属性范围内元素数量的算法
  • 比较两个元素范围的算法
  • 复制或移动范围的算法
  • 设置或更改某个范围内的元素的算法

测试元素属性

algorithm头中定义了三种算法,用于测试给定谓词在应用于一系列元素时何时返回true。这些算法的前两个参数是输入迭代器,它们定义了谓词应用的范围;第三个参数指定谓词。测试元素以查看谓词是否返回true可能看起来过于简单,但它仍然是一个强大的工具。例如,您可以测试是否有任何或所有学生通过了所有考试,或者是否所有学生都去上课了,或者是否没有绿色眼睛的Person物体,甚至是否每个Dog物体都有过辉煌的一天。谓词可以很简单,也可以很复杂。测试元素属性的三种算法是:

  • 如果谓词为范围内的所有元素返回true,则all_of()算法返回true
  • 如果谓词为范围中的任何元素返回true,则any_of()算法返回true
  • 如果谓词对于范围内的所有元素都不返回true,则none_of()算法返回true

不难想象这些是如何运作的。这里有一些代码来说明如何使用none_of()算法:

std::vector<int> ages {22, 19, 46, 75, 54, 19, 27, 66, 61, 33, 22, 19};

int min_age{18};

std::cout << "There are "

<< (std::none_of(std::begin(ages), std::end(ages),

min_age { return age < min_age; }) ? "no": "some")

<< " people under " << min_age << std::endl;

谓词是一个 lambda 表达式,它将作为参数传递的ages中的元素与min_age的值进行比较。由none_of()返回的bool值用于选择包含在输出消息中的"no""some"。当ages中没有元素小于 min_age 时,none_of()算法返回true,因此在这种情况下选择"no"。当然,您可以使用any_of()来产生相同的结果:

std::cout << "There are "

<< (std::any_of(std::begin(ages), std::end(ages),

min_age { return age < min_age; }) ? "some": "no")

<< " people under " << min_age << std::endl;

any_of()算法仅在一个或多个元素小于min_age时返回true。没有少于min_age的元素,所以这里也选择了"no"

下面的代码片段展示了如何使用all_of()来测试ages容器中的元素:

int good_age{100};

std::cout << (std::all_of(std::begin(ages), std::end(ages),

good_age { return age < good_age; }) ? "None": "Some")

<< " of the people are centenarians." << std::endl;

lambda 表达式将ages中的一个元素与good_age的值进行比较,后者是100。所有元素都小于100,因此all_of()将返回true,输出消息将正确报告没有记录任何百岁老人。

count()count_if()算法告诉您在前两个参数指定的范围内有多少元素满足您用第三个参数指定的条件。count()算法返回等于第三个参数的元素个数。count_if()算法返回第三个参数谓词返回true的元素数量。下面的代码展示了应用于ages容器的这些功能:

std::vector<int> ages {22, 19, 46, 75, 54, 19, 27, 66, 61, 33, 22, 19};

int the_age{19};

std::cout << "There are "

<< std::count(std::begin(ages), std::end(ages), the_age)

<< " people aged " << the_age << std::endl;

int max_age{60};

std::cout << "There are "

<< std::count_if(std::begin(ages), std::end(ages),

max_age { return age > max_age; })

<< " people aged over " << max_age << std::endl;

第一条输出语句使用count()算法来确定ages中等于the_age的元素数量。第二个输出语句使用count_if()来报告超过max_age值的元素数量。

当您想要了解某个元素范围的一般特征时——当您只想知道某个特征是否适用,或者有多少符合某个标准时,可以使用本节中的所有算法。当你想知道细节——范围中的哪些元素匹配——你可以使用在第六章中遇到的查找算法。

比较范围

可以用类似于比较字符串的方式来比较两个范围。如果两个范围长度相同,并且对应的元素对相等,则equal()算法返回trueequal()算法有四个版本,其中两个使用==操作符比较元素,另两个使用您作为参数提供的函数对象比较元素。所有指定范围的迭代器必须至少是输入迭代器。

使用==操作符比较两个范围的一个版本需要三个输入迭代器参数。前两个参数是第一个范围的开始和结束迭代器。第三个参数是第二个范围的 begin 迭代器。如果第二个范围包含的元素比第一个范围少,则结果是未定义的。使用==操作符的第二个版本需要四个参数:第一个范围的开始和结束迭代器,第二个范围的开始和结束迭代器。如果两个范围的长度不同,那么结果总是false。我将演示这两个版本,但是我建议您总是使用接受四个参数的equal()版本,因为它不会导致未定义的行为。下面是一个工作示例,展示了如何应用这些功能:

// Ex7_01.cpp

// Using the equal() algorithm

#include <iostream>                                      // For standard streams

#include <vector>                                        // For vector container

#include <algorithm>                                     // For equal() algorithm

#include <iterator>                                      // For stream iterators

#include <string>                                        // For string class

using std::string;

int main()

{

std::vector<string> words1 {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};

std::vector<string> words2 {"two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"};

auto iter1 = std::begin(words1);

auto end_iter1 = std::end(words1);

auto iter2 = std::begin(words2);

auto end_iter2 = std::end(words2);

std::cout << "Container - words1:  ";

std::copy(iter1, end_iter1, std::ostream_iterator<string>{std::cout, " "});

std::cout << "\nContainer - words2:  ";

std::copy(iter2, end_iter2, std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

std::cout << "\n1\. Compare from words1[1] to end with words2:                              ";

std::cout << std::boolalpha << std::equal(iter1 + 1, end_iter1, iter2) << std::endl;

std::cout << "2\. Compare from words2[0] to second-to-last with words1:                   ";

std::cout << std::boolalpha << std::equal(iter2, end_iter2 - 1, iter1) << std::endl;

std::cout << "3\. Compare from words1[1] to words1[5] with words2:                        ";

std::cout << std::boolalpha << std::equal(iter1 + 1, iter1 + 6, iter2) << std::endl;

std::cout << "4\. Compare first 6 from words1 with first 6 in words2:                     ";

std::cout << std::boolalpha << std::equal(iter1, iter1 + 6, iter2, iter2 + 6) << std::endl;

std::cout << "5\. Compare all words1 with words2:                                         ";

std::cout << std::boolalpha << std::equal(iter1, end_iter1, iter2) << std::endl;

std::cout << "6\. Compare all of words1 with all of words2:                               ";

std::cout << std::boolalpha << std::equal(iter1, end_iter1, iter2, end_iter2) << std::endl;

std::cout << "7\. Compare from words1[1] to end with words2 from first to second-to-last: ";

std::cout << std::boolalpha

<< std::equal(iter1 + 1, end_iter1, iter2, end_iter2 - 1) << std::endl;

}

输出将是:

Container - words1:  one two three four five six seven eight nine

Container - words2:  two three four five six seven eight nine ten

1\. Compare from words1[1] to end``with

2\. Compare from words2[0] to second-to-last with words1:                   false

3\. Compare from words1[1] to words1[5] with words2:                        true

4\. Compare first 6 from words1 with first 6 in words2:                     false

5\. Compare all words1 with words2:                                         false

6\. Compare all of words1 with all of words2:                               false

7\. Compare from words1[1] to end with words2 from first to second-to-last: true

该示例比较了来自words1words2容器的各种元素序列。equal()调用产生输出的原因是:

  • 第一个输出产生true,因为从第二个到最后的words1元素匹配从第一个开始的words2元素。第二个范围中的元素数量比第一个范围中的数量多一个,但是第一个范围中的元素数量决定了要比较多少个对应的元素。
  • 第二个输出产生false,因为有一个直接的不匹配;words2words1中的第一个元素是不同的。
  • 第三个语句显示true,因为从第二个开始的来自words1的五个元素与来自words2的前五个元素相同。
  • 在第四条语句中,来自words2的元素范围由 begin 和 end 迭代器指定。范围长度相同,但第一个元素不同,因此结果是false
  • 在第五个语句中,两个范围中的第一个元素是直接不匹配的,所以结果是false
  • 第六个语句产生false,因为范围不同。该语句不同于前面的equal()调用,因为为第二个范围指定了结束迭代器。
  • 第七个语句从第二个开始比较来自words1的元素,从第一个开始比较来自words2的相同数量的元素,所以输出是true

当第二个范围被 begin 迭代器标识为equal()时,第二个范围中与第一个范围相比的元素数量由第一个范围的长度决定。第二个范围可以比第一个范围有更多的元素,并且equal()仍然可以返回true。当您为两个范围提供 begin 和 end 迭代器时,范围必须是相同的长度才能得到一个true结果。

虽然您可以使用equal()来比较两个相同类型容器的全部内容,但是最好使用容器的operator==()成员来完成这项工作。示例中的第六条输出语句可以写成:

std::cout << std::boolalpha << (words1 == words2) << " ";             // false

接受谓词作为附加参数的两个版本的equal()以相同的方式工作。谓词定义了元素之间相等的比较。下面的代码片段说明了它们的用法:

std::vector<string> r1 {"three", "two", "ten"};

std::vector<string> r2 {"twelve", "ten", "twenty"};

std::cout << std::boolalpha

<< std::equal(std::begin(r1), std::end(r1), std::begin(r2),

[](const string& s1, const string& s2) { return s1[0] == s2[0]; })

<< std::endl;                                               // true

std::cout << std::boolalpha

<< std::equal(std::begin(r1), std::end(r1), std::begin(r2), std::end(r2),

[](const string& s1, const string& s2) { return s1[0] == s2[0]; })

<< std::endl;                                               // true

第一次使用equal()仅通过 begin 迭代器指定第二个范围。谓词是一个 lambda 表达式,当string参数中的第一个字符相等时,它返回true。最后一条语句显示了完全指定两个范围并使用相同谓词的equal()算法。

你不应该使用equal()来比较无序的mapset容器中的元素范围。一个无序容器中给定元素集的顺序可能与存储在另一个无序容器中的相同元素集的顺序不同,因为元素在桶中的分配可能因容器而异。

查找范围不同的地方

equal()算法告诉你两个范围是否匹配。mismatch()算法告诉你两个范围是否匹配,如果不匹配,它们在哪里不同。四个版本的mismatch()和四个版本的equal()有相同的参数——有和没有第二个范围的结束迭代器,每个版本都有和没有一个函数对象的额外参数来定义比较。mismatch()算法返回一个包含两个迭代器的pair对象。first成员是来自前两个参数指定范围的迭代器,第二个成员是来自第二个范围的迭代器。当范围不匹配时,pair包含指向第一对不匹配元素的迭代器;因此,对象将是pair<iter1 + n, iter2 + n>,其中范围中索引n处的元素是第一个不匹配的元素。

当范围匹配时,pair成员取决于您使用的mismatch()版本和环境。iter1end_iter1代表定义第一个范围的迭代器,iter2end_iter2代表第二个范围的开始和结束迭代器,为匹配范围返回的pair的内容如下:

对于mismatch(iter1, end_iter1, iter2):

  • 返回pair<end_iter1, (iter2 + (end_iter1 - iter1))>,所以第二个成员是iter2加上第一个范围的长度。如果第二个范围比第一个范围短,则行为未定义。

For mismatch(iter1, end_iter1, iter2, end_iter2):

  • 当第一个范围比第二个范围长时,返回pair<end_iter1, (iter2 + (end_iter1 - iter1))>,所以second成员是iter2加上第一个范围的长度。
  • 当第二个范围比第一个范围长时pair<(iter1 + (end_iter2 - iter2)), end_iter2>被返回,所以第一个成员是iter1加上第二个范围的长度。
  • 当范围相同时,返回长度pair<end_iter1, end_iter2>

这同样适用于您是否添加了为比较定义函数对象的参数。

下面是一个工作示例,展示了使用默认比较来比较是否相等的mismatch():

// Ex7_02.cpp

// Using the mismatch() algorithm

#include <iostream>                                      // For standard streams

#include <vector>                                        // For vector container

#include <algorithm>                                     // For equal() algorithm

#include <string>                                        // For string class

#include <iterator>                                      // For stream iterators

using std::string;

using word_iter = std::vector<string>::iterator;

int main()

{

std::vector<string> words1 {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};

std::vector<string> words2 {"two", "three", "four", "five", "six", "eleven", "eight", "nine", "ten"};

auto iter1 = std::begin(words1);

auto end_iter1 = std::end(words1);

auto iter2 = std::begin(words2);

auto end_iter2 = std::end(words2);

// Lambda expression to output mismatch() result

auto print_match = [](const std::pair<word_iter, word_iter>& pr, const word_iter& end_iter)

{

if(pr.first != end_iter)

std::cout << "\nFirst pair of words that differ are "

<< *pr.first << " and " << *pr.second << std::endl;

else

std::cout << "\nRanges are identical." << std::endl;

};

std::cout << "Container - words1:  ";

std::copy(iter1, end_iter1, std::ostream_iterator<string>{std::cout, " "});

std::cout << "\nContainer - words2:  ";

std::copy(iter2, end_iter2, std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

std::cout << "\nCompare from words1[1] to end with words2:";

print_match(std::mismatch(iter1 + 1, end_iter1, iter2), end_iter1);

std::cout << "\nCompare from words2[0] to second-to-last with words1:";

print_match(std::mismatch(iter2, end_iter2 - 1, iter1), end_iter2 - 1);

std::cout << "\nCompare from words1[1] to words1[5] with words2:";

print_match(std::mismatch(iter1 + 1, iter1 + 6, iter2), iter1 + 6);

std::cout << "\nCompare first 6 from words1 with first 6 in words2:";

print_match(std::mismatch(iter1, iter1 + 6, iter2, iter2 + 6), iter1 + 6);

std::cout << "\nCompare all words1 with words2:";

print_match(std::mismatch(iter1, end_iter1, iter2), end_iter1);

std::cout << "\nCompare all``of

print_match(std::mismatch(iter2, end_iter2, iter1, end_iter1), end_iter2);

std::cout << "\nCompare from words1[1] to end with words2[0] to second-to-last:";

print_match(std::mismatch(iter1 + 1, end_iter1, iter2, end_iter2 - 1), end_iter1);

}

注意,words2的内容与前面的例子略有不同。每次应用mismatch()的结果都是由定义为print_match的λ表达式生成的。参数是一对对象和一个vector<string>容器的迭代器。用于别名word_iterusing指令使得 lambda 的定义更加简单。main()中的代码使用不包含比较函数对象参数的版本对mismatch()进行了修改。当第二个范围仅由一个 begin 迭代器标识时,只需要它的元素数量至少与第一个匹配范围一样多,但可以更长。当完全指定第二个范围时,最短的范围决定了要比较多少个元素。

以下是输出结果:

Container - words1:  one two three four five six seven eight nine

Container - words2:  two three four five six eleven eight nine ten

Compare from words1[1] to end with words2:

First pair of words that differ are seven and eleven

Compare from words2[0] to second-to-last with words1:

First pair of words that differ are two and one

Compare from words1[1] to words1[5] with words2:

Ranges are identical.

Compare first 6 from words1 with first 6 in words2:

First pair of words that differ are one and two

Compare all words1 with words2:

First pair of words that differ are one and two

Compare all of words2 with all of words1:

First pair of words that differ are two and one

Compare from words1[1] to end with words2[0] to second-to-last:

First pair of words that differ are seven and eleven

输出显示了每次应用mismatch()的结果。

当您提供自己的比较对象时,您可以完全灵活地定义等式。例如:

std::vector<string> range1 {"one", "three", "five", "ten"};

std::vector<string> range2 {"nine", "five", "eighteen", "seven"};

auto pr = std::mismatch(std::begin(range1), std::end(range1), std::begin(range2), std::end(range2),

[](const string& s1, const string& s2)

{ return s1.back() == s2.back(); });

if(pr.first == std::end(range1) || pr.second == std::end(range2))

std::cout << "The ranges are identical." << std::endl;

else

std::cout << *pr.first << " is not equal to " << *pr.second << std::endl;

当两个字符串的最后一个字母相等时,比较返回true,因此执行这段代码的输出将是:

five is not equal to eighteen

当然,这是正确的——而且根据比较函数,"one"等于"nine""three"等于"five"

词典范围比较

两个字符串的字母顺序是通过比较相应的字符对获得的,从第一个字符开始。第一对不同的对应字符决定了哪个字符串先出现。字符串的顺序将是不同字符的顺序。如果字符串长度相同,并且所有字符都相等,则字符串相等。如果字符串长度不同,并且较短字符串中的字符序列与较长字符串中的初始序列相同,则较短字符串小于较长字符串。因此,“年龄”先于“美丽”,“平静”先于“风暴”同样显而易见的是,“先有鸡”而不是“先有蛋”

词典排序是对任何类型的对象序列的字母排序思想的推广。两个序列中的对应对象从第一个开始连续比较,前两个不同的对象决定序列的顺序。显然,序列中的对象必须具有可比性。lexicographical_compare()算法比较由 begin 和 end 迭代器定义的两个范围。前两个参数定义第一个范围,第三和第四个参数是第二个范围的开始和结束迭代器。默认情况下,<操作符用于比较元素,但是您可以在必要时提供一个实现小于比较的函数对象作为可选的第五个参数。如果第一个范围按字典顺序小于第二个范围,算法返回true,否则返回false。因此,错误的返回意味着第一个范围大于或等于第二个范围。这些范围是逐元素比较的。不同的第一对对应元素决定了范围的顺序。如果范围具有不同的长度,并且较短的范围匹配较长范围中的元素的初始序列,则较短的范围小于较长的范围。长度相同且对应元素相等的两个范围相等。空范围总是小于非空范围。下面是一个使用lexicographical_compare()的例子:

std::vector<string> phrase1 {"the", "tigers", "of", "wrath"};

std::vector<string> phrase2 {"the", "horses", "of", "instruction"};

auto less = std::lexicographical_compare(std::begin(phrase1), std::end(phrase1),

std::begin(phrase2), std::end(phrase2));

std::copy(std::begin(phrase1), std::end(phrase1), std::ostream_iterator<string>{std::cout, " "});

std::cout << (less ? "are" : "are not") << " less than ";

std::copy(std::begin(phrase2), std::end(phrase2), std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

因为范围中的第二个元素不同,并且"tigers"大于"horses,所以此代码将生成以下输出:

the tigers of wrath are not less than the horses of instruction

您可以向lexicographical_compare()调用添加一个参数,得到相反的结果:

auto less = std::lexicographical_compare(std::begin(phrase1), std::end(phrase1),

std::begin(phrase2), std::end(phrase2),

[](const string& s1, const string& s2){ return s1.length() < s2.length(); });

该算法使用第三个参数 lambda 表达式来比较元素。这将比较范围内字符串的长度,因为phrase1中第四个元素的长度小于phrase2中相应元素的长度,phrase1小于phrase2

范围的排列

如果你对这个术语不熟悉的话——排列就是一系列对象或值的一种排列。例如,"ABC"中字符的可能排列是:

"ABC", "ACB", "BAC", "BCA", "CAB", and "CBA"

三个不同的字符有六种可能的排列,数字是3 × 2 × 1的结果。一般来说,n不同的物体有n!种可能的排列,其中n!n × (n-1) × (n-2) × ... × 2 × 1。很容易理解为什么会这样。使用n对象,您可以为序列中的第一个对象选择n。对于第一个对象的每个选择,序列中的第二个对象还有n-1个可供选择,因此前两个对象有n × (n-1)个可能的选择。选择了前两个之后,还剩下n-2来选择第三个,所以还有n × (n-1) × (n-2)个前三个的可能序列——以此类推,直到序列中的最后一个是霍布森的选择,因为只剩下一个。

如果一个值域包含相同的元素但顺序不同,那么它就是另一个值域的置换。next_permutation()算法生成一个范围的重排,它是所有可能排列的字典顺序中的下一个排列。默认情况下,它使用小于运算符来实现这一点。参数是定义范围的迭代器,当新的排列大于先前的元素排列时,函数返回一个bool值,即true,如果先前的排列是序列中最大的排列,则返回false,这样就创建了字典上最小的排列。

下面是如何创建包含四个整数的vector的排列:

std::vector<int> range {1,2,3,4};

do

{

std::copy(std::begin(range), std::end(range), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;

} while(std::next_permutation(std::begin(range), std::end(range)));

next_permutation()返回false时,循环结束,表示排列到达最小值。这恰好创建了序列在该范围内的所有排列,但仅仅是因为初始排列1 2 3 4是可能排列集合中的第一个。确保创建所有排列的一种方法是使用next_permutation()获得最小值:

std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};

while(std::next_permutation(std::begin(words), std::end(words)))     // Change to minimum

;

do

{

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

} while(std::next_permutation(std::begin(words), std::end(words)));

words中的初始序列不是最小排列序列,但是while循环继续,直到words包含最小值。do-while 循环然后输出完整的集合。如果您想执行这个片段,请记住它将产生8!,这是输出的40,320行,因此您可以考虑首先减少words中的元素数量。

元素序列的最小排列是当每个元素小于或等于后面的元素时,因此您可以使用min_element()算法返回一个指向某个范围内最小元素的迭代器,同时使用iter_swap()算法交换两个迭代器指向的元素以创建最小排列,如下所示:

std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};

for (auto iter = std::begin(words); iter != std::end(words)-1 ;++iter)

std::iter_swap(iter, std::min_element(iter, std::end(words)));

for循环从容器范围的第一个到倒数第二个遍历迭代器。作为 for 循环主体的语句将iter指向的元素与min_element()返回的迭代器指向的元素交换。这将最终产生最小排列,您可以将它用作next_permutation()生成所有排列的起点。

您可以在开始创建所有排列之前,通过创建原始容器的副本并更改do-while循环来避免达到最小排列的所有开销:

std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};

auto words_copy = words;                              // Copy the original

do

{

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

std::next_permutation(std::begin(words), std::end(words));

} while(words != words_copy);                           // Continue until back to the original

该循环现在继续创建新的排列,直到到达原始排列。

下面是一个工作示例,它查找一个单词中所有字母的排列:

// Ex7_03.cpp

// Finding rearrangements of the letters in a word

#include <iostream>                                   // For standard streams

#include <iterator>                                   // For iterators and begin() and end()

#include <string>                                     // For string class

#include <vector>                                     // For vector container

#include <algorithm>                                  // For next_permutation()

using std::string;

int main()

{

std::vector<string> words;

string word;

while(true)

{

std::cout << "\nEnter a word, or Ctrl+z to end: ";

if((std::cin >> word).eof()) break;

string word_copy {word};

do

{

words.push_back(word);

std::next_permutation(std::begin(word), std::end(word));

} while(word != word_copy);

size_t count{}, max{8};

for(const auto& wrd : words)

std::``cout

std::cout << std::endl;

words.clear();                                         // Remove previous permutations

}

}

它从标准输入流中读入一个单词到word,在word_copy中复制一份,然后将word中所有字母的排列存储到words容器中。程序继续处理单词,直到你输入Ctrl+Z。word 的副本用于决定何时存储所有排列。然后排列被写入标准输出流,8 个一行。我已经说过,排列的数目随着被排列的元素数目迅速增加,所以不要用长词来尝试这个。这个例子并不是很有用,但是我会在第九章的中重新访问这个程序,其中介绍了更多使用 STL 文件的细节。在那里,可以读取一个包含大量英语单词的文件,并搜索这些单词来确定哪些排列是有效的单词。因此,程序找到原始单词的变位词并输出它们。

您可以提供一个 function 对象作为第三个参数给next_permutation(),它定义了一个比较函数,作为缺省函数的替代。下面是如何使用这个版本通过比较最后几个字母来生成单词序列的排列:

std::vector<string> words {"one", "two", "four", "eight"};

do

{

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

} while(std::next_permutation(std::begin(words), std::end(words),

[](const string& s1, const string& s2){return s1.back() < s2.back(); }));

这段代码使用作为最后一个参数传递给next_permutation()的 lambda 表达式来生成words中元素的所有 24 种排列。

next_permutation()算法按升序字典顺序生成排列。当你想产生降序排列时,你可以使用prev_permutation()算法。这与next_permutation()有相同的两个版本,默认情况下使用<来比较元素。因为排列是按降序生成的,所以该算法在大多数情况下返回 true,并且当它创建的排列是最大排列时返回false。例如:

std::vector<double> data {44.5, 22.0, 15.6, 1.5};

do

{

std::copy(std::begin(data), std::end(data), std::ostream_iterator<double> {std::cout, " "});

std::cout << std::endl;

} while(std::prev_permutation(std::begin(data), std::end(data)));

该代码输出data中四个double值的所有二十四种排列,因为初始序列是最大值,而prev_permutation()仅在输入序列是最小值时返回false

您可以使用is_permutation()算法测试一个序列是否是另一个序列的排列,如果是这种情况,该算法将返回true。下面是一些代码,展示了这个算法在 lambda 表达式中的应用:

std::vector<double> data1 {44.5, 22.0, 15.6, 1.5};

std::vector<double> data2 {22.5, 44.5, 1.5, 15.6};

std::vector<double> data3 {1.5, 44.5, 15.6, 22.0};

auto test = [](const auto& d1, const auto& d2)

{

std::copy(std::begin(d1), std::end(d1), std::ostream_iterator<double> {std::cout, " "});

std::cout << (is_permutation(std::begin(d1), std::end(d1), std::begin(d2), std::end(d2)) ?

"is": "is not")

<< " a permutation of ";

std::copy(std::begin(d2), std::end(d2), std::ostream_iterator<double> {std::cout, " "});

std::cout << std::endl;

};

test(data1, data2);

test(data1, data3);

test(data3, data2);

使用auto指定test lambda 的参数类型,这导致编译器将实际类型推断为const std::vector<double>&。使用auto来指定参数类型的 Lambda 表达式被称为泛型 lambda。testλ表达式使用is_permutation()来评估一个参数是否是另一个参数的排列。该算法的参数是两对迭代器,它们定义了要比较的范围。返回的bool值的参数用于选择两个可能的字符串之一进行输出。输出将是:

44.5 22 15.6 1.5 is not a permutation of 22.5 44.5 1.5 15.6

44.5 22 15.6 1.5 is a permutation of 1.5 44.5 15.6 22

1.5 44.5 15.6 22 is not a permutation of 22.5 44.5 1.5 15.6

还有另一个版本的is_permutation()允许第二个范围仅由 begin 迭代器指定。在这种情况下,第二个范围可以包含比第一个范围更多的元素,但是只考虑第一个范围包含的元素数量。但是,我建议您不要使用它,因为如果第二个范围包含的元素比第一个范围少,它会导致未定义的行为。我将展示一些使用它的代码。您可以向data3添加元素,元素的初始序列仍然表示data1的排列。例如:

std::vector<double> data1 {44.5, 22.0, 15.6, 1.5};

std::vector<double> data3 {1.5, 44.5, 15.6, 22.0, 88.0, 999.0};

std::copy(std::begin(data1), std::end(data1), std::ostream_iterator<double> {std::cout, " "});

std::cout << (is_permutation(std::begin(data1), std::end(data1), std::begin(data3)) ?

"is": "is not")

<< " a permutation of ";

std::copy(std::begin(data3), std::end(data3), std::ostream_iterator<double> {std::cout, " "});

std::cout << std::endl;

这将确认data1data3的排列,因为只考虑了data3中的前四个元素。您可以在任一版本的is_permutation()中添加一个额外的参数来指定要使用的比较。

你可以使用shuffle()算法来创建一个范围的随机排列,但是我将把这个讨论推迟到第八章详细讨论 STL 提供的随机数生成能力。

复制范围

本节讨论复制区域的算法;但是不要忘记,当你想把一个容器中的全部内容转移到另一个容器时,你还有其他的可能性。容器定义了赋值操作符,该操作符将一个容器的全部内容复制到同类型的另一个容器中。也有一些容器的构造函数接受一个范围作为初始内容的来源。大多数情况下,本节中的算法用于复制容器中元素的子集。

你已经看到了许多copy()算法的应用,所以你知道它是如何工作的。它将元素从作为输入迭代器的前两个参数定义的源范围复制到从第三个参数(必须是输出迭代器)指定的位置开始的目标范围。你还有三种算法,它们提供的不仅仅是简单的复制过程。

复制一些元素

copy_n()算法将特定数量的元素从源复制到目的地。第一个参数是指向第一个源元素的输入迭代器,第二个参数是要复制的元素数量,第三个参数是指向目标中第一个位置的输出迭代器。该算法返回一个迭代器,该迭代器指向最后一个复制的元素之后的一个元素,或者如果第二个参数为零,则只返回第三个参数——输出迭代器。下面是一个使用它的例子:

std::vector<string> names {"Al",   "Beth",   "Carol", "Dan",  "Eve",

"Fred", "George", "Harry", "Iain", "Joe"};

std::unordered_set<string> more_names {"Janet", "John"};

std::copy_n(std::begin(names) + 1, 3, std::inserter(more_names, std::begin(more_names)));

copy_n()操作从第二个名字开始将三个元素从names容器复制到关联容器more_names。目的地由inserter()函数模板创建的unordered_set容器的insert_iterator对象指定。insert_iterator对象通过调用其insert()成员向容器中添加元素。

当然,copy_n()操作中的目的地可以是一个流迭代器:

std::copy_n(std::begin(more_names), more_names.size()-1,

std::ostream_iterator<string> {std::cout, " "});

这将输出more_names中除最后一个以外的所有元素。请注意,如果要复制的元素数量超过了可用的数量,您的程序将会陷入困境。如果元素数为零或负数,copy_n()算法什么也不做。

条件复制

copy_if()算法从一个谓词返回true的源范围中复制元素,因此您可以把它看作是一个过滤器。前两个参数是定义源范围的输入迭代器,第三个参数是指向目标范围中第一个位置的输出迭代器,第四个参数是谓词。返回一个输出迭代器,它指向最后一个被复制的元素之后的一个元素。这里有一个使用copy_if()的例子:

std::vector<string> names {"Al",   "Beth",   "Carol", "Dan",  "Eve",

"Fred", "George", "Harry", "Iain", "Joe"};

std::unordered_set<string> more_names {"Jean", "John"};

size_t max_length{4};

std::copy_if(std::begin(names), std::end(names), std::inserter(more_names, std::begin(more_names)),

max_length{ return s.length() <= max_length; });

这里的copy_if()操作只复制来自names的四个字符或更少的元素,因为这是第四个参数 lambda 表达式强加的条件。目的地是unordered_set集装箱more_names,它已经包含了两个四个字母的名字。与上一节一样,insert_iterator将符合条件的元素添加到关联容器中。如果您想证明它是有效的,您可以使用copy()算法列出more_names的内容:

std::copy(std::begin(more_names), std::end(more_names), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

当然,copy_if()的目的地也可以是一个流迭代器:

std::vector<string> names {"Al",   "Beth",   "Carol", "Dan",  "Eve",

"Fred", "George", "Harry", "Iain", "Joe"};

size_t max_length{4};

std::copy_if(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "},

max_length { return s.length() > max_length; });

std::cout << std::endl;

这将把具有五个或更多字符的名称从names容器写入标准输出流。这将输出:

Carol George Harry

您可以使用输入流迭代器作为copy_if()算法的源代码,就像您可以使用其他需要输入迭代器的算法一样。这里有一个例子:

std::unordered_set<string> names;

size_t max_length {4};

std::cout << "Enter names of less than 5 letters. Enter Ctrl+Z on a separate line to end:\n";

std::copy_if(std::istream_iterator<string>{std::cin}, std::istream_iterator<string>{}, std::inserter(names, std::begin(names)),

max_length { return s.length() <= max_length; });

std::copy(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

names容器是一个最初为空的unordered_setcopy_if()算法复制从标准输入流中读取的名字,但只限于四个或更少的字符。执行这段代码会产生以下输出:

Enter names of less than 5 letters. Enter Ctrl+Z on a separate line to end:

Jim Bethany Jean Al Algernon Bill Adwina Ella Frederick Don

^Z

Ella Jim Jean Al Bill Don

cin中读取超过五个字母的名称,但将其丢弃,因为在这些情况下,第四个参数指定的谓词返回false。因此,输入的十个名字中只有六个存储在容器中。

逆序复制

不要被copy_backward()算法的名字误导了。它不会颠倒元素的顺序。它就像copy()算法一样复制,但是从最后一个元素开始,并返回到第一个元素。copy_backward()算法复制由前两个迭代器参数指定的范围。第三个参数是目标区域的结束迭代器,通过将源区域的最后一个元素复制到目标区域结束迭代器之前的元素,源区域被复制到目标区域,如图 7-1 所示。copy_backward()的三个参数都必须是双向迭代器,即可以递增或递减的迭代器。这意味着该算法只能应用于序列容器中的范围。

A978-1-4842-0004-9_7_Fig1_HTML.gif

图 7-1。

How copy_backward() works

图 7-1 显示了如何将源范围from中的最后一个元素首先复制到目标范围to中的最后一个元素。从源位置向后穿过源范围的每个后续元素都被复制到目标位置上前一个元素之前的位置。在执行操作之前,目标中的元素必须存在,因此目标中的元素必须至少与源中的元素一样多,但也可以更多。copy_backward()算法返回一个迭代器,该迭代器指向最后一个被复制的元素,这将是该区域在新位置的开始迭代器。

您可能想知道copy_backward()与从第一个元素开始复制元素的常规copy()算法相比有什么优势。一个答案是当范围重叠时。您可以使用copy()将元素复制到左边的重叠目标区域——也就是说,复制到源区域中第一个元素之前的位置。如果您试图使用copy()将相同范围内的元素复制到右边,该操作将不起作用,因为仍要复制的元素将在被复制之前被覆盖。当你想向右复制时,你可以使用copy_backward(),只要目标区域的末端在源区域末端的右边。图 7-2 说明了在重叠范围之间向右复制时两种算法的区别。

A978-1-4842-0004-9_7_Fig2_HTML.gif

图 7-2。

Copying overlapping ranges to the right

图 7-2 显示了将copy()copy_backward()算法应用于右侧前三个位置的结果。很明显,当复制到右边时,copy()算法不能做你想要的,因为一些元素在被复制之前就被覆盖了。在这种情况下,copy_backward()算法确实做了你想做的事情。当在一个范围内向左复制时,情况正好相反- copy()有效,但copy_backward()无效。

这里有一些代码来说明copy_backward()的作用:

std::deque<string> song{"jingle", "bells", "jingle", "all", "the", "way"};

song.resize(song.size()+2);                  // Add 2 elements

std::copy_backward(std::begin(song), std::begin(song)+6, std::end(song));

std::copy(std::begin(song), std::end(song), std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

通过使用其resize()成员创建反向序列复制操作所需的额外元素,增加了deque容器中的元素数量。copy_backward()算法将原始元素向右复制两个位置,保留前两个元素不变,因此这段代码的输出将是:

jingle bells jingle bells jingle all the way

复制和反转元素的顺序

reverse_copy()算法将一个源区域复制到一个目标区域,这样目标区域中的元素顺序相反。源范围由前两个迭代器参数定义,必须是双向的。目的地由第三个参数标识,这是一个输出迭代器,是目的地的 begin 迭代器。如果范围重叠,则行为未定义。该算法返回一个输出迭代器,它指向目标范围中最后一个元素之后的一个元素。这里有一个使用reverse_copy()copy_if()的工作示例:

// Ex7_04.cpp

// Testing for palindromes using reverse_copy()

#include <iostream>                            // For standard streams

#include <iterator>                            // For stream iterators and begin() and end()

#include <algorithm>                           // For reverse_copy() and copy_if()

#include <cctype>                              // For toupper() and isalpha()

#include <string>

using std::string;

int main()

{

while(true)

{

string sentence;

std::cout << "Enter a sentence or Ctrl+Z to end: ";

std::getline(std::cin, sentence);

if(std::cin.eof()) break;

// Copy as long as the characters are alphabetic & convert to upper case

string only_letters;

std::copy_if(std::begin(sentence), std::end(sentence), std::back_inserter(only_letters),

[](char ch) { return std::isalpha(ch); });

std::for_each(std::begin(only_letters), std::end(only_letters), [](char& ch) { ch = toupper(ch); });

// Make a reversed copy

string reversed;

std::reverse_copy(std::begin(only_letters), std::end(only_letters), std::back_inserter(reversed));

std::cout << '"' << sentence << '"'

<< (only_letters == reversed ? " is" : " is not") << " a palindrome." << std::endl;

}

}

这个程序检查一个句子(或者许多句子)是否代表一个回文;回文是这样一个句子,如果你忽略了空格和标点符号这样的小细节,它的前后读起来是一样的。循环允许你检查尽可能多的句子。使用getline()将一个句子读入sentence。如果只读取了Ctrl+Z,则将为输入流设置EOF标志,这将终止循环。使用copy_if()sentence中的字母复制到only_letters。lambda 表达式只为字母返回true,因此任何其他字符都将被忽略。由back_inserter()创建的back_insert_iterator对象将字符附加到only_lettersfor_each()算法将第三个参数指定的函数应用于由前两个参数定义的范围内的元素,因此这里它将only_letters中的字符转换为大写。使用reverse_copy()算法在reverse中创建only_letters内容的反向副本。比较only_lettersreversed确定输入是否是回文。

以下是一些输出示例:

Enter a sentence or Ctrl+Z to end: Lid off a daffodil.

"Lid off a daffodil." is a palindrome.

Enter a sentence or Ctrl+Z to end: Engage le jeu que je le gagne.

"Engage le jeu que je le gagne." is a palindrome.

Enter a sentence or Ctrl+Z to end: Sit on a potato pan Otis!

"Sit on a potato pan Otis!" is a palindrome.

Enter a sentence or Ctrl+Z to end: Madam, I am Adam.

"Madam, I am Adam." is not a palindrome.

Enter a sentence or Ctrl+Z to end: Madam, I’m Adam.

"Madam, I’m Adam." is a palindrome.

Enter a sentence or Ctrl+Z to end: ^Z

回文很难创建,但是一个法国人乔治·佩雷克成功地创建了一个包含一千多个单词的回文。

reverse()算法将由两个双向迭代器参数指定的范围内的元素就地反转。你可以在Ex7_04.cpp中使用这个来代替reverse_copy()——就像这样:

string reversed {only_letters};

std::reverse(std::begin(reversed), std::end(reversed));

这两条语句将取代在Ex7_04.cpp中对reversedreverse_copy()调用的定义。他们创造了reversed作为only_letters的复制品。调用reverse()然后将reversed中的字符顺序颠倒过来。

复制一个区域,删除相邻的重复项

unique_copy()将一个范围复制到另一个范围,同时删除连续的重复元素。默认情况下,它使用==操作符来决定元素何时相等。前两个参数是指定源的迭代器,第三个参数是指向目标中第一个元素的输出迭代器。可选的第四个参数接受一个函数对象,该对象定义了一个对==操作符的替代。该算法返回一个输出迭代器,它指向目标中最后一个元素之后的一个元素。

复制一个序列,如11223,将导致目的地包含123。因为只消除相邻的重复项,所以将复制序列中的所有元素,如12123。当然,如果源区域已经排序,所有重复的区域都将被删除,因此目标区域将包含唯一的元素。

下面是一些显示应用于字符串中字符的unique_copy()的代码:

string text {"Have you seen how green the trees seem?"};

string result{};

std::unique_copy(std::begin(text), std::end(text), std::back_inserter(result));

std::cout << result << std::endl;

复制操作的源是整个字符串text,目的地是resultback_insert_iterator,所以每个被复制的字符将被附加到result。这输出了几乎无用的句子:

Have you sen how gren the tres sem?

尽管输出确认了unique_copy()消除了相邻的重复。

当你提供你自己的比较对象时,你并不局限于一个简单的等式——你可以把它变成你喜欢的。这使得有可能选择不被复制的重复元素。下面的代码展示了如何从字符串中删除重复的空格:

string text {"there’s   no air  in   spaaaaaace!"};

string result {};

std::unique_copy(std::begin(text), std::end(text), std::back_inserter(result),

[](char ch1, char ch2) { return ch1 == ' ' && ch1 == ch2; });

std::cout << result << std::endl;

unique_copy()的第四个参数是一个 lambda 表达式,仅当两个参数都是空格时才返回true。执行此代码会产生以下输出:

There’s no air in spaaaaaace!

这表明空格已经被删除,但是spaaaaaace中的a没有被删除。

从范围中删除相邻的重复项

您还可以使用unique()算法来删除序列中的重复项。这需要前向迭代器来指定要处理的范围。它返回一个前向迭代器,该迭代器是删除重复项后新范围的结束迭代器。您可以提供一个 function 对象作为可选的第三个参数,该参数定义了用于比较元素的==的替代。这里有一个例子:

std::vector<string> words {"one", "two", "two", "three", "two", "two", "two"};

auto end_iter = std::unique(std::begin(words), std::end(words));

std::copy(std::begin(words), end_iter, std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

这通过覆盖来消除words中的连续元素。输出将是:

one two three two

当然,不会从输入范围中删除任何元素;该算法无法移除元素,因为它不知道它们的上下文。整个系列仍将存在。然而,如果我在上面的代码中使用std::end(words)而不是end_iter来输出结果,那么在新的 end 之外的元素的状态没有保证,我在我的系统上得到这样的输出:

one two three two  two two

同样数量的元素仍然存在,但是新的 end 迭代器指向的元素只是空字符串;最后两个元素和之前一样。在您的系统上,结果可能有所不同。正因为如此,在执行unique()之后截断原始范围是个好主意,就像这样:

auto end_iter = std::unique(std::begin(words), std::end(words));

words.erase(end_iter, std::end(words));

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

容器的erase()成员从新的末端迭代器中移除元素,因此end(words)将返回end_iter

当然,您可以将unique()应用于字符串中的字符:

string text {"there’s   no air  in   spaaaaaace!"};

text.erase(std::unique(std::begin(text), std::end(text),

[](char ch1, char ch2) { return ch1 == ' ' && ch1 == ch2; }),

std::end(text));

std::cout << text << std::endl;             // Outputs: there’s no air in spaaaaaace!

这使用unique()text字符串中删除相邻的重复空格。代码使用迭代器,迭代器由unique()作为第一个参数返回给texterase()成员,并指向第一个要删除的字符。erase()的第二个参数是text的结束迭代器,所以新字符串后面没有重复空格的所有字符都被删除。

旋转范围

rotate()算法向左旋转一系列元素。其工作原理如图 7-3 所示。为了理解旋转范围的工作原理,您可以将范围中的元素想象成手镯上的珠子。rotate()操作使得一个新元素成为 begin 迭代器指向的第一个元素。旋转后,最后一个元素是新的第一个元素之前的元素。

A978-1-4842-0004-9_7_Fig3_HTML.gif

图 7-3。

How the rotate() algorithm works

rotate()的第一个参数是范围的开始迭代器;第二个参数是一个迭代器,指向新的第一个元素应该是什么,它必须在范围内;第三个参数是范围的结束迭代器。图 7-3 中的例子显示了ns容器上的rotate()操作使得值为 4 的元素成为新的第一个元素,最后一个元素的值为 3。元素的循环顺序保持不变,因此它实际上只是旋转元素的循环,直到新的第一个元素成为范围的开始。该算法返回一个迭代器,指向新位置的原始第一个元素。这里有一个例子:

std::vector<string> words {"one", "two", "three", "four", "five", "six", "seven", "eight"};

auto iter = std::rotate(std::begin(words), std::begin(words)+3, std::end(words));

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl << "First element before rotation: " << *iter << std::endl;

这段代码将旋转应用于words中的所有元素。执行这段代码将产生以下输出:

four five six seven eight one two three

First element before rotation: one

输出表明"four"是新的第一个元素,并且rotate()返回的迭代器确实指向了之前的第一个元素"one".

当然,您旋转的范围不必是容器中的所有元素。例如:

std::vector<string> words {"one", "two", "three", "four", "five",

"six", "seven", "eight", "nine", "ten"};

auto start = std::find(std::begin(words), std::end(words), "two");

auto end_iter = std::find(std::begin(words), std::end(words), "eight");

auto iter = std::rotate(start, std::find(std::begin(words), std::end(words), "five"), end_iter);

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl << "First element before rotation: " << *iter << std::endl;

它使用find()算法获得指向words中匹配“二”和“八”的元素的迭代器。这些定义了要旋转的范围,它是容器中元素的子集。这个范围被旋转以使"five"成为第一个元素,输出显示它按预期工作:

one five six seven two three four eight nine ten

First element before rotation: two

rotate_copy()算法在一个新的范围内生成一个范围的旋转副本,而不影响原始副本。rotate_copy()的前三个参数与rotate()的相同;第四个参数是一个输出迭代器,指向目标范围的第一个元素。该算法返回目的地的输出迭代器,该迭代器指向复制的最后一个元素之后的一个元素。这里有一个例子:

std::vector<string> words {"one", "two", "three", "four", "five",

"six", "seven", "eight", "nine", "ten"};

auto start = std::find(std::begin(words), std::end(words), "two");

auto end_iter = std::find(std::begin(words), std::end(words), "eight");

std::vector<string> words_copy;

std::rotate_copy(start, std::find(std::begin(words), std::end(words), "five"), end_iter,

std::back_inserter(words_copy));

std::copy(std::begin(words_copy), std::end(words_copy), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

这产生了从words开始从"two""seven"的元素的旋转副本。使用一个back_insert_iterator将复制的元素添加到words_copy容器中,该容器将调用words_copypush_back()成员来插入每个元素。这段代码产生的输出是:

five six seven two three four

rotate_copy()在这里返回的迭代器是words_copy中元素的结束迭代器。这段代码中没有记录或使用它,但它可能很有用。例如:

std::vector<string> words {"one", "two", "three", "four", "five",

"six", "seven", "eight", "nine", "ten"};

auto start = std::find(std::begin(words), std::end(words), "two");

auto end_iter = std::find(std::begin(words), std::end(words), "eight");

std::vector<string> words_copy {20};                         // vector with 20 default elements

auto end_copy_iter = std::rotate_copy(start,   std::find(std::begin(words), std::end(words), "five"), end_iter, std::begin(words_copy));

std::copy(std::begin(words_copy), end_copy_iter, std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

words_copy容器是用二十个默认元素创建的。rotate_copy()算法现在将旋转后的范围存储在words_copy的现有元素中,从开始处开始。算法返回的迭代器用于标识输出的words_copy中范围的结束;如果没有它,我们将不得不根据源范围中的元素数量来计算。

移动范围

move()算法将由前两个输入迭代器参数指定的范围移动到目的地,从第三个参数定义的位置开始,第三个参数必须是输出迭代器。该算法返回一个迭代器,该迭代器的值超过了移动到目的地的最后一个元素。这是一个移动操作,所以不能保证操作后元素的输入范围保持不变;源元素仍将存在,但可能不具有相同的值,因此在移动后不应使用它们。如果源范围要被替换,或者要被破坏,你可以使用move()算法。如果你需要源范围不受干扰,使用copy()算法。下面是一个展示如何使用它的示例:

std::vector<int> srce {1, 2, 3, 4};

std::deque<int> dest {5, 6, 7, 8};

std::move(std::begin(srce), std::end(srce), std::back_inserter(dest));

这将把所有来自vector容器srce的元素追加到deque容器dest中。要替换dest中的现有元素,您可以使用std::begin(dest)作为move()的第三个参数。只要目标中的第一个元素在源范围之外,就可以使用move()将元素移动到与源范围重叠的目标中;这意味着在该范围内向左移动。这里有一个例子:

std::vector<int> data {1, 2, 3, 4, 5, 6, 7, 8};

std::move(std::begin(data) + 2, std::end(data), std::begin(data));

data.erase(std::end(data) - 2, std::end(data));       // Erase moved elements

std::copy(std::begin(data), std::end(data), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;                               // 3, 4, 5, 6, 7, 8

这将把data的最后六个元素移回到容器的开头。这是因为目标在源范围之外。移动后不能保证最后两个元素的值。这里的元素被删除了,但是你同样可以将它们重置为一个已知的值——比如零。结果显示在最后一行的注释中。当然,您可以使用rotate()算法而不是move()算法来移动元素,在这种情况下,您肯定会知道最后两个元素的值。

如果移动操作的目的地在源范围内,move()将不能正常工作;这意味着在范围内向右移动。原因是有些元素在移动之前会被覆盖。尽管如此,move_backward()算法仍然有效。前两个参数指定要移动的范围,第三个参数是目标的结束迭代器。这里有一个例子:

std::deque<int> data {1, 2, 3, 4, 5, 6, 7, 8};

std::move_backward(std::begin(data), std::end(data) - 2, std::end(data));

data[0] = data[1] = 0;                                // Reset moved elements

std::copy(std::begin(data), std::end(data), std::ostream_iterator<int> {std::cout, " "});

std::cout << std::endl;                               // 0, 0, 1, 2, 3, 4, 5, 6

我在这里使用了一个deque容器,只是为了改变一下。这会将前六个元素向右移动两个位置。操作后其值不确定的元素被重置为 0。最后一行显示了操作的结果。

您可以使用swap_ranges()算法交换两个范围。该算法需要三个前向迭代器参数。前两个参数是一个区域的开始和结束迭代器,第三个参数是第二个区域的开始迭代器。显然,范围必须是相同的长度。该算法为第二个范围返回一个迭代器,该迭代器指向经过最后一个交换的元素。这里有一个例子:

using Name = std::pair<string, string>;               // First and second name

std::vector<Name> people {Name{"Al", "Bedo"}, Name{"Ann", "Ounce"}, Name{"Jo", "King"}};

std::list<Name> folks {Name{"Stan", "Down"}, Name{"Dan", "Druff"}, Name{"Bea", "Gone"}};

std::swap_ranges(std::begin(people), std::begin(people) + 2, ++std::begin(folks));

std::for_each(std::begin(people), std::end(people),

[](const xName& name){std::cout << '"' << name.first << " " << name.second << "\" ";});

std::cout << std::endl;                               // "Dan Druff" "Bea Gone" "Jo King"

std::for_each(std::begin(folks), std::end(folks),

[](const Name& name){std::cout << '"' << name.first << " " << name.second << "\" "; });

std::cout << std::endl;                             // "Stan Down" "Al Bedo" "Ann Ounce"

vectorlist容器存储代表名字的pair<string,string>类型的元素。swap_ranges()算法用于将people中的前两个元素与folks中的后两个元素进行交换。没有用于将pair对象写入流的operator<<()函数重载,因此copy()不能与列出容器的输出流迭代器一起使用。我选择使用for_each()算法,通过对容器中的每个元素应用 lambda 表达式来产生输出。lambda 表达式只是将传递给它的Name元素的成员写入标准输出流。注释显示了执行这段代码的输出。

有一个函数模板重载了在原型的utility头中定义的swap()算法:

template<typename T1, typename T2> void swap(std::pair<T1,T2> left, std::pair<T1,T2> right);

这交换了pair<T1,T2>对象,并被swap_ranges()用来交换前面代码片段中的元素。

交换两个相同类型的对象Tswap()模板也在utility头中定义。除了对pair对象的重载之外,在utility头中还有模板重载,它将交换任意给定类型的两个容器对象。也就是说,它将交换两个list<T>容器或两个set<T>容器,而不是一个list<T>与一个vector<T>或一个list<T1>与一个list<T2>。另一个swap()模板重载可以交换两个相同类型的数组。还有几个其他的swap()重载来交换其他类型的对象,包括tuple和智能指针类型。正如你在本章前面看到的,iter_swap()算法有点不同;它交换两个前向迭代器指向的元素。

从范围中删除元素

在不知道元素的上下文(存储元素的容器)的情况下,从一个范围中删除元素是不可能的。因此,“移除”元素的算法不会,它们只是覆盖选定的元素或忽略复制元素。移除操作不会改变“移除”的元素范围内的元素数量。有四种删除算法:

  • remove()从前两个前向迭代器参数指定的范围中删除元素,这两个参数等于作为第三个参数的对象。实际上,每个匹配元素都是通过被后面的元素覆盖而被删除的。该算法返回一个迭代器,该迭代器指向范围中新的最后一个元素之后的一个元素。
  • remove_copy()将元素从前两个前向迭代器参数指定的范围复制到第三个参数标识的目标范围,忽略等于第四个参数的元素。该算法返回一个迭代器,它指向复制到目标范围的最后一个元素之后的一个元素。范围不得重叠。
  • remove_if()删除由前两个前向迭代器参数指定的范围内的元素,其中作为第三个参数的谓词返回true
  • remove_copy_if()将元素从前两个前向迭代器参数指定的范围复制到第三个参数标识的目标范围,第四个参数的谓词返回true。该算法返回一个迭代器,它指向复制到目标的最后一个元素之后的一个元素。范围不得重叠。

下面是你如何使用remove():

std::deque<double> samples {1.5, 2.6, 0.0, 3.1, 0.0, 0.0, 4.1, 0.0, 6.7, 0.0};

samples.erase(std::remove(std::begin(samples), std::end(samples), 0.0), std::end(samples));

std::copy(std::begin(samples), std::end(samples), std::ostream_iterator<double> {std::cout, " "});

std::cout << std::endl;                               // 1.5 2.6 3.1 4.1 6.7

samples包含不应为零的物理测量值。remove()算法通过向左移动其他元素来覆盖它们,从而消除虚假的零值。remove()返回的迭代器是操作产生的元素范围的新结束,因此它被用作要通过调用sampleserase()成员来删除的范围的开始迭代器。注释显示了剩余的元素。

当您需要保留原始范围并创建一个新范围时,您可以使用remove_copy(),该新范围是删除了所选元素的副本。例如:

std::deque<double> samples {1.5, 2.6, 0.0, 3.1, 0.0, 0.0, 4.1, 0.0, 6.7, 0.0};

std::vector<double> edited_samples;

std::remove_copy(std::begin(samples), std::end(samples), std::back_inserter(edited_samples), 0.0);

非零元素从samples容器复制到edited_samples容器,这恰好是不同的——这是一个vector。元素由一个back_insert_iterator对象添加到edited_samples,所以容器将只包含从samples复制的元素。

remove_if()算法提供了一个更强大的功能,可以从一个范围中删除只匹配一个值的元素。谓词决定是否删除元素;只要它接受范围中的一个元素作为参数并返回一个bool值,任何事情都可以。这里有一个例子:

using Name = std::pair<string, string>;     // First and second name

std::set<Name> blacklist {Name {"Al", "Bedo"}, Name {"Ann", "Ounce"}, Name {"Jo", "King"}};

std::deque<Name> candidates {Name {"Stan", "Down"}, Name {"Al", "Bedo"}, Name {"Dan", "Druff"},

Name {"Di", "Gress"}, Name {"Ann", "Ounce"}, Name {"Bea", "Gone"}};

candidates.erase(std::remove_if(std::begin(candidates), std::end(candidates),

&blacklist { return blacklist.count(name); }),

std::end(candidates));

std::for_each(std::begin(candidates), std::end(candidates), [](const Name& name)

{std::cout << '"' << name.first << " " << name.second << "\" "; });

std::cout << std::endl;                   // "Stan Down" "Dan Druff" "Di Gress" "Bea Gone"

这个代码模型处理不接受公众投票的俱乐部成员的申请。已知的麻烦制造者的名字存储在blacklist容器中,这是一个set。当前的会员申请存储在candidates容器中,这是一个deque。使用remove_if()算法来确保没有来自blacklist容器的名字通过选择过程。谓词是一个 lambda 表达式,它通过引用捕获blacklist容器。当参数存在时,set容器的count()成员将返回1。谓词返回的值被隐式转换为bool,因此谓词实际上为出现在blacklist中的candidates中的每个元素返回true,因此这些元素将从candidates中移除。通过选择过程的候选人在评论中显示。

remove_copy_if()之于remove_copy()如同remove_if()之于remove()。以下是它的工作原理:

std::set<Name> blacklist {Name {"Al", "Bedo"}, Name {"Ann", "Ounce"}, Name {"Jo", "King"}};

std::deque<Name> candidates {Name {"Stan", "Down"}, Name {"Al", "Bedo"}, Name {"Dan", "Druff"},

Name {"Di", "Gress"}, Name {"Ann", "Ounce"}, Name {"Bea", "Gone"}};

std::deque<Name> validated;

std::remove_copy_if(std::begin(candidates), std::end(candidates), std::back_inserter(validated),

&blacklist { return blacklist.count(name); });

除了结果存储在validated容器中并且不修改candidates容器之外,这段代码与前面的片段完成了相同的任务。

设置和修改范围内的元素

fill()fill_n()算法提供了一种用给定值填充一系列元素的简单方法。fill()填充整个范围;fill_n()从给定迭代器指向的元素开始,为指定的元素数量设置一个值。下面是fill()的用法:

std::vector<string> data {12};                        // Container has 12 elements

std::fill(std::begin(data), std::end(data), "none");  // Set all elements to "none"

fill()的前两个参数是定义范围的前向迭代器。第三个参数是分配给每个元素的值。当然,该范围不必代表容器中的所有元素。例如:

std::deque<int> values(13);                           // Container has 13 elements

int n{2};                                             // Initial element value

const int step {7};                                   // Element value increment

const size_t count{3};                                // Number of elements with given value

auto iter = std::begin(values);

while(true)

{

auto to_end = std::distance(iter, std::end(values));// Number of elements remaining

if(to_end < count)                                  // In case no. of elements not a multiple of count

{

std::fill(iter, iter + to_end, n);                // Just fill remaining elements...

break;                                            // ...and end the loop

}

else

{

std::fill(iter, std::end(values), n);             // Fill next count elements

}

iter = std::next(iter, count);                    // Increment iter

n += step;

}

13元素创建了values容器。在这种情况下,必须使用圆括号将值传递给构造函数;使用大括号将创建一个包含值为 13 的单个元素的容器。在循环中,fill()算法被用来给count元素的序列赋值。iter从容器的 begin 迭代器开始,如果还有足够多的元素,它会在每次循环迭代中增加count,从而指向下一个序列中的第一个元素。执行此操作会将values中的元素设置为:

2 2 2 9 9 9 16 16 16 23 23 23 30

fill_n()的参数是一个前向迭代器,指向要修改的范围中的第一个元素、要修改的元素数量以及要设置的值。distance()next()功能在iterator标题中定义。前者使用输入迭代器,但后者需要前向迭代器。

用函数生成元素值

您已经看到,您可以使用for_each()算法将一个函数对象应用于一个范围内的每个元素。function 对象有一个参数,该参数引用由算法的前两个参数定义的范围内的元素,因此它可以直接更改存储的值。generate()算法略有不同。前两个参数是指定范围的前向迭代器,第三个参数是定义函数形式的函数对象:

T fun();     // T is a type that can be assigned to an element in the range

从函数内部无法访问范围内元素的值。generate()算法只是为范围内的每个元素存储函数返回的值;并且generate()没有返回任何东西。要使算法有用,您需要能够生成不同的值,并将其分配给不带参数的函数中的不同元素。一种可能是将generate()的第三个参数定义为一个函数对象,它捕获一个或多个外部变量。这里有一个例子:

string chars (30, ' ');                          // 30 space characters

char ch {'a'};

int incr {};

std::generate(std::begin(chars), std::end(chars), [ch, &incr]

{

incr += 3;

return ch + (incr % 26);

});

std::cout << chars << std::endl;              // chars is: dgjmpsvybehknqtwzcfiloruxadgjm

chars变量用一串 30 个空格字符初始化。作为generate()的第三个参数的 lambda 表达式返回的值将存储在chars的连续字符中。lambda 通过值捕获ch,通过引用捕获incr,因此后者可以在 lambda 的主体中修改。lambda 返回将incrch相加得到的字符,增量值以26为模,因此返回值总是在'a''z'的范围内,假定起始值为'a'。该操作的结果显示在注释中。有可能设计出一个 lambda,它适用于任何大写或小写字母,并且只生成存储在ch,中的那种类型的字母,但是我将把它留给你作为练习。

generate_n()算法的工作方式与generate()相似。不同之处在于,第一个参数仍然是范围的 begin 迭代器,而第二个参数是由第三个参数设置的元素数量的计数。该范围必须至少包含第二个参数定义的元素数,以避免程序崩溃。这里有一个例子:

string chars (30, ' ');                          // 30 space characters

char ch {'a'};

int incr {};

std::generate_n(std::begin(chars), chars.size()/2,[ch, &incr]

{

incr += 3;

return ch + (incr % 26);

});

这里,chars中只有一半的字符会有算法设置的新值。后半部分将保留为空格字符。

转换范围

transform()算法将一个函数应用于一个范围内的元素,并将该函数返回的值存储在另一个范围内。它返回一个迭代器,指向输出范围中存储的最后一个元素之后的一个元素。该算法的一个版本与for_each()的相似之处在于,您将一元函数应用于一系列可以修改其值的元素,但也有显著的不同。使用for_each()应用的函数必须有一个 void 返回类型,您可以通过函数的引用参数改变输入范围中的值。使用transform()一元函数必须返回一个值,并且您有可能将应用该函数的结果存储在另一个范围中,并且输出范围中的元素可以是与输入范围不同的类型。还有另一个区别:使用for_each(),函数总是按顺序应用于元素,但是使用transform()不能保证。

transform()的第二个版本允许将二元函数应用于两个范围内的相应元素,但是让我们先来看看将一元函数应用于一个范围。在这个版本的算法中,前两个参数是定义输入范围的输入迭代器,第三个参数是目标中第一个元素的输出迭代器,第四个参数是一元函数。该函数必须接受输入范围中的一个元素作为参数,并且必须返回一个可以存储在输出范围中的值。这里有一个例子:

std::vector<double> deg_C {21.0, 30.5, 0.0, 3.2, 100.0};

std::vector<double> deg_F(deg_C.size());

std::transform(std::begin(deg_C), std::end(deg_C), std::begin(deg_F),

[](double temp){ return 32.0 + 9.0* temp/5.0; });  // Result 69.8 86.9 32 37.76 212

transform()算法调用将deg_C容器中的摄氏温度值转换为华氏温度,并将结果存储在deg_F容器中。用存储所有结果所需的元素数量创建了deg_F容器,因此第三个参数是deg_F的 begin 迭代器。您可以使用一个back_insert_iterator作为transform()的第三个参数,将结果存储在一个空容器中:

std::vector<double> deg_F;                       // Empty container

std::transform(std::begin(deg_C), std::end(deg_C), std::back_inserter(deg_F),

[](double temp){ return 32.0 + 9.0* temp/5.0; });  // Result 69.8 86.9 32 37.76 212

存储操作结果的元素由back_insert_iteratordeg_F中创建;结果是一样的。

第三个参数可以是指向输入容器中元素的迭代器。例如:

std::vector<double> temps {21.0, 30.5, 0.0, 3.2, 100.0};            // In Centigrade

std::transform(std::begin(temps), std::end(temps), std::begin(temps),

[](double temp) { return 32.0 + 9.0* temp / 5.0; });  // Result 69.8 86.9 32 37.76 212

这将把temps容器中的数值从摄氏温度转换为华氏温度。第三个参数是输入范围的 begin 迭代器,因此应用由第四个参数指定的函数的结果被存储回它所应用的元素中。

下面的代码说明了目标区域与输入区域的类型不同的情况:

std::vector<string> words {"one", "two", "three", "four", "five"};

std::vector<size_t> hash_values;

std::transform(std::begin(words), std::end(words), std::back_inserter(hash_values),

std::hash<string>());   // string hashing function

std::copy(std::begin(hash_values), std::end(hash_values),

std::ostream_iterator<size_t> {std::cout, " "});

std::cout << std::endl;

输入范围包含string对象,应用于元素的函数是在string头中定义的标准散列函数对象。哈希函数返回类型为size_t,的哈希值,这些值使用back_inserter()助手函数从iterator头返回的back_insert_iterator对象存储在hash_values容器中。在我的系统上,这段代码会产生以下输出:

3123124719 3190065193 2290484163 795473317 2931049365

您的系统可能会产生不同的输出。注意,因为目标范围被指定为一个back_insert_iterator对象,这里的transform()算法将返回一个back_insert_iterator<vector<size_T>>类型的迭代器,所以您不能将它用作copy()算法的输入范围的结束迭代器。为了利用transform()返回的迭代器,代码应该是:

std::vector<string> words {"one", "two", "three", "four", "five"};

std::vector<size_t> hash_values(words.size());

auto end_iter = std::transform(std::begin(words), std::end(words), std::begin(hash_values),

std::hash<string>());   // string hashing function

std::copy(std::begin(hash_values), end_iter, std::ostream_iterator<size_t> {std::cout, " "});

std::cout << std::endl;

现在transform()返回hash_values容器中元素范围的结束迭代器。

没有什么可以阻止您从函数内部调用算法,该函数由transform()函数应用于一系列元素。这里有一个可能性的例子:

std::deque<string> names {"Stan Laurel", "Oliver Hardy", "Harold Lloyd"};

std::transform(std::begin(names), std::end(names), std::begin(names),

[](string& s) { std::transform(std::begin(s), std::end(s),std::begin(s), ::toupper);

return s;

});

std::copy(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "});

std::cout << std::endl;

transform()算法将 lambda 表达式定义的函数应用于names容器中的元素。lambda 表达式调用transform()cctype头中定义的toupper()函数应用于传递给它的字符串中的每个字符。最终结果是将names中的每个元素转换成大写,因此输出将是:

STAN LAUREL OLIVER HARDY HAROLD LLOYD

当然,还有其他可能更简单的方法来达到同样的效果。

应用二元函数的版本transform()需要五个参数:

  • 前两个参数是第一个输入范围的输入迭代器。
  • 第三个参数是第二个输入范围的 begin 迭代器,显然,这个范围必须包含至少与第一个输入范围一样多的元素。
  • 第四个参数是输出迭代器,它是存储函数应用结果的范围的开始迭代器。
  • 第五个参数是一个 function 对象,它定义了一个具有两个参数的函数,该函数将接受来自两个输入范围的元素参数,并返回一个可存储在输出范围中的值。

让我们考虑一些简单的几何计算的例子。折线是点之间的一系列直线。折线可以由一个Point对象的vector来表示,折线中的线段是连接连续点的线。如果最后一个点与第一个点相同,折线将是闭合的,即多边形。图 7-4 显示了一个将Point定义为类型别名的例子,如下所示:

A978-1-4842-0004-9_7_Fig4_HTML.gif

图 7-4。

A polyline that represents a hexagon

using Point = std::pair<double, double>;      //  pair<x,y> defines a point

有七个点,所以图 7-4 中的hexagon物体有六条线段。由于第一个点和最后一个点是相同的,这六条线段确实形成了一个多边形——一个六边形。我们可以使用transform()算法计算线段的长度:

std::vector<Point> hexagon {{1,2}, {2,1}, {3,1}, {4,2}, {3,3}, {2,3}, {1,2}};

std::vector<double> segments;                         // Stores lengths of segments

std::transform(std::begin(hexagon), std::end(hexagon) - 1, std::begin(hexagon) + 1, std::back_inserter(segments), [](const Point& p1, const Point& p2)

{ return std::sqrt(

(p1.first-p2.first)*(p1.first - p2.first) +   (p1.second - p2.second)*(p1.second - p2.second)); });

transform()的第一个输入范围包含从第一个到倒数第二个的hexagon中的Point个对象。第二个输入范围从第二个Point对象开始,因此二元函数的连续调用的参数将是点 1 和 2、点 2 和 3、点 3 和 4 等等,直到输入范围的最后两个点 6 和 7。图 7-4 显示了两点间距离的公式,x 1 ,y 1 和 x 2 ,y 2,以及作为transform()最后一个参数的 lambda 表达式实现了这一点。由 lambda 表达式计算的每个片段长度都存储在segments容器中。我们可以使用另外两种算法输出六边形的线段长度和总周长,如下所示:

std::cout << "Segment lengths: ";

std::copy(std::begin(segments), std::end(segments),                                            std::ostream_iterator<double> {std::cout, " "});

std::cout << std::endl;

std::cout << "Hexagon perimeter: "  << std::accumulate(std::begin(segments), std::end(segments), 0.0) << std::endl;

使用copy()算法输出线段长度。accumulate()函数将segments中元素的值相加,得出周长的总和。

替换范围中的元素

replace()算法用新值替换与给定值匹配的元素。前两个参数是要处理的范围的前向迭代器,第三个参数是要替换的值,第四个参数是新值。以下是它的工作原理:

std::deque<int> data {10, -5, 12, -6, 10, 8, -7, 10, 11};

std::replace(std::begin(data), std::end(data), 10, 99);    // Result: 99 -5 12 -6 99 8 -7 99 11

这里,data容器中所有匹配10的元素都被替换为99

当谓词返回true时,replace_if()算法用新值替换元素。第三个参数是谓词,第四个参数是新值。参数类型通常是对元素类型的const引用;const不是强制性的,但是谓词不应该修改参数。这里有一个使用replace_if()的例子:

string password {"This is a good choice!"};

std::replace_if(std::begin(password), std::end(password),

[](char ch){return std::isspace(ch);}, '_');  // Result: This_is_a_good_choice!

谓词为任何是空格字符的元素返回true,因此这些将被下划线替换。

replace_copy()算法做replace()做的事情,但是结果存储在另一个范围内,原始结果保持不变。前两个参数是输入范围的前向迭代器,第三个是输出范围的 begin 迭代器,最后两个是要替换的值和替换。这里有一个例子:

std::vector<string> words {"one", "none", "two", "three", "none", "four"};

std::vector<string> new_words;

std::replace_copy(std::begin(words), std::end(words), std::back_inserter(new_words),

string{"none"}, string{"0"});  // Result: "one", "0", "two", "three", "0", "four"

执行这段代码后,new_words将包含注释中所示的string元素。

有选择地替换一个范围内元素的最后一个算法是replace_copy_if(),它和replace_if()做的一样,但是结果存储在另一个范围内。前两个参数是输入范围的迭代器,第三个参数是输出的 begin 迭代器,最后两个分别是谓词和替换值。这里有一个例子:

std::deque<int> data {10, -5, 12, -6, 10, 8, -7, 10, 11};

std::vector<int> data_copy;

std::replace_copy_if(std::begin(data), std::end(data),

std::back_inserter(data_copy),

[](int value) {return value == 10;}, 99); // Result: 99 -5 12 -6 99 8 -7 99 11

data_copy容器是一个vector,只是为了说明输出容器可以不同于输入容器。作为执行这段代码的结果,它将包含注释中显示的元素。

应用算法

我将在本章中创建最后一个工作示例,它将一些算法应用于在标准输出流上绘制曲线。这样会更现实一点。曲线将由代表x,y个点的pair<double,double>个对象的范围来定义。我们可以首先定义一个plot()函数模板,它将在标准输出流上绘制一条曲线。模板类型参数将是定义范围的迭代器的类型,所以点可以来自任何序列容器,或者可能是一个数组。每个点将被标为星号,其中x轴穿过页面,y轴向下。因为这个输出是一个字符流,字体的纵横比会影响图形的纵横比。理想情况下,字体应该有相同的宽度和高度,我在我的系统上选择了 8×8 的字体。

plot()函数的参数将是定义曲线上的点的范围的迭代器、指定输出曲线名称的字符串以及图形宽度中的字符数。最后两个参数将有默认值,允许它们被省略。点中的x值的范围必须符合为绘图宽度指定的字符数。这将确定 x 在一个字符和下一个字符之间的步长。为了保持图形的纵横比,各行之间的y值之间的步长(沿页面向下)将与x值之间的步长相同。下面是plot()函数模板的代码:

template<typename Iterator>

void plot(Iterator begin_iter, Iterator end_iter, string name = "Curve", size_t n_x = 100)

{ // n_x is plot width in characters, so it’s the number of characters along the x axis

// Comparison functions for x and for y

auto x_comp = [](const Point& p1, const Point& p2) {return p1.first < p2.first; };

auto y_comp = [](const Point& p1, const Point& p2) {return p1.second < p2.second; };

// Minimum and maximum x values

auto min_x = std::min_element(begin_iter, end_iter, x_comp)->first;

auto max_x = std::max_element(begin_iter, end_iter, x_comp)->first;

// Step length for output - same step applies to x and y

double step {(max_x - min_x) / (n_x + 1)};

// Minimum and maximum y values

auto min_y = std::min_element(begin_iter, end_iter, y_comp)->second;

auto max_y = std::max_element(begin_iter, end_iter, y_comp)->second;

size_t nrows {1 + static_cast<size_t>(1 + (max_y - min_y)/step)};

std::vector<string> rows(nrows, string(n_x + 1, ' '));

// Create x-axis at y=0 if this is within range of points

if(max_y > 0.0 && min_y <= 0.0)

rows[static_cast<size_t>(max_y/step)] = string(n_x + 1, '-');

// Create y-axis at x=0 if this is within range of points

if(max_x > 0.0 && min_x <= 0.0)

{

size_t x_axis {static_cast<size_t>(-min_x/step)};

std::for_each(std::begin(rows), std::end(rows),

x_axis { row[x_axis] = row[x_axis] == '-' ? '+' : '|'; });

}

std::cout << "\n\n     " << name << ":\n\n";

// Generate the rows for output

auto y {max_y};                             // Upper y for current output row

for(auto& row : rows)

{

// Find points to be included in an output row

std::vector<Point> row_pts;               // Stores points for this row

std::copy_if(begin_iter, end_iter, std::back_inserter(row_pts),

&y, &step { return p.second < y + step && p.second >= y; });

std::for_each(std::begin(row_pts), std::end(row_pts),  // Set * for pts in the row

&row, min_x, step {row[static_cast<size_t>((p.first - min_x) / step)] = '*'; });

y -= step;

}

// Output the plot - which is all the rows.

std::copy(std::begin(rows), std::end(rows), std::ostream_iterator<string>{std::cout, "\n"});

std::cout << std::endl;

}

定义了两个λ表达式x_compy_comp,用于比较x和 y 值。这些在max_element()min_element()算法调用中使用,找到x值和y值的上限和下限。x 的限制用于确定在输出行中的字符之间水平应用的步长,以及在一个输出行和下一个输出行之间垂直应用的步长。输出中的行数由y值的范围和步长决定。输出中的每一行都将是一个string对象,因此完整的绘图将在rows容器中创建,该容器是string对象的vector

为了在rows中创建图,需要找到属于每一行的具有y值的点。这些点的y值是从某个当前y值到y+step值。copy_if()算法将输入范围中满足该条件的点复制到每行的row_pts容器中。row_pts中点的x值随后被用于传递给for_each()的函数中。对于每个点,该函数确定当前行中对应于该点的x值的字符的索引,并将其设置为星号。

该示例包括为特定类型的曲线创建点的两个函数。一种是在正弦曲线上创建点,计算起来相对简单;另一个是心形,稍微复杂一点,但却是一条有趣的曲线。正弦曲线很有趣,因为它们出现在很多场合。例如,音频信号可以被视为不同频率和振幅的正弦波的组合。示例中的函数将只计算由公式 y = sin(x)定义的曲线的点,但是您可以轻松地扩展它,以允许不同的频率和幅度。下面是该函数的代码:

// Generate x,y points on curve y = sin(x) for x values 0 to 4π

std::vector<Point> sine_curve(size_t n_pts = 100)

{ // n_pts is number of data points for the curve

std::vector<double> x_values(n_pts);

double value {};

double step {4 * pi / (n_pts - 1)};

std::generate(std::begin(x_values), std::end(x_values),

[&value, &step]() { double v {value};

value += step;

return v; });

std::vector<Point> curve_pts;

std::transform(std::begin(x_values), std::end(x_values), std::back_inserter(curve_pts),

[](double x) { return Point {x, sin(x)}; });

return curve_pts;

}

这些点作为vector容器中的Point元素返回,其中Point是类型pair<double,double>的别名。代码使用generate()算法产生从 0 到的 x 值。然后,transform()算法在返回的curve_pts容器中创建Point对象。

基于半径为r的圆的心形的笛卡尔方程为(x2+y2–r2)2= 4r2((x–r2)2+y2),这对于确定曲线上的点不是很有用。一个更有用的表示是参数形式,其中xy值是根据独立参数 t 定义的:

x = r(cos(t)-cos(2t))y = r(sin(t)-sin(2t))

通过将 t 从 0 变到 2π,我们可以获得心形上的点,对应于将一个半径为r的圆绕另一个半径相同的圆滚动。我们可以很容易地使用这些等式定义一个函数来生成点:

std::vector<Point> cardioid_curve(double r = 1.0, size_t n_pts = 100)

{ // n_pts is number of data points

double step = 2 * pi / (n_pts - 1);                 // Step length for x and y

double t_value {};                                  // Curve parameter

// Create parameter values that define the curve

std::vector<double> t_values(n_pts);

std::generate(std::begin(t_values), std::end(t_values),

[&t_value, step]() { auto value = t_value;

t_value += step;

return value; });

// Function to define an x,y point on the cardioid for a given t

auto cardioid = r

{ return Point {r*(2*cos(t) + cos(2*t)), r*(2*sin(t) + sin(2*t))}; };

// Create the points for the cardioid

std::vector<Point> curve_pts;

std::transform(std::begin(t_values), std::end(t_values), std::back_inserter(curve_pts),

cardioid);

return curve_pts;

}

这与正弦曲线的逻辑基本相同。generate()算法为自变量创建一系列值,在本例中,自变量是方程参数tcardioid lambda 表达式是独立定义的,因为这样更容易理解。它根据参数方程为给定的t创建一个Point对象。transform()算法将心形应用于输入范围的t值的vector,以在曲线上创建Point对象的vector

绘制正弦曲线和心形曲线的main()程序现在非常简单:

// Ex7_05.cpp

// Apply some algorithms to plotting curves

// To get the output with the correct aspect ratio, set the characters

// in the standard output stream destination to a square font, such as 8 x 8 pixels

#include <iostream>                                   // For standard streams

#include <iterator>                                   // For iterators and begin() and end()

#include <string>                                     // For string class

#include <vector>                                     // For vector container

#include <algorithm>                                  // For algorithms

#include <cmath>                                      // For sin(), cos()

using std::string;

using Point = std::pair<double, double>;

static const double pi {3.1415926};

// Definition of plot() function template here...

// Definition of sine_curve() function here...

// Definition of cardioid_curve() function here...

int main()

{

auto curve1 = sine_curve(50);

plot(std::begin(curve1), std::end(curve1), "Sine curve", 90);

auto curve2 = cardioid_curve(1.5, 120);

plot(std::begin(curve2), std::end(curve2), "Cardioid", 60);

}

静态变量pi在全局范围内定义,使其对程序中的所有代码都可用;生成曲线的两个函数都使用它。定义曲线的点数、x 值的范围和绘图宽度之间存在相互作用。字符图中隐含的离散化意味着曲线的某些部分可能看起来有点平坦或者起伏不平。我得到的输出如图 7-5 所示。

A978-1-4842-0004-9_7_Fig5_HTML.gif

图 7-5。

Output for Ex 7_05.cpp - plotting curves

摘要

这一章已经展示了 STL 提供的算法是多么丰富。除了自己编写一个循环之外,一个算法通常有不止一个选项来执行给定的操作。任何情况下的最终选择往往归结为个人偏好。通常,算法通常比显式编程循环更快,但是使用循环的代码有时更容易理解。然而,编写自己的循环更容易出错,所以最好尽可能使用算法。

作为参考,以下是您在本章中看到的算法的总结:

查找具有给定属性的范围中元素的数量

  • 如果p[beg,end)中的所有元素返回true,则all_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)返回true
  • 对于[beg,end)中的任意元素,如果p返回true,则any_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)返回true
  • 如果p[beg,end)中的所有元素返回false,则none_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)返回true
  • count(Input_Iter beg, Input_Iter end, const T& obj)返回[beg,end)中等于obj的元素个数。
  • count_if(Input_Iter beg, Input_Iter end, Unary_Predicate p)返回[beg,end)p返回true的元素个数。

比较范围

  • 如果范围[beg1,end1)中的元素等于范围开始beg2中的相应元素,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2)返回true
  • 如果范围[beg1,end1)中的元素等于范围[beg2,end2)中的相应元素,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2)返回true
  • 对于范围[beg1,end1)和范围开始beg2的对应元素,如果p返回true,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Binary_Predicate p)返回true
  • 对于范围[beg1,end1)[beg2,end2)中的对应元素,如果p返回true,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2, Binary_Predicate p)返回true
  • mismatch(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2)返回一个pair<Input_Iter1, Input_Iter2 >对象,包含指向第一对不相等元素的迭代器。
  • mismatch(Input_Iter1 beg1,Input_Iter1 end1,Input_Iter2 beg2,Input_Iter2 end2)返回与上一版本相同的结果。
  • mismatch(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Binary_Predicate p)返回一个pair<Input_Iter1,Input_Iter2 >对象,该对象包含指向第一对元素的迭代器,对于这些元素p返回false
  • mismatch(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2, Binary_Predicate p)返回与上一版本相同的结果。
  • 如果范围包含相同数量的元素并且对应的元素相等,则lexicographical_compare(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2)返回true;否则返回false
  • 如果范围包含相同数量的元素,并且 p 为所有对应的元素对返回 true,则lexicographical_compare(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2,                         Binary_Predicate p)返回true;否则返回false

置换一系列元素

  • next_permutation(Bi_Iter beg, Bi_Iter end)如果有下一个排列,将元素重新排列成升序字典序的下一个排列,并返回true。否则,元素被重新排列成序列中的第一个排列,算法返回false
  • next_permutation(Bi_Iter beg, Bi_Iter end, Compare compare)根据元素比较函数compare将元素重新排列成字典序中的下一个排列,并返回true。如果没有下一个排列,元素将根据compare重新排列成序列中的第一个排列,算法返回false
  • prev_permutation(Bi_Iter beg, Bi_Iter end)如果有前一个排列,将元素重新排列到前一个排列中,并返回true。否则,元素被重新排列成序列中的最后一个排列,算法返回false
  • next_permutation(Bi_Iter beg, Bi_Iter end, Compare compare)根据元素比较函数compare将元素重新排列成字典顺序中的前一个排列,并返回true。如果没有下一个排列,元素将根据compare重新排列成序列中的最后一个排列,算法返回false
  • 如果从beg2开始的范围中的(end1-beg1)元素是范围[beg1,end1)的排列,则is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2)返回true,否则返回false。使用==比较元素。
  • is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1,                Fwd_Iter2 beg2, Binary_Predicate p)与前一版本相同,除了使用p比较元素是否相等。
  • 如果范围[beg2,end2)是范围[beg1,end1)的置换,则is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1,                Fwd_Iter2 beg2, Fwd_Iter2 end2)返回true,否则返回false。使用==比较元素。
  • is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2, Fwd_Iter2 end2, Binary_Predicate p)除了使用p比较元素是否相等之外,与前一版本相同。

从范围中复制元素

  • copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)将范围[beg1, end1)复制到从beg2开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。
  • copy_n(Input_Iter beg1, Int_Type n, Output_Iter beg2)n元素从beg1开始的范围复制到beg2开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。
  • copy_if(Input_Iter beg1, Input_Iter end1,         Output_Iter beg2, Unary_Predicate p)p返回true的范围[beg1, end1)中的元素复制到从beg2开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。
  • copy_backward(Bi_Iter1 beg1, Bi_Iter1 end1, Bi_Iter2 end2)将范围[beg1, end1)复制到结束于end2的范围。该操作以相反的顺序复制元素,从end1-1指向的元素开始。该算法返回一个迭代器iter,它指向复制到目的地的最后一个元素,因此在操作之后,目的地范围将是[iter, end2)
  • reverse_copy(Bi_Iter beg1, Bi_Iter end1, Output_Iter beg2)将范围[beg1, end1)反向复制到从beg2开始的范围,并返回一个迭代器iter,该迭代器指向在目的地复制的最后一个元素之后的元素。因此[beg2, iter)将以相反的顺序包含来自[beg1, end1)的元素。
  • reverse(Bi_Iter beg, Bi_Iter end)反转范围[beg, end)中元素的顺序。
  • unique_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)将范围[beg1, end1)复制到从beg2开始的范围,忽略连续的重复。使用==对元素进行比较,算法返回一个迭代器,该迭代器指向目标中复制的最后一个元素之后的一个元素。
  • 除了使用p比较元素之外,unique_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2,             Binary_Predicate p)与前面的算法相同。
  • unique(Fwd_Iter beg, Fwd_Iter end)通过向左复制覆盖,从范围[beg, end)中删除连续的重复项。使用==比较元素,算法返回运算结果范围的结束迭代器。
  • 除了使用p比较元素之外,unique(Fwd_Iter beg, Fwd_Iter end, Binary_Predicate p)与前面的算法相同。

移动范围

  • move(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)将范围[beg1, end1)移动到从beg2开始的范围。该算法返回一个迭代器,该迭代器指向目标中移动的最后一个元素之后的一个元素。beg2一定不在[beg1, end1)内。
  • move_backward(Bi_Iter1 beg1, Bi_Iter1 end1, Bi_Iter 2 end2)将范围[beg1, end1)移动到结束于end2的范围,元素以相反的顺序移动。该算法返回一个迭代器,指向移动到目标的最后一个元素。end2不得在[beg1, end1)内。

旋转一系列元素

  • rotate(Fwd_Iter beg, Fwd_Iter new_beg, Fwd_Iter end)逆时针旋转[beg, end)中的元素,使new_beg成为范围中的第一个元素。该算法返回一个迭代器,该迭代器指向范围中最初的第一个元素。
  • rotate_copy(Fwd_Iter beg1, Fwd_Iter new_beg1, Fwd_Iter end1, Output_Iter beg2)[beg1, end1)中的所有元素复制到从beg2开始的范围中,这样new_beg1指向的元素就是目的地中的第一个元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。

从范围中删除元素

  • remove(Fwd_Iter beg, Fwd_Iter end, const T& obj)[beg, end)中删除等于obj的元素,并返回一个迭代器,指向结果范围中最后一个元素之后的一个元素。
  • remove_if(Fwd_Iter beg, Fwd_Iter end, Unary_Predicate p)[beg, end)中删除p返回 true 的元素。该算法返回一个迭代器,指向结果范围中最后一个元素之后的一个元素。
  • remove_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, const T& obj)将元素从[beg1, end1)复制到从beg2开始的范围,忽略等于obj的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。
  • remove_copy_if(Input_Iter beg1, Input_Iter end1,                Output_Iter beg2, Unary_Predicate p)将元素从[beg1, end1)复制到从beg2开始的范围,忽略p返回true的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。

替换范围中的元素

  • replace(Fwd_Iter beg, Fwd_Iter end, const T& obj, const T& new_obj)new_obj替换[beg, end)中等于obj的元素。
  • replace_if(Fwd_Iter beg, Fwd_Iter end,             Unary_Predicate p, const T& new_obj)new_obj替换[beg, end)p返回true的元素。
  • replace_copy(Input_Iter beg1, Input_Iter end1,              Output_Iter beg2, const T& obj, const T& new_obj)将元素从[beg1, end1)复制到从beg2开始的范围,用new_obj替换等于obj的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。范围不得重叠。
  • replace_copy_if(Input_Iter beg1, Input_Iter end1,                 Output_Iter beg2, Unary_Predicate p, const T& new_obj)[beg1, end1)中的元素复制到从beg2开始的范围,用new_obj替换p返回true的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。范围不得重叠。

修改范围内的元素

  • fill(Fwd_Iter beg, Fwd_Iter end, const T& obj)obj存储在[beg, end)范围内的每个元素中。
  • fill_n(Output_Iter beg, Int_Type n, const T& obj)obj存储在从beg开始的范围的前 n 个元素中。
  • generate(Fwd_Iter beg, Fwd_Iter end, Fun_Object gen_fun)存储gen_fun返回的值在[beg, end). gen_fun中的每个元素必须没有参数,并返回一个可以存储在范围内的值。
  • generate_n(Output_Iter beg, Int_Type n, Fun_Object gen_fun)gen_fun返回的n值存储在从beg开始的范围的第一个n元素中。该算法返回一个迭代器,该迭代器指向存储的最后一个值之后的值。
  • transform(Input_Iter beg1, Input_Iter end1,           Output_Iter beg2, Unary_Op op)op应用于范围[beg1, end1)中的每个元素,并将返回值存储在从beg2开始的范围的相应元素中。
  • transform(Input_Iter1 beg1, Input_Iter1 end1,           Input_Iter2 beg2, Output_Iter beg3, Binary_Op op)op应用于范围[beg1, end1)和从beg2开始的范围中的相应元素对,并将返回值存储在从beg3开始的范围的相应元素中。

交换算法

  • swap(T& obj1, T& obj2)交换值obj1obj2。该算法的第二个版本交换两个相同类型的数组,这意味着它们的长度相同。
  • iter_swap(Fwd_Iter iter1, Fwd_Iter iter2)交换iter1iter2指向的值。
  • swap_ranges(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2)在范围[beg1, end1)和从beg2开始的范围之间交换相应的元素。该算法返回一个迭代器,指向从beg2开始的范围中的最后一个元素。

恐怕你还不能高枕无忧——即使你对目前看到的算法感到满意。在接下来的章节中会有更多的内容。特别是,第十章将介绍专门针对数值计算的算法

Exercises

在所有这些练习中尽可能使用算法。

Write a program to read dates from the standard input stream as: month(string) day(integer) year(4-digit integer) Store the dates as tuple objects in a deque container. Find the number of different months that occur in the container and output it. Find the different month names and output them. Copy dates for each different month into separate containers. Output the dates for each month in descending order of days within years.   Read an arbitrary number of heights from the standard input stream as: feet (integer) inches(floating point) and store each height in a container as a pair object. Use the transform() algorithm to convert the heights to metric values stored as tuple objects (meters, centimetres, millimeters - all integer) in another container. Use the transform() algorithm to create pair objects in a third container that combine both corresponding height representations. Use an algorithm to output the elements from this container so that feet, inch and metric representations of each height are displayed.   Write a program that will:

  • 从标准输入流中读取包含标点符号的段落,并将单词作为元素存储在序列容器中。
  • 确定输入中重复单词的数量。
  • 将所有少于五个字符的单词集合在一个容器中并输出。

Write a program to read a phrase of an arbitrary number of words and use algorithms to assemble all different permutations in a container and output them. Test the program with phrases containing up to five words. (You can try more than five if you want but keep in mind that there are n! permutations of n words.)   Read an arbitrary number of names consisting of a first and a second name from the standard input stream. Store the names in a container of pair<string,string> elements. Extract the names that have a common initial letter for the second name into separate containers and output their contents. Reproduce the original set of names in another container of pair<char, string> objects where the first member of the pair is the initial, and the second member is the second name. Output the names in the new form in order of second name lengths.

八、生成随机数

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​8) contains supplementary material, which is available to authorized users.

产生随机数的需要经常出现。大多数游戏程序,模拟真实世界的程序,几乎总是需要生成随机数的能力。测试一个复杂的程序通常需要在某个时候随机输入,以验证该程序在不同的条件下都能正常工作,以编程方式生成这样的输入通常很方便。当然,随机数可以用来生成对象的随机选择,因此您有能力创建任何对象的随机选择。

除非另有说明,本章讨论的所有 STL 模板都在random头文件中定义。在random头中有很多内容,其中一些非常专业,我无法详细讨论。本章的目的是通过解释和演示我认为最有用的功能,让你开始使用 STL 的随机数生成工具。在本章中,您将学习:

  • 随机数的含义是什么。
  • 什么是随机数引擎,STL 提供了哪些引擎。
  • 在随机数生成的上下文中熵的含义是什么。
  • 什么是随机数生成器,以及生成器与引擎的关系。
  • 什么是随机数引擎适配器。
  • 如何生成非确定性随机序列?
  • 什么是发行版,STL 提供了哪些发行版。
  • 如何创建一系列元素的随机重排?

在这一章中有许多数学方程式,为一些算法的工作原理提供了精确的解释。如果数学不是你的强项,你可以忽略这些,而不会限制你理解如何应用所描述的能力。

什么是随机数?

随机性意味着不可预测性。真正的随机性只存在于自然界。闪电会击中哪里是随机的;如果有一场暴风雨正在酝酿,你可以相当肯定会有雷击,但你无法预测确切的位置——只是不要站在树下。英国的天气是随机的。每天都有对明天的预测,尽管用于预测的计算机功能强大,价格昂贵,但预测往往是错误的。

将一个数字描述为随机的需要一个上下文——一个先前的数字序列。2 本身并不是一个随机数,它只是 2。但是,如果序列 46,1011,874,34,998871 中的下一个是 2,那么它可能是一个随机数序列中的值。当一个数出现在一个序列中时,它是随机的,知道序列中前几个数并不能使你预测下一个数。当然,这并不意味着连续的数字必须不同。序列 4,4,4,4 可以是或者不是随机序列的一部分;连续掷骰子肯定会发生这种情况。这使得确定一个给定的序列是否是随机的变得非常困难,对于随机性有特殊的数学测试——我不会在这本书里深入讨论。

数字计算机是确定性的,这意味着对于给定的一组输入值,计算将总是产生完全相同的结果。因此,除非硬件出现严重问题,否则任何程序代码产生的结果都不可能是随机的。因此,计算机不能计算随机数序列;下一个数字总是由产生它的算法中使用的值决定。然而,数字计算机可以产生伪随机序列的数字。它们是序列,因为伪随机属性仅在序列的上下文中有意义;这些数字被称为伪随机,因为它们是由计算机产生的,因此不可能是真正随机的。话虽如此,我将放弃伪——在本章的其余部分只使用随机。

概率、分布和熵

生成随机数隐含地涉及到统计学中的一些概念,我将在这里简要解释。这一部分是在这里,以防你不熟悉这些想法。这一节应该足以使你理解本章的其余部分,即使这些概念对你来说可能是新的。但是要完全理解这些观点,你应该查阅统计学教程。

概率是什么?

概率是一个介于01之间的值,用于衡量事件发生的可能性。零(0)表示一个事件永远不会发生,1表示它是确定的。用未上膛的骰子掷出 6 的概率——或者实际上掷出任何可能值的概率——是1/6。掷出任意一个数字的概率是1

一般来说,事件发生的概率是它发生的次数除以它可能发生的次数。如果一个事件发生的概率是p,那么它不会发生的概率是1-p。这可以为生活中的期待提供有价值的指导。例如,在英国,从 49 个号码中选择 6 个号码的彩票中奖概率大约是 1400 万分之一;这意味着不中奖的概率大约是 13,999,999/14,000,000,这是你能得到的最接近确定性的概率。换个角度来看,你被闪电击中的可能性是中彩票的 10 倍以上。

什么是发行版?

分布描述了一个变量在一个范围内取特定值的可能性。分布可以是离散的或continuous:

  • 离散分布描述了假设一组固定值中的任何一个的变量的概率。根据定义,整数值的分布是离散分布。代表掷骰子结果的变量是离散分布的典型例子;它只能有从 1 到 6 的整数值。离散分布中可能值的概率总和总是 1。
  • 连续分布表示连续变量在一个范围内取特定值的概率。连续变量可以取某一范围内的任何值;一天中特定时间的温度就是一个例子。

代表某个范围内连续随机变量的值的概率的曲线称为概率密度函数(PDF)。具有给定值的变量的概率是 PDF 上对应于该值的点。假设从ab范围内的任何值的变量的概率,是在ab之间的 PDF 曲线下的面积。这意味着在 PDF 下ab之间的区域必须是 1,因为变量必须是范围内的一个可能值。如果是确定的事,概率是 1。

离散变量的等效概率密度函数称为离散概率函数。离散变量不同值的概率通常用一组点或竖线来表示。如我所说,概率的总和必须等于 1。

有许多不同的分布来模拟事件如何发生或测量在现实世界中如何变化。这些大多是用数学方程来描述的,用图形显示时最容易掌握。四种分布的示例如图 8-1 所示。

A978-1-4842-0004-9_8_Fig1_HTML.gif

图 8-1。

Examples of distributions

图 8-1 中的每个图表显示了一系列可能值出现的概率。横轴记录变量的值;纵轴是概率。现实世界中不同种类的变量可以有非常不同的分布。如图 8-1 所示的均匀分布,所有可能的值都是等概率的;掷骰子的结果用均匀分布来表示。正态分布表示在平均值两侧变化的值。灯泡的寿命很可能是正态分布,因为灯泡的工作时间通常会在平均值的两侧变化,通常旧的灯丝灯泡为 2000 小时,而最新的 LED 灯泡可能超过 15,000 小时,尽管后者在实践中并不总是得到证实。指数分布通常与事件随时间发生的方式有关:例如,放射性物质发射粒子的时间间隔。图 8-1 中的第四个例子是柯西分布。在插图中,它看起来模糊地类似于正态分布,但它不是相同的-曲线的形状可以有很大的不同。柯西分布在现实生活中出现的频率比这里展示的其他分布要低;一个背景是量子力学中不稳定状态的能量分布。有时,一个发行版有不同的名称;例如,正态分布也称为高斯分布。正如您将看到的,STL 支持更多的发行版本。

熵是什么?

熵是无序的一种度量。宇宙的热寂将是达到最大熵的时候,根据热力学第二定律这是不可避免的。不过现在还不会——你还有时间读完这本书。数据环境中熵的含义是由美国数学家克劳德·香农提出的。熵衡量信息表现的效率;这也是对数据混乱程度的一种衡量。使用无损方法压缩文件(如用于生成 ZIP 文件的方法)会增加熵。如果您将一个文件压缩到 ZIP 存档中,发现文件大小没有显著减少,这是因为原始数据具有非常高的熵——换句话说,它是非常随机的——因此无法更有效地表示。英语文本的熵很低,因为它不是很随机。因为它不是随机数据,所以包含这一章的文件的大小可以通过压缩成 ZIP 文件而显著减小。文件内容的随机性越小,数据的熵就越低,压缩的潜力就越大。

在随机数生成的上下文中,熵度量比特序列的随机性。最大熵意味着一个完全随机的序列,每个比特都可能是一个1,一个0;这种序列的信息量是最大的,因为信息不能用更短的序列来表示。最小熵意味着一个序列是完全可预测的;交替的10,或者像1010 1010 1010 ...1100 1101 1100 1101...这样的序列具有非常低的熵。这种序列的信息含量低,因为它是可预测的,并且信息可以容易地用短得多的序列来表示。当你生成随机数时,最大化序列的熵是可取的,尽管这必须与生成值所需的计算开销相平衡。

用 STL 生成随机数

STL 在随机数生成的上下文中使用了四个术语:

  • 随机数引擎是一个类模板,它定义了一种产生无符号整数序列的机制,无符号整数序列是随机位序列。STL 定义了三个表示随机数引擎的类模板。我将在本章后面简要介绍这些,但是除非你对它们使用的算法有深入的了解,否则你不会想直接使用它们。你将使用一个随机数发生器。
  • 随机数生成器是随机数引擎类模板的预定义实例。每个生成器将一组特定的模板参数应用于一个随机数引擎类模板——因此它是一个类型别名。STL 提供了几个预定义的随机数生成器,它们实现了众所周知的随机数生成算法。
  • 随机数引擎适配器是一个类模板,用于通过修改由另一个随机数引擎生成的序列来生成随机数序列。
  • 分布表示随机序列中的数字在整个范围内出现的概率。STL 定义了类模板,这些模板定义了各种不同发行版的函数对象,包括图 8-1 中所示的那些。

使用多个分布类模板生成随机数的原因是,您希望在给定上下文中生成的序列将取决于数据的性质。患者到达医院的模式可能与顾客到达商店的模式非常不同,因此将应用不同的分布。此外,商店顾客的模式将根据商店的种类及其位置等因素而变化,因此可能需要不同的分布来为不同商店的顾客到达建模。

有几种随机数引擎和生成器,因为没有一种算法可以生成适合所有情况的随机数。一些算法可以产生比其他算法更长的非重复序列;一些比另一些需要更少的计算开销。当您理解了要建模的数据的特征后,您就可以决定使用哪种分布和哪种随机序列生成功能了。

随机数生成中的种子

随机数生成算法总是从一个或多个种子开始,这些种子代表产生随机数的计算的初始输入。种子决定了随机数发生器的初始状态,并决定了整个序列。随机数发生器的状态由计算序列中下一个值所需的所有数据组成。算法是递归的,所以种子创建初始状态,用于产生序列中的第一个值;生成该值会改变状态,然后用于生成下一个值,依此类推。因此,对于给定的一个或多个种子,随机数发生器将总是产生相同的序列。这在程序测试过程中显然非常有用;至少可以说,当输入数据从一次运行到下一次运行可能任意改变时,确定程序是否正常工作是不容易的。当然,一旦程序被测试,您可能希望每次程序运行时从随机数生成器得到不同的序列。总是做同样事情的游戏程序不会有趣。为了在不同的时间产生不同的序列,你必须提供不同的种子——最好是随机值。这些值被称为不确定值,因为它们无法预测。

STL 中的所有随机数生成算法都可以用一个种子来启动。如果需要更多种子来定义初始状态,则会自动创建它们。显然,随机数序列的熵将取决于种子。种子中的位数很重要。对于 1 字节的种子,只有 255 个可能的值,因此最多只能产生 255 个不同的序列。为了最大化随机序列的熵,你需要两件事情:你需要一个真正随机的种子值——而不是伪随机的,你需要种子的可能值的范围很大。

获得随机种子

random_device类定义了函数对象,用于生成可以用作种子的随机无符号整数值。该类应该使用不确定的值源,这些值通常由操作系统提供。C++ 14 标准确实允许在不确定的来源不可用时使用随机数引擎,但在大多数实现中这是不必要的。不确定性来源可以是诸如连续键盘击键之间的时间、鼠标点击之间的间隔、当前时钟时间,或者通过测量一些物理属性。

您可以像这样创建一个random_device对象:

std::random_device rd;             // Source of seeds!

构造函数有一个类型为string&的参数,该参数有一个实现定义的默认值。当您省略它时,就像这里一样,您将获得您的环境的默认random_device对象。理论上,该参数允许您提供一个字符串来标识要使用的非确定性源,但是您需要查看您的文档来了解该选项是否适用于您的 C++ 库。下面是如何从random_device对象中创建一个种子值:

auto my_1st_seed = rd();

这用来自函数对象rd的初始值创建了my_1st_seed。这里有一个程序可以成功产生一系列种子:

// Ex8_01.cpp

// Generating a succession of 8 seeds

#include <random>                                // For random_device class

#include <iostream>                              // For standard streams

int main()

{

std::random_device rd;                         // A function object for generating seeds

for(size_t n {}; n < 8; ++n)

std::cout << rd() << " ";

std::cout << std::endl;

}

这只是调用了rd表示的函数八次,并输出它返回的值。我运行了两次,得到了下面两行输出:

3638171046 3646201712 2185708196 587272045 1669986587 2512598564 1822700048 3845359386

360481879 3886461477 1775290740 2501731026 161066623 1931751494 751233357 3821236773

您会注意到两次运行输出的值完全不同。除了operator()()random_device类只定义了另外三个函数成员。min()max()成员分别返回输出的最小和最大可能值。如果实现使用的是随机数引擎而不是不确定的源,则entropy()成员返回源的熵的估计值,类型为 double 或 0。

种子序列

seed_seq类是一个设置随机数生成器初始状态的帮助器。正如您将看到的,您可以创建一个随机数生成器,并通过向其构造函数传递一个种子值来设置其初始状态。构造函数参数也可以是一个seed_seq对象,它可以生成几个 32 位无符号值,这些值为生成器提供了比单个整数更多的熵。您也可以使用由一个seed_seq对象生成的值作为几个随机数生成器的种子。

seed_seq类不仅仅是一组值的简单容器。一个seed_seq对象基于您传递给构造函数的一组整数生成任意数量的无符号整数值。生成的值是通过应用预定义的算法产生的。您可以为seed_seq构造函数指定一个或多个整数,或者作为一个范围,或者作为一个初始化列表。生成的值将分布在 32 位无符号整数值的整个范围内,而不管输入值是如何分布的,也不管输入值有多少。对于相同的seed_seq构造函数参数,您总是得到相同的生成值序列。下面是一些说明创建一个seed_seq对象的各种方法的语句:

std::seed_seq seeds1;                                      // Default object

std::seed_seq seeds2 {2, 3, 4, 5};                         // Create from simple integers

std::vector<unsigned int> data {25, 36, 47, 58};           // A vector of integers

std::seed_seq seeds3 {std::begin(data), std::end(data)};    // Create from a range of integers

当然,您也可以使用由random_device对象返回的值作为seed_seq构造函数的参数:

std::random_device rd {};

std::seed_seq seeds4 {rd(), rd()};                 // Create from non-deterministic integers

这段代码每次执行时,seeds4对象都会生成不同的值。

通过将两个迭代器指定的范围传递给seed_seq对象的generate()函数成员,可以在容器中存储来自seed_seq对象的给定数量的值。例如:

std::vector<unsigned int> numbers (10);            // Stores 10 integers

seeds4.generate(std::begin(numbers), std::end(numbers));

调用seeds4generate()成员将生成的值存储在numbers数组中。通过一个工作示例,我们可以看到seed_seq对象在各种条件下生成的值:

// Ex8_02

// Values generated by seed_seq objects

#include <random>                                  // For seed_seq, random_device

#include <iostream>                                // For standard streams

#include <iterator>                                // For iterators

#include <string>                                  // For string class

using std::string;

// Generates and list integers from a seed_seq object

void gen_and_list(const std::seed_seq& ss, const string title = "Values:", size_t count = 8)

{

std::vector<unsigned int> values(count);

ss.generate(std::begin(values), std::end(values));

std::cout << title << std::endl;

std::copy(std::begin(values), std::end(values),

std::ostream_iterator<unsigned int>{std::cout, " "});

std::cout << std::endl;

}

int main()

{

std::random_device rd {};                           // Non-deterministic source - we hope!

std::seed_seq seeds1;                             // Default constructor

std::seed_seq seeds2 {3, 4, 5};                        // From consecutive integers

std::seed_seq seeds3 {rd(), rd()};

std::vector<unsigned int> data {25, 36, 47, 58};

std::seed_seq seeds4(std::begin(data), std::end(data));  // From a range

gen_and_list(seeds1, "seeds1");

gen_and_list(seeds1, "seeds1 again");

gen_and_list(seeds1, "seeds1 again", 12);

gen_and_list(seeds2, "seeds2");

gen_and_list(seeds3, "seeds3");

gen_and_list(seeds3, "seeds3 again");

gen_and_list(seeds4, "seeds4");

gen_and_list(seeds4, "seeds4 again");

gen_and_list(seeds4, "seeds4 yet again", 12);

gen_and_list(seeds4, "seeds4 for the last time", 6);

}

gen_and_list()是一个助手函数,它从一个seed_seq对象生成给定数量的值,并在一个标识标题后输出它们。它在main()中被用来显示从以各种方式创建的seed_seq对象中生成的值。我得到了下面的输出,但是您的输出至少在某些方面会有所不同:

seeds1

3071959997 669715714 1197567577 671623915 1173633267 2920800313 1209690436 2235109613

seeds1 again

3071959997 669715714 1197567577 671623915 1173633267 2920800313 1209690436 2235109613

seeds1 again

3527767669 372316564 1386412362 441784 2145070594 2276674640 2205342996 1276311706 1119507491 75413245 2656280031 1908737279

seeds2

3388710944 2239790942 3836628790 2213304795 3411013659 2658117409 3275085354 3542843550

seeds3

3899021117 3310665364 4171568438 3922561248 250650243 1402466647 3483637440 3437969619

seeds3 again

3899021117 3310665364 4171568438 3922561248 250650243 1402466647 3483637440 3437969619

seeds4

2664408363 1749470183 3260020574 1632320446 534203587 2689329558 3154702548 1526239767

seeds4 again

2664408363 1749470183 3260020574 1632320446 534203587 2689329558 3154702548 1526239767

seeds4 yet again

2165145204 3274376652 3408995137 1909945219 3899048536 1143678586 807504975 3977354488 3428929103 552995692 24106733 509227013

seeds4 for the last time

1443036549 3195987434 1624705198 3337303804 479673966 3579734797

输出显示了关于seed_seq对象生成的值的一些事情:

  • 不管如何创建seed_seq对象,都会生成各种各样的 32 位整数。即使是默认构造函数创建的对象,也会在整个范围内生成值。
  • generate()成员产生尽可能多的不同值来填充您指定的范围。
  • 你想打多少次generate()都可以。
  • generate()成员产生的序列中的值取决于序列的长度。给定长度的序列是相同的。不同长度的序列将包含不同的值。

如果您执行该程序两次,您将看到对于seed_seq构造函数的相同参数值,生成的值是相同的。如果提供了不同的构造函数参数,序列只能在不同的运行中有所不同,就像由rd函数对象返回的值一样。

还有另外两个seed_seq类的函数成员。成员返回用于创建对象的种子值的数量。param()成员提供原始种子值;它需要一个输出迭代器,将值的目的地标识为参数,并且没有返回值。param()成员将您提供给构造函数的原始种子值存储在以迭代器参数开始的范围内。下面的代码片段说明了这两个函数成员是如何工作的:

std::seed_seq seeds {3, 4, 5};

std::vector<unsigned int> data(seeds.size());                 // Element for each seed value

seeds.param(std::begin(data));                                // Stores 3 4 5 in data

这会创建一个vector,其元素数量由seeds对象的size()成员返回的值决定。然后,seedsparam()成员将传递给构造函数的三个值存储在data中。您还可以将这些值追加到容器中,如下所示:

seeds.param(std::back_inserter(data));                        // Appends 3 4 5 to data

当然,您不需要存储这些值——您可以将一个输出流迭代器作为参数传递给param():

seeds.param(std::ostream_iterator<unsigned int>{std::cout, " "});  // 3 4 5 to cout

分发类别

STL 中的发行版是一个函数对象:代表特定发行版的类的实例。要生成具有给定分布的随机数,可以将该分布应用于随机数生成器生成的数字。这是通过将随机数生成器对象作为参数传递给作为分布的函数对象来实现的;函数对象将返回一个符合分布的随机值。在我详细讨论随机数发生器之前,我就介绍分布,这似乎有点奇怪。这有几个原因:

  • 您应该始终使用分发对象来获取随机数。分发对象将随机数生成器生成的序列限制并形成应用程序上下文所需的形式。选择和使用适当的分布对象可以保证随机值在您想要的分布范围内。将您自己的算法直接应用于来自随机数生成器的值不太可能产生具有适当特征的分布。
  • 要完全理解随机数生成器之间的差异,您需要了解它们实现的算法的数学知识。大多数程序员不需要也不想涉足这个领域。STL 提供了一个默认的随机数生成器,当与一个合适的分布对象结合使用时,它将满足许多(如果不是大多数)程序员的需求。

我将在这一节介绍默认的随机数生成器,这样我们就可以有知识地将它用于分布对象,然后讨论 STL 提供的分布类型。在我讨论了 STL 提供的分布类型之后,我将解释其他的随机数生成器。

有 21 个用于发行版的类模板,其中许多非常专业。我将详细讨论那些我认为可能是最有趣的,并且我将展示你如何使用它们。我只概述其余的,如果需要的话,让您更深入地研究它们。如果你只是想生成随机序列,而不考虑所有的可能性,掌握本章的这一节可能就是你所需要的。但是首先,为了有效地使用发行版,我们需要一个随机数生成器,这是下一个主题。

默认的随机数生成器

默认的随机数生成器是由std::default_random_engine类型别名定义的随机无符号整数的通用来源。这个别名所代表的是实现定义的;您应该查阅您的库的文档,了解它所提供的详细信息。它通常是我将在本章后面介绍的三个随机数引擎类模板之一的实例。模板类型的参数将被选择来为临时用户提供满意的序列。下面是创建类型为default_random_engine的生成器的最简单方法:

std::default_random_engine rng1;         // Create random number generator with default seed

这将调用默认构造函数,因此将使用默认种子值设置初始状态。由于种子保持不变,当随机数序列在不同的场合执行时,它将始终与来自rng1生成器的随机数序列相同。你当然可以提供自己的种子:

std::default_random_engine rng2 {10};    // Create rng with 10 as seed

这将创建以10为种子的rng2,因此来自该生成器的随机序列将不同于rng1,但仍然是固定的。如果希望每次代码执行时获得不同的序列,可以提供一个不确定的种子:

std::random_device rd;                   // Non-determinstic seed source

std::default_random_engine rng3 {rd()};   // Create random number generator

使用类型为random_device的函数对象rd获得种子值。每个rd()调用将返回一个不同的值,如果你的random_device的实现是不确定的,连续调用rd()产生的序列对于程序的每次执行将是不同的。

另一种选择是提供一个seed_seq对象作为default_random_engine构造函数的参数:

std::seed_seq sd_seq {2, 4, 6, 8};

std::default_random_engine rng4 {sd_seq};  // Same random number sequence each time

std::random_device rd;                   // Non-determinstic seed source

std::default_random_engine rng5 {std::seed_seq{rd(), rd(), rd()}};

这首先使用从固定初始种子创建的seed_seq对象生成rng4生成器;来自rng4的序列在每次代码执行时都是相同的。rng5由一个seed_seq对象构成,该对象的值由一个random_device函数对象产生,所以你永远不知道序列会是什么——每次都是一个惊喜。我们现在可以研究如何创建和使用分布对象,并开始一些严肃的随机活动。

创建分发对象

如前所述,分布由 function 对象表示,它需要一个随机数生成器对象作为参数来生成分布中的值。每次执行 distribution 对象时,它都会返回它所代表的分布中的一个随机数,该随机数是从随机数生成器中获得的值生成的。分布返回的第一个值之后的后续值取决于前一个值。创建分布对象将需要一组参数值,这些参数值将取决于分布的类型。例如,均匀分布需要生成的值的上限和下限,而正态分布需要平均值和标准偏差的值。尽管不同类型的发行版之间有很大的差异,但它们确实有很多共同点。所有分发对象都有以下public成员:

  • result_type是在类内为生成的值的类型定义的类型别名。
  • min()是一个函数成员,返回分布对象可以生成的最小值。
  • max()是一个函数成员,返回一个分布对象可以生成的最大值。
  • reset()是一个函数成员,它应该将分布对象重置为其原始状态,这样返回的下一个值就不会依赖于上一个值。这是否发生取决于您的实现。如果分布返回的值是独立的,reset()什么也不做。
  • param_type是在类中为struct定义的类型别名。不同的分布将需要不同的参数值集合,可能是不同的类型,这些值存储在特定于该分布的param_type类型的struct中。
  • param()是一个函数成员,它接受类型为param_type的参数,将分布对象的参数重置为新值。
  • param()是上一个成员的重载,该成员没有参数,返回分布对象包含的param_type对象。
  • 默认构造函数,它具有定义分布的参数的默认值。
  • 接受类型为param_type的参数来定义分布的构造函数。

我将展示这些成员如何用于某些类型的分发。流插入和提取操作符<<>>为分布类型重载,以将分布对象的内部状态传输到流,或者从流中读回状态。这提供了从程序的先前执行中恢复发行状态的可能性。

均匀分布

在均匀分布中,一个范围内的所有值都有相同的可能性。均匀分布可以是离散的或连续的,如图 8-2 所示。

A978-1-4842-0004-9_8_Fig2_HTML.gif

图 8-2。

Uniform distributions

注意图 8-2 中的范围规格。离散均匀分布包括上限和下限;连续均匀不包括上界,所以变量永远不可能是上界的值。

离散均匀分布

uniform_int_distribution类模板定义了分布对象,这些对象将返回在一个封闭范围内均匀分布的随机整数[a,b]。模板参数是要生成的整数类型,默认类型是int:在类中定义的类型别名result_type对应于分布生成的值的类型。只有整数类型可以作为模板类型参数。一个构造函数有两个参数,用于标识范围的上限和下限;下限值的默认值为0,,上限值的默认值为所生成值类型的最大值。这里有一个例子:

std::uniform_int_distribution<> d;    // Distribution over 0 to  max for type int, inclusive

std::cout << "Range from 0 to "

<< std::numeric_limits<std::uniform_int_distribution<>::result_type>::max()

<< std::endl;               // Range from 0 to 2147483647

第一条语句调用默认构造函数来创建分布对象d。一切都是默认的,所以生成的值的类型将是int,范围将从0到类型int的最大值。最后一个注释显示了我得到的范围限制的输出;上限是int类型的最大值,由limits标题中定义的numeric_limits()功能模板产生。有一种更简单的方法来掌握范围限制。您可以调用所有分发对象都拥有的min()max()成员:

std::cout << "Range from " << d.min() << " to " << d.max() << std::endl;

这种情况下还有另一种可能。uniform_int_distribution类模板还定义了函数成员a()b(),它们分别返回范围的下限和上限,因此您可以将前面的语句写成:

std::cout << "Range from "<< d.a() << " to " << d.b() << std::endl;

成员a()b()的名字比min()max()更好地表明了它们返回的值与均匀分布的关系。

如果您只想得到大于或等于给定值的整数范围的分布,只需提供第一个构造函数参数:

std::uniform_int_distribution<> d {500};

std::cout << "Range from "<< d.a() << " to " << d.b()

<< std::endl;                            // Range from 500 to 2147483647

当然,构造函数参数可以是负的。通常,您会希望指定两个范围限制,因此这里有一个更现实的示例:

std::uniform_int_distribution<long> dist {-5L, 5L};

std::random_device rd;                               // Non-deterministic seed source

std::default_random_engine rng {rd()};               // Create random number generator

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(2) << dist(rng) << " ";     // -3  0  5  1 -2 -4  0  4

第一条语句为类型为long的随机整数定义了一个分布对象。范围是从-5+5,所以分布对象可以返回 11 个可能的值。因此,每个可能值出现的概率是1/11。一般来说,对于在[a,b]范围内的整数的均匀分布,任何特定值被返回的概率是1/(1+b-a)。在我的系统上,通过执行这段代码,我得到了附加在最后一条语句后面的注释中显示的输出,但是在您的系统上肯定会有所不同。

您可以调用均匀分布的param()成员来更改它生成的值的范围。传递给这里的param()成员Uniform distribution:uniform_int_distribution类的对象指定了新的范围限制——其类型由分布类定义的param_type别名指定。您也可以不带参数地调用param()来获得一个封装了当前分布参数集的对象。以下代码说明了这两种可能性:

std::uniform_int_distribution<> dist {0, 6};

std::random_device rd;                               // Non-determinstic seed source

std::default_random_engine rng {rd()};               // Create random number generator

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(3) << dist(rng) << " ";     // first output line

std::cout << std::endl;

// Save old range and set new range

auto old_range = dist.param();                       // Get current params

dist.param(std::uniform_int_distribution<>::param_type {-10,20});

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(3) << dist(rng) << " ";     // Second output line

std::cout << std::endl;

// Restore old range...

dist.param(old_range);

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(3) << dist(rng) << " ";     // Third output line

std::cout << std::endl;

这段代码在我的系统上产生以下输出:

6   1   5   6   1   3   6   2

19  16  15   5   0   7   6  -8

0   0   0   3   2   6   6   5

您可以通过定义别名将参数的类型简化为param():

using Range = std::uniform_int_distribution<>::param_type;

现在您可以编写param()调用来设置一个新的范围,如下所示:

dist.param(Range {-10,20});

您可以通过多种方式使用该功能来更改范围限制。一个明显的应用是当你需要一个程序中给定类型的值的几个均匀分布,每个分布有一组不同的参数。您可以只使用一个分布对象,并根据需要在程序中的任何地方设置参数。让我们来举一个例子。

应用均匀整数分布

这个例子,Ex8_03.cpp,将使用一个uniform_int_distribution对象来发牌。代码挺多的,我就一点一点介绍吧。为了允许在代码的不同位置使用单个分布对象,我们可以将其定义为函数中的静态对象,如下所示:

std::uniform_int_distribution<size_t>& dist()

{

static std::uniform_int_distribution<size_t> d;

return d;

}

该分布将返回类型为size_t的值,这些值将是无符号整数。默认限制最初适用,但是我们可以通过调用对象的param()来设置限制。要使用分布对象,只需调用dist()函数来获得对它的引用。我们可以用同样的方式封装一个随机数生成器:

std::default_random_engine& rng()

{

static std::default_random_engine engine {std::random_device()()};

return engine;

}

静态的engine对象是用一个random_device对象返回的值初始化的,所以它每次创建时都会产生一个不同的序列。表达式dist(rng())将返回分布中的一个随机数。

我将用封装花色和面值的pair对象来表示一张牌。我将通过enum类型定义可能的套装和面值:

enum class Suit : size_t { Clubs, Diamonds, Hearts, Spades };

enum class Face : size_t { Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace };

枚举器的默认值从 0 开始,这很重要,因为我们将使用枚举器值来索引一个容器,以获得一个代表SuitFace实例名称的string

我们可以为一张牌、一手牌、一副牌和一组牌的类型定义别名:

using Card = std::pair<Suit,Face>;                       // The type of a card

using Hand = std::list<Card>;                            // Type for a hand of cards

using Deck = std::list<Card>;                            // Type for a deck of cards

using Hands = std::vector<Hand>;                         // Type for a container of hands

using Range = std::uniform_int_distribution<size_t>::param_type;

将一副牌和一手牌定义为list容器将允许快速移除随机牌。您可以使用不同的序列容器,比如vector

我们将希望输出Card对象,因此实现operator<<()Card对象写入流将会很有用。下面是为Card对象重载<<的函数的定义:

std::ostream& operator<<(std::ostream& out, const Card& card)

{

static std::array<string, 4> suits {"C", "D", "H", "S"};             // Suit names

static std::array<string, 13> values {"2", "3", "4", "5", "6", "7",  // Face value names

"8", "9", "10", "J", "Q", "K", "A"};

string suit {suits[static_cast<size_t>(card.first)]};

string value {values[static_cast<size_t>(card.second)]};

return out << std::setw(2) << value << suit;

}

下面是将Deck容器初始化为标准的 52 张卡的函数定义:

Deck& init_deck(Deck& deck)

{

static std::array<Suit,4> suits{Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades};

static std::array<Face, 13> values {Face::Two, Face::Three, Face::Four, Face::Five, Face::Six,

Face::Seven, Face::Eight, Face::Nine, Face::Ten,

Face::Jack,  Face::Queen, Face::King, Face::Ace};

deck.clear();

for(const auto& suit : suits)

for(const auto& value : values)

deck.emplace_back(Card {suit, value});

return deck;

}

代表不同花色的所有对象都存储在suits容器中。代表可能的牌面值的对象是values容器中的元素。两个容器都是array类型,几乎和使用标准 C++ 数组一样高效。主要的优点是array容器总是知道它的大小;当您使用at()成员时,它还提供对超出范围的索引值的检查。嵌套循环在deck中放置元素,这些元素是代表每种花色的所有值的Card对象。

卡片在初始化的Deck对象中按顺序排列。我们需要向手牌发牌,但我们希望手牌随机接收牌。我们可以在发牌前洗牌,但为了实行不同限额的分配,我们将从牌堆中随机选择发牌。这是一个函数:

void deal(Hands& hands, Deck& deck)

{

auto d = dist();

while(!deck.empty())

{

for(auto&& hand : hands)

{

size_t max_index = deck.size() - 1;

d.param(Range{0, max_index});

auto iter = std::begin(deck);

std::advance(iter, d(rng()));

hand.push_back(*iter);

deck.erase(iter);

}

}

}

这将处理作为第二个参数传递的Deck对象中的所有牌。卡片分布在Hands容器中的Hand对象之间,这是第一个参数。外部循环继续,直到deck耗尽,这在其empty()成员返回true时指示。内部循环在hands中给每手牌发一张牌,因此最终deck中的所有Card对象将在hands中的Hand对象之间分配。

在循环中,max_index被初始化为deck中元素的最大合法索引值。由dist()返回的分布对象产生的值的限制被设置为0max_index,这将导致分布对象在每次循环迭代中从不同的范围产生值。iter被初始化为deck中元素范围的 begin 迭代器,然后前进表达式d(rng())的值,这将是从0max_index的随机增量。将*iter指定的元素添加到当前hand后,该元素将从deck容器中删除。

一手牌中的牌将是随机的,因为它们是随机选择的,但是一旦发牌,如果牌是按升序排列的,一手牌就更容易评估。该功能将在Hands容器中对每手牌进行分类:

void sort_hands(Hands& hands)

{

for(auto&& hand : hands)

hand.sort([](const auto& crd1, const auto& crd2) { return crd1.first < crd2.first ||

(crd1.first == crd2.first && crd1.second < crd2.second); });

}

循环遍历hands中的Hand对象。每个Hand容器中的元素通过调用容器对象的sort()成员进行排序。该参数是一个由通用 lambda 表达式定义的比较函数,该表达式根据套装中的面值对Card对象进行排序。

我们肯定希望在发牌后输出这些牌。下面的函数将会解决这个问题:

void show_hands(const Hands& hands)

{

for(auto&& hand : hands)

{

std::copy(std::begin(hand), std::end(hand), std::ostream_iterator<Card> {std::cout, " "});

std::cout << std::endl;

}

}

它使用copy()算法将手从hands容器复制到一个输出流迭代器,该迭代器写入cout。流迭代器将使用我们之前定义的operator<<()函数将每个Card对象写入输出流。每当我们想举手表决时,我们只需叫一下show_hands()

我们现在可以将这些函数放在一个完整的示例中,该示例将创建一副标准牌,发四手牌,然后输出已发的牌:

// Ex8_03.cpp

// Dealing cards at random with a distribution

#include <iostream>                          // For standard streams

#include <ostream>                           // For ostream stream

#include <iomanip>                           // For stream manipulators

#include <iterator>                          // For iterators and begin() and end()

#include <random>                            // For random number generators & distributions

#include <utility>                           // For pair<T1,T2> template

#include <vector>                            // For vector<T> container

#include <list>                              // For list<T> container

#include <array>                             // For array<T,N> container

#include <string>                            // For string class

#include <type_traits>                       // For is_same predicate

using std::string;

enum class Suit : size_t {Clubs, Diamonds, Hearts, Spades};

enum class Face : size_t {Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace};

using Card = std::pair<Suit,Face>;           // The type of a card

using Hand = std::list<Card>;                // Type for a hand of cards

using Deck = std::list<Card>;                // Type for a deck of cards

using Hands = std::vector<Hand>;             // Type for a container of hands

using Range = std::uniform_int_distribution<size_t>::param_type;

// Definition of operator<<() for a Card object goes here...

// Definition of rng() to return a reference to a static default_random_engine object goes here...

// Definition of dist() for a reference to a static uniform_int_distribution<size_t> object here...

// Definition of init_deck() to initialize a deck to a full set of 52 cards here...

// Definition of deal() that deals a complete deck here...

// Definition of sort_hands() to sort cards in hands here...

// Definition of show_hands to output all hands here...

int main()

{

// Create the deck

Deck deck;

init_deck(deck);

// Create and deal the hands

Hands hands(4);

deal(hands, deck);

// Sort and show the hands

sort_hands(hands);

show_hands(hands);

}

我得到了下面的举手表决:

3C  9C 10C  QC  AC  2D  3D  9D  QD  2H  6H  JS  QS

2C  4C  6D  8D  JD  KD  3H  8H  9H  KH  9S 10S  KS

5C  6C  8C  5D  AD  5H 10H  JH  QH  3S  4S  7S  AS

7C  JC  KC  4D  7D 10D  4H  7H  AH  2S  5S  6S  8S

当然,利用这个例子进行分配的另一种可能性是玩家现在玩,每个玩家在一轮中从他们的手中随机打出一张牌。我不会告诉你怎么做。就在本章末尾Exercise 2让你去实现!

连续均匀分布

uniform_real_distribution类模板定义了一个连续分布,它返回默认情况下类型为double的浮点值。您可以创建一个分布对象,它将返回范围0, 10)内的值,如下所示:

std::uniform_real_distribution<> values {0.0, 10.0};

std::random_device rd;                            // Non-determinstic seed source

std::default_random_engine rng {rd()};            // Create random number generator

for(size_t i {}; i < 8; ++i)

std::cout << std::fixed << std::setprecision(2)

<< values(rng) << " ";                // 8.37 6.72 6.41 6.08 6.89 6.10 9.75 4.07

您创建和使用uniform_real_distribution功能对象的方式与您看到的uniform_int_distribution对象非常相似。您将一个随机数生成器对象作为参数传递给分布函数对象,以获得一个随机值。您可以通过调用对象的param()成员来获取和设置范围限制。除了返回分布范围限制的min()max()成员之外,uniform_real_distribution对象还有a()b()成员。请注意,连续分布的范围是半开放的,上限被排除在分布对象可以返回的可能值的范围之外。

在现实世界中,均匀连续分布适用于一个变量的情况很少见。例如,与天气相关的参数的测量在一个范围内不具有同样可能的值。一个可能的真实例子是,当你看手表时,手表上秒针的角度位置很可能是均匀分布的——但这并不是特别有用。均匀连续分布用于蒙特卡洛方法,该方法在金融行业以及工程和科学领域都有应用。我将在另一个上下文中给出一个工作示例——一个使用连续均匀分布来获得π值的程序。

使用连续均匀分布

你知道你可以用一根棍子确定π的值吗?这并不包括用棍子威胁数学家来说服他们告诉你数值。只需要把棍子扔到地板上。它甚至不一定是一根棍子——你可以扔任何直的物体——一支铅笔甚至一根法兰克福香肠都可以。不过,它必须是一块有地板的裸地,而且你扔的物体是直的,其长度必须小于地板的宽度。这个过程很简单——你只需要数你扔了多少次棍子,以及棍子落地时越过木板边缘多少次。

你需要扔棍子很多次才能得到一个不错的结果。当然,这可能需要一些时间,而且有些乏味,更不用说很累了。不过,在几个均匀实数分布的帮助下,我们可以让计算机来做投掷和计数。图 [8-3 显示了地板上任意位置的棍子及其与地板的关系。解释发生了什么需要一点数学,但这并不困难。

A978-1-4842-0004-9_8_Fig3_HTML.gif

图 8-3。

A stick lying on the floor

图 8-3 显示了位于任意位置的杆,该位置距离板的底部边缘y并且与板成角度theta。棍子总会落在一块或另一块板子上,所以我们只需要考虑一块板子。图 8-3 显示了棍子与木板边缘相交的条件。当由p1p2表示的杆的任一端在板的边缘上或越过板的边缘时,就会出现这种情况。为了使棍子穿过纸板边缘,从棍子中心到最近边缘的距离必须小于L*sin(theta)/2

我们投掷木棒很多次,然后计算投掷的总次数为throws,木棒与棋盘边缘重叠的次数为hits。那么现在我们有了这两个计数,我们如何从它们得到π的值呢?图 8-4 应该有帮助。

A978-1-4842-0004-9_8_Fig4_HTML.gif

图 8-4。

Determining π from the probability of the stick crossing an edge of a floorboard

木棒穿过地板边缘的概率P将是hitsthrows的比值。因此,我们可以使用P和图 8-4 中的最后一个等式得到π的值。π是以下表达式的结果:

2*stick_length*throws/(board_width*hits)

现在我们知道了它是如何工作的,下面是实现它的代码:

// Ex8_04.cpp

// Finding pi by throwing a stick

#include <iostream>                                  // For standard streams

#include <random>                                    // For distributions, random number gen

#include <cmath>                                     // For sin() function

int main()

{

const double pi = 3.1415962;

double stick_length{};                             // Stick length

double board_width {};                             // Board width

std::cout << "Enter the width of a floorboard: ";

std::cin >> board_width;

std::cout << "Enter the length of the stick (must be less than " << board_width << "): ";

std::cin >> stick_length;

if(board_width < stick_length) stick_length = 0.9*board_width;

std::uniform_real_distribution<> angle {0.0, pi};   // Distribution for angle of stick

// Distribution for stick center position, relative to board edge

std::uniform_real_distribution<> position {0.0, board_width};

std::random_device rd;                                // Non-deterministic seed source

std::default_random_engine rng {rd()};                 // Create random number generator

const size_t throws{5'000'000};                     // Number of random throws

size_t hits {};                                     // Count of stick intersecting the board

// Throw the stick down throws times

for(size_t i {}; i < throws; ++i)

{

double y {position(rng)};

double theta {angle(rng)};

// Check if the stick crosses the edge of a board

if(((y + stick_length*sin(theta)/2) >= board_width) ||                                                    ((y - stick_length*sin(theta) / 2) <= 0))

++hits;                                                 // It does, so increment count

}

std::cout << "Probability of the stick crossing the edge of a board is: "

<< (static_cast<double>(hits)/ throws) << std::endl;

std::cout << "Pi is: " << (2* stick_length*throws)/(board_width*hits) << std::endl;

}

您可能已经发现了这个程序中的一个小弱点——要确定π的值,您需要在开始之前知道π的值。然而,这只是一个模拟-和一个使用均匀真实分布的借口。唯一的选择是真的扔棍子 5,000,000 次,如果你非常健康并且有一根非常耐用的棍子,这可能是一个选择。如果你能做到每 3 秒扔一次,你应该在大约 8 个月内完成——只要你不需要吃饭或睡觉...

木棒中心相对于地板边缘的随机位置由position分布对象产生,木棒在每个位置的角度由angle分布产生。循环体实现图 8-3 中的计算,循环后的代码只是使用图 8-4 中的方程。您可以将投掷次数更改为系统中任何合理的值。我得到了以下输出:

Enter the width of a floorboard: 12

Enter the length of the stick (must be less than 12): 5

Probability of the stick crossing the edge of a board is: 0.265281

Pi is: 3.14132

当然,输出可能在运行之间有所不同,因为每次程序执行时随机序列都是不同的。球杆相对于地板宽度的长度也有影响,投掷次数也是如此。

Note

对于那些需要知道的人来说,显示图 8-4 中曲线下的阴影面积只是棍子的长度需要一点微积分——面积将:

$$ area=\frac{L}{2}\kern0.5em \underset{0}{\overset{p}{{\displaystyle \int }}} \sin q $$

sin θ的积分是$$ \left(- \cos q\right) $$所以面积是$$ \frac{L}{2}\left(\left(- \cos p\right)-\left(- \cos 0\right)\right) $$,计算得到$$ \frac{L}{2}\left(1+1\right) $$,简单来说就是L

创建标准的均匀分布

标准的均匀分布是在[0,1)范围内的连续分布。generate_canonical()函数模板用给定数量的随机位在[0,1)范围内提供了浮点值的标准均匀分布。有三个模板参数:浮点类型、尾数中的随机位数和使用的随机数生成器的类型。该函数的参数是随机数生成器,因此推导出最后一个模板参数。下面是它的使用方法:

std::vector<double> data(8);                         // Container with 8 elements

std::random_device rd;                               // Non-determinstic seed source

std::default_random_engine rng {rd()};               // Create random number generator

std::generate(std::begin(data), std::end(data),

[&rng] { return std::generate_canonical<double, 12>(rng); });

std::copy(std::begin(data), std::end(data), std::ostream_iterator<double>{std::cout, " "});

在 lambda 表达式中调用了generate_canonical()函数,该表达式是generate()算法的第三个参数。lambda 将返回具有 12 个随机位的double类型的随机值,因此generate()将用这样的值填充data中的元素..在我的系统上执行这些语句会产生以下输出:

0.766197 0.298056 0.409951 0.955263 0.419199 0.737496 0.547764 0.91622

上面的输出显示的数字可能比我们想要的多,记住只指定了 12 个随机位。您可以像这样限制输出:

std::copy(std::begin(data), std::end(data),

std::ostream_iterator<double>{std::cout << std::fixed << std::setprecision(4), " "});

流操纵器应用于每个输出值,因此现在输出类似于:

0.8514 0.5707 0.8322 0.6626 0.7026 0.8854 0.5427 0.8886

如果您真的想得到这些位,您可以使用hexfloat操纵器以十六进制格式输出这些值。

显然,随机比特越少,可能的随机值的范围就越有限。您可以通过将位数指定为该类型的最大值来最大化范围。下面的一些代码展示了如何:

std::vector<long double> data;                       // Empty container

std::random_device rd;                               // Non-determinstic seed source

std::default_random_engine rng {rd()};               // Create random number generator

std::generate_n(std::back_inserter(data), 10, [&rng]

{ return std::generate_canonical<long double, std::numeric_limits<long double>::digits>(rng); });

std::copy(std::begin(data), std::end(data), std::ostream_iterator<long double>{std::cout, " "});

std::cout << std::endl;

注意与前面代码的不同之处。这个时间generate_n()与第一个参数一起用作data容器的back_insert_iterator,因此通过调用其push_back()成员将元素添加到datagenerate_canonical()的第二个模板参数是long double类型的numeric_limits对象的digits成员的值。这是该类型尾数的位数,因此我们指定了该类型可能的最大随机位数(在我的系统上只有53)。我得到了这个输出,但是您的输出会有所不同:

0.426365 0.0635646 0.208444 0.198286 0.338378 0.490884 0.841733 0.975676 0.193322 0.346017

正态分布

正态(或高斯)分布如图 8-5 所示。它是一条连续的钟形曲线,其值平均分布在平均值的两边——平均值就是平均值。这是一个概率分布,所以曲线下的面积是 1。正态分布完全由两个参数定义,即平均值和标准偏差——标准偏差是对平均值两侧数值分布情况的度量。

A978-1-4842-0004-9_8_Fig5_HTML.gif

图 8-5。

The normal distribution

平均值和标准偏差分别由希腊字符μ - mu 和σ - sigma 表示,对于变量xn样本,它们由以下等式定义:

$$ \mu =\frac{{\displaystyle {\sum}_0^n}{x}_i}{n}\kern2em \sigma =\sqrt{\frac{{\displaystyle {\sum}_0^n}{\left({x}_i-\mu \right)}²}{n-1}} $$

因此,平均值就是这些值的总和除以多少,换句话说就是平均值。通过将每个值和平均值之间的差的平方相加,除以n-1,然后取结果的平方根,可以获得标准偏差。正态分布曲线的相对宽度和高度可以随着平均值和标准偏差的不同值而有相当大的变化。然而,数值的分布总是如图 8-5 所示。这意味着,如果你知道一个符合正态分布的变量的平均值和标准差,比如一个大群体中个体的身高,那么你就知道 95%的人的身高与平均值相差不超过2 σ。标准正态分布的均值为0,标准差为1

uniform_distribution模板定义了产生随机浮点值的分布对象类型;默认情况下,这些是类型double。默认的构造函数创建了一个标准的正态分布——所以平均值是0,标准差是1.0:

std::normal_distribution<> dist;                           // mu: 0 sigma: 1

以下是创建具有特定平均值和标准差的正态分布的方法:

double mu {50.0}, sigma {10.0};

std::normal_distribution<> norm {mu, sigma};

这定义了一个分布对象来产生平均值为50.0和标准偏差为10.0double值。为了生成值,您将一个随机数生成器传递给norm函数对象。例如:

std::random_device rd;

std::default_random_engine rng {rd()};

std::cout << "Normally distributed values: "

<< norm(rng) << " " << norm(rng) << std::endl;   // 39.6153 45.5608

您可以通过调用对象的mean()stddev()成员来获得平均值和标准差的值:

std::cout << "mu: " << norm.mean() << " sigma: " << norm.stddev()

<< std::endl;                                    // mu: 50 sigma: 10

通过调用不带参数的param()成员,可以获得封装在param_type对象中的两个值。要设置平均值和/或标准偏差,需要将一个param_type对象传递给param()成员。param_type对象将拥有与其相关的分布类成员同名的函数成员,以提供均值和标准差。这里有一个使用这些的例子:

using Params = std::normal_distribution<>::param_type;     // Type alias for readability

double mu {50.0}, sigma {10.0};

std::normal_distribution<> norm {mu, sigma};               // Create distribution

auto params = norm.param();                                // Get mean and standard deviation

norm.param(Params {params.mean(), params.stddev() + 5.0}); // Modify params

std::cout << "mu: " << norm.mean() << " sigma: " << norm.stddev()

<< std::endl;                                // mu: 50 sigma: 15

这将不带参数地调用param()来获取包含平均值和标准偏差的param_type对象。一个Params对象被传递给param()的第二次调用,以通过5.0增加标准偏差。

您可以通过将一个param_type对象作为分布对象调用中的第二个参数来临时设置平均值和标准偏差:

using Params = std::normal_distribution<>::param_type; // Type alias for readability

std::random_device rd;

std::default_random_engine rng {rd()};

std::normal_distribution<> norm {50.0, 10.0};          // Create distribution

Params new_p {100.0, 30.0};                            // mu=100 sigma=30

std::cout << norm(rng, new_p) << std::endl;            // Generate value with new_p: 100.925

std::cout << norm.mean() << " " << norm.stddev()

<< std::endl;                                // 50 10

new_p定义的平均值和标准偏差仅适用于作为第二个参数传递的norm的执行。原始均值和标准差将应用于后续的norm调用,无需第二个参数。

min()max()成员返回分布可以产生的最小值和最大值。这对于正态分布不是特别有用,因为这些值将是可以由返回值的类型表示的最小和最大值:

std::cout << "min: " << norm.min() << " max: " << norm.max()

<< std::endl;                                // min: 4.94066e-324 max: 1.79769e+308

使用正态分布

这个工作示例将允许使用从键盘输入的平均值和标准偏差值来创建正态分布对象。该程序将使用分布对象生成大量的随机值,然后将这些值绘制成直方图,以显示曲线的形状。概率会在页面上,样本值在页面下。下面是绘制一系列值的函数模板的代码:

template<typename Iter>

void dist_plot(Iter& beg_iter, Iter& end_iter, size_t width=90)

{

// Create data for distribution plot

std::map<int, size_t> plot_data;                     // Elements are pair<value, frequency>

// Make sure all values are present in the plot

auto pr = std::minmax_element(beg_iter, end_iter, [](const double v1, const double v2)

{return v1 < v2; });

for(int n {static_cast<int>(*pr.first)}; n < static_cast<int>(*pr.second); ++n)

plot_data.emplace(n,0);

// Create the plot data

std::for_each(beg_iter, end_iter,

&plot_data { ++plot_data[static_cast<int>(std::round(value))]; });

// Find maximum frequency to be plotted - must fit within page width

size_t max_f {std::max_element(std::begin(plot_data), std::end(plot_data),

[](const std::pair<int,int>& v1, const std::pair<int,int>& v2)

{ return v1.second < v2.second; })->second};

// Draw distribution as histogram

std::for_each(std::begin(plot_data), std::end(plot_data),

max_f, width

{std::cout << std::setw(3) << v.first << " -| "

<< string((width*v.second) / max_f, '*') << std::endl; });

}

首先创建一个包含pair元素的map容器,每个元素存储一个整数值及其出现的频率。如果要绘制的值数量较少,则可能无法生成某个范围内的值。尽管如此,这些仍然应该出现在直方图中。为了确保要绘制的范围内的所有值都存在,我们使用minmax_element()算法找到最小值和最大值,然后在map中创建这个范围内的所有元素,计数为零。minmax_element()算法返回指向最小和最大元素的迭代器,因此这些迭代器必须解引用才能获得值。因为输入范围内的值可以是浮点型的,所以它们在存储到map之前会被转换成整数。在将值转换为类型size_t之前,for_each()算法应用于每个元素的 lambda 表达式使用cmath头中定义的round()函数将每个值四舍五入为最接近的整数。结果随后存储在map中。万一整数值已经不在map中,它将作为一个新元素被添加,其中second成员的频率增加到 1;如果它已经在map中,通常情况下,pair的第二个成员将递增。round()函数将整数中间的值向远离零的方向舍入,从而避免偏向零。

使用max_element()算法获得最大频率值。在这种情况下,元素通过 lambda 表达式进行比较,该表达式只比较元素的second成员。用由max_element()返回的迭代器指向的pairsecond成员初始化max_f变量。直方图由for_each()算法绘制成一系列string对象。每个string包含的星号数量对应于缩放后的频率计数,因此最大值在一行的width个字符内。

下面是利用dist_plot()的程序:

// Ex8_05.cpp

// Checking out normal distributions

#include <random>                                          // For distributions and random number generators

#include <algorithm>                                       // For generate(), for_each(),

//     max_element(), transform()

#include <numeric>                                         // For accumulate()

#include <vector>                                          // For vector container

#include <map>                                             // For map container

#include <cmath>                                           // For pow(), round() functions

#include <iostream>                                        // For standard streams

#include <iomanip>                                         // For stream manipulators

#include <string>                                          // For string class

using std::string;

using Params = std::normal_distribution<>::param_type;

// Template for dist_plot() function goes here...

int main()

{

std::random_device rd;

std::default_random_engine rng {rd()};

std::normal_distribution<> norm;

double mu {}, sigma {};

const size_t sample_count {20000};

std::vector<double> values(sample_count);

while(true)

{

std::cout << "\nEnter values for the mean and standard deviation, or Ctrl+Z to end: ";

if((std::cin >> mu).eof()) break;

std::cin >> sigma;

norm.param(Params{mu, sigma});

std::generate(std::begin(values), std::end(values), [&norm, &rng]{ return norm(rng); });

// Create data to plot histogram and plot it

dist_plot(std::begin(values), std::end(values));

// Get the mean and standard deviation for the generated random values

double mean {std::accumulate(std::begin(values), std::end(values), 0.0)/ values.size()};

std::transform(std::begin(values), std::end(values), std::begin(values),

&mean { return std::pow(value-mean,2); });

double s_dev

{std::sqrt(std::accumulate(std::begin(values), std::end(values), 0.0)/(values.size() - 1))};

std::cout << "For generated values, mean = " << mean

<< " standard deviation = " << s_dev << std::endl;

}

}

在每次while循环迭代中,系统会提示您输入正态分布的平均值和标准偏差。循环继续,直到您输入Ctrl+Z而不是平均值。您输入的值用于创建一个param_type对象,该对象被传递给分布对象normparam()成员,该成员设置分布的平均值和标准偏差。创建一个包含类型为doublesample_count元素的vector,通过generate()算法将每个元素设置为由分布对象norm返回的随机值。然后通过调用dist_plot()产生对应于这些值的分布。为了查看生成值的平均值和标准偏差与原始规格的接近程度,分别使用accumulate()transform()算法进行计算。

对于所示的输入,我得到了以下输出:

Enter values for the mean and standard deviation, or Ctrl+Z to end: 8 3

-3 -|

-2 -|

-1 -| *

0 -| **

1 -| ******

2 -| ************

3 -| *********************

4 -| **********************************

5 -| **************************************************

6 -| ******************************************************************

7 -| ********************************************************************************

8 -| *************************************************************************************

9 -| **********************************************************************************

10 -| ********************************************************************

11 -| *****************************************************

12 -| ***********************************

13 -| **********************

14 -| ***********

15 -| *****

16 -| **

17 -| *

18 -|

19 -|

20 -|

21 -|

For generated values, mean = 8.02975 standard deviation = 2.99916

Enter values for the mean and standard deviation, or Ctrl+Z to end: ^Z

输出显示生成值的分布具有正确的形状,从随机值计算的平均值和标准偏差非常接近为分布对象指定的值。你可以尝试不同的方法和标准偏差来了解形状是如何变化的。

对数正态分布

对数正态分布与正态分布相关,因为它表示随机变量的分布,其中值的对数分布是正态分布。对数正态分布由平均值和标准差定义,但这些参数与变量无关,它们与变量的对数有关。具体来说,具有均值μ和标准差σ的随机变量x的对数正态分布意味着log x是具有均值μ和标准差σ的正态分布。图 8-6 显示了对数正态分布曲线,以及改变平均值和标准偏差的影响。

A978-1-4842-0004-9_8_Fig6_HTML.gif

图 8-6。

Lognormal distributions

对于自然界中的许多随机变量,对数正态分布比正态分布更接近于概率的表示。许多疾病的感染率遵循对数正态模式。

模板lognormal_distribution的一个实例定义了一个对数正态分布对象,该对象默认返回类型为double的浮点值。以下是对数正态分布对象的定义,其平均值为5.0,标准差为0.5:

double mu {5.0}, sigma {0.5};

std::lognormal_distribution<> norm {mu, sigma};

构造函数的参数有默认值 0 和1,所以省略参数定义了一个标准的对数正态分布。还有另一个构造函数接受一个param_type对象,该对象封装了平均值和标准偏差作为参数。

与所有分布类型都具有的函数成员一样,lognormal_distribution对象具有函数成员m()s(),它们分别返回平均值和标准差。您使用对象的方式与您在其他发行版中看到的方式相同,所以让我们在一个示例中尝试一下。

使用对数正态分布

本例将使用Ex8_05中的 dist_ plot()函数模板,并稍作修改,以隐藏图中包含零个星号的输出行。这是因为对数正态曲线可以有一个很长的尾巴,你不需要看到它来欣赏分布的形状。在plot_data()的最后声明将是:

std::for_each(std::begin(plot_data), std::end(plot_data),

max_f, width

{ if((width*v.second)/max_f > 0)

std::cout << std::setw(3) << v.first << " -| "

<< string((width*v.second)/max_f, '*') << std::endl;

});

程序是这样的:

// Ex8_06.cpp

// Checking out lognormal distributions

#include <random>                  // For distributions and random number generators

#include <algorithm>               // For generate(), for_each(), max_element(), transform()

#include <numeric>                 // For accumulate()

#include <iterator>                // For back_inserter()

#include <vector>                  // For vector container

#include <map>                     // For map container

#include <cmath>                   // For pow(), round(), log() functions

#include <iostream>                // For standard streams

#include <iomanip>                 // For stream manipulators

#include <string>

using std::string;

using Params = std::lognormal_distribution<>::param_type;

// Modified plot_data template goes here...

int main()

{

std::random_device rd;

std::default_random_engine rng {rd()};

std::lognormal_distribution<> log_norm;

double mu {}, sigma {};

const size_t sample_count {20000};

std::vector<double> values(sample_count);

std::vector<double> log_values;

while(true)

{

std::cout << "\nEnter values for the mean and standard deviation, or Ctrl+Z to end: ";

if((std::cin >> mu).eof()) break;

std::cin >> sigma;

log_norm.param(Params {mu, sigma});

std::generate(std::begin(values), std::end(values), [&log_norm, &rng] { return log_norm(rng); });

// Create data to plot lognormal curve

dist_plot(std::begin(values), std::end(values));

// Create logarithms of values

std::vector<double> log_values;

std::transform(std::begin(values), std::end(values), std::back_inserter(log_values),

[] (double v){ return log(v); });

// Create data to plot curve for logarithms of values

std::cout << "\nThe distribution for logarithms of the values:\n";

dist_plot(std::begin(log_values), std::end(log_values));

// Get the mean and standard deviation - for the logarithms of the values

double mean {std::accumulate(std::begin(log_values), std::end(log_values), 0.0)/log_values.size()};

std::transform(std::begin(log_values), std::end(log_values), std::begin(log_values),

&mean { return std::pow(value - mean, 2); });

double s_dev {std::sqrt(std::accumulate(std::begin(log_values),                                        std::end(log_values), 0.0)/(log_values.size() - 1))};

std::cout << "For generated values, mean = " << mean

<< " standard deviation = " << s_dev << std::endl;

}

}

这段代码的工作方式与Ex8_05基本相同,带有一个不确定的while循环,允许您尝试各种参数。明显的区别是它使用了一个lognormal_distribution对象。在每次循环迭代中都有一个额外的图,显示生成值的对数分布。平均值和标准偏差也与数值的对数有关。

以下是一些输出示例:

Enter values for the mean and standard deviation, or Ctrl+Z to end: 3 .3

8 -| *

9 -| ******

10 -| ***********

11 -| *******************

12 -| *********************************

13 -| *******************************************

14 -| ************************************************************

15 -| ********************************************************************

16 -| **********************************************************************************

17 -| ***************************** **********************************************************

18 -| ******************************************************************************************

19 -| ***************************************************************************************

20 -| ************************************************************************************

21 -| **********************************************************************************

22 -| *********************************************************************

23 -| ********************************************************************

24 -| **********************************************************

25 -| **************************************************

26 -| **********************************************

27 -| ****************************************

28 -| ************************************

29 -| **************************

30 -| *********************

31 -| *****************

32 -| **************

33 -| ************

34 -| **********

35 -| ********

36 -| ******

37 -| ******

38 -| *****

39 -| ****

40 -| **

41 -| **

42 -| **

43 -| *

45 -| *

The``distribution

2 -| ****

3 -| ******************************************************************************************

4 -| ****

For generated values, mean = 2.99837 standard deviation = 0.298659

Enter values for the mean and standard deviation, or Ctrl+Z to end: ^Z

你可以看到数值的对数是正态分布的;这是一个非常窄的图,标准差非常小。我为标准差选择了一个小值,以避免对数正态图中出现非常长的尾巴,这会占用书中太多的空间,但我建议您尝试不同的值,看看形状如何变化。σ越大,第一个图就越长,第二个图看起来就越像典型的正态分布。值为 3 和 2.1 应该很好。

与正态分布相关的其他分布

STL 为用正态分布分类的分布定义了另外四个模板,默认情况下,它们都生成类型为double的随机值。我将在这里概述它们——它们的创建和使用方式与您所看到的发行版类似。如果你需要它们,你已经知道很多了:

  • chi_squared_distribution模板定义了对象的分布类型,这些对象是由一个浮点参数定义的,该参数是自由度的数量;默认值为 1.0。一个对象有一个函数成员n(),它返回自由度的数量。这种分布在假设检验中被广泛使用——检验现实世界与理论的匹配程度。在一些学科中,现实世界和理论之间的不匹配是如此令人失望,以至于调整现实世界的测量以适应理论被认为是必要的。
  • cauchy_distribution模板定义了类似于正态分布的分布类型,但是在中位数的两边有较重的尾部。一个实例由两个参数值定义,一个是定义中点的中值a,另一个是决定曲线宽度的扩散b。这些值可以通过调用对象的a()b()函数成员来获得。形式上,柯西分布没有均值或标准差,尽管中位数标识中点值。
  • fisher_f_distribution模板定义了用于确定两个差异何时相等的分布类型;它是两个卡方分布的比值。一个实例由两个参数值定义——分子分布中的自由度数量和分母的自由度数量;默认值都是 1.0。
  • student_t_distribution模板定义了用于少量样本和/或标准偏差未知时的分布类型。使用指定自由度数量的单个参数值创建实例;默认值为 1.0。

抽样分布

当您需要根据一组样本值(通常是来自现实世界的样本)来定义分布时,抽样分布非常有用。这些分布没有固定的概率曲线——您可以定义一个范围或一组范围内的值的可能性。STL 支持三种采样分布——离散分布、分段常数分布和分段线性分布。

离散分布

根据从0n-1的每个可能值的概率权重,discrete_distribution模板定义了返回范围[0, n)内的随机整数的分布。权重使您能够决定返回值的分布。这种分布的一个显而易见的应用是使用返回的值从可以使用索引访问的范围中选择随机对象或值。该范围可以包含任何类型的对象,包括函数对象,因此这提供了极大的灵活性。如果你想实现一个水果机模拟器,这个发行版会有所帮助。

您必须为要生成的值提供多个权重;权重的数量将决定可以生成的可能值的数量,并且权重的值用于确定概率。下面是一个如何模拟投掷面值从 1 到 6 的骰子的示例:

std::discrete_distribution<size_t> d{1, 1, 1, 1, 1, 3};    // Six possible values

std::random_device rd;

std::default_random_engine rng {rd()};

std::map<size_t, size_t> results;                          // Store the results of throws

for(size_t go {}; go < 5000; ++go)                         // 5000 throws of the die

++results[d(rng)];

for(const auto& pr : results)

std::cout << "A " << (pr.first+1) << " was thrown " << pr.second << " times\n";

构造函数的初始化列表包含 6 个权重,所以分布将只生成范围[0,6]内的值——这意味着值 0 到 5。最后一个值的权重是其他值的三倍,因此它出现的可能性是其他值的三倍。执行此操作会产生:

A 1 was thrown 607 times

A 2 was thrown 645 times

A 3 was thrown 637 times

A 4 was thrown 635 times

A 5 was thrown 617 times

A 6 was thrown 1859 times

六号更有可能。权重是浮点值,表示要生成的整数值的相对概率。每个值的概率是其权重除以所有权重的总和,因此前五个值的概率分别为1/8,最后一个值的概率为3/8。这个语句会产生相同的分布:

std::discrete_distribution<size_t> d{20, 20, 20, 20, 20, 60};

04的每个值的概率是20/160,也就是1/8,最后一个值的概率是60/160或者3/8

您也可以按范围指定权重。下面是使用相同随机数生成器的第一段代码的变体:

std::array<double,6> wts {10.0, 10.0, 10.0, 10.0, 10.0, 30.0};

std::discrete_distribution<size_t> d{std::begin(wts), std::end(wts)};

std::array<string, 6> die_value {"one", "two", "three", "four", "five", "six"};

std::map<size_t, size_t> results;                        // Store the results of throws

for(size_t go {}; go < 5000; ++go)                       // 5000 throws of the die

++results[d(rng)];

for(const auto& pr : results)

std::cout << "A " << die_value[pr.first] << " was thrown " << pr.second << " times\n";

这里的重量来自一个array容器。这一次,由 distribution 对象产生的值用于索引输出数组。这是我得到的:

A one was thrown 653 times

A two was thrown 601 times

A three was thrown 611 times

A four was thrown 670 times

A five was thrown 600 times

A six was thrown 1865 times

为一个discrete_distribution对象定义权重的另一个选项是为构造函数提供一个一元函数,从两个参数值创建给定数量的权重。这种工作方式有点复杂,所以我们将一步一步地检查它。

此构造函数有四个参数:

  • 砝码数量的计数,n
  • 用于计算概率的类型为doublexminxmax的两个值,
  • op一元运算符。

xmax必须大于xmin,如果n为零,那么只会产生一个值为1的概率,所以在这种情况下分布会一直产生同一个值——0

一个增量,我称之为step,定义为(xmax - xmin)/n。通过从0n-1执行值为kop(xmin + (2*k+1)*step/2)来计算概率。

因此,权重将为:

op(xmin + step/2), op(xmin + 3*step/2), op(xmin + 5*step/2), ... op(xmin + (2*n-1)*step/2)

一个带有数值的例子将有助于澄清正在发生的事情。假设n6xminxmax012,那么step的值就是 2。如果我们假设op被定义为使参数值加倍,那么权重将为:26101418、22。因此,概率将为1/361/125/367/361/411/36。这是分发对象的定义:

std::discrete_distribution<size_t> dist {6, 0, 12, [](double v) { return 2*v; }};

一元运算符由返回两倍参数值的 lambda 表达式定义。您可以通过调用probabilities()成员从discrete_distribution对象中检索概率。你可以像这样获得dist物体的概率:

auto probs = dist.probabilities();                 // Returns type vector<double>

std::copy(std::begin(probs), std::end(probs),

std::ostream_iterator<double> { std::cout << std::fixed << std::setprecision(2), " " });

std::cout << std::endl;                            // Output:  0.03 0.08 0.14 0.19 0.25 0.31

一般来说,概率的数量是任意的,对应于您指定的任意数量的权重,因此它们被返回到一个vector<double>容器中。注释中显示的输出对应于我前面显示的分数的值。

您可以通过使用一组不同的权重值调用其param()成员来为discrete_distribution对象设置新的概率;重量的数量也可以不同:

dist.param({2, 2, 2, 3, 3});                       // New set of weights

auto parm = dist.param().probabilities();

std::copy(std::begin(parm), std::end(parm),

std::ostream_iterator<double> {std::cout << std::fixed << std::setprecision(2), " "});

std::cout << std::endl;                            // Output: 0.17 0.17 0.17 0.25 0.25

第一个param()成员调用的参数是一个权重列表,其值和数量都不同于原始值和数量。不带参数调用param()会返回一个param_type对象,但是你并不知道这个别名代表什么类型。但是,您知道它支持与原始分布对象相同的访问参数的函数成员。这意味着在这种情况下,您可以通过调用param_type对象的probabilities()成员来获取param_type对象中的值。这将返回一个可以访问的vector<double>容器。注释显示了它包含的概率,您可以看到它们与新的权重集相对应。

使用离散分布

我们可以应用discrete_distribution对象来实现一个简单的游戏,你可以用四个骰子和计算机进行游戏。骰子是不寻常的,因为面的数量是非标准的,并且每个骰子是不同的。骰子上的面具有以下值:

die 1: 3 3 3 3 3 3

die 2: 0 0 4 4 4 4

die 3: 1 1 1 5 5 5

die 4: 2 2 2 2 6 6

为了玩这个游戏,你首先选择四个骰子中的任何一个。然后计算机从剩下的三个骰子中选择一个。两个骰子共掷 15 次,每掷一次,面值最高的骰子获胜。谁在 15 次投掷中得分最高,谁就是获胜者。因为没有两个骰子有相同的数值,所以一个骰子或另一个骰子总是赢得一次投掷,如果投掷次数为奇数,则不能进行游戏。

实现这一点的一个简单方法是定义一个Die类来表示骰子,这样每个Die对象存储它的 6 个面的值。该类可以定义一个函数成员,使用一个discrete_distribution对象来模拟投掷骰子,如下所示:

std::discrete_distribution<size_t> throw_die{1, 1, 1, 1, 1, 1};

然而,一个所有值都具有相同概率的对象并不能很好地演示离散分布,所以我将采用一种不同的、更复杂的方法。我们可以定义这个类来表示标题Die.h中的骰子,如下所示:

#ifndef DIE_H

#define DIE_H

#include <random>                    // For discrete_distribution and random number generator

#include <vector>                    // For vector container

#include <algorithm>                 // For remove()

#include <iterator>                  // For iterators and begin() and end()

// Alias for param_type for discrete distribution

using Params = std::discrete_distribution<size_t>::param_type;

std::default_random_engine& rng();

// Class to represent a die with six faces with arbitrary values

// Face values do not need to be unique.

class Die

{

public:

Die() { values.push_back(0); };

// Constructor

Die(std::initializer_list<size_t> init)

{

std::vector<size_t> faces {init}; // Stores die face values

auto iter = std::begin(faces);

auto end_iter = std::end(faces);

std::vector<size_t> wts;         // Stores weights for face values

while(iter != end_iter)

{

values.push_back(*iter);

wts.push_back(std::count(iter, end_iter, *iter));

end_iter = std::remove(iter, end_iter, *iter++);

}

dist.param(Params {std::begin(wts), std::end(wts)});

}

size_t throw_it() { return values[dist(rng())]; }

private:

std::discrete_distribution<size_t> dist; // Probability distributtion for values

std::vector<size_t> values;           // Face values

};

#endif

该类有两个私有成员,一个是用于为骰子生成随机面值的discrete_distribution<size_t>对象dist,另一个是存储唯一面值的vector容器values。默认构造函数创建一个对象,该对象的默认分布对象将总是返回0,而values成员包含一个值0。第二个构造函数需要一个初始化列表来指定骰子的面值。初始化列表用于用面值初始化本地faces容器。面值是可以复制的,每个面值的权重——决定其概率——就是面值出现的次数。构造函数中的while循环遍历faces中的当前元素范围,并计算第一个元素的面值出现的次数。计数存储在wts容器中,它所应用的面值被添加到values成员中。然后从faces中删除当前面值的所有出现,并将删除产生的新结束迭代器存储在end_iter中。当循环结束时,values将包含骰子的所有唯一面值,而wts将包含相应的权重。为dist成员调用param()会将分布参数设置为wts容器中的权重。

使用Die类实现游戏的程序代码如下所示:

// Ex8_07.cpp

// Implementing a dice throwing game using discrete distributions

#include <random>                       // For discrete_distribution and random number generator

#include <array>                        // For array container

#include <utility>                      // For pair type

#include <algorithm>                    // For count(), remove()

#include <iostream>                     // For standard streams

#include <iomanip>                      // For stream manipulators

#include "Die.h"                        // Class to define a die

// Random number generator available throughout the program code

std::default_random_engine& rng()

{

static std::default_random_engine engine {std::random_device()()};

return engine;

}

int main()

{

size_t n_games {};                    // Number of games played

const size_t n_dice {4};              // Number of dice

std::array<Die, n_dice> dice          // The dice

{

Die {3, 3, 3, 3, 3, 3},

Die {0, 0, 4, 4, 4, 4},

Die {1, 1, 1, 5, 5, 5},

Die {2, 2, 2, 2, 6, 6}

};

std::cout <<

"For each game, select a die from the following by entering 1, 2, 3, or 4 (or Ctrl+Z to end):\n"

<< "die 1: 3 3 3 3 3 3\n"

<< "die 2: 0 0 4 4 4 4\n"

<< "die 3: 1 1 1 5 5 5\n"

<< "die 4: 2 2 2 2 6 6\n";

size_t you {}, me {};                                    // Stores index of my dice and your dice

while(true)

{

std:: cout << "\nChoose a die: ";

if((std::cin >> you).eof()) break;                // For EOF - it’s all over

if(you == 0 || you > n_dice)                      // Only 1 to 4 allowed

{

std::cout << "Selection must be from 1 to 4, try again.\n";

continue;

}

// Choose my die as next in sequence

me = you-- % n_dice;                                  // you from 0 to 3, and me you+1 mod 4

std::cout << "I’ll choose:  " << (me+1) << std::endl;

// Throw the dice

const size_t n_throws {15};

std::array<std::pair<size_t, size_t>, n_throws> goes; // Results of goes -

// pair<me_value, you_value>

std::generate(std::begin(goes), std::end(goes),      // Make the throws

[&dice, me, you] { return std::make_pair(dice[me].throw_it(), dice[you].throw_it()); });

// Output result of game

std::cout << "\nGame " << ++n_games << ":\n";

// Output results of my throws...

std::cout << "Me : ";

std::for_each(std::begin(goes), std::end(goes),

[](const std::pair<size_t, size_t>& pr)

{  std::cout << std::setw(3) << pr.first; });

auto my_wins = std::count_``if

[](const std::pair<size_t, size_t>& pr)

{ return pr.first > pr.second; });

std::cout << " My wins:   " << std::setw(2) <<  std::right << my_wins

<< "   I " << ((my_wins > n_throws / 2) ? "win!!" : "lose {:-(")

<< std::endl;

// Output results of your corresponding throws - aligned below mine...

std::cout << "You: ";

std::for_each(std::begin(goes), std::end(goes), [](const std::pair<size_t, size_t>& pr)

{ std::cout << std::setw(3) << pr.second; });

std::cout << " Your wins: " << std::setw(2) << std:: right << n_throws - my_wins

<< "   You " << ((my_wins <= n_throws / 2) ? "win!!" : "lose!!!")

<< std::endl;

}

}

dice数组容器保存四个不同骰子的Die对象。在提示输入之后,游戏在while循环中进行,每次迭代一个完整的游戏。玩家的骰子选择存储在you中,电脑的选择存储在me中。选择是从14的,所以这被递减以允许其用作dice数组的索引。me变量被任意设置为序列中的下一个芯片,以 4 为模,因此如果you选择索引3处的最后一个芯片,me将选择索引0处的第一个芯片。

n_throws变量指定一场比赛的投掷次数,在本例中为15;奇数场比赛确保总有一个赢家。generate()算法执行游戏中两个骰子的15投掷,并将每次投掷的结果存储在pair对象中,其中first成员存储计算机的骰子值,second成员存储玩家的骰子值。两个骰子的投掷由 lambda 表达式执行,它是generate()的第三个参数。投掷每个骰子的结果是通过调用其throw_it()成员产生的。

计算机获胜的投掷次数由count_if()算法计算并存储在my_wins中。如果来自goes容器的每个pair对象的first成员大于second,则计数递增。玩家的赢数是n_throws-my_wins

我得到了以下输出:

For each game, select a``die

die 1: 3 3 3 3 3 3

die 2: 0 0 4 4 4 4

die 3: 1 1 1 5 5 5

die 4: 2 2 2 2 6 6

Choose a die: 2

I’ll choose:  3

Game 1:

Me :   5  1  5  1  5  5  1  1  5  5  5  5  1  5  5 My wins:   11   I win!!

You:   4  0  4  4  4  4  4  4  4  4  4  0  4  4  4 Your wins:  4   You lose!!!

Choose a die: 4

I’ll choose:  1

Game 2:

Me :   3  3  3  3  3  3  3  3  3  3  3  3  3  3  3 My wins:    9   I win!!

You:   6  6  2  2  2  2  2  2  6  6  2  2  2  6  6 Your wins:  6   You lose!!!

Choose a die: 1

I’ll choose:  2

Game 3:

Me :   0  0  0  0  4  4  4  4  4  0  4  4  4  4  4 My wins:   10   I win!!

You:   3  3  3  3  3  3  3  3  3  3  3  3  3  3  3 Your wins:  5   You lose!!!

Choose a die: 3

I’ll choose:  4

Game 4:

Me :   6  2  2  2  2  6  2  2  2  2  6  2  2  2  2 My wins:    9   I win!!

You:   5  5  5  5  1  5  5  5  1  1  5  1  1  1  5 Your wins:  6   You lose!!!

Choose a die: 3

I’ll choose:  4

Game 5:

Me :   2  2  6  2  2  2  6  2  2  2  2  6  2  6  6 My wins:   12   I win!!

You:   5  5  5  1  1  5  5  1  1  1  1  1  1  1  5 Your wins:  3   You lose!!!

Choose a die: 3

I’ll choose:  4

Game 6:

Me :   6  2  2  2  2  2  6  2  2  2  2  6  6  2  2 My wins:    8   I win!!

You:   5  5  5  5  5  5  1  1  1  1  5  5  1  5  1 Your wins:  7   You lose!!!

Choose a die: 2

I’ll choose:  3

Game 7:

Me :   5  5  1  1  5  5  5  5  5  5  5  1  1  1  1 My wins:   10   I win!!

You:   4  4  4  4  0  4  4  4  0  4  4  4  4  0  4 Your wins:  5   You lose!!!

Choose a die: ^Z

这台电脑非常成功——它赢了每一场比赛。它的成功是因为玩家先选择一个骰子。四骰子是非传递性骰子的一个例子,是由美国统计学家布拉德利·埃夫隆发明的。一般来说,数字关系是可传递的,这意味着如果a>bb>c,可以说a>c。这些骰子就不是这样了。对于dice数组中的Die对象,下面是Ex8_07实现游戏的情况:

  • dice[3]击败dice[2]击败dice[1]击败dice[0]击败dice[3]

这是因为投掷产生面值的概率:

  • dice[3]击败dice[2]是因为来自dice[3(概率1/3的一个6总是赢,当dice[2]1(概率1/2)时,来自dice[3](概率2/3)的一个2总是赢。所以dice[3]获胜的总体概率是1/3 + 2/3 × 1/2,也就是2/3
  • dice[2]击败dice[1]是因为来自dice[2(概率1/2的一个5总是赢,当dice[1]0(概率1/3)时,来自dice[2](概率1/2)的一个1总是赢。所以dice[2]获胜的总体概率是1/2 + 1/2 × 1/3,也就是2/3
  • dice[1]打败dice[0]是因为dice[1(概率2/3)的一个4总是赢。
  • dice[0]击败dice[3]是因为dice[3]2(概率2/3)时dice[0(概率1)的一个3赢了。所以dice[0]获胜的整体概率是1 × 2/3,也就是2/3

这意味着无论你选择哪个骰子,计算机都可以从剩下的三个骰子中选择一个有 66%几率击败你的骰子。

分段常数分布

piecewise_constant_distribution模板定义了一个用一组分段子区间生成浮点值的分布。给定子区间的值在其中均匀分布,每个子区间都有自己的权重。一个对象由定义 n-1 个常数子区间的一组 n 个区间边界和应用于子区间的一组 n-1 个权重来定义。图 8-7 说明了这一点。

A978-1-4842-0004-9_8_Fig7_HTML.gif

图 8-7。

The piecewise constant distribution

图 8-7 中的分布定义了三个区间,每个区间都有自己的权重。三个间隔由容器b中定义的四个边界值定义。每个区间都有一个由容器w的元素定义的权重。前两个构造函数参数是指定边界范围的迭代器,第三个参数是指向权重范围内第一个元素的迭代器。每个区间内的值将均匀分布,随机值在特定区间内的概率由该区间的权重决定。

除了所有分布对象实现的函数成员之外,piecewise_constant_distribution还有intervals()densities()成员,它们返回区间边界和区间中值的概率密度;这两个函数都返回vector容器中的值。我们可以应用这些成员,并通过尝试类似于图 8-7 所示的分布来获得对分布效果的一些了解,但是间隔更窄,因此输出需要的空间更少:

// Ex8_08.cpp

// Demonstrating the piecewise constant distribution

#include <random>                          // For distributions and random number generator

#include <vector>                          // For vector container

#include <map>                             // For map container

#include <utility>                             // For pair type

#include <algorithm>                           // For copy(), count(), remove()

#include <iostream>                            // For standard streams

#include <iterator>                            // For stream iterators

#include <iomanip>                             // For stream manipulators

#include <string>                              // For string class

using std::string;

int main()

{

std::vector<double> b {10, 20, 35, 55};      // Intervals: 10-20, 20-35, 35-55

std::vector<double> w {4, 10, 6};            // Weights for the intervals

std::piecewise_constant_distribution<> d {std::begin(b), std::end(b), std::begin(w)};

// Output the interval boundaries and the interval probabilities

auto intvls = d.intervals();

std::cout << "intervals: ";

std::copy(std::begin(intvls), std::end(intvls), std::ostream_iterator<double>{std::cout, " "});

std::cout << "  probabilities: ";

auto probs = d.densities();

std::copy(std::begin(probs), std::end(probs), std::ostream_iterator<double>{std::cout, " "});

std::cout << '\n' << std::endl;

std::random_device rd;

std::default_random_engine rng {rd()};

std::map<int, size_t> results;               //Stores and counts random values as integers

// Generate a lot of random values...

for(size_t i {}; i < 20000; ++i)

++results[static_cast<int>(std::round(d(rng)))];

// Plot the integer values

auto max_count = std::max_element(std::begin(results), std::end(results),

[](const std::pair<int, size_t>& pr1, const std::pair<int, size_t>& pr2)

{ return pr1.second < pr2.second; })->second;

std::for_each(std::begin(results), std::end(results),

max_count

{ if(!(pr.first % 10))  // Display value if multiple of  10

std::cout << std::setw(3) << pr.first << "-|";

else

std::cout << "    |";

std::cout << std::string(pr.second * 80 / max_count, '*')

<< '\n'; });

}

这将使用您看到的间隔和权重创建一个分布,使用该分布生成大量的值,然后在将这些值转换为整数后,以直方图的形式绘制这些值出现的频率。这些值沿着页面向下排列,条形从左到右表示相对频率。我得到了以下输出:

intervals: 10 20 35 55   probability densities: 0.02 0.0333333 0.015

10-|************************

|**********************************************

|*******************************************

|*******************************************

|*********************************************

|**********************************************

|************************************************

|***********************************************

|******************************************

|********************************************

20-|************************************************************

|****************************************************************************

|***********************************************************************

|**************************************************************************

|*****************************************************************************

|***************************************************************************

|*************************************************************************

|*******************************************************************************

| ************************************************************************

|******************************************************************************

30-|******************************************************************************

|*****************************************************************************

|***********************************************************************

|***************************************************************************

|********************************************************************************

|*********************************************************

|*******************************

|***********************************

|********************************

|**********************************

40-|***************************************

|********************************

|*********************************

|**********************************

|*********************************

|********************************

|**********************************

|**********************************

|***********************************

|*******************************

50-|********************************

|*******************************

|*******************************

|************************************

|*********************************

|****************

输出的有趣特征是概率密度的值,以及第一个和最后一个间隔中条形的相对长度。区间的权重分别为 4 和 6,因此值位于第一个区间的概率为4/20,即0.2,值位于第二个区间的概率为10/20,即0.5,值位于最后一个区间的概率为6/20,即0.3。然而,最后一个区间的输出棒线低于第一个区间的输出棒线,这似乎与这些概率相冲突。反正输出中的概率密度值是不一样的,为什么会这样呢?

原因是它们不一样。概率密度是区间中给定值出现的概率,而不是随机值出现在区间中的概率。值的概率密度对应于区间中的值将出现的概率,除以区间中值的范围。因此,三个范围内的值的概率密度分别为0.2/100.5/150.3/20,幸运的是,这与输出相同。最后一个时间间隔中生成的值大约是第一个时间间隔的两倍,但是这些值分布在更大的范围内,因此条形更短。因此,条形长度反映了概率密度。

分段线性分布

piecewise_linear_distribution模板定义了浮点值的连续分布,其中概率密度函数由一组样本值定义的点连接而成;每个样本值都有一个确定其概率密度值的权重。图 8-8 显示了一个例子。

A978-1-4842-0004-9_8_Fig8_HTML.gif

图 8-8。

A piecewise linear distribution

图 8-8 显示了由容器v中定义的五个样本值确定的分布。每个值都有一个由容器w的相应元素定义的权重,每个权重决定了相应值的概率密度。一个样本和下一个样本之间的值的概率密度在两个样本的概率密度之间是线性的。前两个构造函数参数是指定值范围的迭代器,第三个参数是指向权重范围内第一个元素的迭代器。该分布将产生从 10 到 60 的随机值,概率密度由分段线性曲线表示。通过调用intervals()成员,可以在vector中获得分布的样本值。您可以通过调用分布对象的densities()vector容器中获得这些的概率密度。

确定整个范围内的值的概率密度有点复杂。整个概率曲线下的面积表示整个范围内任何值出现的概率,因此它必须为 1。为了适应这种情况,区间内值的概率计算如下:

First s计算为定义间隔的值的加权平均值之和,每个值乘以间隔长度。因而在一般情况下,s是由方程式定义的:

$$ s={\displaystyle \sum}_0^{n-1}\left({v}_{i+1}-{v}_i\right)\frac{\left({w}_{i+1}+{w}_i\right)}{2} $$

其中 v i 是样本值,w i 是它们对应的权重。

两个样本值之间的区间中的任何值x的概率p,【v i$$ {v}_{i+1} $$由样本值的概率的线性组合来确定,其中来自区间的每一端的概率贡献与 x 到样本值的距离成比例。用数学术语来说,x 的概率是:

$$ p=\frac{w_i\left({v}_{i+1}-x\right)+{w}_{i+1}\left(x-{v}_i\right)}{s\left({v}_{i+1}-{v}_i\right)} $$

因此对于图 8-8 中的例子,s为:

(30 - 10)×(12 + 6)/2 + (40 - 30)×(9 + 12)/2 +(55 - 40)×(6 + 9)/2 + (60 - 55)×(0 + 6)/2

这相当于。

i个样本值的概率为 w i /s,因此图 8-8 中值的概率为6/412.512/412.59/412.56/412.50/412.5。我可靠的计算器显示这些分别对应于0.01450.0290.02180.01450。类似于Ex8_08的工作示例将显示这是否正确,并显示分段线性分布的总体特征:

// Ex8_09.cpp

// Demonstrating the piecewise linear distribution

#include <random>                              // For distributions and random number generator

#include <vector>                              // For vector container

#include <map>                                 // For map container

#include <utility>                             // For pair type

#include <algorithm>                           // For copy(), count(), remove()

#include <iostream>                            // For standard streams

#include <iterator>                            // For stream iterators

#include <iomanip>                             // For stream manipulators

#include <string>                              // For string class

using std::string;

int main()

{

std::vector<double> v {10, 30, 40, 55, 60};  // Sample values

std::vector<double> w {6, 12, 9, 6, 0};      // Weights for the samples

std::piecewise_linear_distribution<> d {std::begin(v), std::end(v), std::begin(w)};

// Output the interval boundaries and the interval probabilities

auto values = d.intervals();

std::cout << "Sample values: ";

std::copy(std::begin(values), std::end(values), std::ostream_iterator<double>{std::cout, " "});

std::cout << "  probability densities: ";

auto probs = d.densities();

std::copy(std::begin(probs), std::end(probs), std::ostream_iterator<double>{std::cout, " "});

std::cout << '\n' << std::endl;

std::random_device rd;

std::default_random_engine rng {rd()};

std::map<int, size_t> results;              // Stores and counts random values as integers

// Generate a lot of random values...

for(size_t i {}; i < 20000; ++i)

++results[static_cast<int>(std::round(d(rng)))];

// Plot the integer values

auto max_count = std::max_element(std::begin(results), std::end(results),

[](const std::pair<int, size_t>& pr1, const std::pair<int, size_t>& pr2)

{ return pr1.second < pr2.second; })->second;

std::for_each(std::begin(results), std::end(results),

max_count

{

if(!(pr.first % 10))  // Display value if multiple of  10

std::cout << std::setw(3) << pr.first << "-|";

else

std::cout << "    |";

std::cout << std::string(pr.second * 80 / max_count, '*')

<< '\n';

});

}

Ex8_08的唯一区别是分布对象的定义。对于分段线性分布,权重的数量必须与样本值的数量相同。这是我的程序输出:

Sample values: 10 30 40 55 60   probability densities: 0.0145455 0.0290909 0.0218182 0.0145455 0

10-|******************

|*****************************************

|**************************************

|*********************************************

|********************************************

|*********************************************

|************************************************

|*************************************************

|***************************************************

|****************************************************

20-|********************************************************

|****************************************************

|**********************************************************

|*************************************************************

|*****************************************************************

|*****************************************************************

|*****************************************************************

|*****************************************************************

|***********************************************************************

|*********************************************************************

30-|********************************************************************************

|************************************************************************

|********************************************************************

|*******************************************************************

|**************************************************************

|********************************************************************

|*****************************************************************

|**********************************************************

|***********************************************************

|***********************************************************

40-|*******************************************************

|***************************************************

|*********************************************************

|*************************************************

|*************************************************

|************************************************

|***********************************************

|***********************************************

|*********************************************

|****************************************

50-|****************************************

|**************************************

|*************************************

|****************************************

|**************************************

|**********************************

|*****************************

|********************

|***************

|********

60-|*

概率密度与我的计算器得出的值非常相似,这让我松了一口气。这种分布为定义任何形状的概率密度函数提供了一个强有力的工具。

其他分布

STL 为另外九种类型的发行版定义了模板。我将在这里概述它们以供参考,并展示由它们中的大多数产生的随机值的图的例子。请记住,分布曲线的形状会因不同的参数集而有很大的不同,并且显示的插图已被缩放以适合页面宽度。

泊松分布

泊松分布定义了一个离散的 PDF,表示一组独立事件的发生频率,每个事件只有两种可能的结果,并且不同事件的成功概率可能不同。事件可以分布在时间或位置上。分配是由著名的法国数学家西米恩·泊松创立的。它的第一个应用是模拟普鲁士军队中士兵被马踢死的概率。它可用于模拟一段时间内设备故障的可能性或交通事故的发生率。

默认情况下,poisson_distribution模板定义了生成随机非负整数的函数对象的类型,这些整数的类型为int。根据分布平均值的double值创建一个对象,例如

double mean {5.5};

std::poisson_distribution<> poisson_d {mean};

默认构造函数为平均值 1.0 分配一个默认值。一个对象有一个返回平均值的mean()成员。图 8-9 显示了上面定义的poisson_d产生的随机值的出现次数。

A978-1-4842-0004-9_8_Fig9_HTML.gif

图 8-9。

Examples of Poisson, geometric, and exponential distributions

几何分布

几何分布是一种离散分布,用于模拟为了在有两种可能结果的事情上取得成功而必须进行的试验次数。一个例子可能是建模你需要问多少随机选择的人来找到一个已经买了这本书的人。geometric_distribution模板定义了离散分布类型,这些分布类型返回类型为int的非负整数值。构造函数参数是一个指定试验成功概率的double值,因此它必须在01之间。这里有一个例子:

double p_success {0.4};

std::geometric_distribution<> geometric_d {p_success};

该对象有一个函数成员p(),它返回成功的概率。geometric_d分布图如图 8-9 所示。

指数分布

指数分布模拟事件发生之间的时间。你可以把它看作是几何分布的连续等价物。默认情况下,exponential_distribution模板定义了返回类型为double的浮点值的分布类型。构造函数的参数是一个代表到达率的double值,通常标识为λ。这里有一个例子:

double lambda {0.75};

std::exponential_distribution<> exp_d {lambda};

对象的lambda()函数成员返回到达率的值。该分布图如图 8-9 所示。

伽马分布

伽玛分布是一种连续分布,通常用于模拟事件的等待时间,但比指数分布更通用。分布由两个参数定义:形状值α和速率值β。默认情况下,gamma_distribution模板定义了返回类型为double的浮点值的分布。这里有一个例子:

double alpha {5.0}, beta {1.5};

std::gamma_distribution<> gamma_d {alpha, beta};

alpha()beta()函数成员返回分布对象的参数。图 8-10 显示了gamma_d产生的值的曲线图。

A978-1-4842-0004-9_8_Fig10_HTML.gif

图 8-10。

Examples of Gamma, Weibull, and binomial distributions

威布尔分布

威布尔分布定义了一个连续的 PDF,它将故障率建模为时间(通常是材料)的函数。它由两个参数定义,形状a和比例b。下面是一个创建weibull_distribution模板实例的例子:

double a {2.5};                             // Shape

double b {7.0};                             // Scale

std::weibull_distribution<> weibull_d {a, b};

默认情况下,weibull_d对象返回类型为double的随机值。你可以通过调用它的a()b()成员来获得分布参数。图 8-10 中显示了weibull_d产生的值。

二项式分布

二项式分布是一种离散分布,它在一组独立的二元事件中模拟成功率。一个事件只有两种可能的结果——成功或失败,所有事件成功的概率都是一样的。它由两个参数定义,tp,其中t是试验次数,p 是试验成功的概率。下面是如何使用binomial_distribution模板创建一个对象:

int t {20};                                 // Number of trials

double p {0.75};                            // Probability of success

std::binomial_distribution<> binomial_d {t, p};

对象的t()p()成员返回参数值。binomial_d产生的值的曲线图如图 8-10 所示。

伯努利分布是一种二项式分布,其中t参数的值为 1。STL 提供了bernoulli_distribution类来定义这种分布。由于t固定为 1,所以只需要给构造函数提供一个p的值,对象就会返回随机的bool值。有一个p()成员返回成功的概率。下面是演示创建和使用对象的代码片段:

std::random_device rd;

std::default_random_engine rng {rd()};

double p {0.75};                            // Probability of success

std::bernoulli_distribution bernoulli_d {p};

std::cout << std::boolalpha;                // Output bool as true or false

for(size_t i {}; i < 15; ++i)

std::cout << bernoulli_d(rng) << ' ';

std::cout << std::endl;

执行此操作后,我得到了以下输出:

true true false true true true true true false true false true true false true

负二项分布

负二项式分布是一种离散分布,它模拟在指定数量的成功之前发生的一系列试验中的失败次数。这些试验只有两种可能的结果,并且相互独立。如果成功次数为 1,则分布与几何分布相同。您也可以将这种分布可视化为在给定的成功次数之前模拟失败次数。默认情况下,negative_binomial_distribution模板定义了返回类型为int的整数的对象类型。negative_binomial_distribution模板的构造函数需要两个参数,失败次数k和成功概率p。下面是一个创建对象的示例:

int k {5};                                  // Number of successes

double p {0.4};                             // Probability of success

std::negative_binomial_distribution<> neg_bi_d {k, p};

neg_bi_dk()和 p()成员返回参数的值。由neg_bi_d对象产生的数值如图 8-11 所示。

A978-1-4842-0004-9_8_Fig11_HTML.gif

图 8-11。

Examples of a negative binomial distribution and an extreme value distribution

极值分布

极值分布是模拟以相同方式分布的独立变量序列的最大值或最小值分布的连续分布。一个应用是模拟极端的自然现象,如降雨或地震。默认情况下,extreme_value_distribution模板定义了返回浮点值的对象类型,这些浮点值是类型double。构造函数需要两个参数,位置参数a和比例参数b;这两个参数都是浮点值。这里有一个例子:

double a {1.5};                             // location

double b {4.0};                             // Scale

std::extreme_value_distribution<> extreme_value_d {a, b};

可以通过调用对象的a()b()成员来获取对象的参数值。由extreme_value_d对象产生的数值如图 8-11 所示。

随机数引擎和生成器

STL 中有三个随机数引擎的类模板。它们都实现了一种众所周知的有效的生成随机数序列的算法,但是各有优缺点。这三个模板是 STL 提供的所有十个标准随机数生成器类类型的基础。除了实现定义的default_random_engine生成器类型之外,还有另外九种生成器类类型,它们定制引擎来实现已知的生成随机序列的可靠算法。还有三个随机数引擎适配器的模板,它们可以从引擎实例中定制序列。它们每个都有一个模板参数,用于标识它们所应用的引擎。引擎适配器模板包括:

  • independent_bits_engine适配器模板将引擎生成的值修改为指定的位数。
  • discard_block_engine适配器模板修改引擎生成的值,从给定长度的值序列中丢弃一些值。
  • shuffle_order_engine适配器模板以不同的顺序返回引擎生成的值。它通过存储来自引擎的给定长度的值序列,然后以随机序列返回这些值来实现这一点。

生成器类或者使用一组特定的模板参数值直接定制一个引擎模板,或者使用随机数引擎适配器定制另一个生成器。图 8-12 显示了发动机生产发电机的方式。

A978-1-4842-0004-9_8_Fig12_HTML.gif

图 8-12。

The connection between random number generators and random number engines

每个生成器类类型都是通过将一组特定的模板参数值应用于引擎模板而创建的。我将在这里概述随机数引擎,以便让您了解它们的作用,但是我强烈建议您使用定制引擎的随机数生成器类类型之一,而不是尝试自己定制引擎模板。让我们更详细地检查一下引擎和由它们定义的生成器类型。

线性同余发动机

linear_congruential_engine类模板实现了一种最古老也是最简单的生成随机整数序列的算法,叫做线性同余法。该算法涉及三个参数,一个乘数a,一个增量c,以及一个模数m。这些值的选择对于产生合理质量的随机序列是至关重要的。该过程需要一个整数seed值,第一个随机值x的概念计算如下:

unsigned int x = (a*seed + c) % m;

每个随机数 x n 用于生成下一个$$ {x}_{n+1} $$,使用公式:

$$ {x}_{n+1}=\left(a{x}_n+c\right)\kern0.5em\mod \kern0.5em m $$

很明显,由于随机值是余数,所以能产生的不同值的最大数量是m,对于ac的糟糕选择,会比这个少很多。虽然这种算法简单而快速,但当高质量的随机序列对应用程序很重要时,作为其他引擎之一的实例的生成器(如mersenne_twister_engine)是更好的选择。

基于线性同余发动机的发电机

有两种随机数生成器类型被定义为linear_congruential_engine模板实例的别名minstd_rand0minstd_rand,它们生成 32 位无符号整数。这些名字来自“最小标准随机数生成器”。minstd_rand0由 Stephen K. Park 和 Keith W. Miller 于 1988 年提出,作为生成随机数的最低标准,因为当时周围存在数量不佳的生成器;定义为a16807,c 为0,而m2147483647m的值是小于 2 32 的最大梅森素数。minstd_rand发生器是minstd_rand0的改进型,将a改为48271

knuth_b随机数发生器通过将shuffle_order_engine适配器应用于minst_rand0发生器产生的值,实现了唐纳德·克努特(Donald Knuth)的算法。这在他的经典著作《计算机编程的艺术》第二卷中有所描述,同时还有很多关于生成随机数和随机性测试的方法。通过消除连续值之间的相关性,应用适配器增加了序列的“随机性”。

你使用这些发生器——实际上是所有的发生器——就像你看到的default_random_engine一样,例如:

std::random_device rd;

std::minstd_rand rng {rd()};

std::uniform_int_distribution<long> dist {-5L, 5L};

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(2) << dist(rng) << " ";     // 3 -5 -2  4 -5  4  1  0

梅森龙卷风引擎

mersenne_twister_engine类模板实现了 Mersenne twister 算法,之所以这么叫是因为周期长度是一个 Mersenne 素数。梅森素数是形式为2 n -1的素数,所以 7 和 127 是梅森素数;当然,算法中使用的梅森素数要大得多。这种引擎使用非常广泛,因为它可以生成非常长的高质量序列,但它的缺点是相对较慢。该算法很复杂,涉及许多参数,所以我不会在这里解释它。

作为 Mersenne Twister 引擎实例的发生器

定义特定生成器的mersenne_twister_engine实例有两个类型别名。mt19937生成随机无符号 32 位整数,mt19937_64生成无符号 64 位整数。mt19937随机数发生器的周期长度为2 19937 -1,因此得名。

您可以像使用其他发生器一样使用它们:

std::random_device rd;

std::mt19937_64 rng {rd()};        // Generates random 64-bit integers

std::uniform_real_distribution<long double> dist {-5.0L, 5.0L};

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(5)

<< dist(rng)

<< " ";                // -2.57481 3.0546 -1.6438 2.14798

// -3.84095 0.973843 -2.98971 -2.1067

带进位引擎的减法器

subtract_with_carry_engine模板定义了一个随机数引擎,它实现了带进位的减法算法,这是对线性同余算法的改进。与线性同余法一样,带进位的减法算法使用递归关系来定义序列中的连续值,但是每个值 x i 都是从序列中两个较早的值$$ {x}_{i-r} $$$$ {x}_{i-s} $$计算出来的,而不仅仅是前一个值。r和 s 分别称为长滞后和短滞后,且都必须为正,r 必须大于s。产生该序列的方程式是:

$$ {D}_i=\left({x}_{i-r}-{x}_{i-s}-{c}_{i-1}\right)\kern0.5em\mod \kern0.5em m $$其中m2nn为一个字的位数。

$$ {x}_i={D}_i $$$$ {c}_n=0 $$如果$$ {D}_i{}³⁰ $$

$$ {x}_i=m+{D}_i $$$$ {c}_n=1 $$如果$$ {D}_i<0 $$

c 是一个“进位”,根据先前的状态,它可以是 0 或 1。该算法需要r种子值和进位的初始值c。与线性同余法一样,带进位的减法算法对参数值的选择非常敏感。

作为带进位减法引擎实例的生成器

ranlux24_base生成器类产生 24 位整数的随机序列,其中r24,而s10ranlux48_base类生成 48 位整数序列,其中r12,而s5。如图 8-12 所示,还有两类使用带进位减法引擎的生成器,ranlux24ranlux48,它们是通过将ranlux24_baseranlux48_base传递给discard_block_engine适配器的一个实例产生的。一个ranlux24实例从ranlux24_base产生的每个223值块中丢弃200;一个ranlux48实例丢弃了由ranlux48_base产生的每个389值块中的378,所以它们都对从底层来源接受的值很挑剔。

ranlux24ranlux48在蒙特卡罗模拟中广泛使用。ranlux这个名字是由 Fred James 首创的,他首先用 Fortran 实现了这个算法;这个名字来自于 random 和从底层序列中丢弃如此多的值的奢侈。这里有一个使用ranlux24的例子:

std::random_device rd;

std::ranlux24 rng {rd()};

std::uniform_real_distribution<long double> d {-5.0L, 5.0L};

for(size_t i {}; i < 8; ++i)

std::cout << std::setw(5) << d(rng)

<< " ";     // 2.02142 -0.920689 -0.277198 -1.33417 4.70217 -3.31706 -3.32692 4.36376

代码基本上与其他生成器相同。生成器对象将根据构造函数的参数产生初始状态所需的值。

打乱一系列元素

shuffle()算法将一个范围内的元素重新排列成随机排列。shuffle()的函数模板是在algorithm头中定义的,但是我把它放在这里是因为它需要一个随机数发生器。元素的所有可能的排列都是同样可能的。shuffle()的前两个参数是定义范围的随机访问迭代器,第三个参数是一个函数对象,它是一个统一随机数生成器,将用于生成随机序列。这段代码说明了它是如何工作的:

std::random_device rd;

std::mt19937 rng {rd()};

std::vector<string> words {"one", "two", "three", "four", "five", "six", "seven", "eight"};

for(size_t i {}; i < 4 ; ++i)

{

std::shuffle(std::begin(words), std::end(words), rng);

std::for_each(std::begin(words), std::end(words),

[](const string& word) {std::cout << std::setw(8) << std::left << word; });

std::cout << std::endl;

}

梅森扭扭器引擎rng作为最后一个参数传递给for循环中的shuffle()算法。这将重新排列每次迭代的words容器中的元素。我得到了这样的输出:

two     seven   three   five    six     eight   one     four

eight   five    seven   six     three   four    one     two

seven   one     five    six     eight   four    two     three

three   four    six     five    seven   one     two     eight

摘要

使用 STL 工具生成随机数序列通常包括三个部分:

  • 可以生成随机位序列的随机数引擎。有三个定义随机数引擎的类模板:
    • 能够产生最高质量的序列,但它是三个中最慢的。
    • linear_congruential_engine -最简单、最快但质量不如其他两个引擎的序列。
    • subtract_with_carry_engine -能够生成比linear_congruential_engine实例质量更好的序列,但是状态占用更多内存,速度稍慢。
  • 一个随机数生成器,它定制一个引擎模板来实现一个特定的算法,用于生成非负整数的统一随机序列。除了由实现定义的default_random_engine,还有九个类定义了不同的生成器:
    • 来自mersenne_twister_engine模板的mt19937mt19937_64
    • 来自linear_congruential_engine模板的minstd_rand0minstd_randknuth_b
    • 来自subtract_with_carry_engine模板的ranlux24_baseranlux48_baseranlux24ranlux48
  • 一种分布函数对象,它使用随机数生成器中的序列来生成具有给定概率分布的整数或浮点值序列。有 21 个模板定义了发行版——除了一个以外都是类模板:
    • 均匀分布:uniform_int_distributionuniform_real_distributiongenerate_canonical()函数模板。
    • 正态分布:normal_distributionlognormal_distributionchi_squared_distributioncauchy_distributionfisher_f_distributionstudent_t_distribution
    • 抽样分布:discrete_distributionpiecewise_constant_distributionpiecewise_linear_distribution
    • 伯努利分布:bernoulli_distributiongeometric_distributionbinomial_distributionnegative_binomial_distribution
    • 泊松分布:poisson_distributiongamma_distributionweibull_distributionextreme_value_distributionexponential_distribution

一些随机数发生器类型使用随机数引擎适配器来修改来自引擎的随机序列。引擎适配器有三种类别模板:

  • 用于定义knuth_bshuffle_order_engine
  • 用于定义ranlux24ranlux48
  • 没有在 STL 中应用的。

随机数生成器需要一个或多个种子值来初始化其状态。random_device类定义了可以返回均匀分布的非负整数序列的函数对象,这些非负整数序列在大多数实现中都是不确定的。这些可以用作随机数生成器的种子值。为了确保你将得到高质量的序列,你不应该直接使用随机数引擎——总是使用随机数生成器。

ExercisesModify Ex8_03 to use the shuffle() algorithm to shuffle the cards before dealing.   Extend the solution to the previous exercise to play a game after dealing the four random hands. Each player will play a random card from their hand in turn. Output the cards played in each round and identify the player with the winning card - the winning card being the highest in the sort sequence.   Simulate throwing two standard dice using a single discrete distribution so the distribution object generates values for the sum of the two dice. Generate 5000 throws and plot them in a histogram.   Write a program to estimate the probability of all possible faces being shown together when throwing six standard dice simultaneously by simulating a large number of throws.

九、流操作

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​9) contains supplementary material, which is available to authorized users.

本章回顾了我在第一章中介绍的流迭代器,并详细讨论了它们的功能。它还介绍了流缓冲迭代器,并解释了如何将流和流缓冲迭代器与其他 STL 功能结合使用。在本章中,您将学习:

  • 流迭代器类提供了哪些函数成员。
  • 如何用流迭代器读写单个数据项?
  • 什么是流缓冲迭代器,它们与流迭代器有何不同。
  • 如何使用流迭代器读写文件?
  • 如何使用流缓冲迭代器读写文件?
  • 什么是字符串流,STL 定义了不同类型的字符串流。
  • 如何对字符串流使用流迭代器和流缓冲区迭代器。

流迭代器

如你所知,一个流迭代器是一个单遍迭代器,如果它是一个输入流迭代器,它从一个流中读取;如果它是一个输出流迭代器,它向一个流中写入。流迭代器只能将一种给定类型的数据传入或传出流。如果您想使用流迭代器来传输一系列不同类型的数据项,您必须安排将数据项打包到一个单一类型的对象中,并确保该类型的流插入和/或提取操作符函数存在。与其他迭代器相比,流迭代器有点奇怪。例如,增加一个输入流迭代器不仅仅是移动迭代器指向下一个数据项——它从流中读取一个值。让我们进入细节。

输入流迭代器

输入流迭代器是一种可以在文本模式下从流中提取数据的输入迭代器,这意味着您不能将它用于二进制流。两个流迭代器通常用于读取流中的所有值:指向要读取的第一个值的 begin 迭代器和指向流末尾的 end 迭代器。当输入流的文件尾(EOF)流状态被识别时,结束迭代器被识别。在iterator头中定义的istream_iterator模板使用提取操作符>>从流中读取T类型的值。为此,必须有一个从istream对象中读取T类型值的operator>>()函数重载。因为是输入迭代器,istream_iterator的一个实例是单遍迭代器;它只能使用一次。默认情况下,流被认为包含类型char的字符。

通过向构造函数传递一个输入流对象来创建一个istream_iterator对象。有一个复制构造函数用于复制istream_iterator对象。下面是一个创建输入流迭代器的示例:

std::istream_iterator<string> in  {std::cin};    // Reads strings from cin

std::istream_iterator<string> end_in;            // End-of-stream iterator

默认的构造函数创建一个表示流结束的对象——也就是识别出EOF的时候。

虽然默认情况下流被认为包含类型为char的字符,但是您可以定义输入流迭代器来读取包含另一种类型字符的流。例如,下面是如何定义流迭代器来读取包含wchar_t字符的流:

std::basic_ifstream<wchar_t> file_in {"no_such_file.txt"};      // File stream of wchar_t

std::istream_iterator<std::wstring, wchar_t> in {file_in};      // Reads strings of wchar_t

std::istream_iterator<std::wstring, wchar_t> end_in;            // End-of-stream iterator

第一条语句定义了一个由wchar_t个字符组成的输入文件流。我将在下一节提醒您文件流的一些关键细节。第二条语句定义了一个用于读取文件的流迭代器。流中的字符类型由第二个模板类型参数指定,在本例中为istream_iteratorwchar_t。当然,指定要从流中读取的对象类型的第一个模板类型参数现在必须是wstring,这是由wchar_t字符组成的字符串的类型。

一个istream_iterator对象有以下函数成员:

  • 返回流中当前对象的引用。您可以多次应用该运算符来重新读取相同的值。
  • operator->()返回当前对象在流中的地址。
  • operator++()从底层输入流中读取一个值,并将其存储在迭代器对象中。返回对迭代器对象的引用。因此,表达式*++in的值将是存储的最新新值。这是not的典型用法,因为它可能会跳过流中的第一个值。
  • operator++(int)从底层输入流中读取一个值,并将其存储在迭代器对象中,准备好使用operator*()operator->()进行访问。该函数在存储流中的新值之前返回迭代器对象的代理。这意味着表达式*in++的值是在底层流的最新值被读取和存储之前存储在迭代器中的对象。

还有非成员函数,operator==()operator!=(),用于比较两个相同类型的迭代器对象。如果两个输入迭代器都是同一流的迭代器,或者都是流尾迭代器,则它们相等;否则它们是不平等的。

迭代器和流迭代器

认识到输入流迭代器不同于常规迭代器是很重要的,因为它们与数据项序列相关。正则迭代器指向数组或容器中的元素。递增一个正则迭代器会改变它所指向的对象;这对指向同一序列中元素的其他迭代器没有影响。可以有几个迭代器对象,每个对象指向同一序列中的不同元素。流迭代器就不是这样了。

当您考虑使用流迭代器读取标准输入流时会发生什么时,这一点就很明显了;当流迭代器用于文件时,这可能不那么明显,但它仍然适用。如果创建两个与同一个流相关的输入流迭代器,它们最初都指向第一个数据项。如果使用一个迭代器从流中读取,另一个迭代器将不再引用第一个数据值。当从标准输入流中读取时,值由第一个迭代器使用。这是因为迭代器在读取值时会修改流对象。输入流迭代器不仅改变了它所指向的内容——解引用时得到的内容——还改变了底层流中标识下一个读操作开始位置的位置。因此,给定流的两个或多个输入流迭代器总是指向该流中可用的下一个数据项。这意味着由两个输入流迭代器指定的范围只能由一个开始迭代器和一个流尾迭代器组成;您无法创建两个指向同一流中两个不同值的流迭代器。这并不是说你不能使用输入流迭代器来访问数据项。正如你将会看到的。

使用输入流函数成员读取

下面的代码演示了如何使用函数成员来读取字符串:

std::cout << "Enter one or more words. Enter ! to end:\n";

std::istream_iterator<string> in {std::cin};     // Reads strings from cin

std::vector<string> words;

while(true)

{

string word = *in;

if(word == "!") break;

words.push_back(word);

++in;

}

std::cout << "You entered " << words.size() << " words." << std::endl;

循环从标准输入流中读取单词,并将它们添加到一个向量容器中,直到输入"!"为止。表达式*in的值是来自底层流的当前string对象。++in从流中读取下一个字符串对象,并存储在迭代器中,in。以下是执行此代码的输出示例:

Enter one or more words. Enter ! to end:

Yes No Maybe !

You entered 3 words.

下面是一个工作示例,它说明了如何使用函数成员来读取数字数据,但不一定说明应该如何使用它们:

// Ex9_01.cpp

// Calling istream_iterator function members

#include <iostream>                                   // For standard streams

#include <iterator>                                   // For stream iterators

int main()

{

std::cout << "Enter some integers - enter Ctrl+Z to end.\n";

std::istream_iterator<int> iter {std::cin};       // Create begin input stream iterator...

std::istream_iterator<int> copy_iter {iter};      // ...and a copy

std::istream_iterator<int> end_iter;              // Create end input stream iterator

// Read some integers to sum

int sum {};

while(iter != end_iter)                           // Continue until Ctrl+Z read

{

sum += *iter++;

}

std::cout << "Total is " << sum << std::endl;

std::cin.clear();                                 // Clear EOF state

std::cin.ignore();                                // Skip characters

// Read integers using the copy of the iterator

std::cout << "Enter some more integers - enter Ctrl+Z to end.\n";

int product {1};

while(true)

{

if(copy_iter == end_iter) break;                // Break if Ctrl+Z was read

product *= *copy_iter++;

}

std::cout << "product is " << product << std::endl;

}

在显示输入提示后,我们创建一个输入流迭代器,从cin中读取类型int的值;然后我们复制迭代器对象。在原始对象iter被使用后,我们将能够使用副本copy_iter来读取来自cin的输入,我们只需要一个结束迭代器对象,因为它永远不会改变。第一个循环对使用输入流迭代器读取的所有值求和,直到识别出EOF流状态,该状态通过从流中读取Ctrl+Z标志来设置。解引用iter使得它所指向的值可用,之后后增量操作将iter移动到下一个输入。如果这是Ctrl+Z,循环将结束。

在我们可以从cin读取更多数据之前,我们必须通过调用流对象的clear()来重置EOF标志;我们还需要跳过留在输入缓冲区中的'\n'字符,这是通过调用流对象的ignore()来完成的。第二个循环使用copy_iter读取值并计算它们的乘积。与第一个循环的主要区别在于,通过比较copy_iterend_iter是否相等来终止循环。

下面是一个输出示例:

Enter some integers - enter Ctrl+Z to end.

1 2 3 4^Z

Total is 10

Enter some more integers - enter Ctrl+Z to end.

3 3 2 5 4^Z

product is 360

这不是大多数情况下使用输入流迭代器的方式。通常,您只需使用流开始和流结束迭代器作为函数的参数。您可能意识到了第一个循环和跟随它的输出语句可以被一个语句代替:

std::cout << "Total is " << std::accumulate(iter, end_iter, 0) << std::endl;

下面是一些通过使用输入流迭代器将浮点值从cin插入容器的代码:

std::vector<double> data;

std::cout << "Enter some numerical values - enter Ctrl+Z to end.\n";

std::copy(std::istream_iterator<double>{std::cin}, std::istream_iterator<double>{},

std::back_inserter(data));

任意数量的值将被copy()算法追加到vector容器中,直到Ctrl+Z被读取。有一个用于vector容器的构造函数,它接受一个范围来初始化元素,因此您可以在创建容器的语句中使用输入流迭代器来读取值:

std::cout << "Enter some numerical values - enter Ctrl+Z to end.\n";

std::vector<double> data {std::istream_iterator<double>{std::cin}, std::istream_iterator<double>{}};

这将从标准输入流中读取浮点值,并将它们用作容器中元素的初始值。

输出流迭代器

输出流迭代器由ostream_iterator模板定义,该模板具有第一个模板参数,即要写入的值的类型,以及第二个模板参数,即流中字符的类型;第二个模板参数的默认值为char。一个ostream_iterator对象是一个输出迭代器,它可以以文本模式将任何类型的对象T写入输出流,只要已经实现了将T对象写入流的operator<<()。因为它是一个输出迭代器,所以它支持前增量和后增量操作,并且它是一个单程迭代器。一个输出流迭代器定义了它的复制赋值操作符,这样它就可以使用插入操作符将一个T对象写到一个流中。默认情况下,输出流迭代器将值写成char字符序列。通过将类型指定为第二个模板类型参数,可以编写包含不同类型字符的流。一个ostream_iterato r 类型定义了以下函数成员:

  • 构造函数:第一个构造函数从作为第一个参数的ostream对象和作为第二个参数的分隔符字符串为输出流创建一个 begin 迭代器。输出流对象在其写入流的每个对象后写入分隔符字符串。第二个构造函数省略了第二个参数,它创建了一个迭代器,这个迭代器只写没有后续分隔符的对象。
  • operator=(const T& obj)obj写入流,然后写入分隔符字符串(如果给构造函数指定了一个)。该函数返回对迭代器的引用。
  • 除了返回迭代器对象之外,不做任何事情。要使迭代器符合输出迭代器的条件,必须定义这个操作。
  • 定义了operator++()operator++(int),但是除了返回迭代器对象之外,它们什么也不做。对于一个符合输出迭代器条件的迭代器,必须支持前增量和后增量操作。

不做任何事情的操作符函数是必不可少的,因为它们是输出迭代器的规范的一部分。如果您以文本模式写入一个流,并且随后打算以文本模式读取,则需要在流中的值之间使用分隔符。因此,虽然您可以显式编写分隔符,但带有两个参数的构造函数通常是合适的。

使用输出流迭代器的函数成员编写

下面的示例展示了函数成员的各种使用方式:

// Ex9_02.cpp

// Using output stream iterator function members

#include <iostream>                                   // For standard streams

#include <iterator>                                     // For iterators and begin() and end()

#include <vector>                                     // For vector container

#include <algorithm>                                  // For copy() algorithm

#include <string>

using std::string;

int main()

{

std::vector<string> words {"The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"};

// Write the words container using conventional iterator notation

std::ostream_iterator<string> out_iter1 {std::cout};  // Iterator with no delimiter output

for(const auto& word : words)

{

*out_iter1++ = word;                              // Write a word

*out_iter1++ = " ";                               // Write a delimiter

}

*out_iter1++ = "\n";                                // Write newline

// Write the words container again using the iterator

for(const auto& word : words)

{

(out_iter1 = word) = " ";                         // Write the word and delimiter

}

out_iter1 = "\n";                                   // Write newline

// Write the words container using copy()

std::ostream_iterator<string> out_iter2 {std::cout, " "};

std::copy(std::begin(words), std::end(words), out_iter2);

out_iter2 = "\n";

}

这以三种不同的方式将words容器的元素写入标准输出流。out_iter1流迭代器是通过调用构造函数创建的,只使用输出流作为参数。第一个循环使用传统的输出迭代器符号,在解引用迭代器后递增迭代器,并将word的当前值复制到解引用的结果out_iter1。循环后的语句会向流中写入一个换行符。请注意,您不能这样写:

out_iter1 = '\n';                                      // Won’t compile!

迭代器被定义为将string对象写入流,因此它不能写入任何其他类型的数据。operator=()成员将只接受一个字符串参数,所以语句不会被编译。

如前所述,operator*()成员和前后递增操作符除了返回对迭代器的引用之外什么也不做。因此,您可以省去这些操作,并在没有这些操作的情况下生成相同的输出,如第二个循环中的语句所示。语句中的括号对于确保应用于分隔符的第二个赋值操作将输出迭代器作为其左操作数非常重要。

第三行输出是由copy()算法以你在前面章节中看到的方式产生的。元素的值被复制到out_iter2,它由第二个构造函数参数定义,该参数指定了每个输出值后面的分隔符字符串。

重载插入和提取操作符

您必须为任何想要与流迭代器一起使用的类类型重载插入和提取操作符。这对你自己的班级来说很容易。您可以根据需要提供getset函数来访问任何privatepublic数据成员,或者您可以将运算符函数指定为friend函数。下面是一个简单的表示名称的类的例子,它说明了这一点:

class Name

{

private:

std::string first_name{};

std::string second_name{};

public:

Name() = default;

Name(const std::string& first, const std::string& second) :

first_name{first}, second_name {second} {}

friend std::istream& operator>>(std::istream& in, Name& name);

friend std::ostream& operator<<(std::ostream& out, const Name& name);

};

// Extraction operator for Name objects

inline std::istream& operator>>(std::istream& in, Name& name)

{ return in >> name.first_name >> name.second_name; }

// Insertion operator for Name objects

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{ return out << name.first_name << ' ' << name.second_name; }

通过这里定义的操作符重载,您可以使用流迭代器读写 name 对象,例如:

std::cout << "Enter names as first-name second-name. Enter Ctrl+Z on a separate line to end:\n";

std::vector<Name> names {std::istream_iterator<Name> {std::cin}, std::istream_iterator<Name>{}};

std::copy(std::begin(names), std::end(names), std::ostream_iterator<Name>{std::cout, " "});

容器names将被初始化为你输入的尽可能多的Name对象,直到输入Ctrl+Z结束输入。copy()算法将Name对象复制到输出流迭代器表示的目的地,迭代器将对象写入标准输出流。我们在这里写名字和第二个名字将通过指定字段宽度来防止列中的名字对齐。例如,这不会很好地工作:

for(const auto& name: names)

std::cout << std::setw(20) << name << std::endl;

想法是在一列中输出对齐的名称。这将不起作用,因为宽度规格仅适用于first_name成员。您可以通过更改operator<<()函数来实现这一点,以便它在写出名称之前将它们连接起来:

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{ return out << name.first_name + ' ' + name.second_name; }

由于沿途创建的临时string对象,这比原来的效率低,但是它允许前面的循环按要求工作。

有时,您可能希望根据目标是否是文件来区别对待输出。例如,您可能希望在标准输出流的输出中包含您在写入文件时不想包含的附加信息。您可以通过测试ostream对象的实际类型来解决这个问题:

inline std::ostream& operator<<(std::ostream& out, const Name& name)

{

if(typeid(out) != typeid(std::ostream))

return out << name.first_name << " " << name.second_name;

else

return out << "Name: " << name.first_name << ' ' << name.second_name;

}

现在,一个名字只有在被写到一个属于ostream对象的流中时才会被加上前缀"Name: "。对于输出到文件流,或从ostream派生的其他类型的流,前缀被省略。

对文件使用流迭代器

流迭代器不知道底层流的性质。当然,他们只在文本模式下处理流,否则他们不会关心数据是什么。任何类型的流都可以使用流迭代器在文本模式下读写。这意味着您可以使用流迭代器在文本模式下读写文件。在我详细介绍对文件使用流迭代器之前,我将提醒您文件流的一些基本特征,以及如何创建封装文件的流对象。

对象

文件流封装了一个物理文件。一个文件流有一个长度,它是流中的字符数,所以对于一个新的输出文件它是 0;它有一个开头,是 stream - index 0中第一个字符的索引;它有一个 end,是流中最后一个字符后面的索引。它还有一个当前位置,是下一个读或写操作开始的索引。您可以在文本模式或二进制模式下与流来回传输数据。

在文本模式下,数据是一个字符序列。可以使用提取和插入操作符读取或写入数据,因此至少对于输入,数据项必须由一个或多个空白字符分隔。数据通常被写成由'\n'终止的一系列行。有些系统,如 Microsoft Windows,在阅读或书写时会转换换行符。Microsoft Windows 将换行符写成两个字符:回车和换行符。当读取回车符和换行符时,它们被映射成一个字符'\n'。在其他系统中,换行符是作为单个字符来读写的。因此,文件输入流的长度可以取决于它所源自的系统环境。

在二进制模式下,字节在内存和流之间传输,不进行转换。流迭代器只在文本模式下工作,所以不能使用流迭代器来读写二进制文件。我将在本章后面解释的流缓冲迭代器可以读写二进制文件。

尽管二进制模式操作可以不加修改地在内存中来回传输字节,但是在处理写在不同系统上的二进制文件时,仍然存在缺陷。一个考虑因素是写入文件的系统的字节顺序与读取文件的系统的字节顺序。字节序决定了内存中一个字的字节写入的顺序。在小字节序处理器中,例如 Intel 的 x86 处理器,最低有效字节在最低地址中,因此字节以从最低有效到最高有效的顺序写入。在 IBM 大型机这样的大端序处理器中,字节的顺序相反,最高有效字节位于低位地址,因此它们在文件中的顺序与小端序处理器相反。因此,当您在小端系统上从大端系统读取二进制文件时,您需要考虑字节顺序的差异。

Note

大端字节顺序也称为网络字节顺序,因为数据通常以大端顺序在互联网上传输。

文件流类模板

有三个表示文件流的类模板:ifstream表示文件输入流,ofstream定义输出的文件流,fstream定义可以读写的文件流。这些的等级结构如图 9-1 所示。

A978-1-4842-0004-9_9_Fig1_HTML.gif

图 9-1。

Inheritance hierarchy for class templates that represent file streams

文件流模板继承自istream和/或ostream,因此在文本模式下,它们的工作方式与标准流相同。你可以对文件流做什么是由它的打开模式决定的,你可以通过下列常量的组合来指定,这些常量在ios_base类中定义:

  • binary设置二进制模式。如果未设置二进制模式(这是默认设置),则模式为文本模式。
  • app:每次写入前移动到文件末尾(app end 操作)。
  • ate打开文件后移动到文件末尾(在末尾)。
  • in打开文件进行阅读。这是一个ifstream对象和一个fstream对象的默认值。
  • out打开文件进行写入。这是一个ostream对象和一个fstream对象的默认值。
  • trunc将现有文件截短为零长度。

默认情况下,文件流对象以文本模式创建;要获得二进制模式,必须指定binary常量。文本模式操作使用>><<操作符来读取和写入数据,数值在写入流之前被转换成字符表示。在二进制模式下,没有数据转换;内存中的字节直接写入文件。当您为一个不存在的文件指定一个名称作为ofstream构造函数的参数时,该文件将被创建。如果在创建或打开文件输出流对象时没有指定appate,任何现有的文件内容都将被覆盖。

本章中的一些工作示例所读取的dictionary.txt文件包含在代码下载中。这是一个在 Microsoft Windows 环境中以文本模式编写的文件,但是如果您在不同的环境中执行它,示例仍然应该可以读取它。示例使用驱动器G:上的 Microsoft Windows 路径。我这样做是为了让您更有可能需要更改这些以适应您的系统环境。这让您有责任确保不会覆盖重要的文件。

使用流迭代器的文件输入

一旦创建了用于读取文件的文件流对象,使用流迭代器访问数据本质上与从标准输入流读取数据是一样的。我们可以编写一个程序,通过在代码下载中的字典文件中查找一个单词的变位词。在这种情况下,我们将使用流迭代器将字典文件中的所有单词读入一个容器。下面是代码:

// Ex9_03.cpp

// Finding anagrams of a word

#include <iostream>                                    // For standard streams

#include <fstream>                                     // For file streams

#include <iterator>                                       // For iterators and begin() and end()

#include <string>                                      // For string class

#include <seT>                                         // For set container

#include <vector>                                      // For vector container

#include <algorithm>                                   // For next_permutation()

using std::string;

int main()

{

// Read words from the file into a set container

string file_in {"G:/Beginning_STL/dictionary.txt"};

std::ifstream in {file_in};

if(!in)

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

std::set<string> dictionary {std::istream_iterator<string>(in), std::istream_iterator<string>()};

std::cout << dictionary.size() << " words in dictionary." << std::endl;

std::vector<string> words;

string word;

while(true)

{

std::cout << "\nEnter a word, or Ctrl+z to end: ";

if((std::cin >> word).eof()) break;

string word_copy {word};

do

{

if(dictionary.count(word))

words.push_back(word);

std::next_permutation(std::begin(word), std::end(word));

} while(word != word_copy);

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

words.clear();                                              // Remove previous permutations

}

in.close();                                                // Close the file

}

字典文件中有超过 100,000 个单词,因此可能需要几秒钟来阅读它。使用文件的完整路径dictionary.txt创建一个ifstream对象。这是一个文本文件,包含合理数量的不同单词,可以通过搜索来检查字谜。整个文件内容被用作集合容器的初始值。如你所知,一个集合容器将按升序存储单词,容器中的每个单词都是它自己的键。words容器存储从cin输入的单词的变位词。在 while 循环的第一个 if 表达式中读取每个单词。这将调用流对象的eof(),当输入Ctrl+Z时将返回 true。通过调用内部do-while循环中的next_permutation()算法来重新排列输入单词中的字母。为每个排列调用count(),包括第一个,确定单词是否在字典容器中。如果是,这个单词将被追加到words容器中。当排列返回到原始单词时,do-while循环结束。当一个单词的所有变位词都被找到时,使用copy()算法将这些单词写入cout,输出流迭代器作为目的地。如果您预计会出现八个以上的变位词,您可以使用一个循环在多行上生成输出:

size_t count {}, max {8};

for(const auto& wrd : words)

std::cout << wrd << ((++count % max == 0) ? '\n' : ' ');

以下是一些输出示例:

109582 words in dictionary.

Enter a word, or Ctrl+z to end: realist

realist retails saltier slatier tailers

Enter a word, or Ctrl+z to end: painter

painter pertain repaint

Enter a word, or Ctrl+z to end: dog

dog god

Enter a word, or Ctrl+z to end: ^Z

使用流迭代器重复读取文件

当然,如果字典文件非常大,您可能不希望将它全部读入内存。在这种情况下,每次想要查找变位词时,可以使用流迭代器来重新读取文件。这里有一个版本可以做到这一点——尽管它的性能并不令人印象深刻:

// Ex9_04.cpp

// Finding anagrams of a word by re-reading the dictionary file

// include directives & using directive as Ex9_03.cpp...

int main()

{

string file_in {"G:/Beginning_STL/dictionary.txt"};

std::ifstream in {file_in};

if(!in)

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

auto end_iter = std::istream_iterator<string> {};

std::vector<string> words;

string word;

while(true)

{

std::cout << "\nEnter a word, or Ctrl+z to end: ";

if((std::cin >> word).eof()) break;

string word_copy {word};

do

{

in.seekg(0);                                           // File position at beginning

// Use find() algorithm to read the file to check for an anagram

if(std::find(std::istream_iterator<string>(in), end_iter, word) != end_iter)

words.push_back(word);

else

in.clear();                                         // Reset EOF

std::next_permutation(std::begin(word), std::end(word));

} while(word != word_copy);

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

words.clear();                                             // Remove previous permutations

}

in.close();                                               // Close the file

}

结束流迭代器没有改变,所以它被定义为end_iter以允许它被多次使用。这个循环基本上是相同的,只是使用了find()算法来发现给定的排列是否在文件中,因此是一个变位词。文件位置需要是第一个字符位置,调用文件流对象的seekg()可以确保这一点。find()的前两个参数是istream_iterator<string>对象,它们定义了从当前文件位置(设置为开头)到文件结尾的范围。find()算法返回一个迭代器,指向与第三个参数匹配的元素,如果不存在,则返回结束迭代器。因此当find()返回结束流迭代器时,word没有找到;返回的任何其他迭代器都意味着找到了它。当没有找到word时,调用clear()让文件流对象清除EOF标志是必要的。如果不这样做,随后读取文件的尝试将会失败,因为EOF标志被设置。

下面是一些示例输出,展示了它的工作原理:

Enter a word, or Ctrl+z to end: rate

rate tare tear erat

Enter a word, or Ctrl+z to end: rat

rat tar art

Enter a word, or Ctrl+z to end: god

god dog

Enter a word, or Ctrl+z to end: ^Z

我选择输入短词,因为检查字谜的过程非常慢。一个有n个字符的单词有n!种排列。检查一个排列是否在文件中需要大约 100,000 次读取操作,这取决于它是否在文件中。因此,检查像“retain”这样的单词需要超过 700 万次的读取操作,所以这是一个缓慢过程的原因之一。一个istream_iterator<T>对象从一个流中一次读取一个T对象,所以如果有很多对象,它总是会很慢。一旦文件被读取以初始化set容器,Ex9_03.cppEx9_04.cpp快得多,因为所有后续操作都是在内存中使用字典wordsEx9_03.cpp更快的第二个原因是访问一个集合容器涉及一个二分搜索法,它是O(log n);串行访问文件包括从第一个单词开始读取每个单词,直到找到匹配,这是O(n)。如果文件中的数据是有序的(如dictionary.txt中的单词),你可以使用二分搜索法技术来查找数据项。在这种情况下,使用流迭代器是多余的,因为您将总是读取单个单词,使用流对象的>>操作符可以更容易地做到这一点。然而,这并不容易实现,因为这些字的大小不同。

使用流迭代器的文件输出

写入文件与写入标准输出流没有什么不同。例如,您可以使用流迭代器复制dictionary.txt文件的内容,如下所示:

// Ex9_05.cpp

// Copying file contents using stream iterators

#include <iostream>                              // For standard streams

#include <fstream>                               // For file streams

#include <iterator>                              // For iterators and begin() and end()

#include <string>                                // For string class

using std::string;

int main()

{

string file_in {"G:/Beginning_STL/dictionary.txt"};

std::ifstream in {file_in};

if(!in)

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

string file_out {"G:/Beginning_STL/dictionary_copy.txt"};

std::ofstream out {file_out, std::ios_base::out | std::ios_base::trunc };

std::copy(std::istream_iterator<string> {in}, std::istream_iterator<string> {},

std::ostream_iterator<string> {out, " "});

in.clear();                                              // Clear EOF

std::cout << "Original file length: " << in.tellg() << std::endl;

std::cout << "File copy length: " << out.tellp() << std::endl;

in.close();

out.close();

}

这个程序将单词从输入文件复制到输出文件,在输出中用空格分隔单词。这个程序总是覆盖输出文件的内容。这是我得到的输出:

Original file length: 1154336

File copy length: 1154336

除了ios_base::out标志之外,输出文件流还指定了打开模式标志ios_base::trunc,因此如果文件已经存在,它将被截断。如果多次运行该示例,这可以防止创建不断增长的文件。如果你用编辑器检查dictionary.txt的内容,你会看到单词被一个空格隔开。我们写文件副本时,单词之间只有一个空格,所以文件的长度是一样的。但是,如果原始文件中的单词由两个或更多空格分隔,文件副本会更短。为了确保使用流迭代器精确地复制原始文件,必须一个字符一个字符地读取文件,并防止>>操作符忽略空白。你可以这样做:

std::copy(std::istream_iterator<char>{in >> std::noskipws}, std::istream_iterator<char>{},

std::ostream_iterator<char> {out});

这会将in流复制为字符,包括空白。你可以用流缓冲迭代器更快地复制文件,我将在本章后面解释。

流迭代器和算法

您已经看到,您可以将诸如find()copy()这样的算法与流迭代器一起使用。您可以使用流迭代器为任何接受输入迭代器来指定数据源的算法指定数据源。如果算法需要正向、双向或随机访问迭代器来定义输入,则不能使用流迭代器。当一个算法接受一个输出迭代器作为目的地时,它可以是一个输出流迭代器。这里有一个例子,使用带有流迭代器的count_if()算法来确定首字母相同的单词在dictionary.txt中出现的频率:

// Ex9_06.cpp

// Using count_if() with stream iterators to count word frequencies

#include <iostream>                                    // For standard streams

#include <iterator>                                        // For iterators and begin() and end()

#include <iomanip>                                     // For stream manipulators

#include <fstream>                                     // For ifstream

#include <algorithm>                                   // For count_if()

#include <string>

using std::string;

int main()

{

string file_in {"G:/Beginning_STL/dictionary.txt"};

std::ifstream in {file_in};

if(!in)

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

string letters {"abcdefghijklmnopqrstuvwxyz"};

const size_t perline {9};

for(auto ch : letters)

{

std::cout << ch << ": "

<< std::setw(5)

<< std::count_if(std::istream_iterator<string>{in}, std::istream_iterator<string>{},

&ch

{ return s[0] == ch; })

<< (((ch - 'a' + 1) % perline) ? " " : "\n");

in.clear();                                            // Clear EOF...

in.seekg(0);                                           // ... and back to the beginning

}

std::cout << std::endl;

}

我得到了这样的输出:

a:  6541 b:  6280 c: 10324 d:  6694 e:  4494 f:  4701 g:  3594 h:  3920 i:  4382

j:  1046 k:   964 l:  3363 m:  5806 n:  2475 o:  2966 p:  8448 q:   577 r:  6804

s: 12108 t:  5530 u:  3312 v:  1825 w:  2714 x:    79 y:   370 z:   265

这个程序使用流迭代器演示了count_if()算法,但是效率非常低。for循环遍历letter中的字符,并在每次迭代时调用count_if(),通过遍历文件中的所有单词来计算从当前字母开始的单词数。因为输入文件是有序的,所以不需要每次都读取整个文件。使用for_each()算法,我们可以更快地得到同样的结果:

std::map <char, size_T> word_counts;           // Stores word count for each initial letter

size_t perline {9};                            // Outputs per line

// Get the words counts for each initial letter

std::for_each(std::istream_iterator<string>{in}, std::istream_iterator<string>{},

&word_counts {word_counts[s[0]]++;});

std::for_each(std::begin(word_counts), std::end(word_counts),     // Write out the counts

perline

{ std::cout << pr.first << ": "

<< std::setw(5) << pr.second

<< (((pr.first - 'a' + 1) % perline) ? " " : "\n");

});

std::cout << std::endl;

第一次调用for_each()算法遍历文件中的单词,并在第一次将带有给定首字母的单词传递给 lambda 表达式时,在word_counts容器中存储一个新的pair。当一个单词遇到先前已经找到的首字母时,pair的值递增。第二个for_each()调用从map输出元素。这个文件只被处理一次,所以它比以前的版本快了 26 倍。

generate_n()算法与流迭代器一起工作。下面是如何将一个流迭代器传递给一个算法来创建一个包含斐波那契数列中的一系列数字的文件,然后读取该文件以验证它是否工作:

// Ex9_07.cpp

// Using stream iterators to write Fibonacci numbers to a file

#include <iostream>                                // For standard streams

#include <iterator>                                // For iterators and begin() and end()

#include <iomanip>                                 // For stream manipulators

#include <fstream>                                 // For fstream

#include <algorithm>                               // For generate_n() and for_each()

#include <string>

using std::string;

int main()

{

string file_name {"G:/Beginning_STL/fibonacci.txt"};

std::fstream fibonacci {file_name, std::ios_base::in | std::ios_base::out |   std::ios_base::trunc};

if(!fibonacci)

{

std::cerr << file_name << " not open." << std::endl;

exit(1);

}

unsigned long long first {0ULL}, second {1ULL};

auto iter = std::ostream_iterator<unsigned long long> {fibonacci, " "};

(iter = first) = second;                         // Write the first two values

const size_t n {50};

std::generate_n(iter, n, [&first, &second]

{ auto result = first + second;

first = second;

second = result;

return result; });

fibonacci.seekg(0);                                      // Back to file beginning

std::for_each(std::istream_iterator<unsigned long long> {fibonacci},

std::istream_iterator<unsigned long long> {},

[](unsigned long long k)

{ const size_t perline {6};

static size_t count {};

std::cout << std::setw(12) << k << ((++count % perline) ? " " : "\n");

});

std::cout << std::endl;

fibonacci.close();                                       // Close the file

}

这使用了一个fstream对象来封装文件,文件最初不会存在。一个fstream对象既可以写也可以读文件,默认情况下,它只打开存在的文件。将ios_base::trunc指定为打开模式标志会导致文件被创建(如果它不存在的话),如果它存在的话会导致内容被截断。Fibonacci 数增长很快,所以我使用unsigned long long作为值的类型,并且将数字限制为 50,除了前两个。前两个数字在firstsecond中定义,并使用iter写入文件,这是一个输出流迭代器。这使得文件位置比文件中的second值多一位,因此由generate_n()算法写入的 50 个值将跟随其后。写入值后,调用seekg()(查找获取数据)将文件设置回起始位置,准备读取。您可以使用seekp()(查找以存放数据)来重置文件位置以写入数据。

使用for_each()算法将文件内容写入标准输出流。lambda 表达式将六个值写入一行。您可以使用generate_n()将任何类型的值序列写入一个文件,您可以使用 function 对象生成该文件。假设您需要一个具有正态分布的随机温度值文件作为测试数据源。下面是如何使用流迭代器和generate_n()来实现这一点:

// Ex9_08.cpp

// Using stream iterators to create a file of random temperatures

#include <iostream>                        // For standard streams

#include <iterator>                        // For iterators and begin() and end()

#include <iomanip>                         // For stream manipulators

#include <fstream>                         // For file streams

#include <algorithm>                       // For generate_n() and for_each()

#include <random>                          // For distributions and random number generator

#include <string>                          // For string class

using std::string;

int main()

{

string file_name {"G:/Beginning_STL/temperatures.txt"};

std::ofstream temps_out {file_name, std::ios_base::out | std::ios_base::trunc};

const size_t n {50};                                // Number of temperatures required

std::random_device rd;                              // Non-determistic source

std::mt19937 rng {rd()};                            // Mersenne twister generator

double mu {50.0}, sigma {15.0};                     // Mean: 50 degrees SD: 15

std::normal_distribution<> normal {mu, sigma};      // Create distribution

// Write random temperatures to the file

std::generate_n(std::ostream_iterator<double> { temps_out, " "}, n,

[&rng, &normal]

{ return normal(rng); });

temps_out.close();                                  // Close the output file

// List the contents of the file

std::ifstream temps_in {file_name};                 // Open the file to read it

for_each(std::istream_iterator<double> {temps_in}, std::istream_iterator<double> {},

[](double t)

{ const size_t perline {10};

static size_t count {};

std::cout << std::fixed << std::setprecision(2) << std::setw(5) << t

<< ((++count % perline) ? " " : "\n");

});

std::cout << std::endl;

temps_in.close();                                       // Close the input file

}

我得到了以下输出:

59.61 53.71 42.76 61.45 48.43 43.48 59.09 36.76 62.12 35.13

55.85 58.72 35.34 39.95 49.31 33.42 41.88 46.63 57.89 32.39

52.36 49.56 68.11 44.49 49.72 48.30 33.48 77.92 58.02 19.17

47.75 31.14 24.13 37.18 44.04 30.64 65.47 55.15 68.73 54.17

62.88 35.45 70.11  9.67 25.89 39.71 72.83 90.08 57.25 51.40

这种工作方式与前面的例子Ex9_07类似,除了文件是用一个ofstream对象创建的,然后用一个ifstream对象读取。λ表达式是generate_n()的最后一个参数,它产生写入文件的值;它返回随机浮点温度,正态分布,平均值为50,标准差为15normal对象定义了分布,rng对象是随机数生成器。虽然可以在generate_n()中使用流迭代器,但是不能在generate()算法中使用,因为它需要前向迭代器。

流缓冲迭代器

流缓冲迭代器与流迭代器的不同之处在于,它们只将字符传入或传出流缓冲区。它们直接访问流的缓冲区,因此不涉及插入和提取操作符。没有数据转换,也不需要数据中的分隔符,尽管如果有分隔符,您可以自己处理它们。因为流缓冲区迭代器不需要数据转换就可以读写字符,所以它们可以处理二进制文件。对于读写字符,流缓冲迭代器比流迭代器更快。istreambuf_iterator模板定义输入迭代器,而ostreambuf_iterator模板定义输出迭代器。您可以构造流缓冲区迭代器,读取或写入任何类型的字符charwchar_tchar16_tchar32_t

输入流缓冲区迭代器

要创建输入流缓冲区迭代器以从流中读取给定类型的字符,需要将流对象传递给构造函数:

std::istreambuf_iterator<char> in {std::cin};

这个对象是一个输入流缓冲迭代器,它将从标准输入流中读取类型为char的字符。表示流尾迭代器的对象由默认构造函数生成:

std::istreambuf_iterator<char> end_in;

您可以使用这两个迭代器将一个字符序列从cin读入到string中,直到Ctrl+Z被输入到单独的一行中,以表示流的结束——例如:

std::cout << "Enter something: ";

string rubbish {in, end_in};

std::cout << rubbish << std::endl;               // Whatever you enter will be output

string对象rubbish将用您从键盘输入的所有字符进行初始化,直到识别出流的结尾。

输入流缓冲区迭代器具有以下函数成员:

  • operator*()返回流中当前字符的副本。流位置不会前移,因此您可以重复获取当前字符。
  • 访问当前角色的成员——如果它有成员的话。
  • operator++()operator++(int)都将流位置移动到下一个字符。operator++()在移动位置后返回流迭代器,operator++(int)在移动位置前返回流迭代器的代理。前缀++运算符很少使用。
  • equal()接受另一个输入流缓冲区迭代器的参数,如果当前迭代器和参数都不是流尾迭代器,或者都是流尾迭代器,则返回true。如果其中只有一个是流尾迭代器,则返回false

还有非成员函数,operator==()operator!=(),它们比较两个迭代器。您不必依赖流的结尾来终止输入。您可以使用递增和取消引用操作符从流中读取字符,直到找到特定的字符。例如:

std::istreambuf_iterator<char> in {std::cin};

std::istreambuf_iterator<char> end_in;

char end_ch {'*'};

string rubbish;

while(in != end_in && *in != end_ch) rubbish += *in++;

std::cout << rubbish << std::endl;               // Whatever you entered up to '*' or EOF

while循环从cin开始读取字符,直到识别出流的结尾,或者直到输入星号并按下 Enter 键。循环体中应用于in的解引用操作符返回流中的当前字符,然后后缀增量操作符移动迭代器指向下一个字符。注意,在循环表达式中解引用in表明它不会改变迭代器;只要不是'*',在迭代器递增之前,在循环体中再次读取同一个字符。

输出流缓冲区迭代器

您可以创建一个ostreambuf_iterator对象,通过将 stream 对象传递给构造函数,将给定类型的字符写入流中:

string file_name {"G:/Beginning_STL/junk.txt"};

std::ofstream junk_out {file_name};

std::ostreambuf_iterator<char> out {junk_out};

out对象可以将类型为char的字符写入文件输出流junk_out,该输出流封装了名为junk.txt的文件。要编写不同类型的字符,例如char32_t,只需指定模板类型参数作为字符类型。当然,必须为字符类型创建流,所以不能使用ofstream,因为ofstream是类型basic_ofstream<char>的别名。这里有一个你可以怎么做的例子:

string file_name {"G:/Beginning_STL/words.txt"};

std::basic_ofstream<char32_T> words_out {file_name};

std::ostreambuf_iterator<char32_T> out {words_out};

这个流缓冲区迭代器可以将 Unicode 字符写入流缓冲区。类型为wchar_t的字符的文件流由别名wofstream定义。

还可以通过将流缓冲区的地址传递给构造函数来创建输出流缓冲区对象。您可以通过编写以下代码生成上面的对象out:

std::ostreambuf_iterator<char> out {junk_out.rdbuf()};

对象的成员返回流的内部缓冲区的地址。rdbuf()成员继承自ios_base,它是所有流对象的基类。

一个ostreambuf_iterator对象有以下函数成员:

  • 将作为参数的字符写入流缓冲区。如果EOF被识别,这将是当流缓冲区满时,写操作失败。
  • 当前一次写入缓冲器失败时,failed()返回true。这将是当EOF被识别,因为输出流缓冲区已满。
  • operator*()无所作为。之所以这样定义,是因为它要求一个ostreambuf_iterator对象是一个输出迭代器。
  • operator++()operator++(int)什么都不做。定义这些是因为它们是ostreambuf_iterator对象成为输出迭代器所必需的。

您通常关心的唯一函数成员是赋值操作符。下面是使用它的一种方法:

string ad {"Now is the discount of our winter tents!\n"};

std::ostreambuf_iterator<char> iter {std::cout};      // Iterator for output to cout

for(auto ch: ad)

iter = ch;                                          // Write the character to the stream

执行这段代码会将字符串逐字符写入标准输出流。当然,您可以通过使用copy()算法获得相同的结果:

std::copy(std::begin(ad), std::end(ad), std::ostreambuf_iterator<char> {std::cout});

我相信您知道,这两个例子都是对以下语句进行编码的可笑方式:

std::cout << ad;

尽管它没有告诉你太多关于输出流缓冲迭代器的信息...

对文件流使用流缓冲区迭代器

您可以使用流缓冲迭代器一个字符一个字符地复制文件,没有格式化读写的开销。这是一个复制dictionary.txt的程序:

// Ex9_09.cpp

// Copying a file using stream buffer iterators

#include <iostream>                                    // For standard streams

#include <iterator>                                    // For iterators and begin() and end()

#include <fstream>                                     // For file streams

#include <string>                                      // For string class

using std::string;

int main()

{

string file_name {"G:/Beginning_STL/dictionary.txt"};

std::ifstream file_in {file_name};

if(!file_in)

{

std::cerr << file_name << " not open." << std::endl;

exit(1);

}

string file_copy {"G:/Beginning_STL/dictionary_copy.txt"};

std::ofstream file_out {file_copy, std::ios_base::out | std::ios_base::trunc};

std::istreambuf_iterator<char> in {file_in};             // Input stream buffer iterator

std::istreambuf_iterator<char> end_in;                   // End of stream buffer iterator

std::ostreambuf_iterator<char> out {file_out};           // Output stream buffer iterator

while(in != end_in)

out = *in++;                                           // Copy character from in to out

std::cout << "File copy completed." << std::endl;

file_in.close();                                         // Close the file

file_out.close();                                        // Close the file

}

这会将由ifstream对象file_in封装的文件复制到由ofstream对象file_out封装的文件中。通过将输入文件流缓冲区逐字符复制到输出文件流缓冲区来复制输入文件。while循环使用流缓冲对象inout进行复制。解引用in返回输入缓冲区中的当前字符,后缀++操作符将迭代器推进到输入缓冲区中的下一个字符。输出流缓冲区对象的赋值操作将作为右操作数的字符存储在输出流缓冲区中,并将迭代器推进到输出缓冲区中的下一个位置。

这演示了直接使用流缓冲对象的函数成员,但是你也可以使用copy()算法。您可以用一条语句替换while循环和定义inend_inout的语句:

std::copy(std::istreambuf_iterator<char> {file_in}, std::istreambuf_iterator<char> {},

std::ostreambuf_iterator<char>{file_out});

这会将前两个迭代器指定的范围复制到第三个参数指定的迭代器。文件缓冲区代表整个文件流的窗口,必要时会进行调整。因此,当输入缓冲区被读取时,它从流中被补充,当输出缓冲区满时,它被写入输出流。

流缓冲迭代器不关心原始文件是如何编写的。您可以将文件流定义为由wchar_t字符组成的流,这是两个字节的字符,如下所示:

std::wifstream file_in {file_name};

std::wofstream file_out {file_copy, std::ios_base::out | std::ios_base::trunc};

然后,您可以将原始文件复制为wchar_t字符:

std::copy(std::istreambuf_iterator<wchar_T>{file_in}, std::istreambuf_iterator<wchar_T>{},

std::ostreambuf_iterator<wchar_T> {file_out});

只需要改变流缓冲迭代器的模板类型参数。

字符串流、流和流缓冲区迭代器

您可以使用流迭代器和流缓冲区迭代器在字符串流之间来回传输数据。字符串流是代表 I/O 内存中字符缓冲区的对象,是在sstream标题中定义的三个模板之一的实例:

  • basic_istringstream支持从内存中的字符缓冲区读取数据。
  • 支持将数据写入内存中的字符缓冲区。
  • 支持字符缓冲区的输入和输出操作。

字符数据类型是一个模板参数,对于类型char : istringstreamostringstreamstringstream,字符串流有类型别名。这些的继承层次如图 9-2 所示。

A978-1-4842-0004-9_9_Fig2_HTML.gif

图 9-2。

Inheritance hierarchy for string stream types

我相信您会注意到,直接和间接基类与文件流类型的基类是相同的。这意味着几乎任何你可以用文件流做的事情,你也可以用字符串流做。您可以使用插入和提取运算符对字符串流执行格式化的 I/O;这意味着您可以使用流迭代器读取或写入它们。它们还支持文件流支持的无格式 I/O 操作,因此您可以使用流缓冲区迭代器来读取或写入它们。

字符串流类型有别名来存储类型wchar_t的字符;这些名字是以'w'为前缀的char别名的名字。我将只对类型char使用字符串流,因为它们是最常用的。

能够对内存中的缓冲区执行 I/O 操作提供了巨大的灵活性。当你需要多次读取数据时,从内存中的缓冲区读取要比从外部设备读取快得多。出现这种情况的一种情况是,输入流的内容是可变的,您需要多次读取它,以确定数据是什么。我可以用新版本的Ex9_03.cpp演示如何使用流缓冲迭代器的字符串流:

// Ex9_10.cpp

// Using a string stream as the dictionary source to anagrams of a word

#include <iostream>                                    // For standard streams

#include <fstream>                                     // For file streams

#include <iterator>                                       // For iterators and begin() and end()

#include <string>                                      // For string class

#include <seT>                                         // For set container

#include <vector>                                      // For vector container

#include <algorithm>                                   // For next_permutation()

#include <sstream>                                     // For string streams

using std::string;

int main()

{

string file_in {"G:/Beginning_STL/dictionary.txt"};

std::ifstream in {file_in};

if(!in)

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

std::stringstream instr;                             // String stream for file contents

std::copy(std::istreambuf_iterator<char>{in}, std::istreambuf_iterator<char>(),

std::ostreambuf_iterator<char>{instr});

in.close();                                          // Close the file

std::vector<string> words;

string word;

auto end_iter = std::istream_iterator<string> {};    // End-of-stream iterator

while(true)

{

std::cout << "\nEnter a word, or Ctrl+z to end: ";

if((std::cin >> word).eof()) break;

string word_copy {word};

do

{

instr.clear();                                   // Reset string stream EOF

instr.seekg(0);                                  // String stream position at beginning

// Use find() to search instr for word

if(std::find(std::istream_iterator<string>(instr), end_iter, word) != end_iter)

words.push_back(word);                             // Store the word found

std::next_permutation(std::begin(word), std::end(word));

} while(word != word_copy);

std::copy(std::begin(words), std::end(words), std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;

words.clear();                                         // Remove previous anagrams

}

}

通过copy()算法将dictionary.txt的全部内容复制到一个stringstream对象中。复制过程使用流缓冲迭代器,所以不涉及数据转换——来自文件的字节被复制到instr对象。当然,您可以将格式化的 I/O 操作与流迭代器一起使用,在这种情况下,复制操作应该是:

std::copy(std::istream_iterator<string>{in}, std::istream_iterator<string>(),

std::ostream_iterator<string>{instr, " "});

这当然证明了流迭代器可以处理字符串流对象,但是会比前一个版本慢很多。有一种更快的方法将文件内容复制到stringstream对象:

instr << in.rdbuf();

对象的成员返回封装文件内容的 ?? 对象的地址。basic_filebufbasic_streambuf作为基类,并且operator<<()被重载以将字符从右操作数指向的basic_streambuf对象插入到左操作数的basic_ostream对象中。这是一个快速操作,因为不涉及格式化或数据转换。

搜索instr的字谜和搜索文件流是一样的,因为它是一个流——它只是碰巧在内存中。从字符串流中读取会移动当前位置,所以当您想要再次读取内容时,您必须调用它的seekg()成员来将位置重置回起始位置。类似地,读取到instr中数据的末尾会设置 EOF 标志,您必须调用clear()成员来重置该标志;否则,后续的读取操作将会失败。

下面是来自Ex9_10.cpp的一些示例输出:

Enter a word, or Ctrl+z to end: part

part prat rapt tarp trap

Enter a word, or Ctrl+z to end: painter

painter pertain repaint

Enter a word, or Ctrl+z to end: ^Z

这在我的系统上比Ex9_04.cpp要快,但还是不令人印象深刻。它分析四个字母的单词相当快,但七个字母的单词需要更长时间——比将文件内容读入set容器的Ex9_03.cpp版本慢。除了七个字母的单词大约是四个字母的单词的 210 倍之外,这在一定程度上表明了使用提取操作符进行格式化输入的开销有多大。另一个慢得多的原因是访问set容器来查找单词使用了二分搜索法,但是这里我们是从开始顺序搜索字符串流中的单词。

摘要

在这一章中,我解释了 STL 帮助你处理流的各种方法。流迭代器读写格式化的字符流,流缓冲区迭代器在内存和流之间传输字节,不进行转换。流迭代器是由类模板定义的。istream_iterator定义用于读取流的单次输入迭代器,而ostream_iterator定义用于写入流的单次输出迭代器。要读取或写入的数据类型由第一个模板类型参数定义。第二个模板类型参数标识流的字符类型,并具有类型char的默认值。istreambuf_iterator类模板定义了读取流的流缓冲迭代器,而ostreambuf_iterator模板定义了写入流的迭代器。流中的字符类型由第一个模板类型参数定义,默认类型为char

您可以使用 stream 和 steam buffer 迭代器的函数成员来读取和写入流,正如一些示例所演示的那样,但这很少是必要的或可取的。直接对流使用流提取或插入操作符通常更简单、更有效。这些迭代器主要用于算法。能够使用输入流迭代器将文件的内容转移到算法,并使用输出流迭代器将结果写入另一个文件,这是一种非常强大的机制。流迭代器和流缓冲区迭代器通常可以极大地简化读写文件所需的代码,但是与使用流类提供的 I/O 功能相比,您要付出执行时间增加的代价。在数据量不大的情况下,为了代码的简单性,开销是一个合理的代价。但是,当读取或写入大量数据,或者重复读取或写入流时,开销可能是不可接受的。

ExercisesWrite a program that stores a first name and the age of a person as an object of type std::pair<string, size_t>. The program should read an arbitrary number of first name/age pairs and write them to an output file. The program should then close the file, open it as an input file, read the pair objects from the file, and write them to the standard output stream. All input and output should be carried out using stream iterators.   Write a program that will read the file produced by the solution to Exercise 1, and write a new file containing the pair objects in reverse order. All input and output should use stream iterators.   Write a program to read the contents of the file produced by the solution to Exercise 1 into a stringstream object using stream buffer iterators. Access the string and size_t values in the stringstream using an input stream iterator and write them as pair objects to a container; choose the container such that the pair objects are in ascending sequence of the names. Output the contents of the container to the standard output stream using stream iterators to demonstrate that everything works as it should.   Use stream iterators to write one hundred random integers to a file with values that are uniformly distributed between zero and one million. Use algorithms and stream iterators to determine the minimum and maximum values, and to calculate the average. Output the calculated values, then the values from the file, eight on each line. Use iterators for all input and output.

十、处理数字、时间和复杂数据

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-0004-9_​10) contains supplementary material, which is available to authorized users.

这一章是关于 STL 支持的三个领域,它们比其他领域更专业。numeric头定义了 STL 特征,使数字数据处理更容易或更有效。chrono表头提供处理时间的功能,包括挂钟时间和时间间隔。最后,complex头定义了支持复数运算的类模板。

在本章中,您将学习:

  • 如何创建用于存储数字数据的valarray对象。
  • 什么是对象,以及如何创建和使用它们。
  • 什么是对象以及如何使用它们。
  • ratio类模板的用途以及如何使用它。
  • 如何访问和使用硬件的时钟。
  • 如何创建封装复数的对象以及可以应用于这些对象的操作。

数值计算

数值计算的效率在许多工程、科学和数学领域都非常重要。虽然这些上下文可能是专用的,但是有许多相对常见的应用环境可能涉及密集的数值计算。语音识别或数字录音等音频处理将涉及数字滤波,这是一个非常耗费处理器资源的过程。数字图像处理在 CT 和 MRI 扫描仪等医疗设备中非常普遍,但在其他常见的应用中也是如此——如果您使用过编辑套件来改善照片,您会体验到一些操作可能需要多长时间。在大多数游戏程序中,执行数值计算的效率是至关重要的。下一节是关于数值计算的 STL 算法,其中一些你已经见过了。之后,我将介绍一个类模板,它被设计来尽可能高效地用数字数组进行数值计算。

数字算法

在本章中,我偶尔会用到矩阵(复数矩阵)和向量这两个术语。在数学和科学中,矩阵是数字的二维数组。向量是一个一维数组——一个线性数字序列。当我在正文中正常情况下使用术语 vector 时,我指的是一个一维数字数组,它不一定在vector容器中,但可能在。一个矩阵通常会被存储为一个valarray对象,在我解释完算法之后你会了解到这个。你已经了解了一些处理数字数据的 STL 算法,但是我将在本章中介绍这些算法,以及那些新的算法。所有这些算法都处理由输入迭代器指定范围的数据源。

存储范围内的增量值

numeric标题中定义的iota()函数模板用T类型的连续值填充一个范围。前两个参数是定义范围的前向迭代器,第三个参数是初始的T值。指定为第三个参数的值存储在该范围的第一个元素中。存储在第一个元素之后的元素中的值是通过对前面的元素应用递增运算符获得的。当然,这意味着类型T必须支持operator++()。下面是如何创建一个包含连续浮点值的元素的vector容器:

std::vector<double> data(9);

double initial {-4};

std::iota(std::begin(data), std::end(data), initial);

std::copy(std::begin(data), std::end(data),

std::ostream_iterator<double>{std::cout << std::fixed << std::setprecision(1), " "});

std::cout << std::endl;              // -4.0 -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0

调用iota() with an initial value of -4data中元素的值设置为从-4+4的连续值。

当然,初始值不必是整数:

std::iota(std::begin(data), std::end(data), -2.5);                                      // Values are -2.5 -1.5 -0.5 0.5 1.5 2.5 3.5 4.5 5.5

增量仍然是 1,所以值将和注释中的一样。您可以将iota()算法应用于任何类型的范围,只要 increment 运算符有效。这是另一个例子:

string text {"This is text"};

std::iota(std::begin(text), std::end(text), 'K');

std::cout << text << std::endl;      // Outputs: KLMNOPQRSTUV

很容易看出注释中显示的输出是什么——字符串中的每个字符都被设置为代码以“K”开头的字符序列。这个例子中发生的事情并不明显:

std::vector<string> words (8);

std::iota(std::begin(words), std::end(words), "mysterious");

std::copy(std::begin(words), std::end(words),std::ostream_iterator<string>{std::cout, " "});

std::cout << std::endl;            // mysterious ysterious sterious terious erious rious ious ous

输出如注释所示。这是该算法的一个有趣的应用,但不是很有用。这仅仅是因为第三个参数是一个字符串。如果参数是string{"mysterious"},它将不会被编译,因为没有为string类定义的operator++()。对应于字符串文字的参数值是一个类型为const char*的指针,并且++操作正被应用于该指针。因此,对于第一个元素之后的words中的每个元素,指针都会递增,导致从字符串文字的前面删除一个字母。将++应用于指针的结果用于创建一个string对象,然后存储在当前元素范围内。只要++可以应用于一个范围内的元素类型,就可以对它们应用iota()算法。

Note

有趣的是,iota()算法的思想起源于 IBM 编程语言 APL 中的 iota 运算符ι。在 APL 中,表达式ι 10创建了一个从 1 到 10 的整数向量。APL 是由 Ken Iverson 在 20 世纪 60 年代开发的。它是一种非常简洁的语言,具有处理向量和数组的隐含能力。一个完整的 APL 程序,从键盘上读取任意数量的值,计算它们的平均值,然后输出结果,可以用 10 个字符表示。

对范围求和

您已经见过的accumulate()算法的基本版本使用+运算符对一系列元素求和。前两个参数是定义范围的输入迭代器,第三个参数是总和的初始值;第三个参数的类型决定了返回值的类型。还有第二个版本,带有第四个参数,是一个二元函数对象,用于定义在 total 和一个元素之间应用的操作。这使您能够在必要时定义自己的加法运算。例如:

std::vector<int> values {2, 0, 12, 3, 5, 0, 2, 7, 0, 8};

int min {3};

auto sum = std::accumulate(std::begin(values), std::end(values), 0, min

{

if(v < min) return sum;

return sum + v;

});

std::cout << "The sum of the elements greater than " << min-1

<<" is " << sum << std::endl;                       // 35

这将忽略值小于 3 的元素。条件可以像你喜欢的那样复杂,例如,你可以在一个给定的范围内对元素求和。运算不需要做加法。它可以是任何操作,只要它不修改操作数或使定义范围的迭代器无效。例如,将数值元素的函数定义为乘法运算将产生元素的乘积,只要初始值为 1。如果初始值为 1,实现浮点元素除法运算的函数将产生元素乘积的倒数。你可以这样生产元素的乘积:

std::vector<int> values {2, 3, 5, 7, 11, 13};

auto product = std::accumulate(std::begin(values), std::end(values), 1,

std::multiplies<int>()); // 30030

它使用函数头中的函数对象作为第四个参数。如果可能有零值元素,您可以用 lambda 表达式忽略它们,就像前面的代码片段中那样。

string类支持加法,所以你可以将accumulate()应用到一系列string对象:

std::vector<string> numbers {"one", "two",   "three", "four", "five",

"six", "seven", "eight", "nine", "ten"};

auto s = std::accumulate(std::begin(numbers), std::end(numbers), string{},

[](string& str, string& element)

{

if(element[0] == 't') return str + ' ' + element;

return str;

});       // Result: " two three ten"

这段代码将从't'开始的string对象连接起来,并用空格分隔。也有可能执行accumulate()算法的结果与应用该算法的范围内的元素类型不同:

std::vector<int> numbers {1, 2, 3, 10, 11, 12};

auto s = std::accumulate(std::begin(numbers), std::end(numbers), string {"The numbers are"},

[](string& str, int n)

{   return str + ": " + std::to_string(n);  });

std::cout << s << std::endl;           // Output: The numbers are: 1: 2: 3: 10: 11: 12

lambda 表达式使用的to_string()函数返回数值参数的string表示。因此,将accumulate()算法应用于这里的整数范围会返回注释中显示的string

内积

两个向量的内积是相应元素乘积的和。要做到这一点,向量的长度必须相同。内积是矩阵运算中的基本运算。两个矩阵的乘积是第一个矩阵的每一行与第二个矩阵的每一列的内积。如图 10-1 所示。

A978-1-4842-0004-9_10_Fig1_HTML.gif

图 10-1。

Matrix multiplication and the inner product operation

为了使矩阵乘积成为可能,矩阵中作为左操作数的列数必须与作为右操作数的行数相同。如果左操作数有m行和n列(一个m×n矩阵),右操作数有 n 行和 k 列(一个n×k矩阵),结果就是一个有m行和k列的矩阵(一个m×k矩阵)。

numeric头中定义的inner_product()算法计算两个向量的内积。函数模板有四个参数:前两个是定义第一个向量的输入迭代器,第三个是标识第二个向量的 begin 输入迭代器,第四个参数是总和的初始值。该算法返回向量的内积。这里有一个例子:

std::vector<int> v1(10);

std::vector<int> v2(10);

std::iota(std::begin(v1), std::end(v1), 2);      // 2 3 4 5 6 7 8 9 10 11

std::iota(std::begin(v2), std::end(v2), 3);      // 3 4 5 6 7 8 9 10 11 12

std::cout << std::inner_product(std::begin(v1), std::end(v1), std::begin(v2), 0)

<< std::endl;                          // Output: 570

对于两个向量的内积的标准定义,初始值是 0,但是您可以选择为相应元素的乘积之和指定不同的初始值。使用inner_product()时,使用正确类型的文字很重要。下面将说明我的意思:

std::vector<double> data {0.5, 0.75, 0.85};

auto result1 = std::inner_product(std::begin(data), std::end(data), std::begin(data), 0);

double result2 = std::inner_product(std::begin(data), std::end(data), std::begin(data), 0);

auto result3 = std::inner_product(std::begin(data), std::end(data), std::begin(data), 0.0);

std::cout << result1 << " " << result2

<< " " << result3 << std::endl;        // Output: 0 0 1.535

第二个和第三个语句显然做了同样的事情,但是返回值的类型由第四个参数决定。即使迭代器指向浮点参数,当初始值为整数类型时,合并相应元素相乘结果的操作也使用整数运算。这同样适用于accumulate()算法,所以要确保文字初始值是适当的类型。幸运的是,当初始值的文字与操作中涉及的元素类型不同时,大多数编译器都会发出警告。在一个工作示例中,我们可以尝试一下inner_product()算法和其他一些算法。

应用内积

最小二乘线性回归是一种寻找线$$ y= ax+b $$的系数ab的方法,该线是通过一组n x,y 点的最佳拟合,其中这些点通常是某种真实世界的数据样本。由高斯提出的方法找到系数ab,使得样本点到直线的垂直距离的平方和最小。我将展示实现这一点的方程,而不去探究它们是如何开发的,但是如果你不想为任何数学问题而烦恼,你可以直接跳到代码。

给定n点,(x i ,y i ,该方法涉及求解以下方程:

$$ nb+a{\displaystyle \sum }{x}_i={\displaystyle \sum};{y}_i $$

$$ b{\displaystyle \sum};{x}_i+a{\displaystyle \sum;}{x_i}²={\displaystyle \sum};{x}_i{y}_i $$

求解这两个方程的系数ab得到:

$$ a=\frac{n{\displaystyle \sum }{x}_i{y}_i-{\displaystyle \sum }{x}_i{\displaystyle \sum }{y}_i}{n{\displaystyle \sum }{x_i}²-{\left({\displaystyle \sum }{x}_i\right)}²} $$

$$ b={m}_y-a{m}_x $$

如果我们可以计算各种总和以及 x 和 y 值的平均值,我们可以将它们代入这些方程,以获得回归线的系数。你在第八章中看到,变量xn值的平均值μ的等式是:

$$ {m}_x=\frac{{\displaystyle \sum }{x}_i}{n} $$

显然,accumulate()inner_product()算法将会对此非常有帮助。

此示例将对文件中的一组数据点拟合一条回归线。该文件位于代码下载中,记录了几个欧洲国家的每千瓦时电费和人均可再生发电装机容量。程序输出应显示已安装的可再生能源容量和消费者成本之间是否存在线性关系。代码如下:

// Ex10_01.cpp

// Least squares regression

#include <numeric>                               // For accumulate(), inner_product()

#include <vector>                               // For vector container

#include <iostream>                             // For standard streams

#include <iomanip>                              // For stream manipulators

#include <fstream>                              // For file streams

#include <iterator>                             // For iterators and begin() and end()

#include <string>                               // For string class

using std::string;

int main()

{

// File contains country_name renewables_per_person kwh_cost

string file_in {"G:/Beginning_STL/renewables_vs_kwh_cost.txt"};

std::ifstream in {file_in};

if(!in)                                       // Verify  we have a file

{

std::cerr << file_in << " not open." << std::endl;

exit(1);

}

std::vector<double> x;                        // Renewables per head

std::vector<double> y;                        // Corresponding cost for a kilowatt hour

// Read the file and show the data

std::cout << "   Country   " << " Watts per Head " << " kwh cost(cents) " << std::endl;

while(true)

{

string country;

double renewables {};

double kwh_cost {};

if((in >> country).eof()) break;                           // EOF read - we are done

in >> renewables >> kwh_cost;

x.push_back(renewables);

y.push_back(kwh_cost);

std::cout << std::left << std::setw(12) << country         // Output the record

<< std::right

<< std::fixed << std::setprecision(2) << std::setw(12) << renewables

<< std::setw(16) << kwh_cost << std::endl;

}

auto n = x.size();                                            // Number of points

auto sx = std::accumulate(std::begin(x), std::end(x), 0.0);   // Sum of x values

auto sy = std::accumulate(std::begin(y), std::end(y), 0.0);   // Sum of y values

auto mean_x = sx/n;                                           // Mean of x values

auto mean_y = sy/n;                                           // Mean of y values

// Sum of x*y values and sum of x-squared

auto sxy = std::inner_product(std::begin(x), std::end(x), std::begin(y), 0.0);

auto sx_2 = std::inner_product(std::begin(x), std::end(x), std::begin(x), 0.0);

double a {}, b {};                                            // Line coefficients

auto num = n*sxy - sx*sy;                                     // Numerator for a

auto denom = n*sx_2 - sx*sx;                                  // Denominator for a

a = num / denom;

b = mean_y - a*mean_x;

std::cout << std:: fixed << std::setprecision(3) << "\ny = "  // Output equation

<< a << "*x + " << b << std::endl;                  // for regression line

}

while循环中读取文件。只存储数字值,并将每个完整的国家名称、人均可再生能源装机容量的瓦特数以及每千瓦时的费用写入标准输出流。这两个数值存储在向量容器中;x 存储每个国家的人均可再生能源容量,y 存储相应的千瓦时成本。

x 值和 y 值的平均值是通过使用accumulate()算法对每个容器中的元素求和,然后将结果除以元素数来计算的。通过inner_product()算法计算出x值的平方和以及 xy 乘积的和。这些结果用于通过使用我之前展示的等式来计算线的ab系数。

注意,我们可以简化系数a的等式。如果我们把分子和分母除以n 2 ,方程可以这样写:

$$ a=\frac{{\displaystyle \sum }{x}_i{y}_i/n-{m}_x{m}_y}{{\displaystyle \sum }{x_i}²/n-{m_x}²} $$

现在 x 值和 y 值的和并不需要明确。计算系数的代码可以写成:

auto n = x.size();                                                 // Number of points

// Calculate mean values for x, y, xy, and x-squared

auto mean_x = std::accumulate(std::begin(x), std::end(x), 0.0)/n;

auto mean_y = std::accumulate(std::begin(y), std::end(y), 0.0)/n;

auto mean_xy = std::inner_product(std::begin(x), std::end(x), std::begin(y), 0.0)/n;

auto mean_x2 = std::inner_product(std::begin(x), std::end(x), std::begin(x), 0.0)/n;

// Calculate coefficients

auto a = (mean_xy - mean_x*mean_y)/(mean_x2 - mean_x*mean_x);

auto b = mean_y - a*mean_x;

这可以用更少的语句达到相同的结果。图 10-2 在右边显示了程序的输出,在左边显示了回归线和原始数据点的曲线图。

A978-1-4842-0004-9_10_Fig2_HTML.gif

图 10-2。

Result of least squares linear regression

这个情节相当有说服力——原著的观点与它相当接近。看起来好像每增加 100 瓦的人均可再生能源发电量,你使用的每千瓦时的成本就会增加 2 美分。

定义替代的内积工序

您看到的版本inner_product()将两个输入范围中的相应元素相乘,然后将结果相加。第二个版本有两个定义函数对象的参数。第二个函数对象定义了要在两个范围中的对应元素对之间应用的二元运算,第一个函数对象定义了要用来代替加法运算以组合结果的二元运算。作为参数提供的函数对象不能使任何迭代器无效,也不能修改任何输入范围内的元素。这里有一个例子,说明如何产生和的乘积,而不是乘积的和:

std::vector<int> v1(5);

std::vector<int> v2(5);

std::iota(std::begin(v1), std::end(v1), 2);      // 2 3 4 5 6

std::iota(std::begin(v2), std::end(v2), 3);      // 3 4 5 6 7

std::cout << std::inner_product(std::begin(v1), std::end(v1), std::begin(v2), 1,

std::multiplies<>(), std::plus<>())

<< std::endl;                          // Output: 45045

inner_product()调用中用作参数的函数对象在functional头中定义。一个plus<T>对象计算类型为T的两个值的和,这里的模板实例定义了应用于来自输入范围的类型为int的相应元素的操作。作为inner_product()的第五个参数的multiples的实例通过将结果相乘来组合它们。注意,因为结果是一个乘积,如果您想避免总是得到零结果,初始值一定不能是0。函数头还定义了其他二进制算术运算的模板,您可以使用inner_product() - minusdividesmodulus。您还可以使用定义位运算的函数对象的模板;这些是bit_andbit_orbit_eor

相邻差异

来自numeric头的adjacent_difference()算法计算一个输入范围内相邻元素对之间的差异,并将结果存储在另一个范围内。将第一个元素原封不动地复制到新范围中,然后从第二个元素中减去第一个元素,作为第二个元素存储在新范围中,从第三个元素中减去第二个元素,作为第三个元素存储在新范围中,依此类推。这里有一个例子:

std::vector<int> data {2, 3, 5, 7, 11, 13, 17, 19};

std::cout << "Differences: ";

std::adjacent_difference(std::begin(data), std::end(data),

std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;                        // Differences: 2 1 2 2 4 2 4 2

因为输出范围的迭代器是写入cout的输出流迭代器,所以data容器中元素之间的差异由adjacent_difference()算法直接输出。这产生的输出显示在注释中。

该算法的第二个版本允许您指定应用于元素对的减法运算符的替代运算符。这里有一个例子:

std::vector<int> data {2, 3, 5, 7, 11, 13, 17, 19};

std::cout << "Products: ";

std::adjacent_difference(std::begin(data), std::end(data),

std::ostream_iterator<int>{std::cout, " "},

std::multiplies<>());

std::cout << std::endl;                          // Products: 2 6 15 35 77 143 221 323

第四个参数是一个 function 对象,它指定元素之间的操作——在本例中是来自functional头的multiplies的一个实例。你可以看到这产生了data中连续元素的乘积。只要不改变输入范围或使迭代器无效,任何二元操作都是可以接受的。下面是一个使用plus<T>函数对象作为元素对之间的运算符来计算斐波那契数的示例:

std::vector<size_t> fib(15, 1);                  // 15 elements initialized with 1

std::adjacent_difference(std::begin(fib), std::end(fib)-1, std::begin(fib)+1, std::plus<size_t>());

std::copy(std::begin(fib), std::end(fib), std::ostream_iterator<size_t>{std::cout, " "});

std::cout << std::endl;                // Output: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610

这里的adjacent_difference()算法在fib容器中添加成对的元素,并将结果从第二个元素开始写回同一个容器。fib中的最后一个元素不包括在输入范围内,输入范围内最后两个元素的总和会覆盖最后一个元素中的值。运算后,fib将包含一个从 1 开始的斐波那契数列。注释显示了由copy()算法产生的输出。

部分和

numeric标题中定义的partial_sum()算法计算输入范围内元素的部分和,并将结果存储在输出范围内。它从长度为 1 的序列开始计算输入范围内长度递增的序列的和,因此第一个输出值只是第一个元素,下一个值是前两个元素的和,下一个是前三个元素的和,依此类推。这是adjacent_difference()算法的逆运算,所以partial_sum()撤销adjacent_difference()做的事情。这里有一个例子:

std::vector<int> data {2, 3, 5, 7, 11, 13, 17, 19};

std::cout << "Partial sums: ";

std::partial_sum(std::begin(data), std::end(data), std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;                // Partial sums: 2 5 10 17 28 41 58 77

您可以看到输出由长度稳定增长的序列的总和组成。通过执行以下命令,您可以很容易地证明这一点:

std::vector<int> data {2, 3, 5, 7, 11, 13, 17, 19};

std::cout << "Original data: ";

std::copy(std::begin(data), std::end(data), std::ostream_iterator<int>{std::cout, " "});

std::adjacent_difference(std::begin(data), std::end(data), std::begin(data));

std::cout << "\nDifferences: ";

std::copy(std::begin(data), std::end(data), std::ostream_iterator<int>{std::cout, " "});

std::cout << "\nPartial sums: ";

std::partial_sum(std::begin(data), std::end(data), std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;

注意,这里的输出迭代器与输入范围的 begin 迭代器相同。这是合法的。您可能认为数据可能会被覆盖,但是算法被定义为防止这种情况发生。执行这段代码的输出是:

Original data: 2 3 5 7 11 13 17 19

Differences: 2 1 2 2 4 2 4 2

Partial sums: 2 3 5 7 11 13 17 19

输出显示,计算差异的部分和会得到原始值,这并不奇怪。

adjacent_difference()算法一样,您可以提供一个函数对象作为partial_sum()的额外参数,它定义了一个用来代替加法的操作符。这可能是如何应用的:

std::vector<int> data {2, 3, 5, 7, 11, 13, 17, 19};

std::cout << "Partial sums: ";

std::partial_sum(std::begin(data), std::end(data),

std::ostream_iterator<int>{std::cout, " "}, std::minus<int>());

std::cout << std::endl;                // Partial sums: 2 -1 -6 -13 -24 -37 -54 -73

使用减法运算符,因此这些值是、22-32-3-52-3-5-7等的结果。

最大值和最小值

你已经看到了一些确定最小值和最大值的算法,但我还是会把它们都包含在这一节中。在algorithm头中定义了三种适用于范围的算法:min_element()返回一个指向输入范围最小值的迭代器,max_element()返回一个指向最大值的迭代器,minmax_element()返回两者的迭代器作为一个pair对象。该范围必须由正向迭代器指定;仅有输入迭代器是不够的。对于这三种算法,除了范围的开始和结束迭代器之外,还可以选择提供第三个参数来定义比较函数。以下代码展示了应用于整数的三种算法:

std::vector<int> data {2, 12, 3, 5, 17, -11, 113, 117, 19};

std::cout << "From values ";

std::copy(std::begin(data), std::end(data), std::ostream_iterator<int>{std::cout, " "});

std::cout << "\n     Min = " << *std::min_element(std::begin(data), std::end(data))

<< "  Max = " << *std::max_element(std::begin(data), std::end(data))

<< std::endl;

auto start_iter = std::begin(data) + 2;

auto end_iter = std::end(data) - 2;

auto pr = std::minmax_element(start_iter, end_iter);       // Get min and max

std::cout << "From values ";

std::copy(start_iter, end_iter, std::ostream_iterator<int>{std::cout, " "});

std::cout << "\n     Min = " << *pr.first << "  Max = " << *pr.second << std::endl;

min_element()max_element()用于从应用于相同范围的data. minmax_element()中找到最小值和最大值,但是省略了前两个和后两个元素。执行此操作将输出以下内容:

From values 2 12 3 5 17 -11 113 117 19

Min = -11  Max = 117

From values 3 5 17 -11 113

Min = -11  Max = 113

algorithm头还定义了min()max()minmax()的模板,这些模板返回两个对象的最小值、最大值或者最小值和最大值,或者返回对象的初始化列表。你已经看到它们和两个要比较的参数一起使用。下面是一个使用初始化列表的例子:

auto words = {string {"one"}, string {"two"}, string {"three"}, string {"four"},

string {"five"}, string {"six"}, string {"seven"}, string {"eight"}};

std::cout << "Min = " << std::min(words)

<< std::endl;                // Min = eight

auto pr = std::minmax(words, [] (const string& s1, const string& s2)

{return s1.back() < s2.back();});

std::cout << "Min = " << pr.first << "  Max = " << pr.second

<< std::endl;                // Min = one  Max = six

wordsstring对象的初始化列表。重要的是元素是string对象。如果你简单地使用char*,那么算法将不能正常工作,因为那样你将比较地址而不是字符串内容。min()算法使用默认的operator<()对象来确定单词中的最小对象。然后使用定制的比较函数比较字符串中的最后几个字符,使用minmax()算法找到列表中的最小和最大对象。结果显示在评论中。有一些版本的min()max()接受一个函数对象作为定义比较的最后一个参数。

存储和使用数值

valarray头中定义的valarray类模板定义了可以存储和操作数值序列的对象类型。它主要用于处理整数值和浮点值,但是只要类满足某些条件,您就可以使用它来存储类类型的对象:

  • 该类不能是抽象的。
  • 公共构造函数必须包括一个默认构造函数和一个复制构造函数。
  • 析构函数必须是public
  • 该类必须定义赋值运算符,并且必须是public
  • 上课绝对不能霸王operator&()
  • 函数成员不能抛出异常。

您不能在valarray中存储引用,或constvolatile限定的对象。如果你的课程满足所有这些限制,那么你就成功了。

与任何序列容器(如vector)相比,valarray模板为数字数据处理提供了更多的功能。首先,也是最重要的,它被设计成使编译器能够以一种不适用于序列容器的方式优化其操作的性能。然而,你的编译器是否优化了valarray操作取决于实现。第二,在内置于类型中的valarray对象上有大量的一元和二元操作。第三,还有大量内置的一元函数,用于将cmath头中定义的许多操作应用到每个元素。第四,valarray类型提供了内置的功能,可以根据需要将数据作为任意维数的数组来处理。

创建一个valarray对象很容易。以下是一些例子:

std::valarray<int> numbers(15);                 // 15 elements with default initial values 0

std::valarray<size_t> sizes {1, 2, 3};          // 3 elements with values 1 2 and 3

std::valarray<size_t> copy_sizes {sizes};       // 3 elements with values 1 2 and 3

std::valarray<double> values;                   // Empty array

std::valarray<double> data(3.14, 10);           // 10 elements with values 3.14

每个构造函数用给定数量的元素创建一个对象。在定义data的最后一个语句中使用括号是很重要的;如果使用大括号,data将包含两个元素,值分别为3.1410.0。还可以创建一个valarray对象,用普通数组中指定数量的值初始化。例如:

int vals[] {2, 4, 6, 8, 10, 12, 14};

std::valarray<int> vals1 {vals, 5};             // 5 elements from vals: 2 4 6 8 10

std::valarray<int> vals2 {vals + 1, 4};         // 4 elements from vals: 4 6 8 10

我稍后将介绍其他构造函数,因为它们有我尚未解释的类型的参数。

对 valarray 对象的基本操作

一个valarray对象类似于一个array容器,因为您不能添加或删除元素。但是,您可以更改一个valarray对象包含的元素数量,并给它们分配一个新值。例如:

data.resize(50, 1.5);                           // 50 elements with value 1.5

如果在此操作之前有元素存储在data中,它们的值将会丢失。当需要获取元素个数时,可以调用size()成员。

swap()成员将当前对象的元素与作为参数传递的valarray对象的元素进行交换。例如:

std::valarray<size_t> sizes_3 {1, 2, 3};

std::valarray<size_t> sizes_4 {2, 3, 4, 5};

sizes_3.swap(sizes_4);                             // sizes_3 now has 4 elements and sizes_4 has 3

包含在valarray对象中的元素数量可以不同,但显然两个对象中的元素必须是同一类型。swap()成员没有返回值。有一个非成员swap()函数模板做同样的事情,所以最后一个语句可以替换为:

std::swap(sizes_3, sizes_4);                    // Calls sizes_3.swap(sizes_4)

通过调用min()max()函数成员,可以找到valarray中元素的最小值和最大值。例如:

std::cout << "The elements are from " << sizes_4.min() << " to " << sizes_4.max() << '\n';

为此,元素必须是支持operator<()的类型。

sum()成员返回元素的和,它使用+=操作符计算元素的和。因此,您可以像这样计算valarray中元素的平均值:

std::cout << "The average of the elements " << sizes_4.sum()/sizes_4.size() << '\n';

这比必须使用accumulate()算法要简单得多。

没有返回元素迭代器的valarray成员,但是有专门的非成员版本的begin()end()返回随机访问迭代器。这使您能够使用基于范围的for循环来访问valarray元素,并对它们应用算法;稍后您将看到示例。您不能使用带有valarray的插入迭代器,因为实现它所必需的成员不存在,这是因为大小是固定的。

有两个函数成员用于移动元素——移动序列,而不是移动单个值中的位。首先,shift()成员按照参数指定的元素数量移动整个元素序列。该函数将结果作为一个新的valarray对象返回,保持原来的不变。如果参数为正,元素左移,参数为负,元素右移。这有点像移位。从左侧或右侧移入序列的元素将为 0,或其类型的等效值。当然,如果你不把移位操作的结果存储回同一个valarray对象,那么原来的对象是不变的。下面是一些说明这是如何工作的代码:

std::valarray<int> d1 {1, 2, 3, 4, 5, 6, 7, 8, 9};

auto d2 = d1.shift(2);                           // Shift left 2 positions

for(int n : d2) std::cout << n << ' ';

std::cout << '\n';                               // Result: 3 4 5 6 7 8 9 0 0

auto d3 = d1.shift(-3);                          // Shift right 3 positions

std::copy(std::begin(d3), std::end(d3), std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;                          // Result: 0 0 0 1 2 3 4 5 6

评论解释了发生了什么。我使用不同的方式输出两个案例的结果,只是为了展示可能的结果。d1不会因这些陈述而改变。valarray模板为对象定义了赋值操作符,所以如果你想替换原来的,你可以写:

d1 = d1.shift(2);                                // Shift d1 left 2 positions

移动元素的第二种可能性是使用cshift()成员。这将按照参数指定的位置数循环移动元素序列。元素序列向左或向右旋转,这取决于参数是正还是负。这个函数成员也返回一个新的对象。这里有一个例子:

std::valarray<int> d1 {1, 2, 3, 4, 5, 6, 7, 8, 9};

auto d2 = d1.cshift(2);                          // Result d2 contains: 3 4 5 6 7 8 9 1 2

auto d3 = d1.cshift(-3);                         // Result d3 contains: 7 8 9 1 2 3 4 5 6

apply()函数是valarray的一个非常强大的成员,它将函数应用于每个元素,并将结果作为一个新的valarray对象返回。在valarray类模板中定义了两个apply()函数成员:

valarray<T> apply(T func(T)) const;

valarray<T> apply(T func(const T&)) const;

有三点需要注意。第一,两个版本都是const,所以原始元素不能被函数修改。第二,形参是一个特定形式的函数,带有类型为T的实参或引用Tconst,它返回类型为T的值;如果你尝试使用apply()和一个不对应的参数,它不会编译。第三,返回值是类型valarray<T>,所以结果总是与原始类型和大小相同的元素的数组。

下面是一个使用apply()成员的例子:

std::valarray<double> time {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0};  // Seconds

auto distances = time.apply([](double t)

{

const static double g {32.0};  // Acceleration due to gravity ft/sec/sec

return 0.5*g*t*t;

});                          // Result: 0 16 64 144 256 400 576 784 1024 1296

如果你把砖块从高楼上扔下来,那么在相应的秒数后,distances对象将包含砖块下落的距离;建筑高度必须超过1296英尺,最后的结果才有效。请注意,您不能使用 lambda 表达式从封闭范围捕获变量作为参数,因为这与函数模板中的参数规范不匹配。例如,这不会编译:

const double g {32.0};

auto distances = times.apply(g { return 0.5*g*t*t; });   // Won’t compile!

在 lambda 表达式中通过值捕获g会改变它的类型,因此它不符合应用模板规范。对于可接受作为apply()的参数的 lambda 表达式,capture 子句必须为空,它必须具有与数组相同类型的参数,并且它必须返回该类型的值。

valarray头为来自cmath头的大多数函数定义了重载,以便它们应用于valarray对象中的所有元素。接受valarray对象作为参数的函数有:

abs()pow()sqrt()exp()log()log10()

sin()cos()tan()asin()acos()atan()atan2()

sinh()cosh()tanh()

这里有一个例子,它将本节中的代码片段拖在一起,并提供了一个机会来使用带有valarray对象的cmath函数之一:

// Ex10_02.cpp

// Dropping bricks safely from a tall building using valarray objects

#include <numeric>                                  // For iota()

#include <iostream>                                 // For standard streams

#include <iomanip>                                  // For stream manipulators

#include <algorithm>                                // For for_each()

#include <valarray>                                 // For valarray

const static double g {32.0};                       // Acceleration due to gravity ft/sec/sec

int main()

{

double height {};                                 // Building height

std::cout << "Enter the approximate height of the building in feet: ";

std::cin >> height;

// Calculate brick flight time in seconds

double end_time {std::sqrt(2 * height / g)};

size_t max_time {1 + static_cast<size_T>(end_time + 0.5)};

std::valarray<double> times(max_time+1);               // Array to accommodate times

std::iota(std::begin(times), std::end(times), 0);      // Initialize: 0 to max_time

*(std::end(times) - 1) = end_time;                     // Set the last time value

// Calculate distances each second

auto distances = times.apply([](double t) { return 0.5*g*t*t; });

// Calculate speed each second

auto v_fps = sqrt(distances.apply([](double d) { return 2 * g*d;}));

// Lambda expression to output results

auto print = [](double v) { std::cout << std::setw(6) << static_cast<int>(std::round(v)); };

// Output the times - the last is a special case...

std::cout << "Time (seconds): ";

std::for_each(std::begin(times), std::end(times)-1, print);

std::cout << std::setw(6) << std::fixed << std::setprecision(2) << *(std::end(times)-1);

std::cout << "\nDistances(feet):";

std::for_each(std::begin(distances), std::end(distances), print);

std::cout << "\nVelocity(fps):  ";

std::for_each(std::begin(v_fps), std::end(v_fps), print);

// Get velocities in mph and output them

auto v_mph = v_fps.apply([](double v) { return v*60/88; });

std::cout << "\nVelocity(mph):  ";

std::for_each(std::begin(v_mph), std::end(v_mph), print);

std::cout << std::endl;

}

这决定了当你从高楼上扔下一块砖头时会发生什么。这里是迪拜塔的一些输出示例,假设您从一根足够长的柱子上释放砖块,以避免砖块撞到建筑物的侧面:

Enter the approximate height of the building in feet: 2722

Time (seconds):      0     1     2     3     4     5     6     7     8     9    10    11    12    13 13.04

Distances(feet):     0    16    64   144   256   400   576   784  1024  1296  1600  1936  2304  2704  2722

Velocity(fps):       0    32    64    96   128   160   192   224   256   288   320   352   384   416   417

Velocity(mph):       0    22    44    65    87   109   131   153   175   196   218   240   262   284   285

首先,如果你真的这么做了,实际上会发生的是你最终会进监狱——或者更糟。其次,我知道计算忽略了阻力,但这本书是关于 STL 的,不是物理。第三,我知道你可以通过加速度乘以经过的时间得到速度,但是我不能把sqrt()应用到valarray上,对吗?

所有代码都非常简单。常量g是在全局范围内定义的,因为这是使它在代码的不同地方可用的最简单的方法,包括 lambda 表达式。以秒为单位存储经过时间的times数组由从0开始的整数值填充。使用iota()算法,最后一个时间值(对应于砖块落地的时间)几乎肯定不是整数,因此存储特定值。我使用了for_each()来产生输出,因为它比使用copy()和输出流迭代器允许更多的输出值控制。最后一个时间值不太可能是整数秒,因此这被视为输出的特例。print lambda 是显式定义的,因此可以重用它来输出每组值。

您可以使用下标操作符[]来获取或设置valarray中给定索引处元素的值,但是下标操作符的作用远不止这些,您将在本章后面看到。

一元运算符

有四个一元运算符可以应用于一个valarray对象:+-!。其效果是将操作符应用于数组中的每个元素,并在新的valarray对象中返回结果,保持原来的不变。如果元素类型支持操作符,你只能将它们应用于一个valarray对象,它们的作用——特别是对于类类型的对象——将取决于类型。将!操作符应用于valarray对象所产生的新元素总是属于bool类型,因此操作的结果是一个valarray<bool>类型的对象。在本章的后面,我将讨论这可能有用的上下文。其他运算符必须生成与原始元素类型相同的结果,运算才合法。例如,一元减法运算符只能反转有符号数值元素的符号,因此它不适用于无符号类型。这显示了!操作符的效果:

std::valarray<int> data {2, 0, -2, 4, -4};

auto result = !data;                       // result is of type valarray bool

std::copy(std::begin(result), std::end(result),

std::ostream_iterator<bool>{std::cout << std::boolalpha, " "});

std::cout << std::endl;                    // Output: false true false false false

!应用于data中的值时,这些值首先被隐式转换为bool,然后运算符应用于结果。如果您使用copy()算法将data中的值输出为布尔值,结果将是true false true true true,这解释了为什么上面代码的输出如下所示。

运算符是按位非或 1 的补码。这里有一个例子:

std::valarray<int> data {2, 0, -2, 4, -4}; // 0x00000002 0 0xfffffffe 0x00000004 0xfffffffc

auto result = ∼data;

std::copy(std::begin(result), std::end(result), std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;                    // Output: -3 -1 1 -5 3

通过翻转原始整数值中的位来产生结果,以产生result中的元素。例如,data中的第二个元素的所有位都是 0,因此应用∞会产生一个所有位都是 1 的值,这相当于十进制的-1。

+运算符对数值没有影响;-操作员将改变符号。例如:

std::valarray<int> data {2, 0, -2, 4, -4};

auto result = -data;

std::copy(std::begin(result), std::end(result), std::ostream_iterator<int>{std::cout, " "});

std::cout << std::endl;                    // Output: -2 0 2 -4 4

当然,如果您愿意,您可以覆盖原始对象。为了使代码不那么混乱,从现在开始,我将假设有一个针对std::valarrayusing指令生效,并在代码中去掉针对valarray类型的std名称空间限定符。

valarray 对象的复合赋值运算符

所有复合赋值运算符都有一个左操作数,即一个valarray对象。右操作数可以是与存储的元素类型相同的值,在这种情况下,该值按照运算符确定的方式与每个元素的值组合。右操作数也可以是一个与左操作数拥有相同数量和类型元素的valarray对象。在这种情况下,通过组合右操作数中的相应元素来修改左操作数中的元素。此类别中的操作员包括:

  • 复合算术赋值运算符+=-=*=/=%=,例如:valarray<int> v1 {1, 2, 3, 4}; valarray<int> v2 {3, 4, 3, 4}; v1+= 3;                                // v1 is: 4 5 6 7 v1 -= v2;                              // v1 is: 1 1 3 3

  • 复合班次赋值运算符>>=<<=,例如:valarray<int> v1 {1, 2, 3, 4}; valarray<int> v2 {4, 8, 16, 32}; v2 <<= v1;                             // v2 is: 8 32 128 512 v2 >>= 2;                              // v1 is: 2 8 32 128

  • 复合按位赋值运算符&=|=、【例如:】valarray<int> v1 {1, 2, 4, 8}; valarray<int> v2 {4, 8, 16, 32}; v1 |= 4;                               // v1 is: 5 6 4 12 v1 &= v2;                              // v1 is: 4 0 0 0 v1 ^= v2;                              // v1 is: 0 8 16 32

复合按位和复合移位赋值运算符通常适用于整数类型。

valarray 对象上的二元运算

您可以将适用于基本类型值的任何二元运算符应用于valarray对象,或者组合两个valarray对象的相应元素,或者组合valarray中具有与元素相同类型值的元素。以下二元运算的非成员运算符函数在valarray头中定义:

  • 算术运算符+-*/%
  • 按位运算符&|^
  • 移位操作符>><<
  • 逻辑运算符&&||

所有这些操作符都有不同的版本,允许在一个valarray<T>对象和一个T类型的对象之间,一个T类型的对象和一个valarray对象之间,或者两个valarray对象之间应用操作。两个valarray对象之间的操作要求它们都有相同数量的相同类型的元素。逻辑运算符返回一个与valarray操作数具有相同元素数量的valarray<bool>对象。其他操作符返回一个与valarray操作数具有相同类型和元素数量的valarray对象。

能够将一个valarray对象的内容输出到cout来显示发生了什么将会很有用。我将使用下面的函数模板来完成这项工作:

// perline is the number output per line, width is the field width

template<typename T>

void print(const std::valarray<T> values,  size_t perline = 8, size_t width = 8)

{

size_t n {};

for(const auto& value : values)

{

std::cout << std::setw(width) << value << " ";

if(++n % perline == 0) std::cout << std::endl;

}

if(n % perline != 0) std::cout << std::endl;

std::cout << std::endl;

}

这将适用于包含支持输出流的operator<<()的任何类型T元素的valarray对象。

我不会反复列举使用所有二元运算符的例子——只是举几个例子说明。下面是一些使用valarray对象的二进制算术运算的例子:

valarray<int> even {2, 4, 6, 8};

valarray<int> odd {3, 5, 7, 9};

auto r1 = even + 2;

print(r1, 4, 3);                       // r1 contains:   4   6  8  10

auto r2 = 2*r1 + odd;

print(r2, 4, 3);                       // r2 contains: 11  17  23  29

r1 += 2*odd - 4*(r2 - even);

print(r1, 4, 3);                       // r1 contains: -26 -36 -46 -56

最后一条语句使用复合赋值运算符(函数成员)将右操作数表达式的结果相加。这展示了如何将涉及valarray对象的操作以与数值基本相同的方式组合起来,包括使用括号。下面是一个使用移位操作的语句:

print(odd << 3, 4, 4);                 // Output is:  24   40   56   72

print()的第一个参数是将odd中的元素左移三位产生的valarray对象。在宽度为 4 的字段中,输出是 4 个值到一行。

valarray头中还定义了非成员函数,用于将一个valarray<T>对象与另一个valarray<T>对象进行比较,或者将一个valarray<T>对象的每个元素与一个T类型的值进行比较。比较的结果是一个valarray<bool>对象具有与所涉及的valarray相同数量的元素。支持的操作有==!=<. <=>>=。以下是使用这些方法的一些例子:

valarray<int> even {2, 4, 6, 8};

valarray<int> odd {3, 5, 7, 9};

std::cout << std::boolalpha;

print(even + 1 == odd, 4, 6);          // Output is:   true   true   true   true

auto result = (odd < 5) && (even + 3 != odd);

print(result);                       // Output is:   true   false  false false

倒数第二个语句使用二元&&运算符来组合比较结果。当3添加到even元素后odd元素少于 5 且even对应的元素不等于odd中的元素时,结果显示;这仅适用于evenodd中的第一个元素,因为odd < 5仅适用于odd中的第一个元素true,而even + 3 != odd始终为true

有一些助手类定义了用于处理valarray中元素子集的对象。主要的助手类是std::slicestd::gslice。我将在代码中删除这些名称空间的std限定符。在深入了解如何处理valarray对象之前,让我们先来看看如何使用这些助手类来处理valarray

访问 valarray 对象中的元素

一个valarray对象将其元素存储为一个线性序列。如前所述,您可以获得对任何元素的引用,并通过使用带有下标操作符的索引来获取或设置值。以下是一些例子:

std::valarray<int> data {1,2,3,4,5,6,7,8,9};

data[1] = data[2] + data[3];                     // Data[1] is 7

data[3] *= 2;                                    // Data[3] is 8

data[4] = ++data[5] - data[2];                   // data[4] is 4, data[5] is 7

这就像从常规数组中访问元素一样。然而,valarray对象的下标操作符可以做更多的事情。您可以使用带有下标操作符的助手类实例来代替索引。这使您能够指定和访问元素的子集。helper 类定义的元素选择机制使您能够像在二维或多维数组中一样处理元素。理解这是如何工作的很重要,因为这是valarray相对于序列容器的主要优势之一。

有很多细节需要讨论,所以让我们看看路线图。我们将首先探讨元素选择机制一般是如何工作的,然后讨论如何从二维数组中选择特定的行或列。我将解释助手类如何以各种方式与valarray对象一起工作来选择不同的元素子集,以及子集是如何表示的。在我解释了生成子集的各种可能性之后,我将讨论您可以用它做什么。之后,我将介绍如何在应用程序环境中应用这些技术。

创建切片

valarray头中定义了std::slice类。一个片由一个slice对象定义,您将它传递给一个valarray对象的下标操作符,就像一个索引一样。使用slice对象作为valarray对象的下标,选择两个或更多元素的子集。所选择的元素在阵列中不一定是连续的。slice选择的数组元素可作为引用,因此您可以访问和/或更改这些元素的值。

本质上,slice对象封装了一系列索引值,用于从valarray中选择元素。通过向slice构造函数传递三个size_t类型的值来定义一个slice对象:

  • valarray对象中标识子集中第一个元素的起始索引。
  • 大小,即子集中元素的数量。
  • 跨距,这是从子集中的一个元素到下一个元素的索引增量。

构造函数的参数按照我描述的顺序排列,所以你可以像这样定义一个切片:

slice my_slice {3, 4, 2};                        // start index = 3, size = 4, stride = 2

该对象从索引 3 开始标识 4 个元素,随后的索引增量为 2。有一个复制构造函数,所以你可以复制slice对象。默认构造函数将起始索引、大小和步幅设置为 0,其唯一目的是允许创建slice对象的数组。

您可以通过调用start()成员从slice对象获得开始索引。一个slice对象也有分别返回大小和步幅的size()stride()成员。所有三个值都作为类型size_t返回。

一般来说,当您使用一个slice{start, size, stride}对象作为一个valarray对象的下标时,您是在索引值处选择元素:

startstart + stridestart + 2*stride、...start +(size - 1)*stride

图 10-3 用一个包含值从 1 到 15 的元素的valarray对象举例说明了这一点。

A978-1-4842-0004-9_10_Fig3_HTML.gif

图 10-3。

Subset of elements in a valarray selected by a slice object

在图 10-3 中,下标操作符应用于带有slice对象作为参数的data,选择索引位置 3、5、7 和 9 处的元素,它们是数组中的第四、第六、第八和第十个元素。slice构造函数的第一个参数是第一个元素的索引,第二个参数是索引值的数量,第三个参数是从一个索引值到下一个索引值的增量。使用一个slice对象作为一个valarray<T>对象的索引的结果是另一个对象——还能是什么呢?它是一个类型为slice_array<T>的对象,封装了对valarray<T>中由slice选择的元素的引用。在我解释了更多关于如何使用 slice 之后,我将回到你可以用一个slice_array对象做什么。

选择一行

假设图 10-3 中的data对象中的值表示一个二维数组,其中有三行五个元素——按行顺序排列。图 10-4 显示了如何使用slice对象选择第二行。

A978-1-4842-0004-9_10_Fig4_HTML.gif

图 10-4。

Selecting a single row of a two-dimensional array

起始索引是第二行第一个元素的索引,是5。步幅是1,因为每行中的元素是连续存储的,大小是5,因为一行中有5个元素。调用代表一行的 slice 对象的start()成员将返回该行中第一个元素的索引,这在处理多行时非常有用。当然,由a_slice定义的valarray对象的行中第n个元素(从 0 开始索引)的索引是a_slice.start()+n

选择列

假设您想从二维数组中选择一列。一列中的元素在数组中是不连续的,这可能吗?“这当然是,斯坦利,”奥利会说。图 10-5 显示了如何定义一个slice对象来选择与图 10-4 中相同的数组中的第三列。

A978-1-4842-0004-9_10_Fig5_HTML.gif

图 10-5。

Selecting a single column from a two-dimensional array

和往常一样,起始值是子序列中第一个元素的索引。从一列中的一个元素到下一个元素的索引增量是5,所以这是跨距值。一列中有三个元素,所以大小是3

使用切片

当您使用slice对象作为下标时,slice_array<T>对象是从valarray<T>对象中选择的元素子集的代理。该模板定义了有限数量的函数成员。唯一可用于slice_array的公共构造函数是复制构造函数,所以除了使用slice对象作为下标之外,创建对象的唯一可能性是创建一个重复的slice_array对象。例如:

valarray<int> data(15);

std::iota(std::begin(data), std::end(data), 1);

size_t start {2}, size {3}, stride {5};

auto d_slice = data[slice {start, size, stride}]; // References data[2], data[7], data[12]

slice_array<int> copy_slice {d_slice};            // Duplicate of d_slice

没有默认的构造函数,所以你不能创建slice_array对象的数组。唯一可以应用于slice_array对象的操作是赋值和复合赋值。赋值操作符将一个slice_array对象引用的所有元素设置为一个给定值。您也可以使用它来设置引用到另一个valarray中相应元素值的元素,只要valarrayslice_array对象相关的valarray具有相同数量的相同类型的元素。例如:

valarray<int> data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

valarray<int> more {2, 2, 3, 3, 3, 4, 4, 4, 4,  5,  5,  5,  5,  5,  6};

data[slice{0, 5, 1}] = 99;             // Set elements in 1st row to 99

data[slice{10, 5, 1}] = more;          // Set elements in last row to values from more

std::cout << "data:\n";

print(data, 5, 4);

您可以看到,您可以愉快地使用像data[slice{0, 5, 1}]这样的表达式,在赋值的左边创建一个slice_array。这调用了slice_array对象的operator=()成员。右操作数可以是单个元素值,或者是包含相同类型元素的valarray,或者是另一个相同类型的slice_array。给slice_array分配一个值会将它引用的valarray中的元素设置为该值。当右操作数是一个valarray或一个slice_array时,你必须确保它包含的元素至少和左操作数一样多;如果少了,结果不会是你想要的。执行上述代码的输出是:

data:

99   99   99   99   99

6    7    8    9   10

2    2    3    3    3

您可以看到数据中的第一行和第三行已经被修改。

您可以对slice_array对象使用以下任意复合赋值运算符(op=):

  • 算术运算:+=-=*=/-%=
  • 按位运算&=|=^=
  • 移位操作>>=<<=

在任何情况下,左操作数必须是一个slice_array对象,右操作数必须是一个valarray对象,包含与slice_array相同类型的元素。op=操作将在引用了slice_array的每个元素和作为右操作数的valarray中的相应元素之间应用op。请注意,不支持单值的右操作数;你总是需要一个valarray对象作为右边的操作数,即使右边所有对应的元素都有相同的值。

作为右操作数的valarray通常包含与右操作数相同数量的元素,但这不是绝对必要的。它不能包含更少的元素,但可以包含更多的元素,在这种情况下,如果左操作数的slice_array包含n元素,则右操作数的第一个n元素用于运算。下面是一个使用+=修改valarray对象的切片的例子:

valarray<int> data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

auto d_slice = data[slice {2, 3, 5}];  // References data[2], data[7], data[12]

d_slice += valarray<int>{10, 20, 30};  // Combines the slice with the new valarray

std::cout << "data:\n";

print(data, 5, 4);

d_slice引用的data中的元素具有添加到它们的more对象中相应索引位置的元素值。输出是:

data:

1    2   13    4    5

6    7   28    9   10

11   12   43   14   15

操作之后,切片选择的data数组的列中的元素具有从3+108+2013+30得到的值。将一个切片中的元素相乘同样简单:

valarray<int> factors {22, 17, 10};

data[slice{0, 3, 5}] *= factors;       // Values of the 1st column: 22 102 110

slice对象从data中选择第一列,该列中的每个元素都乘以factors对象中相应的元素。如果您只想将切片乘以一个给定值,只需创建一个合适的valarray对象:

valarray<int> data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

slice row3 {10, 5, 1};

data[row3] *= valarray<int>(3, row3.size());          // Multiply 3rd row of data by 3

这将把data中的最后一个5元素乘以3。通过调用slice对象的size()成员来设置最后一条语句中右操作数valarray中的元素数量。这确保了元素的数量与从data中选择的数量相同。

假设您想要将data中一列的元素添加到另一列。你不能给一个slice_array加一个slice_array,但是你仍然可以做你想做的事情。一种方法是使用接受slice_array作为参数的valarray构造函数。使用这个构造函数,slice_array对象引用的值用于初始化所创建的valarray对象中的元素。然后,您可以使用这个对象作为带有slice_array的复合赋值的右操作数。下面是如何将data中的第五列添加到第二列和第四列的方法:

valarray<int> data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

valarray<int> col5 {data[slice{4, 3, 5}]};            // Same as 5th column in data

data[slice{1, 3, 5}] += col5;                         // Add to 2nd column

data[slice{3, 3, 5}] += col5;                         // Add to 4th column

print(data, 5, 4);

使用slice对象作为data的索引生成的slice_array对象作为参数传递给valarray构造函数以创建对象col5。这个valarray构造函数没有被定义为explicit,所以它可以用于从slice_array类型到valarray类型的隐式转换。col5对象可以这样定义:

valarray<int> col5 = data[slice{4, 3, 5}];            // Convert slice_array to valarray

这调用了col5对象的operator=()成员,它期望右操作数是一个valarray对象。编译器将插入对valarray构造函数的调用,该构造函数接受一个slice_array对象作为参数,将slice_array转换为valarray。请注意,这与以下语句不同:

auto col = data[slice{4, 3, 5}];                      // col will be type slice_array

这里没有转换。这只是将col定义为通过用slice对象索引data而得到的slice_array对象。

前面调用print()的代码块产生的输出将是:

1    7    3    9    5

6   17    8   19   10

11   27   13   29   15

当然,也可以用一个普通的旧循环做同样的事情:

size_t row_len {5}, n_rows {3};                       // Row length, number of rows

for(size_t i {}; i < n_rows*row_len; i += row_len)

{

data[i+1] += data[i+4];                             // Increment 2nd column

data[i+3] += data[i+4];                             // Increment 4th column

}

循环索引i,遍历选择第一列data中元素的索引值。使用形式为i+n的表达式作为循环体中data的下标,选择第n列中的元素。让我们看看slice对象在一个更像真实应用程序的程序中的运行。

应用切片求解方程

我们可以开发一个程序,用slice对象和valarray对象来解一组线性方程组。下面是一组典型的线性方程组:

$$ 2{x}_1-2{x}_2-3{x}_3+\kern0.5em {x}_4=23 $$

$$ 5{x}_1+3{x}_2+\kern0.5em {x}_3+\kern0.5em 2{x}_4=77 $$

$$ {x}_1+\kern1em {x}_2-2{x}_3-\kern1em {x}_4=14 $$

$$ 3{x}_1+4{x}_2+5{x}_3+6{x}_4=23 $$

有四个线性方程涉及四个变量,因此只要每个方程独立于其他三个方程,就有可能找到满足这些方程的x1x2x3x 4 的值。我们的程序将使用众所周知的高斯消去法来求解一组未知量为nn线性方程,我们将使用一个valarray对象来存储这些方程。valarray对象将存储变量的系数和每个等式右侧的值。例如,您可以将上面的等式存储为下面的valarray:

valarray<double> equations {2, -2, -3,  1, 23,

5,  3,  1,  2, 77,

1,  1, -2, -1, 14,

3,  4,  5,  6, 23 };

注意,equations对象中的数据是一个四行五列的二维矩阵。一般来说,n变量中的n方程会用n行和n+1列来表示。在我们能写任何代码之前,我们需要理解方法。

高斯消去法

高斯消去法包括两个基本步骤。第一步是将原始方程组转换成允许确定变量值的不同形式,第二步是确定变量值。图 10-6 说明了这个概念。

A978-1-4842-0004-9_10_Fig6_HTML.gif

图 10-6。

What the Gaussian Elimination method does

图 10-6 显示了四个未知数的四个方程的一般表示,其中a是系数,x是变量,c是方程右边的值。第一步把左边的方程转换成右边的形式,叫做行梯队形式。图 10-6 描述了第二步的程序,该程序从列梯队形式的方程中获得所有变量的值。这个过程叫做回代。那么我们如何将左边的方程组转化为行梯队形式呢?

淘汰过程

我相信你知道,你可以在一个方程的两边加上或减去相同的东西,你仍然有一个有效的方程。这意味着你可以从一个方程中加上或减去另一个方程的倍数,你仍然有一个有效的方程。图 10-7 展示了如何应用这一思想将一组四个线性方程转化为行梯队形式。

A978-1-4842-0004-9_10_Fig7_HTML.gif

图 10-7。

Transforming linear equations into row echelon form

图 10-7 显示了如何分三步将矩阵中连续行的元素设置为零。第一步是从随后的每一行中减去第一行的特定倍数。重复这一过程,减去第二行和第三行的倍数,得到行梯队形式。从第一行开始往下做很方便,但是你可以从任何顺序的行中删除。

矩阵中被选择用来从其他行中消除相应元素的元素称为主元。每一次从一行中减去另一行的倍数的操作都将对应于支点的元素的系数改变为0,从而消除它。当然,该操作也会改变其他系数的值,因此这在图 10-7 中通过代表它们的字母的变化反映出来。只要方程是可解的,这个消去过程就是可能的。如果一个方程可以由一个或多个其它方程组合而成,情况就不是这样了。

寻找最佳支点

当然,有些系数可能是零,所以不能任意套用这个消去过程。例如,如果a 110,那么从第二行中减去第一行就会导致灾难。您需要确保 pivot 元素不为零。如果它的绝对值在列中是最大的,在数值上也是有利的。矩阵中行所代表的方程的顺序是任意的,所以行的顺序可以在任何时候改变而不会影响问题。因此,如果给定的透视不是最大值,您可以通过将当前透视行与包含最大绝对值的行交换来安排它成为最大值。图 10-8 说明了这一过程。

A978-1-4842-0004-9_10_Fig8_HTML.gif

图 10-8。

Choosing the best pivot

图 10-8 显示了第一次消除完成后五个方程的情况。透视的最佳值在倒数第二行,因此在下一个消除步骤之前,该行与第二行交换。当有许多变量时,交换一行中的所有元素在时间上是昂贵的,所以最好避免这种情况。通过使用slice对象来标识行,您可以交换行,而无需移动包含等式矩阵的valarray中的任何元素。

我们对高斯消去法如何开发代码有足够的了解。我们将把方程的所有数据存储在一个valarray<double>对象中。程序中会有几个函数,所以我会把除了main()以外的所有函数的定义放在一个单独的源文件gaussian.cpp中。我将从一个从标准输入流中读取方程数据的函数开始。

获取输入数据

n变量中的每个方程的形式为:

$$ {a}_1{x}_1+{a}_2{x}_2+\dots +{a}_n{x}_n=b $$

总会有n个方程和n个系数、a i ,对于每个方程和右边的值将作为连续元素存储在valarray对象中。因此,输入函数必须读取n*(n+1)值并将它们存储在valarray中。下面是实现这一点的代码:

// Read the data for n equations in n unknowns

valarray<double> get_data(size_t n)

{

valarray<double> equations(n*(n + 1));    // n rows of n+1 elements

std::cout << "Enter " << n + 1

<< " values for each of "<< n << " equations.\n"

<< "(i.e. including coefficients that are zero and the rhs):\n";

for(auto& coeff: equations) std::cin >> coeff;

return equations;

}

该函数期望将与变量数量相同的方程数量作为参数提供,因此调用程序(将是main())必须提供这个参数。这段代码可以做到:

size_t n_rows {};

std::cout << "Enter the number of variables: ";

std::cin >> n_rows;

auto equations = get_data(n_rows);

get_data()中创建valarray对象,并根据参数值创建所需数量的元素,基于范围的for循环从cin中读取每个元素的值。在get_data()本地创建并返回的对象将被移动到调用位置。

作为切片对象的行

当我们选择一个枢轴时,我们希望避免在equations对象中移动数据。我们可以通过创建一个slice对象来定义每一行,并将这些对象存储在一个序列容器中。在main()中可以这样创建slice对象:

std::vector<slice> row_slices;                        // Objects define rows in sequence

std::generate_n(std::back_inserter(row_slices), n_rows,

[n_rows]()

{ static size_t index {};

return slice {(n_rows+1)*index++, n_rows+1, 1};

});

generate_n()算法将n_rows slice对象存储在row_slices容器中,使用 lambda 表达式来创建它们。lambda 通过值捕获n_rowsslice对象的不同之处仅在于它们的起始索引值,从 0 开始以n_rows+1为步长运行,?? 是一行的长度。每个切片代表步长为1n_rows+1索引值。要交换两行,我们只需要用slice对象交换row_slices容器中的那些行;valarray中的元素可以留在原处。

您可以在容器中存储指向slice对象的指针,但是由于slice对象非常小,在我的系统中只有 12 个字节,所以似乎不值得这么麻烦。为了处理这些等式,我们只需要访问包含它们的数据的valarray对象和定义矩阵中行的row_slices容器。row_slices对象的大小是行数,所以当我们访问row_slices容器时,我们知道行数和长度。

寻找最佳支点

你在图 10-7 中看到了如何通过从后面的行中减去每一行的倍数来产生方程的行梯队形式。每一步都消除对角线左侧的一列变量,因此最后一行只有一个变量。在每个消除步骤之前,需要从该步骤中涉及的行中找到最佳支点,驱动消除过程的整个循环将从第一行到倒数第二行迭代这些行。寻找支点总是包括在一列中搜索元素,该列从equations矩阵的对角线开始,一直到最后一行。通过使用您计算的两个索引值访问equations对象中的元素,您可以识别并找到一列中的最大元素。我将使用一个slice对象进行练习。

我们需要能够定义一个slice对象来选择从任意行的对角线元素开始的一列元素。图 10-9 显示了这是如何确定的。

A978-1-4842-0004-9_10_Fig9_HTML.gif

图 10-9。

Determining the slice object for the column to search for a pivot

图 10-9 中有n_rows行表示方程,一行中有n_rows+1个元素。第一排是排0。从任何元素到下面的元素的跨度总是行长度。第n行中第一个元素的索引是n乘以行长度。第n行中对角线元素的索引将是第n行中第一个元素的索引加上n

下面是为任意行设置最佳透视的函数代码:

// Selects the best pivot in row n (rows indexed from 0)

void set_pivot(const valarray<double>& equations, std::vector<slice>& row_slices, size_t n)

{

size_t n_rows {row_slices.size()};             // Number of rows

size_t row_len {n_rows + 1};                   // Row length = number of columns

// Create an object containing the elements in column n, starting row n

valarray<double> column {equations[slice {n*row_len + n, n_rows - n, row_len}]};

column = std::abs(column);                     // Absolute values

size_t max_index {};                           // Index to best pivot in column

for(size_t i {1}; i < column.size(); ++i)      // Find index for max value

{

if(column[max_index] < column[i]) max_index = i;

}

if(max_index > 0)

{ // Pivot is not the 1st in column - so swap rows to make it so

std::swap(row_slices[n], row_slices[n + max_index]);

}

else if(!column[0])                            // Check for zero pivot

{ // When pivot is 0, matrix is singular

std::cerr << "No solution. Ending program." << std::endl;

std::exit(1);

}

}

这将为第n行的枢纽找到最佳选择,该枢纽位于第n列。其过程如图 10-9 所示。column对象包含感兴趣的列中元素的值,第一个元素在第n行。最初假设最佳枢轴是列中的第一个,即第n行。如果在第n行之后的一行中找到了透视,则透视不能是0,因为根据定义,它大于第n行中的元素。如果没有找到新的枢纽,则第n行的枢纽可能是0。这意味着列中的其他元素是0,在这种情况下,方程无解。

生成行梯队形式

在减少列中的元素之前,reduce_matrix()函数将使用set_pivot()函数选择最佳枢轴,将equations中的值矩阵转换为行梯队形式:

// Reduce the equations matrix to row echelon form

void reduce_matrix(valarray<double>& equations, std::vector<slice>& row_slices)

{

size_t n_rows {row_slices.size()};             // Number of rows

size_t row_len {n_rows + 1};                   // Row length

for(size_t row {}; row < n_rows - 1; ++row)    // From 1st row to second-to-last

{

set_pivot(equations, row_slices, row);       // Find best pivot

// Create an object containing element values for pivot row

valarray<double> pivot_row {equations[row_slices[row]]};

auto pivot = pivot_row[row];                 // The pivot element

pivot_row /= pivot;                          // Divide pivot row by pivot

// For each of the rows following the current row,

// subtract the pivot row multiplied by the row element in the pivot column

for(size_t next_row {row + 1}; next_row < n_rows; ++next_row)

{

equations[row_slices[next_row]] -=

equations[row_slices[next_row].start() + row] * pivot_row;

}

}

}

该函数从第一行到倒数第二行遍历equations中的行。对于每一行,通过调用set_pivot()来选择最佳支点。一个valarray对象被创建,包含当前行——数据透视表行——中元素的副本。valarray对象的operator/=()成员将左操作数的每个元素除以右操作数的值,并将其应用于pivot_row,将主元元素的值作为右操作数,使主元系数为 1。对于后续的每一行,pivot 行乘以 pivot 列中元素的值,然后从该行中减去结果的valarray对象。这会将 pivot 列中元素的值设置为 0。

倒转代换

利用行梯队形式的方程矩阵,我们可以进行回代以找到变量的值。由矩阵中最后一行定义的方程的所有可变系数都为零,除了最后一行。因此,最后一个变量的值将是右手边除以系数值。如果我们将最后一行除以系数,最后一个系数将是 1,变量的值将是该行中最后一个元素的值。然后,我们可以将最后一行乘以前面每一行中变量的系数,并依次从前面每一行中减去它。这将消除所有行中的最后一个变量,倒数第二个变量现在只有一个非零系数。然后我们可以重复这个过程,这听起来很像一个循环。图 10-10 显示了四个方程的过程。

A978-1-4842-0004-9_10_Fig10_HTML.gif

图 10-10。

Back substitution

该过程的结果是,除了对角线上的系数为 1 之外,所有系数都为零。因此,最后一列中的值代表方程的解。下面是实现图 10-10 所示过程的函数代码:

// Perform back substitution and return the solution

valarray<double> back_substitution(valarray<double>& equations,                                                       const std::vector<slice>& row_slices)

{

size_t n_rows{row_slices.size()};

size_t row_len {n_rows + 1};

// Divide the last row by the second to last element

// Multiply the last row by the second to last element in each row and subtract it from the row.

// Repeat for all the other rows

valarray<double> results(n_rows);              // Stores the solution

for(int row {static_cast<int>(n_rows - 1)}; row >= 0; --row)

{

equations[row_slices[row]] /=                        valarray<double>(equations[row_slices[row].start() + row], row_len);

valarray<double> last_row {equations[row_slices[row]]};

results[row] = last_row[n_rows];             // Store value for x[row]

for(int i {}; i < row; ++i)

{

equations[row_slices[i]] -= equations[row_slices[i].start() + row] * last_row;

}

}

return results;

}

最重要的是要记住,在这一点上是row_slices定义了方程的序列。寻找最佳枢纽元素的过程几乎肯定会改变方程的顺序,这是通过交换row_slices中的元素来完成的,而不是通过移动equations数组中的元素。因此,回代过程必须按照row_slices确定的顺序处理行,而不是按照它们在equations中出现的顺序处理行。因此,有必要定义results对象来存储方程解的值。外部循环以相反的顺序遍历行。在外部循环的每次迭代中,当前行除以对角线上的系数。然后创建一个包含当前行副本的valarray对象last_row。然后,内部循环从每个前面的行中减去last_row的倍数,其中倍数是该行中对角线元素的值。当外部循环结束时,返回包含解决方案值的results对象。

完整的程序

该程序由两个文件组成。gaussian.cpp文件内容将是:

// Gaussian.cpp

// Functions to implement Gaussian elimination

#include <valarray>                              // For valarray, slice, abs()

#include <vector>                                // For vector container

#include <iterator>                              // For ostream iterator

#include <algorithm>                             // For copy_n()

#include <utility>                               // For swap()

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

using std::valarray;

using std::slice;

// Definition for get_data() ...

// Definition for set_pivot() ...

// Definition for reduce_matrix() ...

// Definition for back_substitution() ...

main()程序只需读取数据并按正确的顺序调用gaussian.cpp中的函数,然后输出结果。Ex10_03.cpp的内容将是:

// Ex10_03.cpp

// Using the Gaussian Elimination method to solve a set of linear equations

#include <valarray>                              // For valarray, slice, abs()

#include <vector>                                // For vector container

#include <iterator>                              // For ostream iterator

#include <algorithm>                             // For generate_n()

#include <utility>                               // For swap()

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <string>                                // For string type

using std::string;

using std::valarray;

using std::slice;

// Function prototypes

valarray<double> get_data(size_t n);

void reduce_matrix(valarray<double>& equations, std::vector<slice>& row_slices);

valarray<double> back_substitution(valarray<double>& equations,

const std::vector<slice>& row_slices);

int main()

{

size_t n_rows {};

std::cout << "Enter the number of variables: ";

std::cin >> n_rows;

auto equations = get_data(n_rows);

// Generate slice objects for rows in row order

std::vector<slice> row_slices;                           // Objects define rows in sequence

size_t row_len {n_rows + 1};

std::generate_n(std::back_inserter(row_slices), n_rows,

[row_len]()

{ static size_t index {};

return slice {row_len*index++, row_len, 1};

});

reduce_matrix(equations, row_slices);                    // Reduce to row echelon form

auto solution = back_substitution(equations, row_slices);

// Output the solution

size_t count {}, perline {8};

std::cout << "\nSolution:\n";

string x{"x"};

for(const auto& v : solution)

{

std::cout << std::setw(3) << x + std::to_string(count+1) << " = "

<< std::fixed << std::setprecision(2) << std::setw(10)

<< v;

if(++count % perline) std::cout << '\n';

}

std::cout << std::endl;

}

main()中的第一个动作是读取待输入问题的变量数量。接下来,通过调用get_data()读取方程的数据,结果valarray对象将被移动到equations。在equations中选择连续行的slice对象的vector被创建。起始索引从 0 开始,以行长度为增量递增。所有slice对象的大小和步幅分别为行长和 1。调用reduce_matrix()后跟back_substitution()会返回解决方案,除非没有可能的解决方案。解的值在最后一个循环中输出。您可以使用基于范围的 for 循环来遍历valarray对象中的元素,因为迭代器是可用的。

以下是求解六个方程时的输出示例:

Enter the number of variables: 6

Enter 7 values for each of 6 equations.

(i.e. including coefficients that are zero and the rhs):

1  1  1  1  1  1   8

2  3 -5 -1  1  1 -18

-1  5  2  7  2  3  40

3  1 10  2  1 11 -15

3 17  5  1  3  2  41

5  7  3 -4  2 -1   9

Solution:

x1 =      -2.00

x2 =       1.00

x3 =       3.00

x4 =       4.00

x5 =       7.00

x6 =      -5.00

您可以通过在reduce_matrix()中的适当点添加equations的输出来有效地跟踪矩阵缩减的过程。你可以用类似的方式追踪回代机制。这将让您很好地了解高斯消去法的作用。你可以使用本章前面看到的print()函数模板来实现。使用slice对象很简单。是时候看看更有挑战性的东西了。

多个切片

valarray头定义了gslice类,这是对slice思想的一种概括。一个gslice对象从一个起始索引生成索引值,就像一个slice,但是它生成两个或更多的片,并且它的方式有点复杂。本质上,gslice假设元素是从一个valarray对象中选择的,该对象将多维数组表示为元素的线性序列。一个gslice对象由三个参数值定义。第一个构造函数参数是一个开始索引,是一个类型为size_t的值,它标识第一个片的第一个元素,就像一个slice一样。第二个参数是一个valarray<size_T>对象,其中每个元素指定一个大小。对于第二个构造函数参数指定的每个大小,都有对应的stride;这些跨度由第三个参数定义,它是一个与第二个参数具有相同元素数量的valarray<size_T>对象。第二个参数中每个尺寸的步幅是第三个参数中相应的元素。

当然,gslice表示的每个切片都有一个起始索引、一个大小和一个步幅。第一个片的起始索引是gslice构造函数的第一个参数。第一个切片的大小是大小的valarray中的第一个元素,步幅是步幅的valarray中的第一个元素。你会发现这很简单,但是现在变得有点棘手,但是坚持下去。

由第一切片生成的索引值是应用第二切片的开始索引。换句话说,第二个切片从第一个切片产生的每个索引中定义了一组新的索引。这个过程还在继续。

第一个切片之后的每个切片被应用到由前一个切片生成的每个索引,并且它们每个都产生一组索引值。例如,如果来自一个gslice对象的第一个切片的大小为3,它定义了三个索引值;如果第二个切片的大小为2,它将生成2索引值。第二个切片的大小和跨度使用了三次,第一个切片的每个索引值都作为起始索引。这样你从两个切片中得到了所有的6索引值,这就从一个valarray中选择了6个元素。

使用gslice作为valarray的下标的结果可以包括对给定元素的多个引用。当gslice的最后一个切片产生的索引序列重叠时,就会出现这种情况。图 10-11 显示了一个gslicevalarray中选择元素的简单示例。

A978-1-4842-0004-9_10_Fig11_HTML.gif

图 10-11。

How a gslice object selects elements from a valarray

图 10-11 中的gslice对象定义了两个切片。图 10-11 显示了第一个切片如何产生2索引值,这些索引值是重复应用大小为3的第二个切片的开始索引。因为最后两个索引序列重叠,所以索引5处值为6的元素在结果中是重复的。一般来说,gslice对象选择的元素数量是由第二个构造函数参数valarray对象定义的大小值的乘积。

slice对象一样,使用gslice索引valarray<T>会产生一个封装了对valarray中元素的引用的对象,但是该对象属于不同的类型——它属于类型gslice_array<T>。稍后我将介绍如何使用它。首先,让我们看看我们可以用gslice对象做的一些事情。

选择多行或多列

您可以使用一个gslice对象作为下标操作符的参数,从一个valarray对象中选择多行或多列。所选的行或列必须均匀间隔,这意味着连续行或列中的第一个元素之间的增量是相同的。选择两行或更多行相对简单。gslice的起始索引将是被选中的第一行中第一个元素的索引。第一个大小将是行数,对应的步幅将是行与行之间的步长,它是行长度的倍数。第二个大小和步幅值选择每一行中的元素,因此第二个大小是行长度,第二个步幅是 1。

假设你定义了名为sizesstridesvalarray对象,如下所示:

valarray<size_T> sizes {2, 5};         // Two size values

valarray<size_T> strides {10, 1};      // Stride values corresponding to the size values

从图 10-11 的数组中选择第一行和第三行的表达式为:

data[gslice{0, sizes, strides}]

这两行从索引值010开始;这些索引是由起始索引 0 定义的第一个片的结果,起始索引 0 是gslice构造函数的第一个参数,第一个大小及其在valarray对象中对应的步幅值是gslice构造函数的第二个和第三个参数。每一行都有5个连续的元素,这些行是由第二个大小5及其对应的步幅值 1 选择的。请注意,您没有义务明确定义sizesstrides。您可以编写表达式来选择两行,如下所示:

data[gslice{0, {2, 5}, {10, 1}}]

现在让我们考虑选择两列或更多列的更困难的任务。作为一个例子,让’看看如何从图 10-11 的数组中选择第一、第三和最后一列。图 10-12 说明了这一点,在元素的二维表示中被选中的列是灰色的。

A978-1-4842-0004-9_10_Fig12_HTML.gif

图 10-12。

Selecting multiple columns from a two-dimensional array

第一大小和步幅确定了要选择的每一列中的第一元素的索引值。第二个大小和步幅选择每列中的元素;列中元素之间的增量是行长度。因为第一步是固定的,所以您只能选择两个或更多以这种方式等距的列;例如,您不能选择第一、第二和第五列。

使用定义了可以应用于三维或多维数组的3或更多切片的gslice对象会变得更加复杂,但它的工作方式是一样的。你需要注意一个gslice对象不会创建无效的索引值;如果有,结果不明确,效果肯定不好。大多数时候,slicegslice对象被应用于一维或二维数组,所以我将集中讨论这些。

使用 gslice 对象

当你用一个gslice对象索引一个valarray<T>对象时,你得到的一个gslice_array<T>对象与一个slice_array对象有很多共同之处。它与slice_array具有相同的函数成员范围,因此相同范围的操作符可以应用于它。您有一个赋值操作符和相同范围的op=操作符。还有一个接受gslice_array对象作为参数的valarray构造函数,它可以用于从gslice_array<T>类型到valarray<T>类型的隐式转换。

让我们考虑一些我们可以用gslice做的事情。假设我们定义了下面的valarray对象:

valarray<int> data { 2,  4,  6,  8,      // 4 x 4 matrix

10, 12, 14, 16,

18, 20, 22, 24,

26, 28, 30, 32};

这有四行四个元素。我们可以使用前面看到的print()函数模板输出第二行和第三行:

valarray<size_T> r23_sizes {2,4};        // 2 and 4 elements

valarray<size_T> r23_strides {4,1};  // strides: 4 between rows, 1 between elements in a row

gslice row23 {4, r23_sizes, r23_strides};  // Selects 2nd + 3rd rows - 2 rows of 4

print(valarray<int>(data[row23]), 4);    // Outputs 10 12 14 16/18 20 22 24

row23对象以 1 为步长定义了行索引序列 4 到 7 和 8 到 11,这将从data中选择中间的两行。当然,您可以使用一条语句输出这两行:

print(valarray<int>(data[gslice{4, valarray<size_T> {2,4}, valarray<size_T> {4,1}}]), 4);

执行完语句后,gslice对象和包含尺寸和步幅的对象被丢弃。我觉得像这样从data里面选出来的东西比较难看,但是很管用。做同样事情的另一个较短的可能性是:

print(valarray<int>(data[gslice{ 4, {2,4}, {4,1} }]), 4);

下面是如何输出第二和第三列data:

std::valarray<size_T> sizes2 {2,4};    // 2 and 4 elements

// strides: 1 between columns, 4 between elements in a column

std::valarray<size_T> strides2 {1,4};

gslice col23 {1, sizes2, strides2};    // Selects 2nd and 3rd columns - 2 columns of 4

print(valarray<int>(data[col23]), 4);  // Outputs 4 12 20 28/6 14 22 30

gslice的起始索引是第二个元素,它是第二列中的第一个元素。现在应该清楚这是如何识别来自data的两列的了。

我们现在可以将第二行和第三行的值添加到第二列和第三列,如下所示:

data[col23] += data[row23];

print(data, 4);

执行这些语句将产生以下输出:

2       14       24        8

10       24       34       16

18       34       44       24

26       44       54       32

如果您将它与用于初始化data的原始值进行比较,您会看到我们得到了想要的结果。第二列是4+1012+1220+1428+16;第三列是6+1814+2022+2230+24

选择任意元素子集

有时候,您可能希望在访问valarray元素时比slicegslice提供更多的灵活性,它们提供了valarray固有的规则索引。在这种情况下,您可以使用包含任意一组索引值的valarray<size_T>对象作为valarray<T>对象的下标。结果是一个类型为indirect_array<T>的对象,它封装了对索引值处元素的引用。注意,索引值必须是类型size_t;一个valarray<int>不行。

一个indirect_array对象和一个slice_array对象有相同的函数成员,所以你可以用它做同样的事情。还有一个valarray构造函数,支持从类型indirect_array<T>到类型valarray<T>的隐式转换。

可以使用一个valarray<size_T>对象选择数组中元素的任意组合,但是不应该复制索引值。如果一个indirect_array对象包含重复的引用,那么对它的操作结果是未定义的;它可能在某些时候有效,但不一定总是有效。下面是一个从valarray中选择任意一组元素的例子:

valarray<double> data {2,  4,  6,  8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32};

std::valarray<size_T> choices {3, 5, 7, 11};     // Indexes to select arbitrary elements

print(valarray<double>(data[choices]));          // Values selected:  8  12 16   24

data[choices] *= data[choices];                  // Square the values

print(valarray<double>(data[choices]));          // Result:          64 144 256 576

choices对象包含从data对象中选择四个浮点值的索引值。倒数第二条语句平方由choices选择的值,执行这些语句的结果显示在注释中。因为choices是一个valarray,所以您可以对索引值的集合执行算术运算来产生一个新的集合。例如:

size_t incr{3};

data[choices+incr] += valarray<double>(incr+1, choices.size());  // Add 4 to selected elements

print(valarray<double>(data[choices+incr]));                     // 18 22 26 34

表达式choices+incr产生一个新的valarray<size_T>对象,它包含来自choices的索引值,该值增加了3,因此它包含681014。使用新的valarray对象作为data的下标操作符的参数返回一个indirect_array<double>对象,该对象包含对具有值14182230data元素的引用。+=操作通过作为右操作数的valarray<double>对象的相应元素的值来增加这些值,这些值都是4

有条件地选择元素

您在前面已经看到,您可以使用比较运算符将一个valarray<T>对象中的相应元素与另一个valarray<T>对象中的相应元素进行比较,或者与一个类型为T的值进行比较。这两种情况的结果都是一个valarray<bool>对象,它的元素值是元素比较的结果。您可以将一个valarray<bool>对象传递给一个valarray的下标操作符,该操作符将选择对应于true的元素,这样您就有了一种基于任何条件选择元素的方法。使用valarray<bool>对象作为下标的结果是一个mask_array<T>对象,其中T是被访问数组中的值类型。一个mask_array对象与一个slice_array对象具有相同的功能。这里有一个非常人为的例子:

std::uniform_int_distribution<int> dist {0, 25};

std::random_device rd;                            // Non-deterministic seed source

std::default_random_engine rng {rd()};            // Create random number generator

std::valarray<char> letters (52);

for(auto& ch: letters)

ch = 'a' + dist(rng);                           // Random letter 'a' to 'z'

print(letters,26, 2);

auto vowels = letters == 'a'||letters =='e'|| letters == 'i' ||

letters == 'o' || letters == 'u';

valarray<char> chosen {letters[vowels]};             // Contains copies of the vowels in letters

size_t count {chosen.size()};                     // The number of vowels

std::cout << count << " vowels were generated:\n";

print(chosen, 26, 2);

letters[vowels] -= valarray<char>('a'-'A', count);

print(letters, 26, 2);

这段代码演示了有条件地选择元素——这肯定不是最好的方法。它顺便借鉴了你在第八章看到的一些东西。letters对象存储类型为char的元素,并在基于范围的for循环中使用均匀离散分布填充随机小写字母。分布dist生成范围025的值,因此在循环中我们得到从'a''z'的字母。vowels对象是通过对letters中的元素与每个元音的比较结果进行或运算而产生的。每次比较都会产生一个与letters元素数量相同的valarray<bool>对象。当相应的元素是元音时,对象将具有元素true。对这些元素进行“或”运算(使用非成员operator||()函数)会产生一个valarray<bool>对象,其中包含值为true的元素,而letters的对应元素是任何小写元音。使用valarray<bool>对象vowels来下标letters产生一个mask_array<char>对象,该对象引用了letters中的元音元素。最后,调用mask_array<char>operator-=()成员,将letters中为元音的元素减去'a''A'之差,从而转换为大写。

我得到了这样的输出:

d  a  v  i  d  h  o  T&#x00A0; x  v  i  v  d  o  p  i  i  n  d  q  p  g  r  q  f  s

g  i  c  e  w  o  b  r  e  T&#x00A0; a  b  w  l  l  q  j  h  x  f  j  h  n  p  o  y

13 vowels were generated.

a i o i o i i i e o e a o

d  A  v  I  d  h  O  T&#x00A0; x  v  I  v  d  O  p  I  I  n  d  q  p  g  r  q  f  s

g  I  c  E  w  O  b  r  E  T&#x00A0; A  b  w  l  l  q  j  h  x  f  j  h  n  p  O  y

这一成果让我深受鼓舞。前八个随机字母证明了这样一个想法,即有了足够大的数组,并执行代码足够多次,就可以生成莎士比亚的全部作品。

理性算术

ratio头定义了ratio模板类型,这在很多方面都是一个奇怪的东西,特别是因为它所做的一切都是在编译时完成的,而不是在运行时。你不需要创建对象——只需要定义类型的ratio类模板的实例。如果你打算使用我将在本章后面讨论的时钟和定时器,理解ratio类模板是必不可少的。

有理数只是一个分数——两个整数的比率。如你所知,许多十进制分数不能精确地用二进制数来表示,或者用十进制数来表示。例如,你不能用二进制或十进制精确地表示 2/3;这两种表示都需要无限多的精确数字,所以许多有理数的浮点表示总是与它们的精确值略有偏差。当然,误差很小,对于 24 位尾数,误差通常不会大于 2 -24 。这是可以忽略的——除非你用这些值做一些计算。假设一个有理数有确切的值V,但是在浮点中它的值是V-e,其中e是一个非常小的误差。让我们考虑一个简单的例子,浮点值乘以它自己。该值是(V-e)*(V-e)的结果,即评估为V2-2Ve+e2。我们想要的确切结果是V 2 ,所以剩下的就是对正确结果的偏离。e 2 的部分是按照2 -48 的顺序,我们可以忽略不计,但是其余的部分,2Ve就不那么可以忽略不计了。计算结果中的误差比原始值中的误差大2V倍,并且作为后续计算的结果,该误差会进一步增加。ratio模板和ratio头文件定义的其他模板类型提供了一种克服这个问题的方法——至少在编译时。

ratio template定义了表示有理数的类型,有理数由分子和分母定义,分子和分母都是类型intmax_t. intmax_t的整数值,在cstdint头中定义为在您的实现中具有最大范围的整数类型。注意,表示有理数的是类型,而不是对象。因此,您可以通过以下类型来表示2/3:

using v2_3 = std::ratio<2, 3>;                   // A type to represent two thirds

类型的模板参数是有理数的分子和分母的值。这些值分别存储在类类型的静态成员中,numden,它们是常量静态成员,所以在定义了类型之后就不能更改它们。den参数有一个默认值1,因此您可以通过指定第一个类型参数来定义表示整数的类型。例如,ratio<99>是表示值99的类型。我将假设有一个针对std::ratiousing指令对后续代码有效,并删除std名称空间名称限定符。

一个ratio类型总是用最低的术语表示一个数。例如,如果定义类型ratio<4,8>,numden将分别具有值12。您可以用下面的语句在运行时输出v2_3代表的数字:

std::cout << "The v2_3 type represents " << v2_3::num << "/" << v2_3::den << std::endl;

类型之间的算术运算由进一步的模板类型定义,因此由编译器来完成。你可以这样添加2/33/7:

using v2_3 = ratio<2, 3>;                   // A type to represent two thirds

using v3_7 = ratio<3, 7>;                   // A type to represent three sevenths

using sum = std::ratio_add<v2_3, v3_7>;     // A type representing the sum of 2/3 and 3/7

std::cout << sum::num << "/" << sum::den << std::endl;   // Output: 23/21

ratio_add<T1,T2>的一个实例是ratio模板的特殊化,ratio<T3,T4>. T3T4将是对应于加法结果的分子和分母的值。因为它是ratio类型的特殊化,所以sum类型有静态成员numden,它们代表从运算中得到的有理数的分子和分母,所以我们能够输出它们。

您不必为每个ratio类型定义一个别名。编译器可以推断出类型。您可以将加法定义为:

using sum = ratio_add< ratio<2, 3>, ratio<3, 7>>;  // A type for the sum of 2/3 and 3/7

这与前面定义的sum别名相同。还有其他表示有理数之间算术运算的模板类型:

  • 一个ratio_subtracT<T1, T2>类型实例是一个ratio类型,它表示从由类型T1表示的值中减去由类型T2表示的值的结果。
  • 一个ratio_multiply<T1, T2>类型实例是一个ratio类型,它表示由类型T1T2表示的值的乘积。
  • 一个ratio_divide<T1, T2>类型实例是一个ratio类型,它表示由类型T1表示的值除以由类型T2表示的值的结果。

所有这些都在编译时工作,导致了ratio模板的专门化,并且您可以将它们应用于任何您喜欢的组合中。结果是一个ratio类型,所以结果总是在其最低项。这使得在多次算术运算后分子或分母超出整数类型容量的风险最小化。也有对零分母的检查。下面是一个用ratio类型实例进行算术运算的例子:

using result = std::ratio_multiply<std::ratio_add<ratio<2, 3>, ratio<3, 7>>, ratio<15>>;

std::cout << result::num << "/" << result::den << std::endl; // Output: 115/7

result的定义产生了一个从(2/3+3/7)*15产生的ratio实例。它所代表的值显示在注释中。

有表示两个ratio类型所代表的值的比较结果的模板,从模板类型名称中可以明显看出这种比较:

ratio_equal<RT1, RT2>         ratio_less<RT1, RT2>       ratio_less_equal<RT1, RT2>

ratio_not_equal<RT1, RT2>     ratio_greater<RT1, RT2>    ratio_greater_equal<RT1, RT2>

模板参数是代表有理数的ratio模板的实例。每个比较模板类型都有一个静态的bool成员value,如果ratio类型表示的数字的比较结果是true,那么这个成员就是true。您可以在运行时使用它来检查ratio实例之间的关系:

using div1 = std::ratio_divide<ratio<7, 10>, ratio<11, 7>>;

using div2 = std::ratio_divide<ratio<9, 5>, ratio<3, 7>>;

std::cout << "(7/10)/(11/7) "

<< (std::ratio_greater<div1, div2>::value ? "is" : "is_not")

<< " greater than (9/5)/(3/7)" << std::endl;

所有用于比较ratio类型的类型都定义了运算符bool()和函数调用运算符operator()()。前者允许一个比较类型的对象隐式地转换为类型bool,所以您可以写:

std::ratio_greater<div1, div2> cmp;

std::cout << "(7/10)/(11/7) " << (cmp ? "is" : "is_not") << " greater than (9/5)/(3/7)"

<< std::endl;

第一条语句创建一个对象,表示比较ratio类型的结果。通过调用对象的operator bool()成员,对象在输出语句中被隐式转换为类型bool

您也可以将输出语句写成:

std::cout << "(7/10)/(11/7) " << (cmp() ? "is" : "is_not") << " greater than (9/5)/(3/7)"

<< std::endl;

这将调用cmp对象的operator()(),该对象返回值成员,因此结果是相同的。当然,比较的结果是false

ratio头还为代表有用 SI 比率的ratio类型实例定义了以下别名:

| SI 前缀 | 价值 | 类型别名 | 价值 | | --- | --- | --- | --- | | `deca` | `10` | `deci` | `10``-1` | | `hecto` | `10``2` | `centi` | `10``-2` | | `kilo` | `10``3` | `milli` | `10``-3` | | `mega` | `10``6` | `micro` | `10``-6` | | `giga` | `10``9` | `nano` | `10``-9` | | `tera` | `10``12` | `pico` | `10``-12` | | `peta` | `10``15` | `femto` | `10``-15` | | `exa` | `10``18` | `atto` | `10``-18` | | `zetta` | `10``21` | `zepto` | `10``-21` | | `yotta` | `10``24` | `yocto` | `10``-24` |

表示整型常量的类型都有第二个模板参数,因此将den成员作为默认值 1;对于其他的,它是第一个模板参数,因此num成员是 1。如果类型intmax_t在您的系统上所能代表的最大值不够大,类型yoctozeptozettayotta将不会被定义。这些常数有助于将误差的可能性降至最低,尤其是当您需要使用非常大或非常小的 SI 比率时。很容易误填太多或太少的零。

我展示的语句演示了ratio模板类型是如何工作的,但是它是做什么用的呢?你不会在编译时用它来做大量的计算。它的目的是允许在编译时容易地定义有理数,特别是通过模板参数值。在编译时用它们执行任何必要的算术运算有助于避免溢出的可能性。在下一节中,您将遇到一个 STL 模板,它要求您提供一个 ratio 模板类型实例作为模板参数值。

时态模板

程序中经常需要处理时间间隔。游戏程序是一个显而易见的环境,这可能是必要的,并且需要测量许多应用程序的执行性能。当然,测量时间不仅仅涉及软件。底层硬件提供时钟和间隔计时功能,而您的实现提供的 STL 功能是通过操作系统与硬件的接口。STL 提供的所有时间功能最终将通过操作系统提供的接口与硬件中的计时器连接。

chrono头定义了与时间间隔或持续时间、瞬间和时钟相关的类和类模板。稍后您将会看到,您可能想要使用带有时钟时间的ctime头的功能。在chrono头中的所有名字都在std::chrono名称空间中定义。时间间隔、瞬间和时钟是相互关联的,它们之间的关系如下:

  • 持续时间是定义为时间刻度数的时间间隔,您可以用秒来指定刻度。因此,分笔成交点是衡量持续时间的基本周期。作为duration模板实例的对象类型定义了持续时间。tick 表示的默认时间间隔是一秒,但是您可以将其定义为一秒的倍数或分数。例如,如果您将一个分笔成交点定义为 3600 秒,则意味着持续时间 10 是 10 个小时;例如,您也可以将刻度定义为十分之一秒,在这种情况下,持续时间 10 表示一秒。
  • 时钟记录从一个给定的固定瞬间开始的时间流逝——称为一个纪元。一个时期是一个固定的时间点。有三种封装硬件时钟的类类型,我将在后面描述它们。时间是以滴答来度量的,因此给定的时钟将由一个时期和一个决定滴答周期的持续时间来定义。
  • 时间上的一个实例称为时间点,它将由一个time_point类模板类型的对象来表示。时间点是相对于时间开始点的持续时间,其中时间开始点是由时钟定义的时期。因此,给定的时间点将由提供时期和持续时间的时钟来定义,该持续时间定义相对于时期和滴答周期的滴答数量。

我们先来看看如何定义一个持续时间,以及可以用它做什么。

定义持续时间

chrono标题中的std::chrono::duration<T,P>模板类型代表持续时间。模板参数T是值的类型,它通常是基本的算术类型之一,对应于参数P的值是分笔成交点表示的秒数,这是对应于值 1 的秒数。P的值必须由ratio类型指定,其默认值为ratio<1>。以下是一些持续时间的示例:

std::chrono::duration<int,

std::milli> IBM650_divide {15};               // A tick is 1 millisecond so 15 milliseconds

std::chrono::duration<int> minute {60};       // A tick is 1 second by default so 60 seconds

std::chrono::duration<double, ratio<60>> hour {60}; // A tick is 60 seconds so 60 minutes

// A tick is a microsecond so 1 millisecond

std::chrono::duration<long, std::micro> millisec {1000L};

// A tick is fifth of a second so 1.1 seconds

std::chrono::duration<double, ratio<1,5>> tiny {5.5};

第一条语句使用来自对应于ratio<1, 1000>ratio报头的milli别名。第二条语句省略了第二个模板参数值,所以它是ratio<1>,这意味着持续时间以 1 秒为单位。在第三条语句中,ratio<60>模板参数值指定一个刻度为60秒,因此小时对象的值以分钟为单位,其初始值代表一个小时。第四条语句使用了ratio头定义为ratio<1, 1000000>micro类型,因此 tick 是一微秒,而millisec变量有一个代表毫秒的初始值。最后一条语句定义了一个对象,其中滴答是五分之一秒,tiny的初始值是5.5五分之一秒,即1.1秒。

chrono头在std::chrono名称空间中为常用的具有整型值的duration类型定义了别名。标准别名包括:

nanoseconds<integer_type, std::nano>          microseconds<integer_type, std::micro>

milliseconds<integer_type, std::milli>        seconds<integer_type>

minutes<integer_type, std::ratio<60>>         hours<integer_type, std::ratio<3600>>

具有这些别名的持续时间值的整数类型取决于您的实现,但是 C++ 14 标准要求它们允许至少 292 年的持续时间,正的或负的。在我的系统中,类型hoursminutes将持续时间存储为类型int,其他类型将其存储为类型long long。因此,您可以将前面代码片段中的millisec变量定义为:

std::chrono::microseconds millisec {1000};    // Duration is type long long on my system

这并不完全等同于我系统上之前对millisec的定义,因为之前的第一个类型参数是long——这里是long long。您也可以这样定义millisec:

std::chrono::milliseconds millisec {1};         // Duration is also type long long on my system

当然,这个定义和原来更不一样。变量的初始值代表相同的时间间隔——1 毫秒——但是这里持续时间的时间单位是 1 毫秒,而在前面的语句中是 1 微秒。millisec的前一个定义允许更精确地表示持续时间。

持续时间之间的算术运算

您可以对一个duration对象应用前缀和后缀的递增和递减操作符,并且您可以通过调用count()成员来获得一个对象所代表的刻度数。下面的代码说明了这一点:

std::chrono::duration<double, ratio<1,5>> tiny {5.5};     // Measured in 1/5 second

std::chrono::microseconds very_tiny {100};                // Measured in microseconds

++tiny;

very_tiny--;

std::cout << "tiny = " << tiny.count()

<< " very_tiny = " << very_tiny.count()

<< std::endl;                                   // tiny = 6.5 very_tiny = 99

您可以将任何二进制算术运算符、+-*/%应用于duration对象,并获得一个duration对象作为结果。这些是作为非成员运算符函数实现的。这里有一个例子:

std::chrono::duration<double, ratio<1,5>> tiny {5.5};

std::chrono::duration<double, ratio<1,5>> small {7.5};

auto total = tiny + small;

std::cout << "total = " << total.count() << std::endl;    // total = 13

算术运算符还处理类型可以是std::chrono::duration<T,P>模板的不同实例的操作数,其中两个模板参数可以不同。这是通过使用在type_traits头中定义的common_type<class... T>模板的专门化将两个操作数转换成它们的通用类型来实现的。对于类型为duration<T1, P1>duration <T2, P2>的参数,返回值将是持续时间类型,duration<T3, P3>. T3将是T1T2之间的通用类型;这将是通过对这些类型的值应用算术运算而得到的类型。P3将是P1P2的最大公约数。一个例子将使这一点更加清楚:

std::chrono::milliseconds ten_minutes {600000};   // A tick is 1 millisecond so 10 minutes

std::chrono::minutes half_hour {30};              // A tick is 1 minute so 30 minutes

auto total = ten_minutes + half_hour;             // 40 minutes in common tick period

std::cout << "total = " << total.count()

<< std::endl;                           // total = 2400000

加法的结果必须是40分钟,这样你就可以推断出total是一个类型为milliseconds的对象。这是另一个例子:

std::chrono::minutes ten_minutes {10};                            // 10 minutes

std::chrono::duration<double, std::ratio<1,5>> interval {4500.0}; // 15 minutes

auto total_minutes = ten_minutes + interval;

std::cout << "total minutes = " << total_minutes.count()

<< std::endl;                                           // total minutes = 7500

total_minutes的值的类型为double。我们知道结果必须是25分钟,也就是1500秒;结果的值是7500,所以它的滴答周期是ratio<1,5>——五分之一秒。我认为最好尽可能避免混合duration类型的算术运算,因为太容易忘记 tick 是什么。

所有可以应用于duration对象的算术运算符都可以在复合赋值中使用,其中左边的运算是一个duration对象。这些+=-=操作的右操作数必须是一个duration对象。使用*=/=时,右操作数必须是与左操作数的节拍数类型相同的数值,或者可以隐式转换为该数值。%=的右操作数可以是一个duration对象或一个数值。它们每一个都会产生你所期望的结果。例如,下面的代码使用了+=:

std::chrono::minutes short_time {20};

std::chrono::minutes shorter_time {10};

short_time += shorter_time;                                       // 30 minutes

std::chrono::hours long_time {3};                                 // 3hrs = 180 minutes

short_time += long_time;

std::cout << "short_time = " << short_time.count() << std::endl;  // short_time = 210

第一个+=操作的操作数都是相同的类型,所以存储在对象中的值(右操作数)被加到左操作数上。对于第二个+=操作,操作数是不同的类型,但是右操作数被隐式转换为左操作数的类型。这是可能的,因为转换是到具有较短分笔成交点周期的持续时间类型。反过来就不行了——所以你不能用+=long_time作为左操作数,右操作数作为short_time

持续时间类型之间的转换

一般来说,如果一个duration类型和另一个duration类型都是浮点值,那么它们总是可以隐式地转换成另一个持续时间类型。对于整数值,只有当源类型的节拍周期是目标类型的节拍周期的整数倍时,隐式转换才是可能的。以下是一些例子:

std::chrono::duration<int, std::ratio<1, 5>> d1 {50};      // 10 seconds

std::chrono::duration<int, std::ratio<1, 10>> d2 {50};     // 5 seconds

std::chrono::duration<int, std::ratio<1, 3>> d3 {45};      // 15 seconds

std::chrono::duration<int, std::ratio<1, 6>> d4 {60};      // 10 seconds

d2 += d1;                                        // OK - implicit conversion of d1

d1 += d2;                                        // Won’t compile 1/10 not a multiple of 1/5

d1 += d3;                                        // Won’t compile 1/3 not a multiple of 1/5

d4 += d3;                                        // OK - implicit conversion of d3

您可以通过使用duration_cast模板显式指定来强制转换。这里有一个例子,假设d1d2有上面代码中定义的初始值:

d1 += std::chrono::duration_cast<std::chrono::duration<int, std::ratio<1, 5>>>(d2);

std::cout << d1.count() << std::endl;                      // 75 - i.e. 15 seconds

第一条语句使用duration_cast来允许操作将d1增加持续时间d2以继续进行。在这种情况下,结果是准确的,但情况并不总是如此。例如:

std::chrono::duration<int, std::ratio<1, 5>> d1 {50};      // 10 seconds

std::chrono::duration<int, std::ratio<1, 10>> d2 {53};     // 5.3 seconds

d1 += std::chrono::duration_cast<std::chrono::duration<int, std::ratio<1, 5>>>(d2);

std::cout << d1.count() << std::endl;                      // 76 - i.e. 15.2 seconds

您不能将durationd1d2的和表示为.2秒的整数倍,因此结果值会稍微有些偏差。如果d2的值为54,将获得77的正确结果。

duration类型支持赋值,所以你可以将一个duration对象的值赋给另一个。如果我在本节开始时描述的条件适用,隐式转换将适用;否则,您需要显式转换右操作数。

例如,您可以编写以下内容:

std::chrono::duration<int, std::ratio<1, 5>> d1 {50};      // 10 seconds

std::chrono::duration<int, std::ratio<1, 10>> d2 {53};     // 5.3 seconds

d2 = d1;                                                   // d2 is 100 = 10 seconds

比较持续时间

比较两个duration对象有完整的操作符。这些被实现为非成员函数,并允许比较不同类型的duration对象。该过程确定操作数通用的节拍周期,并比较通用节拍周期中表示的duration值。例如:

std::chrono::duration<int, std::ratio<1, 5>> d1 {50};      // 10 seconds

std::chrono::duration<int, std::ratio<1, 10>> d2 {50};     // 5 seconds

std::chrono::duration<int, std::ratio<1, 3>> d3 {45};      // 15 seconds

if((d1 - d2) == (d3 - d1))

std::cout << "both durations are "

<< std::chrono::duration_cast<std::chrono::seconds>(d1 - d2).count()

<< " seconds" << std::endl;

这显示了比较算术运算产生的duration对象。当然,它们是相等的,所以会产生输出。无论结果的duration类型如何,类型seconds的转换都允许显示整数秒。如果您想要秒数的非整数值,您可以转换成类型duration<double>

持续时间文字

chrono头定义了操作符,使您能够指定属于duration对象的文字。这些操作符是在名称空间std::literals::chrono_literals中定义的,其中名称空间literalschrono_literals是内联的。您可以通过声明对duration文字使用文字运算符:

using namespace std::literals::chrono_literals;

但是,如果您指定声明,则会自动包含此声明:

using namespace std::chrono;

您可以将持续时间文字指定为整数或浮点值,并使用后缀来指定节拍周期。您可以使用六种后缀:

  • h是小时。例如3h1.5h
  • min是分钟。例如20min3.5min
  • s是秒,10s1.5s为例。
  • ms是毫秒。例如500ms1.5ms
  • us是微秒。例如500us0.5us
  • ns是纳秒。例如2ns3.5ns

如果一个duration文字有一个整数值,它将是那些在chrono头中定义的合适的别名类型,因此24h将是一个std::chrono::hours类型的文字,而25ms将是std::chrono::milliseconds类型的文字。如果文字的值不是整数,则文字将是具有浮点值类型的duration类型。浮点值的周期取决于后缀;对于后缀hminsmsusns,节拍周期分别为ratio<3600>ratio<60>ratio<1>millimicronano

下面举例说明了它们的一些使用方法:

using namespace std::literals::chrono_literals;

std::chrono::seconds elapsed {10};      // 10 seconds

elapsed += 2min;                        // Adding type minutes to type seconds: 130 seconds

elapsed -= 15s;                         // 115 seconds

当您需要按照图示的数量来改变间隔时,duration文字非常有用。请记住,为了实现算术运算,右操作数的值的时钟周期必须是左操作数的时钟周期的整数倍。例如:

elapsed += 100ns;              // Won’t compile!

elapsed变量的周期为 1,不能添加周期小于 1 的duration

您可以使用文本来定义与文本类型相同的变量。例如:

auto some_time = 10s;          // Variable of type seconds, value 10

elapsed = 3min - some_time;    // Set to difference between literal and variable: result 170

some_time *= 2;                // Doubles the value - now 20s

const auto FIVE_SEC = 5s;      // Cannot be changed

elapsed = 2s + (elapsed - FIVE_SEC)/5;  // ResulT  35

这里,some_time将是一个类型为seconds的变量,类型为duration<long long, ratio<1>>,值为 10。第三条语句说明您可以更改类型为const secondssome_time. FIVE_SEC的值,因此您不能更改它的值。最后一条语句显示了一个算术表达式,包含一个duration文字、一个duration变量、一个const seconds对象和一个整数文字。

时钟和时间点

STL 定义的时钟类型通过操作系统提供了与硬件时钟的接口。时钟有一个滴答周期,时间由时钟以滴答的数量来度量。在std::chrono名称空间中定义了三种时钟:

  • system_clock类封装了当前的实际时钟时间。虽然时间一般会随着这个时钟而增加,但也可能随时减少。当然,当在冬季和夏季之间进行季节性调整时,挂钟的时间会减少。如果它移动到不同的时区,它也会改变。
  • steady_clock类封装了一个适合记录时间间隔的时钟。这个时钟总是在增加,不能减少。
  • high_resolution_clock的一个实例是当前系统中时钟周期最短的时钟。对于某些实现,这可能只是system_clocksteady_clock的别名,在这种情况下,它不提供额外的分辨率。

每种时钟类型都定义了自己的纪元和持续时间。持续时间决定了时钟的滴答周期和记录相对于纪元的滴答数量的类型。如果时钟记录的时间总是增加,并且总是以相同长度的步长增加,那么它就是稳定的——换句话说,时钟滴答之间的时间是恒定的。并非所有的时钟都是如此。一个system_clock通常不是一个稳定的时钟,因为它不能保证总是增加,并且系统活动会影响记录滴答之间的时间。这就是为什么steady_clock型是测量时间间隔的首选。

每种时钟类型都封装了一个物理硬件时钟——作为处理器的一部分或其他地方的芯片——但有三种时钟类类型并不意味着您必须有三个不同的时钟。创建时钟对象没有必要,也没什么意义。时钟类型通过static成员提供它们与硬件时钟的接口。

所有三种时钟类型都有一个类型为bool的数据成员is_steady。此成员的值指示时钟是否是稳定时钟。is_steady对于steady_clock始终是true,对于其他时钟类型可以是truefalse,这取决于您的实现。说到这里,is_steady通常是system_clockfalse。如果你的代码依赖于一个稳定的时钟,你应该总是检查is_steady成员的状态——或者只使用steady_clock。检查稳定的时钟很容易:

std::cout << std::boolalpha << std::chrono::system_clock::is_steady << std::endl;

这条语句在我的系统上输出false,可能也会在你的系统上输出。当然,如果high_resolution_clock类型是system_clock的别名,那么您只有一个稳定的时钟。每个时钟类都将以下类型别名定义为成员:

  • rep是记录分笔成交点数量的算术类型的别名
  • periodratio模板类型的别名,它定义了以秒为单位的分笔成交点
  • durationstd::chrono::duration<rep, period>的别名
  • time_point是时间点类型的别名,表示时钟的时间瞬间。这将是std::chrono::time_point<std::chrono::Clock_Type>

因此,当你需要知道一个类型为system_clock的时钟的周期时,表达式system_clock::period可以提供。这是一个ratio类型,所以一个刻度代表的秒数的数值是system_clock::period::num除以system_clock::period::den

创建时间点

time_point对象表示相对于由时钟定义的时期测量的时间瞬间。因此,time_point对象总是基于时期和相对于该时期的持续时间来定义。当你问时钟时间时,你得到一个time_point对象。std::chrono::time_point类模板定义了time_point类型。这个模板有两个类型参数,一个是时钟类型Clock,它将提供纪元,另一个是持续时间类型,它将是Clock类型默认定义的duration类型。因此,当您定义一个第一个模板类型参数值为std::chrono::system_clocktime_point对象时,第二个类型参数的默认值将为std::chrono::system_clock::duration

一个time_point对象总是与一个特定的时钟类型相关,所以当你创建一个对象时,使用一个特定时钟类型的成员time_point类型别名通常是很方便的。但是,如果您愿意,可以将时钟类型指定为模板参数值。例如:

std::chrono::system_clock::time_point tp_sys1;                // Default object - the epoch

std::chrono::time_point<std::chrono::system_clock> tp_sys2;   // Default object - the epoch

两条语句都调用默认的time_point<system_clock>构造函数。默认构造函数创建一个对象,表示您指定的时钟类型的纪元,因此持续时间为 0。第一个语句不太详细,因此更可取。通过为时钟和时间点类型定义别名,可以使代码更加简洁,我将在后续的代码中这样做。

您可以使用构造函数的一个duration参数创建一个time_point对象,表示相对于一个纪元的一个瞬间。duration对象定义了添加到纪元中的时间:

using Clock = std::chrono::steady_clock;

using TimePoint = std::chrono::time_point<Clock>;

TimePoint tp1 {std::chrono::duration<int> (20)};              // Epoch + 20 seconds

TimePoint tp2 {3min};                                         // Epoch + 180 seconds

TimePoint tp3 {2h};                                           // Epoch + 720 seconds

TimePoint tp4 {5500us};                                       // Epoch + 0.0055 seconds

这些语句说明了传递给time_point构造函数的duration对象可以有任意的周期。TimePoint别名也可以用这个指令定义:

using TimePoint = Clock::time_point;

您可以定义一个time_point对象,其时钟周期不同于定义纪元的时钟类型。例如:

std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes> tp {2h};

除非你有充分的理由,否则你不会这么做。这定义了一个time_point对象tp,其纪元由system_clock类型定义,周期为分钟,初始值为持续时间,表示两个小时。

时间点的持续时间

您可以通过调用其time_since_epoch()函数成员,从表示自纪元以来经过的时间的time_point对象中获得一个duration对象:

using Clock = std::chrono::steady_clock;

using TimePoint = Clock::time_point;

TimePoint tp1 {std::chrono::duration<int> (20)};           // Epoch + 20 seconds

auto elapsed = tp1.time_since_epoch();                     // Duration for the time interval

现在你有了duration对象,你有了它所代表的时间。你不知道elapsed对象的类型,但是你知道它是一个duration类型,因此你可以把它转换成一个已知的duration类型。例如,您可以获得elapsed表示的纳秒数,如下所示:

auto ticks_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();

ticks_ns的值是elapsed代表的间隔中的纳秒数。当然,如果您不需要纳秒分辨率的时间,您可以将它转换为其他持续时间类型别名,如millisecondssecondshours。您可以使用time_since_epoch()函数来定义一个函数模板,该模板将显示任何time_point以秒为单位表示的时间间隔:

// Outputs the exact interval in seconds for a time_poinT<>

template<typename TimePoint>

void print_timepoint(const TimePoint& tp, size_t places = 0)

{

auto elapsed = tp.time_since_epoch();          // duration object for the interval

auto seconds = std::chrono::duration_cast<std::chrono::duration<double>>(elapsed).count();

std::cout << std::fixed << std::setprecision(places) << seconds << " seconds\n";

}

使用duration_cast<double>转换elapsed对象会产生一个duration对象,其中包含作为double值的滴答计数和作为一秒的滴答周期。为此对象调用count()将返回以秒为单位的时间,作为类型double的值。该值按照第二个参数确定的小数点后的位数写出。我们将在下一节的工作示例中使用print_timepoint()函数模板。

带时间点的算术

有一个用于time_point对象的复制赋值操作符,你可以将一个duration对象添加到一个time_point或者从中减去一个duration。加法或减法的结果是一个新的time_point对象,其间隔是由duration对象调整的原始time_point的间隔。加法和减法作为非成员运算符函数实现。你也可以使用带有右操作数的+=-=操作符作为duration对象来递增或递减time_point对象。这些是左操作数time_point对象的函数成员。下面是演示这些操作的完整程序:

// Ex10_04.cpp

// Arithmetic with time-point objects

#include <iostream>                                   // For standard streams

#include <iomanip>                                    // For stream manipulators

#include <chrono>                                     // For duration, time_point templates

#include <ratio>                                      // For ratio templates

using namespace std::chrono;

// Function template for print_timepoint() goes here...

int main()

{

using TimePoint = time_point<steady_clock>;

time_point<steady_clock> tp1 {duration<int>(20)};

time_point<system_clock> tp2 {3min};

time_point<high_resolution_clock> tp3 {2h};

std::cout << "tp1 is ";

print_timepoint(tp1);

std::cout << "tp2 is ";

print_timepoint(tp2);

std::cout << "tp3 is ";

print_timepoint(tp3);

auto tp4 = tp2 + tp3.time_since_epoch();

std::cout << "tp4 is tp2 with tp3 added: ";

print_timepoint(tp4);

std::cout << "tp1 + tp2 is ";

print_timepoint(tp1 + tp2.time_since_epoch());

tp2 += duration<time_point<system_clock>::rep, std::milli> {20'000};

std::cout << "tp2 incremented by 20,000 milliseconds is ";

print_timepoint(tp2);

}

用于std::chrono名称空间的using指令使得类型名可以不受限制地使用,并且隐式地包含了包含用于duration文字的运算符函数的名称空间。这个例子展示了与不同类型的时钟相关的time_point对象的各种算法应用。这种算法总是要给一个time_point加上一个持续时间。您从一个time_point对象获得的持续时间并不知道时间间隔是从哪个时钟开始的,所以您可以将它添加到一个基于不同时钟的time_point中。输出清楚地表明发生了什么:

tp1 is 20 seconds

tp2 is 180 seconds

tp3 is 7200 seconds

tp4 is tp2 with tp3 added: 7380 seconds

tp1 + tp2 is 200 seconds

tp2 incremented by 20,000 milliseconds is 200 seconds

您可以将一个time_point对象转换为一个具有不同持续时间的time_point类型的对象。新对象将具有来自与源对象相同的时钟的纪元。如果目的位置的时间长度比源位置的时间长度分辨率低,数据可能会在此过程中丢失。名称空间std::chrono中的time_point_cast模板进行转换;模板类型参数值是新的duration类型。例如:

using TimePoint = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;

TimePoint tp_sec {75s};                          // 75 seconds

auto tp_min = std::chrono::time_point_casT<std::chrono::minutes>(tp_sec);

print_timepoint(tp_min);                         // 60 seconds

因为转换成分钟,在原来的持续时间额外的 15 秒钟丢失。

比较时间点

您可以使用任何操作符==!=<<=>=>来比较给定时钟的两个time_point对象。比较操作数调用time_since_epoch()的结果产生比较结果。虽然time_point对象必须与同一时钟相关,但它们可以有不同的时钟周期,比较时会考虑到这一点。下面是一些代码,展示了它们的用法示例:

using TimePoint1 = std::chrono::time_point<std::chrono::system_clock>;

using TimePoint2 = std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>;

TimePoint1 tp1 {120s};

TimePoint2 tp2 {2min};

std::cout << "tp1 ticks: "   << tp1.time_since_epoch().count()

<< "  tp2 ticks: " << tp2.time_since_epoch().count() << std::endl;

std::cout << "tp1 is " << ((tp1 == tp2) ? "equal":"not equal") << " to tp2" << std::endl;

这些语句产生输出是:

tp1 ticks: 1200000000  tp2 ticks: 2

tp1 is equal to tp2

从输出中您可以看到tp1tp2的滴答计数根本不相同,因为滴答代表不同的时间量,但是tp1tp2代表从system_clock的纪元开始测量的相同时刻,因此当比较相等时,它们返回true。所有的比较操作符都根据对象所代表的时间段来比较time_point对象,而不是它们的节拍数。

带时钟的操作

除了每个时钟类型都有默认的构造函数外,所有时钟类型的函数成员都是static。所有时钟都包含一个作为固定时间点的纪元、一个持续时间和一个静态成员now(),该静态成员返回一个代表当前时间的time_point对象。所有时钟类都将以下类型别名定义为成员:

  • rep是用于记录分笔成交点数量的类型。
  • period是定义节拍周期的类型,它将是ratio模板的一个实例。这种类型的静态成员numden的比率定义了时钟滴答的时间周期,以秒为单位。
  • duration是持续时间类型,它记录了自纪元以来的滴答数,并将对应于类型std::chrono::duration<rep, period>
  • time_point是时钟的now()函数成员返回的时间点值的类型。这将是模板类型std::chrono::time_point<clock_type>

除了所有三种时钟类型都实现的now()函数之外,system_clock类型还定义了另外两个函数成员,它们是static。这些提供了在类型为time_point的对象(将是std::chrono::time_poinT<std::chrono::system_clock>)和类型为std::time_t的对象(是在ctime头中定义的用于表示时间间隔的类型)之间的转换。system_clockto_time_t()成员接受一个time_point参数并将其作为类型time_t返回,而from_time_t()成员执行相反的操作。to_time_t()函数特别有用,因为它使您能够使用ctime头提供的功能,将system_clocknow()成员返回的time_point对象转换为表示日历时间(时间、日期)的字符串。ctime头文件是 C 头文件time.h的 C++ 版本。在ctime标题中的一些函数被弃用,因为它们是不安全的,但是目前在标准库中没有替代它们的方法。有多种方法可以使用在ctime标题中定义的函数来获得包含时间、星期和日期的字符串。我将展示如何输出这些信息,并留给您来进一步研究ctime头:

using Clock = std::chrono::system_clock;

auto instant = Clock::now();                  // Returns type std::chrono::time_point<Clock>

std::time_t the_time = Clock::to_time_t(instant);

std::cout << std::put_time(std::localtime(&the_time),

"The time now is: %R.%nToday is %A %e %B %Y. The time zone is %Z.%n");

Note

使用localtime()可能会导致编译器错误,因为该函数是不安全的。备选方案不是标准的 c++——微软 Visual Studio 2015 的localtime_s(),或者 Linux 编译器的localtime_r(),所以我在代码中使用了localtime()。当您编译这个时,请使用适合您环境的任何一个。

此时在我的系统上执行该命令的输出是:

The time now is: 13:27.

Today is Thursday  3 September 2015\. The time zone is GMT Summer Time.

来自ctime头的localtime()函数接受一个指向time_t对象的指针,并返回一个指向tm类型的内部静态对象的指针。这被传递给在iomanip头中定义的put_time()操纵器,该操纵器返回一个对象,该对象实际上是第一个参数指向的tm对象的格式化输出函数。第二个参数是一个格式字符串,它决定了存储在tm对象中的数据是如何呈现的。有大量的转换说明符,每个前面都有一个%,它指定了一个tm对象的各种数据成员是如何以及以什么顺序显示的。

一个tm对象是一个struct,包含以下类型int的成员:

  • tm_sec (0 到 60)tm_min(0 到 59)tm_hour(0 到 23)是指定时间的秒、分、小时。
  • tm_mday (1 到 31)、tm_mon (0 到 11)、tm_year指定日期的年、月、日。
  • tm_wday (0 到 6)和tm_yday (0 到 365)指定一周中的某一天和一年中的某一天。
  • tm_isdst如果夏令时有效,则为正值;如果夏令时无效,则为零;如果信息不可用,则为负值。

您可以通过local_time()函数返回的指针访问这些值中的任何一个——例如:

std::time_t t = Clock::to_time_t(Clock::now());

auto p_tm = std::localtime(&t);

std::cout << "Time: " << p_tm->tm_hour << ':'

<< std::setfill('0') << std::setw(2) << p_tm->tm_min

<< std::endl;                // Time: 15:06

put_time()格式字符串中有一系列特定于tm struct成员的格式说明符,它们每个前面都有一个%字符。例如,%Htm_hour写成 24 小时时钟值,%I写成 12 小时时钟值,%A写成全天名称tm_wkday,以及%B写成全月名称tm_montm对象成员的格式说明符可以是任何序列,并且您可以根据需要在格式字符串中包含其他文本,包括用于换行的%n 和用于制表符的%t。在 C++ 标准库中的put_time()文档中,可以找到很多其他的tm数据成员的格式说明符。

定时执行

能够测量一个程序执行所用的时间通常是很有用的,你可以使用时钟很容易地做到这一点。为了说明这一点,我们可以向Ex10_03中的main()添加代码,以确定求解一组方程需要多长时间,并输出经过的时间。这里是Ex10_05.cpp的代码:

// Ex10_05.cpp

// Determining the time to solve a set of linear equations

#include <valarray>                              // For valarray, slice, abs()

#include <vector>                                // For vector container

#include <iterator>                              // For ostream iterator

#include <algorithm>                             // For generate_n()

#include <utility>                               // For swap()

#include <iostream>                              // For standard streams

#include <iomanip>                               // For stream manipulators

#include <string>                                // For string type

#include <chrono>                                // For clocks, duration, and time_point

using std::string;

using std::valarray;

using std::slice;

using namespace std::chrono;

// Function prototypes

valarray<double> get_data(size_t n);

void reduce_matrix(valarray<double>& equations, std::vector<slice>& row_slices);

valarray<double> back_substitution(valarray<double>& equations,

const std::vector<slice>& row_slices);

// Code for print_timepoint() template goes here...

int main()

{

// Code to read the data for the equations as in Ex10_03.cpp...

auto start_time = steady_clock::now();                   // time_point object

// Code to generate slice objects for rows as in Ex10_03.cpp...

// Code to solve equations as in Ex10_03.cpp...

auto end_time = steady_clock::now();                     // time_point object

auto elapsed = end_time - start_time.time_since_epoch();

std::cout << "Time to solve " << n_rows << " equations is ";

print_timepoint(elapsed);

// Code to output the solution as in Ex10_03.cpp...

}

这利用了你在本章前面看到的print_timepoint()函数模板,当然,也需要来自Ex10_03gaussian.cpp文件。完整的程序在代码下载中作为Ex10_05。在main()中,解决方案代码之前只需要一条语句,之后需要四条语句。类似的代码可用于任何应用中的计算计时。在我的系统上,我得到了求解六个方程的输出:

Enter the number of variables: 6

Enter 7 values for each of 6 equations.

(i.e. including coefficients that are zero and the rhs):

1  1  1  1  1  1   8

2  3 -5 -1  1  1 -18

-1  5  2  7  2  3  40

3  1 10  2  1 11 -15

3 17  5  1  3  2  41

5  7  3 -4  2 -1   9

Time to solve 6 equations is 0.000219379 seconds

Solution:

x1 =      -2.00

x2 =       1.00

x3 =       3.00

x4 =       4.00

x5 =       7.00

x6 =      -5.00

在我的系统上大概用了 220 微秒就解决了六个方程,一点也不差。

复数

复数是形式为a + bi的数字,其中ab是实数——c++ 代码中的浮点值——而i$$ \sqrt{-1} $$a被称为复数的实部,与i相乘的b被称为虚部。使用复数的应用程序往往是专门化的;复数用于电学和电磁学理论,例如数字信号处理,当然也用于数学。复数也被用来为 Mandelbrot 集和 Julia 集生成非常漂亮的分形图像。因为与 STL 提供的其他工具相比,对复数的兴趣更小,所以我将以相当简洁的形式介绍基础知识。如果你对复数一无所知,你可以跳过这一节。

complex头定义了处理复数的能力。complex<T>模板类型的实例表示复数,该类型定义了三种专门化:complex<floaT>complex<double>complex<long double>。我将在本节通篇使用complex<double>,但是其他专门化的操作本质上是相同的。

创建表示复数的对象

有一个用于complex<double>类型的构造函数接受两个参数——第一个参数是实部的值,第二个是虚部的值。例如:

std::complex<double> z1 {2, 5};        // 2 + 5i

std::complex<double> z;                // Default parameter values are 0 so 0 + 0i

还有一个复制构造函数,所以你可以像这样复制z1:

std::complex<double> z2 {z1};          // 2 + 5i

很明显,您将需要complex文字和complex对象,并且在名称空间std::literals::complex_literals中定义了三个操作符函数,其中literalscomplex_literals名称空间是内联定义的。您可以使用用于std::literals::complex_literals名称空间的using指令、用于std::literals名称空间的using指令或用于std::complex_literals名称空间的using指令来访问复杂文字的运算符函数。我将假设这些指令中的一个或另一个,并且针对std::complexusing指令对本节剩余部分的代码有效。

operator""i()函数定义了类型为complex<double>的文字,其具有0的实部。因此3i是与complex<double>{0, 3}等价的字面意思。当然,您可以用实部和虚部来表示一个复数,例如:

z = 5.0 + 3i;                          // z is now complex<double>{5, 3}

这展示了如何定义一个两部分都不为零的复数,顺便演示了赋值操作符是为complex对象实现的。对complex<float>文字使用后缀if,对complex<long double>文字使用后缀il,例如22if3.5il。这些由功能operator""if()operator""il()定义。注意不能写1.0+i2.0+il,因为这里的iil会被解释为变量名;必须写1.0 +1i2.0+1.0il

所有复杂类型都定义了函数成员real()imag()。这些可以用来访问对象的实部或虚部,或者通过提供参数来设置这些部分。例如:

complex<double> z{1.5, -2.5};          // z:  1.5 - 2.5i

z.imag(99);                            // z:  1.5 + 99.0i

z.real(-4.5);                          // z: -4.5 + 99.0i

std::cout << "Real part: " << z.real()

<< " Imaginary part: " << z.imag()

<< std:: endl;               // Real part: -4.5 Imaginary part: 99

接受参数的版本real()imag()不返回任何内容。

有一些非成员函数模板实现了复杂对象的流提取和插入操作符。当你从一个流中读取一个复数时,它可以只是实数部分,55例如,只是圆括号之间的实数部分,(2.6),或者是大括号之间的实数部分和虚数部分,用逗号隔开,就像这样,(3, -2)。如果只提供实部,虚部将为 0。这里有一个例子:

complex<double> z1, z2, z3;            // 3 default objects 0+0i

std::cout << "Enter 3 complex numbers: ";

std::cin >> z1 >> z2 >> z3;            // Read 3 complex numbers

std::cout << "z1 = " << z1 << " z2 = " << z2 << " z3 = " << z3 << std::endl;

下面是一个输入和输出的示例:

Enter 3 complex numbers: -4 (6) (-3, 7)

z1 = (-4,0) z2 = (6,0) z3 = (-3,7)

如果复数的输入没有括号,就不可能有虚部。然而,用括号你可以省略虚部。复数的输出总是用括号括起来,即使是0也输出虚部。

复数运算

complex类模板为二元运算符+-*/以及一元运算符+-定义了非成员函数。有定义+=-=*=/=的函数成员。下面是一些使用它们的例子:

complex<double> z {1,2};               // 1+2i

auto z1 = z + 3.0;                     // 4+2i

auto z2 = z*z + (2.0 + 4i);            // -1+8i

auto z3 = z1 - z2;                     // 5-6i

z3 /= z2;                              // -.815385-0.523077i

注意,complex对象和数字文字之间的操作要求数字文字的类型正确。不能向complex<double>对象添加整数文字,如2;要实现这一点,您必须编写2.0

复数的比较和其他运算

有非成员函数模板用于比较两个complex对象是否相等。您还可以使用==!=操作来比较一个complex对象和一个数值,其中该数值被视为一个虚部为 0 的复数。为了平等,两部分必须平等。如果操作数的实部或虚部不同,它们就不相等。例如:

complex<double> z1 {3,4};                        // 3+4i

complex<double> z2 {4,-3};                       // 4-3i

std::cout << std::boolalpha

<< (z1 == z2) << " "                   // false

<< (z1 != (3.0 + 4i)) << " "           // false

<< (z2 == 4.0 - 3i)   << '\n';         // true

注释中的结果应该是清楚的。请注意,在上次比较中,编译器是如何将 4.0 - 3i 视为单个复数的。

比较复数的另一种方法是比较它们的大小。复数的幅度与向量的幅度相同,向量的分量值与实部和虚部相同,所以它是这两个部分的平方和的平方根。非成员函数模板abs()接受类型为complex<T>的参数,并以类型T的形式返回其大小。下面是一个将abs()函数应用于z1z2的示例,如前面的代码片段中所定义的:

std::cout << std::boolalpha

<< (std::abs(z1) == std::abs(z2))      // true

<< " " <<  std::abs(z2 + 4.0 + 9i);    // 10

最后的输出值是10,因为作为abs()的参数的表达式计算结果为(8.0+6i)8 2 加上6 2 就是100而那个的平方根就是10

还有其他提供复数属性的非成员函数模板:

  • norm()函数模板返回一个复数幅度的平方。
  • arg()模板返回以弧度为单位的相位角,对于复数z对应于std::atan(z.imag()/z.real())
  • conj()函数模板返回复共轭,对于数字a+bia-bi
  • polar()函数模板接受幅度和相位角作为参数,并返回与之对应的复杂对象。
  • proj()函数模板返回复数,即复数自变量在黎曼球面上的投影。

有一些非成员函数模板为复杂参数提供了一整套三角函数和双曲函数。还有用于复杂参数的cmath函数的版本exp()pow()log()log10()sqrt()。这里有一个有趣的例子:

complex<double> zc {0.0, std::acos(-1)};

std::cout << (std::exp(zc) + 1.0) << '\n';       // (0, 1.22465e-16) or zero near enough

acos(-1)是π,所以这证明了欧拉惊人方程的真实性,表明π和欧拉数e是如何相关的:

$$ {e}^{ip}+1=0 $$

一个使用复数的简单例子

这个例子使用复数从无限可能的数字中生成一个 Julia 集的分形图像。这不会是一个朱莉娅场景的精彩图像,因为它必须是一个基于角色的演示,但它会给你一个看起来如何的想法。这些通常被绘制成彩色像素,但这需要操作系统函数。通过对复平面中的点 z 应用以下迭代方程,可以创建二次 Julia 集:

$$ {z}_{n+1}=\kern0.5em {z}_n²+c $$,其中c为复数常数。c 的值决定了 Julia 集的形状。

每个新的z是复杂平面中的不同点。Julia 集由复平面中的点组成,对于复平面,方程可以无限地应用,而z的大小不会趋于无穷大。当然,你需要一个策略来决定z是否趋于无穷大。在该程序中,该等式将被应用于代表每个像素的复数z相当大的次数,如果z的幅度保持小于 2,则该点在 Julia 集中。如果它大于 2,它很可能趋向于无穷大,因此不在 Julia 集中。该程序将使用chrono标题的特性来确定生成图像需要多长时间。如果字体是方形的,输出看起来最好——我使用的是 8x8 像素的字体。下面是程序代码:

// Ex10_06.cpp

// Using complex objects to generate a fractal image of a Julia set

#include <iostream> >                                 // For standard streams

#include <iomanip>                                    // For stream manipulators

#include <complex>                                    // For complex types

#include <chrono>                                     // For clocks, duration, and time_point

using std::complex;

using namespace std::chrono;

using namespace std::literals;

// Function template definition for print_timepoint() goes here...

int main()

{

const int width {100}, height {100};                // Image width and height

size_t count {100};                                 // Iterate count for recursion

char image[width][height];

auto start_time = steady_clock::now();              // time_point object for start

complex<double> c {-0.7, 0.27015};                  // Constant in z = z*z + c

for(int i {}; i < width; ++i)                       // Iterate over pixels in the width

{

for(int j {}; j < height; ++j)                    // Iterate over pixels in the height

{

// Scale real and imaginary parts to be between -1 and +1

auto re = 1.5*(i - width/2) / (0.5*width);

auto im = (j - height/2) / (0.5*height);

complex<double> z {re,im};                      // Point in the complex plane

image[i][j] = ' ';                              // Point not in the Julia set

// Iterate z=z*z+c count times

for(size_t k {}; k < count; ++k)

{

z = z*z + c;

}

if(std::abs(z) < 2.0)                           // If point not escaping...

image[i][j] = '*';                            // ...it’s in the Julia set

}

}

auto end_time = std::chrono::steady_clock::now();   // time_point object for end

auto elapsed = end_time - start_time.time_since_epoch();

std::cout << "Time to generate a Julia set with " << width << "x" << height << " pixels is ";

print_timepoint(elapsed, 9);

std::cout << "The Julia set looks like this:\n";

for(size_t i {}; i < width; ++i)

{

for(size_t j {}; j < height; ++j)

std::cout << image[i][j];

std::cout << '\n';

}

}

该程序使用您之前遇到的print_timepoint()模板来输出经过的时间。完整的程序代码在代码下载中称为Ex10_06.cpp。我得到了以下输出 Julia 集的情节如图 10-13 所示:

A978-1-4842-0004-9_10_Fig13_HTML.gif

图 10-13。

The Julia set generated by Ex10-06

Time to generate a Julia set with 100x100 pixels is 0.286463017 seconds

The Julia set looks like this:

这涉及到相当多的计算。在我的系统上,计算集合中的点大约需要三分之一秒。

摘要

valarray头中定义的valarray类模板旨在使编译器能够比其他数组或容器更有效地进行数值计算,并有可能允许并行操作。一个valarray对象为 C++ 中的大规模计算密集型数值计算提供了基础。类型为slice的对象表示从存储在valarrary中的数据中以给定间隔分布的一维元素序列。一个slice对象允许你在一个数组的一整行或一整列上表达操作。gslice对象是切片的一般化,代表一组均匀间隔的切片对象。一个gslice使您能够表达应用于它定义的所有行或列的操作。

ratio头定义了ratio类模板,每个ratio类型定义了一个有理数,所以没有必要定义比率对象。ratio头还定义了以下类模板,用于将二进制加、减、乘、除运算应用于两个ratio类型所代表的有理数:

ratio_add<typename R1, typename R2>        ratio_subtract<typename R1, typename R2>

ratio_multiply<typename R1, typename R2>   ratio_divide<typename R1, typename R2>

这些模板中的每一个都定义了一个新的代表操作结果的ratio类型。还有一些模板通过比较ratio类型生成一个bool值。

chrono头定义了作为硬件时钟接口的类。为时钟定义了三个等级:

  • system_clock表示挂钟时间,可用于确定时间和日期。
  • steady_clock是单调时钟,通常用于测量时间间隔。
  • high_resolution_clock是为时间测量提供最高分辨率的时钟。该时钟类型可能是其他两种时钟类型之一的别名。

时钟类型只有静态成员,所以不需要定义时钟对象。时钟测量相对于一个固定瞬间的时间,这个瞬间被称为纪元。时钟的now()函数成员返回一个时间瞬间,作为一个time_point类型的实例,它包含一个对时钟的引用,该引用定义了纪元和相对于纪元的时间间隔。一个时间间隔由一个duration<typename Rep, typename Period=ratio<1>>模板的实例表示。时间间隔是用类型Rep的值表示的刻度数。一个tick是由第二个duration模板类型参数ratio类型定义的秒数;默认值ratio<1>指定一个节拍为 1 秒。

complex头中定义了complex<T>类模板。模板的实例是表示复数的类型,其实部和虚部存储为类型T的值。对于类型floatdoublelong double,有complex模板的专门化。定义了一系列支持复数运算的函数。

Exercises

$$ a{x}²+bx+c=0 $$

Write a program that generates 100,000 floating-point values that are normally distributed between 1 and 100 and stores them in a valarray. Consider the array to be 100 rows of 1000 elements. Calculate and output the mean for each row using slice objects.   Modify the program from Exercise 1 to calculate the standard deviation for the values in each row in addition to the mean, output the time to complete the calculations, then output the mean and standard deviation for each row. (The formula for the standard deviation is in Chapter 8.) The program should also output the date of execution.   Modify the solution for Exercise 2 to output the time in nanoseconds to calculate the standard deviation for each row.   Modify Ex10_06 from this chapter to use a valarray to store the image.   This exercise is for you if you are a fan of algebra and complex number. Write a program to read the coefficients of an arbitrary quadratic equation:

使用复杂对象确定并输出方程的根使用标准公式:

$$ x=\frac{-b\pm \sqrt{b²-4ac}}{4ac} $$

posted @ 2024-08-05 14:00  绝不原创的飞龙  阅读(36)  评论(0)    收藏  举报