一、标准模板库简介
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 可以细分为四个概念库:
- 容器库定义了用于存储和管理数据的容器。该库的模板在以下头文件中定义:
array
、vector
、stack
、queue
、deque
、list
、forward_list
、set
、unordered_set
、map
和unordered_map
。 - 迭代器库定义了迭代器,迭代器是行为类似指针的对象,用于引用容器中的对象序列。该库在一个头文件
iterator
中定义。 - 算法库定义了广泛的算法,这些算法可以应用于存储在容器中的一组元素。该库的模板在
algorithm
头文件中定义。 - Numerics 库定义了广泛的数字函数,包括容器中元素集的数字处理。该库还包括用于随机数生成的高级函数。该库的模板在标题
complex
、cmath
、valarray
、numeric
、random
、ratio
和cfenv
中定义。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
在指定模板参数时,class
和typename
关键字是可以互换的,因此在定义模板时,您可以编写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<()
的类型的对象将不能用作任何关联容器(如map
和set
)中的键,并且排序算法(如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
迭代器指向的是常量,你仍然可以修改迭代器本身。在本节的后面,我将介绍返回其他类型迭代器的其他全局函数。
图 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+n
或iter-n
和iter+=n
或iter-=n
- 按整数索引:
iter[n]
,相当于*(iter+n)
- 两个迭代器的区别:
iter1-iter2
,产生一个整数指定元素个数。 - 比较迭代器:
iter1<iter2
、iter1>iter2
、iter1<=iter2
和iter1>=iter2
。对一系列元素进行排序需要随机访问迭代器指定范围。可以在随机访问迭代器中使用下标操作符。给定一个迭代器first
,表达式first[3]
等价于*(first+3)
,所以它访问第四个元素。一般来说,在带有迭代器的表达式iter[n]
中,iter
,n
是一个偏移量,表达式返回从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
迭代器的解引用结果。
容器中迭代器的特征取决于容器的类型。比如vector
和deque
容器提供随机访问迭代器;这反映了这些容器中的元素可以被随机访问的事实。另一方面,list
和map
容器总是提供双向迭代器;这些容器不支持对元素的随机访问。输入输出迭代器和前向迭代器类型通常用于为算法指定参数,以反映算法所需的最低能力水平。在本书的后面,我将在将算法应用于容器内容的上下文中,用工作示例进一步解释迭代器——在实际环境中,它们更容易理解。同时,这里有一个简单的例子来展示迭代器在数组中的作用:
// 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_iterator
、insert_iterator
和move_iterator
。
反向迭代器
反向迭代器的工作方式与标准迭代器相反。您可以创建双向或随机访问迭代器的反向迭代器版本。容器的rbegin()
和rend()
函数成员分别返回指向最后一个元素和第一个元素前的反向迭代器;同名的全局函数做同样的事情,如图 1-2 所示。
图 1-2。
Operations with reverse iterators
递增或递减反向迭代器在元素顺序方面与标准迭代器的工作方式相反,因此递增反向 begin 迭代器会导致它指向前面的元素——左边的那个——而递减它会指向下一个元素——右边的那个。图 1-3 显示了与标准迭代器相比,反向迭代器的增量方向。反向迭代器类型的模板是从常规迭代器的模板中派生出来的,它重载操作函数来实现反向操作。在string
头中定义的字符串类也使反向迭代器可用,因此您可以调用string
对象的rbegin()
成员来获得指向最后一个字符的反向迭代器,调用字符串对象的rend()
将返回指向第一个字符之前的反向迭代器。全局(和成员)crbegin()
和crend()
函数返回const
反向迭代器。
图 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()
函数成员在容器末尾添加新元素;vector
、list
和deque
容器都有一个push_back()
成员。如果容器没有定义push_back()
,那么back_insert_iterator
就不能使用。全局back_inserter()
函数为作为参数传递的容器返回一个back_insert_iterator
对象。 - 一个
front_insert_iterator
通过调用它的push_front()
成员在容器的开头添加新元素;list
、forward_list
和deque
容器有一个push_front()
成员。不能对没有push_front()
成员的容器使用front_insert_iterator
。全局front_inserter()
函数为容器返回一个作为参数传递的front_inserter_iterator
对象;显然,容器必须是一个list
、forward_list
或deque
容器。 - 您可以使用一个
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_name
的release()
成员返回包含"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
进行比较。最有用的是相等或不相等的比较,它告诉你两个指针是否指向同一个对象。给定两个指向同一类型T
的shared_ptr<T>
对象pA
和pB
,可以这样比较它们:
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 所示。
图 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()
函数将返回一个包含nullptr
的shared_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 表达式的语法。
图 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>
容器是一系列固定长度N
的T
类型的对象。您不能添加或删除元素。 - 一个
vector<T>
容器是一个可变长度的类型为T
的对象序列,它会在需要时自动增长。您只能在序列的末尾有效地添加或删除元素。 - 一个
deque<T>
容器是一个自动增长的可变长度序列。您可以在序列的两端有效地添加或删除元素。 - 一个
list<T>
容器是一个由类型为T
的对象组成的变量序列,组织成一个双向链表。您可以在序列中的任何位置有效地添加或删除元素。与前三个容器相比,访问序列内部的任意元素相对较慢,因为必须从第一个元素或最后一个元素开始,并在列表中移动,直到到达所需的元素。 - 一个
forward_list<T>
是一个可变长度的类型为T
的对象序列,被组织成一个单链表。这比列表容器更快,需要的内存更少,但是序列内部的元素只能从第一个元素开始访问。
图 2-1 显示了可用的序列容器以及它们之间的差异。
图 2-1。
The standard sequence containers
图 2-1 中所示的每种集装箱的操作都是可以有效执行的操作。正如您将看到的,在某些情况下,其他操作也是可能的,但这些操作会慢得多。
容器之间通用的函数成员
我将在本章的剩余部分详细解释如何使用每个序列容器。序列容器有许多共同的函数成员,它们在每个容器中的行为是相同的。我将在一个容器的上下文中描述每个成员,而不是重复详细描述这些成员在每种类型的容器中做什么。表 2-1 显示了array
、vector
和deque
容器的函数成员,以及两个或更多容器实现同一个函数成员的情况。
表 2-1。
Function members of array
, vector
, and deque
containers
列中没有'Yes'
意味着没有为该容器定义函数。你不需要记住这张桌子。这里仅供参考。当您进一步了解容器如何构造元素时,您会本能地知道哪些功能对于给定的容器是不可用的。
将元素组织成链表的容器在内部组织上与表 2-1 中的容器有很大不同。尽管list
和forward_list
容器彼此非常相似。一个forward_list
拥有一个list
容器拥有的大部分功能成员。forward_list
中缺少的基本上是那些需要向后遍历序列的,所以没有反向迭代器。作为参考,表 2-2 显示了list
和forward_list
容器的功能成员。
表 2-2。
Function members of list
and forward_list
containers
所有序列容器拥有的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 所示。
图 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&& 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()
函数来获取容器的迭代器,因为它们是通用的,所以first
和last
可以这样定义:
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_ht
和ht_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
——不需要带有vector
的N
模板参数。一旦超过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 说明了这一点。
图 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()
函数来引用元素。
vector
的front()
和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()
算法添加元素来演示如何将反向插入器迭代器应用于vector
。copy()
算法将元素从迭代器指定的范围(作为前两个参数提供)复制到迭代器指定的目的地(作为第三个参数)。前两个参数只需要是输入迭代器,所以任何类型的迭代器都可以接受;显然,第三个参数必须是输出迭代器。这里有一个例子:
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()
函数返回的data
的back_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
对于words
的insert()
成员,您可以选择的选项有:
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
末尾的所有插入都会产生开销。插入点之后的所有元素都必须被混洗,以便为新元素腾出空间。当然,如果插入后的元素数量大于容量,就需要分配更多的内存,这会进一步增加开销。
vector
的insert()
成员希望您使用标准迭代器来指定插入点;反向迭代器不会被接受——它不会被编译。当你想找到一个给定对象在一个序列中最后一次出现的位置,并在它旁边插入一个新元素时,就需要使用反向迭代器。反向迭代器的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)
。调用riter
的base()
函数成员,反向返回对应于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
的迭代器可能会失效,所以最好在这个操作之后获得新的迭代器。
您可以调用vector
的erase()
函数成员来删除一个或多个元素。要删除单个元素,需要提供单个迭代器参数,例如:
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)+1
和std::begin(data)+2
处的元素。返回的迭代器指向被删除元素之后的元素,所以如果最后一个元素被删除,它将是std::begin(data)+1
或std::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 说明了这是如何工作的。
图 2-4。
How the remove( ) algorithm works
如果您在remove()
操作之后输出words
中的元素,那么只会显示前五个元素。然而,通过调用size()
为vector
返回的值仍然是 7,所以最后两个元素仍然存在,但是被空的string
对象所取代。为了去掉多余的元素,你必须调用vector
的erase()
成员,而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
容器中,直到容量必须增加,在这种情况下,该点的大小和容量被记录在sizes
和capacities
容器中。对于具有不同初始元素计数的容器,重复这一过程。用我的编译器,我得到了这样的输出:
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_deque
的deque
容器存储了int
类型的元素,如图 2-5 所示;在这个容器中,奇整数被存储在元素中。
图 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
的所有现有迭代器失效,因此您必须重新创建它们。deque
的erase()
成员也以与vector
相同的方式工作。为deque
容器调用clear()
会移除所有元素。
替换队列容器的内容
deque
的assign()
函数成员替换所有现有元素。有三个版本;新内容可以由初始化列表指定,新内容可以是由迭代器指定的范围,或者新内容可以是指定对象的多个副本。下面是如何用初始化列表指定的元素替换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"});
因为words
的assign()
成员需要一个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
的对象的双向链表。这比vector
或deque
容器有优势,你可以在固定时间内在序列中的已知位置插入或删除元素。这个优势是使用list
容器而不是vector
或deque
容器的主要动机。缺点是你不能通过元素在序列中的位置直接访问它——换句话说,没有元素的索引。要访问列表中的内部元素,必须从一个元素遍历到下一个元素,通常从第一个或最后一个元素开始。图 2-6 显示了列表容器中的元素在概念上是如何构成的。
图 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
,省略了第一个和最后一个元素。因为由list
的begin()
和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 个指针。第一个元素的下一个指针被更改为指向新元素;来自原始第二元素的先前指针被改变为指向新元素;新元素的前一个指针将被设置为指向第一个元素,其下一个指针将指向序列中第二个原始元素。与在vector
或deque
中插入相比,这个过程非常快,并且无论新元素插入到哪里,都将花费相同的时间。
您可以在给定位置插入同一元素的多个副本:
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()()
函数成员返回true
。functional
头定义了大量定义谓词的模板,您将在本书的后面看到更多。执行排序后,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;
});
这与前面的语句完全相同。
list
的merge()
函数成员期望另一个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)
,因为当容器为空时,它也返回结束迭代器。
访问元素
list
的front()
和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()
函数模板产生。该模板将从任何支持输出迭代器的容器中输出支持流插入操作符的任何类型的元素。代码展示了使用正向迭代器和反向迭代器的函数模板。
对proverbs
的sort()
成员的第一次调用没有参数,所以默认情况下元素按升序排序。第二个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& 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& operator<<(std::ostream& 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
容器的开头。这是通过调用boxes
的insert_after()
成员来实现的,插入位置由before_begin()
成员返回的迭代器指定。
接下来的操作是对盒子进行排序,然后将more_boxes
的内容合并到boxes
中。在调用merge()
之前,必须对两个容器进行排序,因为只有当两个容器的内容都是升序时,该操作才有效。这显然会导致more_boxes
元素的副本出现在boxes
中,因为副本已经被插入。调用boxes
的unique()
成员可以消除一个元素的连续重复。最后一个操作是调用boxes
的remove_if()
成员从容器中删除元素。要删除的元素由作为参数传递的一元谓词决定。这里是一个 lambda 表达式,为体积小于max_v
的元素返回true
;max_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;
}
这个函数模板的实例旨在交换迭代器参数a
和b
所指向的对象。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_type
、value_type
等等——以及用于创建迭代器模板实例的类型——对应于Iterator
的类型参数。因此对于一个具体的类Boggle
,iterator_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_tag
、output_iterator_tag
、forward_iterator_tag
、bidirectional_iterator_tag
或random_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_type
是iterator<std::string>::value_type
的别名,并且从iterator<std::string>
类定义中知道iterator<std::string>::value_type
是std::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&  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::pointer
。iterator_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<()
功能特殊。这就是所谓的排序关系。它在搜索和比较算法中很重要。
函数测试两个容器或对象是否有相同的内容。这有一个有趣的方面。你可能认为对于任何一对操作数,x
和y
,表达式(x<y || y<x || x==y)
必须总是计算为true
,因为三个组成表达式中必须有一个是true
。事实上,并不一定要这样。很清楚,如果x==y
是true
,那么x<y
和y<x
都不可能是true
。你可以确定的一件事是,相同的元素不能不同。然而,如果x!=y
你一定不能假设x<y
或y<x
中的一个是true
。当表达式(!(x<y))&&(!(y<x))
为true
时,元素x
和y
被说成是不等价的,简单来说就是你在排序时没有偏好。一个常见的例子是在对字符串排序时,忽略了大小写。在不区分大小写的基础上,字符串"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
容器的前面或后面添加或删除元素;在序列内部添加或删除元素会很慢。 array
、vector,
和deque
容器提供了const
和非const
随机访问迭代器和反向迭代器。- 一个
list<T>
容器将类型为T
的元素存储为一个双向链表。可以在列表容器中的任何地方有效地添加或删除元素。 - 只有从序列的开头或结尾开始遍历列表,才能访问
list
容器内部的元素。 - 一个
list
容器提供双向迭代器。 - 一个
forward_list<T>
容器将类型为T
的元素存储在一个单向链表中,该链表只能从第一个元素开始向前遍历。一个forward_list
集装箱比一个list
集装箱更快更紧凑。 - 一个
forward_list
容器提供了前向迭代器。 - 由
algorithm
头中的模板定义的copy()
算法将一系列元素复制到另一个迭代器指定的目的地。 - 您可以使用流迭代器和
copy()
算法从输入流中读取数据并将其复制到容器中,或者将数据从容器写入输出流。 - 在
algorithm
头中定义的sort()
函数模板对随机访问迭代器指定的一系列元素进行排序。默认情况下,元素可以按升序排序,或者按二元谓词确定的顺序排序,该谓词作为参数提供给sort()
。 list
和forward_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.