标准库提供了几种类型的容器;每种类型的容器都是通过模板化的类提供的,因此容器的行为可以用于任何类型的项。 有一些用于顺序容器的类,其中容器中项的顺序取决于项插入容器的顺序。 此外,还有已排序和未排序的关联容器,它们将值与键相关联,随后使用键访问值。
虽然不是容器本身,但在本章中,我们还将介绍两个相关的类:pair
,它将两个值链接在一个对象中;以及tuple
,它可以在单个对象中保存一个或多个值。
在许多情况下,您会希望将两个项关联在一起;例如,关联容器允许您创建一种类型的数组,其中使用数字以外的项作为索引。 <utility>
头文件包含一个名为pair
的模板化类,它有两个名为first
和second
的数据成员。
template <typename T1, typename T2>
struct pair
{
T1 first;
T2 second;
// other members
};
由于类是模板化的,这意味着您可以关联任何项,包括指针或引用。 访问成员很简单,因为它们是公共的。 您还可以使用get
模板化函数,因此对于pair
对象p
,您可以调用get<0>(p)
而不是p.first
。 该类还有一个复制构造函数和一个移动构造函数,以便您可以从另一个对象创建对象。 还有一个名为make_pair
的函数,它将从参数中推断成员的类型:
auto name_age = make_pair("Richard", 52);
要小心,因为编译器将使用它认为最合适的类型;在本例中,创建的pair
对象将是pair<const char*, int>
,但如果希望first
项是string
,使用构造函数会更简单。 您可以比较pair
个对象;对第一个成员执行比较,只有当它们相等时才对第二个成员进行比较:
pair <int, int> a(1, 1);
pair <int, int> a(1, 2);
cout << boolalpha;
cout << a << " < " << b << " " << (a < b) << endl;
参数可以是参照:
int i1 = 0, i2 = 0;
pair<int&, int&> p(i1, i2);
++ p.first; // changes i1
make_pair
函数将从参数中推导出类型。 编译器无法区分变量和对变量的引用。 在 C++ 11 中,您可以使用ref
函数(在<functional>
中)指定pair
将用于引用:
auto p2 = make_pair(ref(i1), ref(i2));
++ p2.first; // changes i1
如果要从一个函数返回两个值,可以通过通过引用传递的参数来实现,但是代码的可读性较差,因为您希望返回值通过函数的返回而不是通过其参数来实现。 pair
类允许您在一个对象中返回两个值。 <algorithm>
中的minmax
函数就是一个例子。 这将返回一个pair
对象,其中包含的参数从最小到最小的顺序排列,如果不应该使用默认操作符<
,则会有一个重载,您可以在其中提供谓词对象。 以下内容将打印{10,20}
:
auto p = minmax(20,10);
cout << "{" << p.first << "," << p.second << "}" << endl;
pair
类关联两个项目。 标准库提供了具有类似功能的tuple
类,但是由于模板是可变的,这意味着您可以拥有任意数量的任意类型的参数。 但是,数据成员并不像pair
中那样命名,而是通过模板化的get
函数访问它们:
tuple<int, int, int> t3 { 1,2,3 };
cout << "{"
<< get<0>(t3) << "," << get<1>(t3) << "," << get<2>(t3)
<< "}" << endl; // {1,2,3}
第一行创建包含三个int
项的tuple
,并使用初始化列表对其进行初始化(您可以使用构造函数语法)。 然后,通过使用模板参数指示项目索引的get
函数版本访问对象中的每个数据成员,将tuple
打印到控制台。 请注意,索引是模板参数,因此不能在运行时使用变量提供它。 如果这是您想要做的,那么它清楚地表明您需要使用像vector
这样的容器。
函数get
返回一个引用,因此可以用它来更改项目的值。 对于tuple t3
,此代码将第一项更改为42
,将第二项更改为99
:
int& tmp = get<0>(t3);
tmp = 42;
get<1>(t3) = 99;
您也可以使用tie
函数一次调用提取所有项目:
int i1, i2, i3;
tie(i1, i2, i3) = t3;
cout << i1 << "," << i2 << "," << i3 << endl;
函数的作用是:返回一个tuple
,其中每个参数都是一个引用,并被初始化为您作为参数传递的变量。 如果您这样编写,前面的代码更容易理解:
tuple<int&, int&, int&> tr3 = tie(i1, i2, i3);
tr3 = t3;
可以从pair
对象创建tuple
对象,因此也可以使用tie
函数从pair
对象中提取值。
有一个名为make_tuple
的帮助器函数,它将推断参数的类型。 与make_pair
函数一样,您必须小心扣减,因此浮点数将被推断为double
,整数将被演绎为int
。 如果希望参数是对特定变量的引用,可以使用ref
函数或cref
函数引用const
。
您可以比较tuple
个对象,只要有相等数量的项目和等效的类型即可。 编译器将拒绝编译具有不同项数的tuple
个对象的比较,或者如果一个tuple
对象的项的类型无法转换为另一个tuple
对象的类型。
标准库容器允许您将零个或多个相同类型的项组合在一起,并通过迭代器顺序访问它们。 每个这样的对象都有一个向第一个项目返回迭代器对象的begin
方法和一个返回容器中最后一个项目之后的项目的迭代器对象的end
函数。 迭代器对象支持类似指针的算法,因此end() - begin()
将给出容器中的项数。 所有容器类型都将实现empty
方法来指示容器中是否没有项,并且(除了forward_list
)size
方法是容器中的项数。 您很想遍历容器,就好像它是一个数组:
vector<int> primes{1, 3, 5, 7, 11, 13};
for (size_t idx = 0; idx < primes.size(); ++ idx)
{
cout << primes[idx] << " ";
}
cout << endl;
问题是,并非所有容器都允许随机访问,如果您认为使用另一个容器更有效,则必须更改访问容器的方式。 如果要使用模板编写泛型代码,则此代码也不能很好地工作。 前面的代码最好使用迭代器编写:
template<typename container> void print(container& items)
{
for (container::iterator it = items.begin();
it != items.end(); ++ it)
{
cout << *it << " ";
}
cout << endl;
}
所有容器都有一个名为iterator
的typedef
成员,它给出了从begin
方法返回的迭代器的类型。 迭代器对象的行为类似于指针,因此您可以使用取消引用操作符获取迭代器引用的项,并使用增量操作符移动到下一项。
对于除vector
之外的所有容器,可以保证即使删除了其他元素,迭代器也将保持有效。 如果插入项,则只有lists
、forward_lists
和关联的容器保证迭代器保持有效。 迭代器将在后面进行更深入的介绍。
所有容器都必须具有名为swap
的异常安全(Nothrot)方法,并且(有两个异常)它们必须具有事务性语义;也就是说,操作必须成功或失败。 如果操作失败,则容器的状态与调用操作之前相同。 对于每个容器,当涉及到多元素插入时,这一规则是宽松的。 例如,如果使用迭代器范围一次插入多个项,而该范围中的一项插入失败,则该方法将无法撤消以前的插入。
需要指出的是,对象被复制到容器中,因此放入容器中的对象类型必须具有复制和复制赋值操作符。 另外,请注意,如果将派生类对象放入需要基类对象的容器中,则复制将对该对象进行切片,这意味着与派生类有关的任何操作都将被删除(数据成员和虚方法指针)。
序列容器存储一系列项及其存储顺序,当您使用迭代器访问它们时,这些项将按照放入容器的顺序进行检索。 创建容器后,可以使用库函数更改排序顺序。
顾名思义,list
对象是由双向链表实现的,其中每一项都有一个指向下一项和前一项的链接。 这意味着可以快速插入项(如第 4 章,使用内存、数组和指针中的示例,使用单链表显示),但由于在链表中,项只能访问它前面和后面的项,所以不能使用[]
索引运算符进行随机访问。
该类允许您通过构造函数提供值,也可以使用成员方法。 例如,assign
方法允许您使用初始值设定项列表在一个操作中填充容器,或者使用迭代器填充到另一个容器中的某个范围。 也可以使用push_back
或push_front
方法插入单个项目:
list<int> primes{ 3,5,7 };
primes.push_back(11);
primes.push_back(13);
primes.push_front(2);
primes.push_front(1);
第一行创建一个包含3
、5
和7
的list
对象,然后(按该顺序)将11
和13
推到末尾,以便list
包含{3,5,7,11,13}
。 然后,代码将数字2
和1
推到前面,这样最终的list
就是{1,2,3,5,7,11,13}
。 不管名称如何,pop_front
和pop_back
方法只删除列表前面或后面的项,但不会返回该项。 如果要获取已移除的项目,必须先通过front
或back
方法访问该项目:
int last = primes.back(); // get the last item
primes.pop_back(); // remove it
clear
方法将删除list
中的所有项,erase
方法将删除项。 有两个版本:一个具有标识单个项的迭代器,另一个具有指示范围的两个迭代器。 通过提供范围中的第一项和在范围之后的项来指示范围。
auto start = primes.begin(); // 1
start++ ; // 2
auto last = start; // 2
last++ ; // 3
last++ ; // 5
primes.erase(start, last); // remove 2 and 3
这是迭代器和标准库容器的一般原则;范围由迭代器指示,由第一个项目和最后一个项目之后的项目表示。 remove
方法将删除具有指定值的所有项:
list<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
planck.remove(6); // {2,0,7,0,0,4,0}
还有一个方法remove_if
,它接受一个谓词,只有在该谓词返回true
时才会删除一项。 同样,您可以使用迭代器将项插入到列表中,并将该项插入到指定项之前:
list<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
auto it = planck.begin();
++ it;
++ it;
planck.insert(it, -1); // {6,6,-1,2,6,0,7,0,0,4,0}
您还可以指定应在该位置多次插入项目(如果插入,则插入多少个副本),并且可以提供要一次插入的多个项目。 当然,如果您传递的迭代器是通过调用begin
方法获得的,则该项将插入到list
的开头。 通过调用push_front
方法也可以实现同样的目的。 类似地,如果迭代器是通过调用end
方法获得的,则在list
的末尾插入该项,这与调用push_back
相同。
当您调用insert
方法时,您提供了一个对象,该对象将被复制到list
或移动到list
(通过右值语义)。 该类还提供了几个emplace方法(emplace
、emplace_front
和emplace_back
),它们将根据您提供的数据构造一个新对象,并将该对象插入到list
中。 例如,如果您有一个可以从两个double
值创建的point
类,则可以通过提供两个double
值来insert
构造point
对象或emplace``point
对象:
struct point
{
double x = 0, y = 0;
point(double _x, double _y) : x(_x), y(_y) {}
};
list<point> points;
point p(1.0, 1.0);
points.push_back(p);
points.emplace_back(2.0, 2.0);
一旦创建了list
,就可以使用成员函数对其进行操作。 swap
方法将合适的list
对象作为参数,它将参数中的项移动到当前对象中,并将当前list
中的项移动到参数中。 因为list
对象是使用链表实现的,所以这个操作很快。
list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number
list<int> num2 { 3,1,4,5,6,8 }; // digits of pi
num1.swap(num2);
此后,代码num1
将包含{3,1,4,5,6,8}
,num2
将包含{2,7,1,8,2,8}
,如下所示:
list
将按照项插入容器的顺序保存项;但是,您可以通过调用sort
方法对它们进行排序,默认情况下,该方法将使用<
运算符对list
容器中的项按升序排序。 您还可以为比较操作传递函数对象。 排序后,您可以通过调用reverse
方法来颠倒项目的顺序。 可以合并两个排序的列表,这涉及到从参数列表中提取项目并将其插入调用列表中,其顺序如下:
list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number
list<int> num2 { 3,1,4,5,6,8 }; // digits of pi
num1.sort(); // {1,2,2,7,8,8}
num2.sort(); // {1,3,4,5,6,8}
num1.merge(num2); // {1,1,2,2,3,4,5,6,7,8,8,8}
合并两个列表可能会导致重复,可以通过调用unique
方法将其删除:
num1.unique(); // {1,2,3,4,5,6,7,8}
顾名思义,forward_list
类类似于list
类,但它只允许在列表前面插入和删除项。 这还意味着与类一起使用的迭代器只能递增;编译器将拒绝允许您递减这样的迭代器。 该类有list
方法的子集,因此它有push_front
、pop_front
和emplace_front
方法,但没有相应的_back
方法。 它还实现了一些其他方法,因为列表项只能向前访问,这意味着插入将发生在现有项之后,因此该类实现了insert_after
和emplace_after
。
类似地,您可以删除列表开头(pop_front
)或指定项(erase_after
)后面的项,或者告诉类在列表中正向迭代并删除具有特定值的项(remove
和remove_if
):
forward_list<int> euler { 2,7,1,8,2,8 };
euler.push_front(-1); // { -1,2,7,1,8,2,8 }
auto it = euler.begin(); // iterator points to -1
euler.insert_after(it, -2); // { -1,-2,2,7,1,8,2,8 }
euler.pop_front(); // { -2,2,7,1,8,2,8 }
euler.remove_if([](int i){return i < 0;});
// { 2,7,1,8,2,8 }
在前面的代码中,用欧拉数的数字初始化euler
,并将值-1
推到前面。 接下来,获得一个迭代器,该迭代器指向容器中的第一个值;即指向-1
的值的位置。 在迭代器的位置之后插入值-2
;也就是说,在值-1
之后插入-2
。 最后两行显示如何删除项;pop_front
删除容器前面的项,remove_if
将删除满足谓词的项(在本例中,当项小于零时)。
vector
类具有动态数组的行为;也就是说,可以对项进行索引随机访问,并且容器将随着向其中插入更多项而增长。 您可以使用初始化列表和指定数量的项目副本来创建vector
对象。 还可以通过传递指示该容器中项范围的迭代器,使vector
基于另一个容器中的值。 您可以通过提供一个 Capacity 作为构造函数参数来创建一个具有预定大小的向量,容器中将创建指定数量的默认项。 如果在以后需要指定容器大小,可以调用reserve
方法指定最小大小,或者调用resize
方法,这可能意味着删除多余的项或创建新项,具体取决于现有的vector
对象是大于还是小于请求的大小。
当您将项插入到vector
容器中时,如果没有分配足够的内存,则容器将分配足够的内存。 这将涉及分配新内存、将现有项复制到新内存中、创建新项,最后销毁项的旧副本并重新分配旧内存。 显然,如果您知道项的数量,并且知道如果没有新的分配,vector
容器将无法容纳它们,则应该通过调用reserve
方法来指示需要多少空间。
插入构造函数以外的项非常简单。 您可以使用push_back
在末尾插入一个项目(这是一个快速操作,假设不需要分配),也可以使用pop_back
删除最后一个项目。 还可以使用assign
方法清除整个容器并插入指定的项(相同项的倍数、项的初始值设定项列表或使用迭代器指定的另一个容器中的项)。 与list
对象一样,您可以清除整个vector
、在某个位置拭除项目或在指定位置插入项目。 但是,没有与remove
方法等效的方法来删除具有特定值的项。
使用vector
类的主要原因是使用at
方法或[]
索引运算符获得随机访问:
vector<int> distrib(10); // ten intervals
for (int count = 0; count < 1000; ++ count)
{
int val = rand() % 10;
++ distrib[val];
}
for (int i : distrib) cout << i << endl;
第一行创建一个包含 10 个项目的vector
,然后在循环中,每次调用 C 运行时函数rand
1000 次,以获得 0 到 32767 之间的伪随机数。 模运算符用于获得大致介于 0 和 9 之间的随机数。该随机数随后被用作distrib
对象的索引,以选择指定的项目,然后递增。 最后,打印出分发内容,正如您所预期的那样,这会给出每一项的值大约为 100。
此代码依赖于这样一个事实,即[]
操作符返回对项目的引用,这就是项目可以以这种方式递增的原因。 []
运算符可用于读取和写入容器中的项。 容器通过begin
和end
方法以及(因为容器适配器需要它们)front
和back
方法提供迭代器访问。
vector
对象可以包含任何具有复制构造函数和赋值运算符的类型,这意味着所有内置类型。 按照目前的情况,bool
个项目的vector
会浪费内存,因为布尔值可以存储为单个位,而编译器会将bool
视为整数(32 位)。 标准库为bool
提供了vector
类的专门化,可以更高效地存储项目。 然而,尽管乍一看这个类看起来是个好主意,但问题是,因为容器以位的形式保存布尔值,这意味着[]
操作符不返回对bool
的引用(相反,它返回一个行为类似的对象)。
如果您希望保存布尔值并对其进行操作,那么只要您在编译时知道有多少项,bitset
类可能是更好的选择。
名称deque
表示双端队列,这意味着它可以从两端增长,虽然您可以在中间插入项,但成本更高。 作为队列,这意味着项目是有序的,但是,因为项目可以从两端放入队列,所以顺序不一定与将项目放入容器的顺序相同。
deque
的接口类似于vector
,因此您可以使用at
函数和[]
运算符进行迭代器访问和随机访问。 与vector
一样,您可以使用push_back
、pop_back
和back
方法从deque
容器的末尾访问项,但与vector
不同的是,您还可以使用push_front
、pop_front
和front
方法访问deque
容器的前面。 虽然deque
类有一些方法允许您在容器内插入和擦除项,resize
,但这些操作的开销很大,如果您需要使用它们,那么您应该重新考虑使用此容器类型。 此外,deque
类没有预分配内存的方法,因此,当您向该容器添加项时,可能会导致内存分配。
使用类似 C 的array
或vector
,每一项都与其数字索引相关联。 早些时候,在vector
一节中的一个示例中就利用了这一点,在该示例中,索引提供了分布的小数,并且为了方便起见,以十进制数据编号的方式对分布进行了拆分。
关联容器允许您提供非数字的索引;这些是键,您可以将值与其关联。 当您将键-值对插入到容器中时,将对它们进行排序,以便容器随后可以通过其键高效地访问值。 通常,此顺序对您来说应该无关紧要,因为您不会使用容器按顺序访问项,而是通过键访问值。 典型的实现将使用二叉树或哈希表,这意味着根据项的关键字查找项是一种快速操作。
对于有序容器(如map
),将使用<
(较少的谓词)在容器中的键和现有键之间进行比较。 默认谓词意味着比较键,如果这是一个智能指针,那么将比较和用于排序的将是智能指针对象,而不是它们包装的对象。 在这种情况下,您需要编写自己的谓词来执行适当的比较,并将其作为模板参数传递。
这意味着插入或擦除项通常很昂贵,并且键被视为不可变的,因此您不能为项更改它。 对于所有关联容器,没有 Remove 方法,但有 Erase 方法。 但是,对于那些保持项目排序的容器,擦除项目可能会影响性能。
关联容器有几种类型,主要区别在于它们如何处理重复键和出现的排序级别。 map
类具有按唯一键排序的键值对,因此不允许重复键。 如果您希望允许重复键,则可以使用multimap
类。 set
类本质上是一个映射,其中键与值相同,同样不允许重复。 multiset
类不允许重复。
有一个键与值相同的关联类可能看起来很奇怪,但在本节中包含该类的原因是,与map
类一样,set
类也有一个类似的接口来查找值。 同样类似于map
类,set
类查找项目的速度也很快。
map
容器存储两个不同的项,一个键和一个值,并根据键按排序顺序维护这些项。 排序的map
表示快速定位项目。 该类具有与其他容器相同的接口来添加项:您可以通过构造函数将它们放入容器中,也可以使用成员方法insert
和emplace
。 您还可以通过迭代器访问项。 当然,迭代器提供对单个值的访问,因此使用映射将访问同时具有键和值的pair
对象:
map<string, int> people;
people.emplace("Washington", 1789);
people.emplace("Adams", 1797);
people.emplace("Jefferson", 1801);
people.emplace("Madison", 1809);
people.emplace("Monroe", 1817);
auto it = people.begin();
pair<string, int> first_item = *it;
cout << first_item.first << " " << first_item.second << endl;
对emplace
的调用将项放入map
,其中关键字是string
(总统的名字),值是int
(总统开始任期的年份)。 然后,代码获得容器中第一个项目的迭代器,并通过取消引用迭代器来访问该项目,从而给出一个pair
对象。 由于项目按排序顺序存储在map
中,因此第一个项目将设置为"Adams"
。 您还可以使用insert
方法将项作为pair
对象插入,或者作为对象或通过迭代器插入到另一个容器中的pair
对象。
大多数emplace
和insert
方法将返回以下形式的pair
对象,其中iterator
类型与map
相关:
pair<iterator, bool>
您可以使用此对象测试两件事。 首先,bool
指示插入是否成功(如果容器中已经存在具有相同键的项,则插入将失败)。 其次,pair
的iterator
部分要么指示新项的位置,要么指示将不被替换的现有项的位置(并且将导致插入失败)。
失败取决于等价,而不是相等。 如果有一个项的键与您尝试插入的项相同,则插入将失败。 等价性的定义取决于与map
对象一起使用的比较器谓词。 因此,如果map
使用谓词comp
,则通过测试!comp(a,b) && !comp(b,a)
来确定两个项a
和b
之间的等价性。 这与测试(a==b)
不同。
假设前面的map
对象,您可以这样做:
auto result = people.emplace("Adams", 1825);
if (!result.second)
cout << (*result.first).first << " already in map" << endl;
测试result
变量中的第二项以查看插入是否成功,如果不成功,则第一项是对现有项pair<string,int>
的迭代器,代码取消对迭代器的引用以获得pair
对象,然后打印出第一项,即关键字(在本例中为人名)。
如果您知道项目在map
中的位置,则可以调用emplace_hint
:
auto result = people.emplace("Monroe", 1817);
people.emplace_hint(result.first, "Polk", 1845);
这里我们知道Polk
在Monroe
之后,所以我们可以将迭代器作为提示传递给Monroe
。 该类通过迭代器提供对项的访问,因此您可以使用 Rangefor
(它基于迭代器访问):
for (pair<string, int> p : people)
{
cout << p.first << " " << p.second << endl;
}
此外,还可以使用at
方法和[]
运算符访问各个项目。 在这两种情况下,该类都将搜索具有所提供键的项,如果找到该项,则返回对该项的值的引用。 在没有具有指定键的项的情况下,at
方法和[]
运算符的行为不同。 如果键不存在,at
方法将抛出异常;如果[]
操作符找不到指定的键,它将使用键并调用值类型的默认构造函数来创建一个新项。 如果键存在,[]
运算符将返回对值的引用,因此您可以编写如下代码:
people["Adams"] = 1825;
people["Jackson"] = 1829;
第二行的行为与您预期的一样:将没有键为Jackson
的项,因此map
将使用该键创建项,通过调用值类型的默认构造函数(int
,因此值被初始化为零)对其进行初始化,然后返回对该值的引用,该值被赋值为1829
。 但是,第一行将查找Adams
,查看是否存在一个项,并返回对其值的引用,然后为其赋值1825
。 与插入新项相反,没有指示项的值已更改。 在某些情况下,您可能想要这种行为,但这不是代码的目的,显然,需要允许重复键的关联容器(如multimap
)。 此外,在这两种情况下,都会搜索键,返回引用,然后执行赋值。 请注意,虽然以这种方式插入项是有效的,但是在容器中放置一个新的键值对会更有效,因为您没有这个额外的赋值。
填写map
后,可以使用以下内容搜索值:
at
方法,传递一个键并返回对该键的值的引用[]
运算符,当传递一个键时,它返回对该键的值的引用find
函数将使用模板中指定的谓词(与后面提到的 globalfind
函数不同),它将为您提供作为pair
对象的整个项的迭代器begin
方法将为您提供第一个项目的迭代器,end
方法将为您在最后一个项目之后提供一个迭代器[T2lower_bound
方法将迭代器返回给键等于**等于或大于作为参数传递的键的项upper_bound
方法返回映射中键大于提供的键的第一个项的迭代器equal_range
方法返回pair
对象中的下限值和上限值
集的行为就像它们是贴图,但关键点与值相同;例如,如下所示:
set<string> people{
"Washington","Adams", "Jefferson","Madison","Monroe",
"Adams", "Van Buren","Harrison","Tyler","Polk"};
for (string s : people) cout << s << endl;
这将按字母顺序打印出9 个人,因为有两个名为Adams
的项目,而set
类将拒绝重复项。 当项目被插入到集合中时,它将被排序,在这种情况下,顺序由比较两个string
对象的词典排序来确定。 如果您希望允许重复,以便在容器中放置 10 个人,那么您应该改用multiset
。
与map
一样,您不能更改容器中项的键,因为键用于确定排序。 对于set
,键与值相同,因此这意味着您根本不能更改该项。 如果要执行查找,那么使用排序的vector
可能会更好。 Aset
将比 Avector
具有更多的内存分配开销。 如果搜索是按顺序进行的,那么在set
容器上查找可能会比在vector
容器上查找更快,但是如果您调用binary_search
(稍后将在排序项部分中解释),它可能会比关联容器更快。
set
类的接口是map
类的受限版本,因此您可以将容器中的insert
和emplace
项分配给另一个容器中的值,这样您就可以使用迭代器访问(begin
和end
方法)。
由于没有不同的键,这意味着find
方法查找的是值,而不是键(与 bound 方法相似;例如,equal_range
)。 没有at
方法,也没有[]
运算符。
map
和set
类允许您快速查找对象,这得益于这些按排序顺序保存项目的类。 如果您遍历这些项(从begin
到end
),那么您将以排序的顺序获得这些项。 如果您想要选择键值范围内的对象,可以调用lower_bound
和upper_bound
方法,使迭代器指向适当的键范围。 这是这些关联容器的两个重要特性:查找和排序。 在某些情况下,值的实际顺序并不重要,您想要的行为是高效查找。 在这种情况下,您可以使用map
和set
类的unordered_
版本。 因为顺序并不重要,所以这些都是使用哈希表实现的。
到目前为止所描述的容器是灵活的,可以用于各种目的。 标准库提供了有特定用途的类,但是因为它们是通过包装其他类来实现的,所以它们被称为容器适配器。 例如,deque
对象可以用作先进先出(FIFO)队列,方法是将对象推到deque
后面(使用push_back
),然后使用front
方法从队列前面访问对象(并使用pop_front
删除它们)。 标准库实现了一个名为queue
的容器适配器,它具有这种 FIFO 行为,并且它基于deque
类。
queue<int> primes;
primes.push(1);
primes.push(2);
primes.push(3);
primes.push(5);
primes.push(7);
primes.push(11);
while (primes.size() > 0)
{
cout << primes.front() << ",";
primes.pop();
}
cout << endl; // prints 1,2,3,5,7,11
您将push
项放入队列并使用pop
移除它们,然后使用front
方法访问下一项。 此适配器可以包装的标准库容器必须实现push_back
、pop_front
和front
方法。 也就是说,项目被放入容器的一端,并从另一端访问(和移除)。
后进先出(LIFO)容器将放入项目,并从同一端存取(和移除)项目。 同样,可以使用deque
对象来实现此行为,方法是使用push_back
推入项,使用front
访问项,并使用pop_back
方法删除它们。 标准库提供了一个名为stack
的适配器类来提供此行为。 这有一个名为push
的方法来将项推入容器,一个名为pop
的方法来删除项,但是奇怪的是,您使用top
方法访问下一个项,即使它是使用包装容器的back
方法实现的。
不管名称如何,适配器类priority_queue
的用法与stack
容器类似;也就是说,使用top
方法访问项。 容器确保当项目被推入时,队列的顶部将始终是具有最高优先级的项目。 谓词(默认值为<
)用于对队列中的项进行排序。 例如,我们可以有一个聚合类型,该聚合类型具有任务名称以及与其他任务相比您必须完成该任务的优先级:
struct task
{
string name;
int priority;
task(const string& n, int p) : name(n), priority(p) {}
bool operator <(const task& rhs) const {
return this->priority < rhs.priority;
}
};
聚合类型很简单;它有两个由构造函数初始化的数据成员。 为了对任务进行排序,我们需要能够比较两个任务对象。 一种选择(前面给出)是定义一个单独的谓词类。 在本例中,我们使用缺省谓词,文档中说它将是less<task>
,这将基于<
运算符比较项。 为了使用缺省谓词,我们为task
类定义了<
操作符。 现在,我们可以将任务添加到priority_queue
容器:
priority_queue<task> to_do;
to_do.push(task("tidy desk", 1));
to_do.push(task("check in code", 10));
to_do.push(task("write spec", 8));
to_do.push(task("strategy meeting", 8));
while (to_do.size() > 0)
{
cout << to_do.top().name << " " << to_do.top().priority << endl;
to_do.pop();
}
此代码的结果是:
check in code 10
write spec 8
strategy meeting 8
tidy desk 1
队列已经根据priority
数据项对任务进行了排序,top
和pop
方法调用的组合按优先级顺序读取这些项,并将它们从队列中删除。 具有相同优先级的项目按其被推入的顺序放置在队列中。
到目前为止,在本章中我们已经指出,容器通过迭代器提供对项的访问。 这意味着迭代器就是简单的指针,这是有意为之的,因为迭代器的行为类似于指针。 但是,它们通常是迭代器类的对象(参见<iterator>
头)。 所有迭代器都有以下行为:
| 操作员 | 行为 |
| *** | 提供对当前位置的元素的访问权限 |
| +++ | 向前移动到下一个元素(通常使用前缀操作符)(这仅在迭代器允许向前移动的情况下) |
| - | 向后移动到上一个元素(通常使用前缀运算符)(仅当迭代器允许向后移动时) |
| ==
和!=
| 比较两个迭代器是否位于相同位置 |
| == | 分配迭代器 |
与 C++ 指针不同,C++ 指针假定数据在内存中是连续的,迭代器可以用于更复杂的数据结构,如链表,其中项可能不是连续的。 运算符++
和--
按预期工作,与底层存储机制无关。
<iterator>
头声明将递增迭代器的next
全局函数和将按指定位置更改迭代器的advance
函数(向前或向后取决于参数是否为负以及迭代器允许的方向)。 还有一个prev
函数可以将迭代器递减一个或多个位置。 函数distance
可用于确定两个迭代器之间有多少项。
所有容器都有一个begin
方法,它返回第一个项目的迭代器,还有一个end
方法,它在最后一个项目之后返回一个迭代器*。 这意味着您可以通过调用begin
然后递增迭代器直到它具有从end
返回的值来迭代容器中的所有项。 迭代器上的*
操作符提供对容器中元素的访问,如果迭代器是读写的(如果从 Begin 方法返回就是这样),这意味着该项可以更改。 容器还有cbegin
和cend
方法,它们将返回一个常量迭代器,该迭代器只提供对元素的只读访问:*
vector<int> primes { 1,2,3,5,7,11,13 };
const auto it = primes.begin(); // const has no effect
*it = 42;
auto cit = primes.cbegin();
*cit = 1; // will not compile
这里const
没有效果,因为变量是auto
,类型是从用于初始化变量的项推导出来的。 cbegin
方法定义为返回const
迭代器,因此不能更改它引用的项。
begin
和cbegin
方法返回个正向迭代器,以便++
操作符将迭代器向前移动。 容器还可以支持反向迭代器,其中rbegin
是容器中的最后一项(即,在由end
返回的位置之前的项*),rend
是在第一项之前的位置。 (还有crbegin
和crend
,它们返回const
个迭代器。)。 需要注意的是,反向迭代器的++
运算符向后移动*,如下例所示:**
vector<int> primes { 1,2,3,5,7,11,13 };
auto it = primes.rbegin();
while (it != primes.rend())
{
cout << *it++ << " ";
}
cout << endl; // prints 13,11,7,5,4,3,2,1
++
运算符根据迭代器应用到的迭代器类型递增迭代器。 需要注意的是,这里使用!=
运算符来确定循环是否应该结束,因为将在所有迭代器上定义!=
运算符。
这里的迭代器类型通过使用auto
关键字被忽略。 事实上,所有容器都将使用typedef
表示它们使用的所有迭代器类型,因此在前面的例子中,我们可以使用以下代码:
vector<int> primes { 1,2,3,5,7,11,13 };
vector<int>::iterator it = primes.begin();
允许正向迭代的容器将具有iterator
和const_iterator
的typedef
,而允许反向迭代的容器将具有reverse_iterator
和const_reverse_iterator
的typedef
。 为了完整,容器还将为返回指向元素的指针的方法使用typedef
表示pointer
和const_pointer
,对于返回对元素的引用的方法使用reference
和const_reference
表示。 这些类型定义使您能够在不知道容器中的类型的情况下编写泛型代码,但代码仍然能够声明正确类型的变量。
尽管迭代器看起来像是指针,但迭代器通常是由类实现的。 这些类型可能只允许单向迭代:正向迭代器将只有++
运算符,反向迭代器将有-
运算符,或者该类型可能允许双向迭代(双向迭代器),因此它们同时实现++
和--
运算符。 例如,list
、set
、multiset
、map
和multimap
类上的迭代器是双向的。 vector
、deque
、array
和string
类具有允许随机访问的迭代器,因此这些迭代器类型具有与双向迭代器相同的行为,但也有类似算术的指针,因此它们可以一次更改多个项目位置。
顾名思义,输入迭代器将只向前移动并具有读取访问权限,而输出迭代器将仅向前移动但将具有写入访问权限。 这些迭代器没有随机访问,也不允许向后移动。 例如,输出流可以与输出迭代器一起使用:您为取消引用的迭代器分配一个数据项,以便将该数据项写入流。 类似地,输入流可以有一个输入迭代器,您可以取消对该迭代器的引用以访问流中的下一项。 此行为意味着,对于输出迭代器,取消引用运算符(*
)的唯一有效用法是在赋值的左侧。 使用!=
检查迭代器的值没有任何意义,并且无法检查通过输出迭代器赋值是否成功。
例如,transform
函数接受三个迭代器和一个函数。 前两个迭代器是输入迭代器,指示函数要转换的项的范围。 结果将放入一个项目范围(与输入迭代器的范围大小相同)中,第一个项目由第三个迭代器表示,这是一个输出迭代器。 执行此操作的一种方法如下:
vector<int> data { 1,2,3,4,5 };
vector<int> results;
results.resize(data.size());
transform(
data.begin(), data.end(),
results.begin(),
[](int x){ return x*x; } );
在这里,begin
和end
方法返回data
容器上的迭代器,这些迭代器可以安全地用作输入迭代器。 只有当容器有足够的已分配项时,results
容器上的begin
方法才能用作输出迭代器,这就是此代码中的情况,因为它们已与resize
一起分配。 然后,该函数将通过将每个输入项传递给最后一个参数(仅返回值的平方)中给出的 lambda 函数来转换每个输入项。 重新评估这里发生的事情很重要;transform
函数的第三个参数是输出迭代器,这意味着您应该期待该函数通过此迭代器写入值。
这段代码可以工作,但是它需要额外的步骤来分配空间,并且您在容器中有额外的默认对象分配,这样您就可以覆盖它们。 值得一提的是,输出迭代器不必位于另一个容器中。 它可以指向相同的容器,只要它引用可以写入的范围:
vector<int> vec{ 1,2,3,4,5 };
vec.resize(vec.size() * 2);
transform(vec.begin(), vec.begin() + 5,
vec.begin() + 5, [](int i) { return i*i; });
调整vec
容器的大小,以便为结果留出空间。 要转换的值的范围是从开始项到第五项(vec.begin() + 5
是下一项),写入转换后的值的位置是第六项到第十项。 如果你打印出矢量,你会得到{1,2,3,4,5,1,4,9,16,25}
。
另一种类型的输出迭代器是插入器。 back_inserter
用于带有push_back
的容器,front_inserter
用于带有push_front
的容器。 顾名思义,插入器调用容器上的insert
方法。 例如,您可以像这样使用back_inserter
:
vector<int> data { 1,2,3,4,5 };
vector<int> results;
transform(
data.begin(), data.end(),
back_inserter(results),
[](int x){ return x*x; } ); // 1,4,9,16,25
转换的结果与从back_inserter
类创建的临时对象一起插入到results
容器中。 使用back_inserter
对象可确保当transform
函数通过迭代器写入时,使用push_back
将项插入到包装容器中。 请注意,结果容器应该与源容器不同。
如果希望值按相反顺序排列,则如果容器支持push_front
(例如,deque
),则可以使用front_inserter
。 vector
类没有push_front
方法,但它有反向迭代器,因此您可以改用它们:
vector<int> data { 1,2,3,4,5 };
vector<int> results;
transform(
data.rbegin(), data.rend(),
back_inserter(results),
[](int x){ return x*x; } ); // 25,16,9,4,1
要颠倒结果的顺序,只需将begin
更改为rbegin
,将end
更改为rend
。
这些是<iterators>
中的适配器类,可用于从输入流读取项目或将项目写入输出流。 例如,到目前为止,我们已经通过 Rangefor
循环使用迭代器来打印容器的内容:
vector<int> data { 1,2,3,4,5 };
for (int i : data) cout << i << " ";
cout << endl;
相反,您可以基于cout
创建一个输出流迭代器,以便使用流运算符<<
通过此迭代器将int
值写入cout
流。 要打印出包含int
值的容器,只需将容器复制到输出迭代器:
vector<int> data { 1,2,3,4,5 };
ostream_iterator<int> my_out(cout, " ");
copy(data.cbegin(), data.cend(), my_out);
cout << endl;
ostream_iterator
类的第一个参数是它将适应的输出流,可选的第二个参数是在每个项目之间使用的分隔符字符串。 copy
函数(在<algorithm>
中)将把作为前两个参数传递的输入迭代器所指示的范围内的项复制到作为最后一个参数传递的输出迭代器。
类似地,还有一个istream_iterator
类,它将包装一个输入流对象并提供一个输入迭代器。 该类将使用 STREAM>>
运算符来提取指定类型的对象,这些对象可以通过流迭代器读取。 然而,从流中读取数据要比向流中写入数据复杂得多,因为必须检测何时输入流中没有更多数据可供迭代器读取(文件情况结束)。
istream_iterator
类有两个构造函数。 一个构造函数只有一个参数,即要读取的输入流,而另一个构造函数(默认构造函数)没有参数,用于创建结束流迭代器。 流结束迭代器用于指示流中没有更多数据:
vector<int> data;
copy(
istream_iterator<int>(cin), istream_iterator<int>(),
back_inserter(data));
ostream_iterator<int> my_out(cout, " ");
copy(data.cbegin(), data.cend(), my_out);
cout << endl;
对copy
的第一次调用提供了两个输入迭代器(作为第一个参数)和一个输出迭代器。 该函数将数据从第一个迭代器复制到最后一个参数中的输出迭代器。 由于最后一个参数是从back_inserter
创建的,这意味着这些项被插入到vector
对象中。 输入迭代器基于输入流(cin
),因此copy
函数将从控制台读取int
值(每个值由空格分隔),直到没有更多的值可用(例如,如果按CTRL+Z结束流或键入非数字项)。 由于您可以使用迭代器给出的一系列值来初始化容器,因此可以使用istream_iterator
作为构造函数参数:
vector<int> data {
istream_iterator<int>(cin), istream_iterator<int>() };
在这里,构造函数是使用初始值设定项列表语法调用的;如果使用圆括号,编译器会将其解释为函数的声明!
如前所述,istream_iterator
将使用流的>>
运算符从流中读取指定类型的对象,并且该运算符使用空格来分隔项目(因此它只忽略所有空格)。 如果您在包含string
个对象的容器中进行读取,那么您在控制台上键入的每个单词都将是容器中的一个项目。 string
是一个字符容器,也可以使用迭代器对其进行初始化,因此您可以尝试使用istream_iterator
从控制台将数据输入到string
:
string data {
istream_iterator<char>(cin), istream_iterator<char>() };
在本例中,流是cin
,但它很容易成为文件的ifstream
对象。 问题是,cin
对象将去掉空格,因此string
对象将包含您键入的除空格以外的所有内容,因此将没有空格和换行符。
此问题是由使用流的>>
运算符的istream_iterator
引起的,只能通过使用另一个类istreambuf_iterator
来避免:
string data {
istreambuf_iterator<char>(cin), istreambuf_iterator<char>() };
这个类从流中读取每个字符,并将每个字符复制到容器中,而不需要处理>>
。
C 标准库通常需要指向数据的指针。 例如,当 C 函数需要一个字符串时,它需要一个指向包含该字符串的字符数组的const char*
指针。 C++ 标准库的设计允许您将其类与 C 标准库一起使用;实际上,C 标准库是 C++ 标准库的一部分。 对于string
对象,解决方案很简单:当需要const char*
指针时,只需在string
对象上调用c_str
方法。
在连续内存(array
、string
或data
)中存储数据的容器有一个名为data
的方法,该方法允许以 C 数组的形式访问容器的数据。 此外,这些容器拥有对其数据的[]
操作符访问权限,因此您还可以将第一项的地址视为&container[0]
(其中container
是容器对象),就像您对 C 数组所做的那样。 但是,如果容器为空,则此地址将无效,因此在使用它之前应该调用empty
方法。 这些容器中的项数是从size
方法返回的,因此对于任何接受指向 C 数组开头及其大小的指针的 C 函数,都可以使用&container[0]
和size
方法的值来调用它。
您可能想通过调用容器的begin
函数来获得具有连续内存的容器的开头,但这将返回迭代器(通常是一个对象)。 因此,要获得指向第一个项目的 C 指针,您应该调用&*begin
;也就是说,取消引用从begin
函数返回的迭代器以获得第一个项目,然后使用地址操作符获得其地址。 坦率地说,&container[0]
更简单,可读性更强。
如果容器不将其数据存储在连续内存中(例如,deque
和list
),则只需将数据复制到临时向量中即可获得 C 指针。
list<int> data;
// do some calculations and fill the list
vector<int> temp(data.begin(), data.end());
size_t size = temp.size(); // can pass size to a C function
int *p = &temp[0]; // can pass p to a C function
在本例中,我们选择使用list
,例程将操作data
对象。 在例程的后面,这些值将被传递给一个 C 函数,因此list
被用来初始化一个vector
对象,并且这些值是从vector
中获得的。
标准库在<algorithm>
头文件中有大量的泛型函数集合。 所谓泛型,我们指的是它们通过迭代器访问数据,而不知道迭代器指的是什么,因此这意味着您可以编写泛型代码来为任何适当的容器工作。 但是,如果您知道容器类型,并且该容器具有执行相同操作的成员方法,则应该使用该成员。
<algorithm>
中的许多例程将获取范围并在这些范围内迭代,以执行某些操作。 顾名思义,fill
函数将用值填充容器。 该函数使用两个迭代器来指定将放入容器每个位置的范围和值:
vector<int> vec;
vec.resize(5);
fill(vec.begin(), vec.end(), 42);
由于将为范围调用fill
函数,这意味着您必须将迭代器传递给已有值的容器,这就是此代码调用resize
方法的原因。 此代码将把42
的值放入容器的每个项中,因此当它完成时,vector
包含{42,42,42,42,42}
。 此函数还有另一个版本,称为fill_n
,它通过单个迭代器指定到范围开始的范围和范围中项目的计数。
generate
函数类似,但它有一个函数,可以是函数、函数对象或 lambda 表达式,而不是单个值。 调用该函数是为了提供容器中的每个项目,因此它没有参数,并返回迭代器访问的类型的对象:
vector<int> vec(5);
generate(vec.begin(), vec.end(),
[]() {static int i; return ++ i; });
同样,您必须确保向generate
函数传递一个已经存在的范围,此代码通过将初始大小作为构造函数参数传递来实现这一点。 在本例中,lambda 表达式有一个static
变量,该变量随着每次调用而递增,因此这意味着在generate
函数完成之后,vector
包含{1,2,3,4,5}
。 此函数还有另一个版本,称为generate_n
,它通过单个迭代器指定到范围开始的范围和范围中项目的计数。
for_each
函数将遍历由两个迭代器提供的范围,并针对该范围中的每一项调用指定的函数。 此函数必须有一个与容器中的项类型相同的参数:
vector<int> vec { 1,4,9,16,25 };
for_each(vec.begin(), vec.end(),
[](int i) { cout << i << " "; });
cout << endl;
for_each
函数迭代迭代器指定的所有项(在本例中是整个范围),取消对迭代器的引用,并将项传递给函数,此代码的效果是打印容器的内容。 该函数可以按值(如本例)或按引用获取项。 如果通过引用传递项,则函数可以更改该项:
vector<int> vec { 1,2,3,4,5 };
for_each(vec.begin(), vec.end(),
[](int& i) { i *= i; });
调用此代码后,vector
中的项将替换为这些项的正方形。 如果使用函数器或 lambda 表达式,则可以传递容器来捕获函数的结果;例如:
vector<int> vec { 1,2,3,4,5 };
vector<int> results;
for_each(vec.begin(), vec.end(),
[&results](int i) { results.push_back(i*i); });
在这里,容器被声明为接受对 lambda 表达式的每次调用的结果,并且通过捕获变量来引用该表达式来传递该变量。
Recall from Chapter 5, Using Functions, that the square brackets contain the names of the captured variables declared outside the expression. Once captured, it means that the expression is able to access the object.
在本例中,每个迭代的结果(i*i
)被推入捕获的集合中,以便存储结果以备以后使用。
transform
函数有两种形式;它们都提供函数(指针、函数器或 lambda 表达式),并且都有通过迭代器传递的容器中项目的输入范围。 在这方面,它们类似于for_each
。 transform
函数还允许您将迭代器传递给用于存储函数结果的容器。 该函数必须有一个与输入迭代器引用的类型的类型(或引用)相同的参数,并且它必须返回输出迭代器访问的类型。
另一个版本的transform
使用一个函数来组合两个范围内的值,因此这意味着该函数必须有两个参数(这将是两个迭代器中的对应项),并返回输出迭代器的类型。 您只需要给出其中一个输入范围内的全部项目范围,因为假设另一个范围至少一样大,因此您只需要提供第二个范围的开始迭代器:
vector<int> vec1 { 1,2,3,4,5 };
vector<int> vec2 { 5,4,3,2,1 };
vector<int> results;
transform(vec1.begin(), vec1.end(), vec2.begin(),
back_inserter(results), [](int i, int j) { return i*j; });
一旦容器中有了值,就可以调用函数来获取有关这些项的信息。 count
函数用于计算一定范围内具有指定值的项数:
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
auto number = count(planck.begin(), planck.end(), 6);
此代码将返回值3
,因为容器中有三个6
副本。 函数的返回类型是容器的difference_type``typedef
中指定的类型,在本例中它将是int
。 count_if
函数的工作方式与此类似,但是您要传递一个谓词,该谓词接受单个参数(容器中的当前项),并返回一个bool
,指定这是否是要计数的值。
函数的作用是:统计特定值出现的次数。 如果希望聚合所有值,则可以使用<numeric>
中的accumulate
函数。 这将在范围内迭代,访问每个项目,并保存所有项目的运行总和。 求和将使用该类型的+
运算符执行,但也有一个版本接受一个二元函数(容器类型的两个参数并返回相同的类型),该函数指定当您将两个这样的类型相加时会发生什么。
向all_of
、any_of
和none_of
函数传递带有相同类型容器的单个参数的谓词;还有指定的迭代器,指示它们迭代的范围,用谓词测试每个项目。 仅当所有项目的谓词为true
时,all_of
函数才返回true
;如果至少有一个项目的谓词为true
,则any_of
函数返回true
;只有当所有项目的谓词为false
时,none_of
函数才返回true
。
如果您有两个数据容器,有多种方法可以比较它们。 对于每种容器类型,都定义了<
、<=
、==
、!=
、>
和>=
运算符。 ==
和!=
运算符比较容器的数量和这些项的值。 因此,如果项具有不同的项数、不同的值或两者都有,则它们不相等。 其他比较更喜欢值而不是项目数:
vector<int> v1 { 1,2,3,4 };
vector<int> v2 { 1,2 };
vector<int> v3 { 5,6,7 };
cout << boolalpha;
cout << (v1 > v2) << endl; // true
cout << (v1 > v3) << endl; // false
在第一个比较中,这两个向量有相似的项目,但是v2
有更少的项目,所以v1
“大于”v2
。 在第二种情况下,v3
的值比v1
大,但数量较少,因此v3
比v1
大。
您还可以使用equal
函数比较范围。 这将传递两个范围(假设这两个范围大小相同,因此只需要一个到第二个范围开始的迭代器),并使用迭代器访问的类型的==
运算符或用户提供的谓词比较两个范围中的相应项。 只有当所有这样的比较都是true
时,函数才会返回true
。 类似地,mismatch
函数比较两个范围内的相应项目。 但是,此函数返回一个pair
对象,其中的迭代器位于第一个不同项的两个范围中的每一个范围内。 您还可以提供比较功能。 is_permutation is
与is_permutation is
类似,因为它比较两个范围内的值,但如果两个范围具有相同的值,但顺序不一定相同,则返回true
。
reverse函数作用于容器中的范围,并颠倒项目的顺序;这意味着迭代器必须是可写的。 copy
和copy_n
函数将每个项目从一个范围向前复制到另一个范围;对于copy
,输入范围由两个输入迭代器给出;对于copy_n
,范围是一个输入迭代器和项目计数。 copy_backward
函数将从范围末尾开始复制项目,以便输出范围的项目与原始项目的顺序相同。 这意味着输出迭代器将指示要复制到的范围的结束。 您还可以仅在项满足谓词指定的某些条件时才能复制项。
reverse_copy
函数将以与输入范围相反的顺序创建副本;实际上,该函数向后迭代原始数据,并将项目向前复制到输出范围。- 尽管名称不同,但
move
和move_backward
函数在语义上等同于copy
和copy_backward
函数。 因此,在下面的操作中,原始容器在操作后将具有相同的值:
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
vector<int> result(4); // we want 4 items
auto it1 = planck.begin(); // get the first position
it1 += 2; // move forward 2 places
auto it2 = it1 + 4; // move 4 items
move(it1, it2, result.begin()); // {2,6,0,7}
- 此代码将从第三个位置的项目开始,将四个项目从第一个容器复制到第二个容器。
remove_copy
和remove_copy_if
函数遍历源范围,并复制具有指定值以外的项。
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
vector<int> result;
remove_copy(planck.begin(), planck.end(),
back_inserter(result), 6);
- 这里,
planck
对象与以前相同,而result
对象将包含{2,0,7,0,0,4,0}
。remove_copy_if
函数的行为类似,但被赋予谓词而不是实际值。 remove
和remove_if
函数并不完全像它们的名字所暗示的那样。 这些函数作用于单个范围并迭代查找特定值(remove
),或者将每个项传递给指示是否应该删除该项的谓词(remove_if
)。 移除项时,容器中后面的项将前移,但容器的大小保持不变,这意味着末尾的项保持原样。remove
函数的行为是这样的,因为它们只知道如何通过迭代器读取和写入项(这对所有容器都是通用的)。 要擦除一个项目,函数需要有权访问容器的erase
方法,而remove
函数只有权访问迭代器。- 如果要删除末尾的项,则必须相应地调整容器的大小。 通常,这意味着在容器上调用合适的
erase
方法,这是因为remove
方法将迭代器返回到新的结束位置:
vector<int> planck { 6,6,2,6,0,7,0,0,4,0 };
auto new_end = remove(planck.begin(), planck.end(), 6);
// {2,0,7,0,0,4,0,0,4,0}
planck.erase(new_end, planck.end()); // {2,0,7,0,0,4,0}
replace
和replace_if
函数迭代单个范围,如果值是指定值(replace
)或从谓词(replace_if
)返回true
,则用指定的新值替换该项。 还有两个函数replace_copy
和replace_copy_if
,它们不会更改原始范围,而会更改到另一个范围(类似于remove_copy
和remove_copy_if
函数)。rotate
函数将范围视为结束连接到开始,因此您可以将项目向前移动,以便当项目从结束位置脱落时,将其放在第一个位置。 如果要将每个项目前移四个位置,可以执行以下操作:
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
auto it = planck.begin();
it += 4;
rotate(planck.begin(), it, planck.end());
- 这种旋转的结果是
{0,7,0,0,4,0,6,6,2,6}
。rotate_copy
函数执行相同的操作,但是它不会影响原始容器,而是将项目复制到另一个容器中。 unique
函数作用于一个范围并“移除”(按照前面解释的方式)相邻项的重复项,您可以为函数提供一个谓词,以测试两个项是否相同。 此函数只检查相邻的项目,因此容器中稍后将保留重复项。 如果要删除所有重复项,则应首先对容器进行排序,以便相似的项目相邻。- 只有当项目是唯一的时,
unique_copy
函数才会将项目从一个范围复制到另一个范围,因此删除重复项的一种方法是在临时容器上使用此函数,然后将原始项分配给临时容器:
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
vector<int> temp;
unique_copy(planck.begin(), planck.end(), back_inserter(temp));
planck.assign(temp.begin(), temp.end());
- 在此代码之后,
planck
容器将具有{6,2,6,0,7,0,4,0}
。 - 最后,
iter_swap
将交换由两个迭代器指示的项,swap_ranges
函数将一个范围中的项交换到另一个范围(第二个范围由一个迭代器指示,并且假定引用与第一个相同大小的范围)。
标准库具有多种搜索项目的功能:
min_element
函数将返回范围内最小项的迭代器,max_element
函数将返回最大项的迭代器。 向这些函数传递要检查的项目范围的迭代器和从两个项目的比较中返回bool
的预测器。 如果不提供预测器,则将使用该类型的<
运算符。
vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 };
auto imin = min_element(planck.begin(), planck.end());
auto imax = max_element(planck.begin(), planck.end());
cout << "values between " << *imin << " and "<< *imax << endl;
imin
和imax
值是迭代器,这就是为什么它们被解除引用以获得值的原因。 如果您想一次性获得最小元素和最大元素,可以调用minmax_element
,它将返回一个带有这些项的迭代器的pair
对象。 顾名思义,adjacent_find
函数将返回具有相同值的前两个项目的位置(并且您可以提供一个谓词来确定相同值意味着什么)。 这允许您搜索重复项并获取这些重复项的位置。
vector<int> vec{0,1,2,3,4,4,5,6,7,7,7,8,9};
vector<int>::iterator it = vec.begin();
do
{
it = adjacent_find(it, vec.end());
if (it != vec.end())
{
cout << "duplicate " << *it << endl;
++ it;
}
} while (it != vec.end());
- 这个代码有一个数字序列,其中有一些相邻的数字重复。 在这种情况下,有三个个相邻重复项:
4
后跟4
,序列7,7,7
是7
,后跟7
,7
后跟7
。do
循环重复调用adjacent_find
,直到它返回end
迭代器,表明它已经搜索了所有项。 当找到重复的对时,代码将打印值,然后递增开始位置以进行下一次搜索。 find
函数在容器中搜索单个值,如果找不到该值,则返回该项目的迭代器或end
迭代器。 向find_if
函数传递一个谓词,它将迭代器返回到它找到的第一个满足该谓词的项;类似地,find_if_not
函数找到不满足该谓词的第一个项。- 有几个函数被赋予两个范围,一个是要搜索的范围,另一个是要查找的值。 不同的函数要么查找搜索条件中的一个项目,要么查找所有项目。 这些函数对容器包含的类型或谓词使用
==
运算符。 - 函数的作用是:返回它在搜索列表中找到的第一个项目的位置。
search
函数查找特定序列,它返回整个序列的第一个个位置,而find_end
函数返回整个搜索序列的最后个位置。 最后,search_n
函数查找在指定容器的范围内重复多次的值(给定值和重复数)的序列。
可以对序列容器进行排序,一旦完成此操作,您就可以使用方法来搜索项、合并容器或获取容器之间的差异。 sort
函数将根据您提供的<
运算符或谓词对某个范围内的项目进行排序。 如果在该范围内有相等的项,则不能保证排序后这些项的顺序;如果此顺序很重要,则应改为调用stable_sort
函数。 如果希望保留输入范围并将排序后的项复制到另一个范围中,可以使用名称混乱的partial_sort_copy
函数。 这不是部分排序。 向该函数传递输入范围的迭代器和输出范围的迭代器,因此必须确保输出范围具有合适的容量。
您可以通过调用is_sorted
函数来检查某个范围是否已排序,如果发现没有按排序顺序排序的项,则会遍历所有项并返回false
,在这种情况下,您可以通过调用is_sorted_until
函数找到第一个排序不正确的项。
顾名思义,partial_sort
函数并不是按照每个项目相对于其他项目的确切顺序放置每个项目。 相反,它将创建两个组或分区,其中第一个分区将具有最小的项目(不一定以任何顺序),而另一个分区将具有最大的项目。 您可以保证最小的项目位于第一个分区中。 要调用此函数,您需要传递三个迭代器,其中两个是要排序的范围,第三个是介于其他两个之间的某个位置,该位置指示在其之前是最小值的边界。
vector<int> vec{45,23,67,6,29,44,90,3,64,18};
auto middle = vec.begin() + 5;
partial_sort(vec.begin(), middle, vec.end());
cout << "smallest items" << endl;
for_each(vec.begin(), middle, [](int i) {cout << i << " "; });
cout << endl; // 3 6 18 23 29
cout << "biggest items" << endl;
for_each(middle, vec.end(), [](int i) {cout << i << " "; });
cout << endl; // 67 90 45 64 44
在本例中,有一个由 10 个项目组成的向量,因此我们将middle
迭代器定义为从开头开始的 5 个项目(这只是一个选择,它可以是其他一些值,具体取决于您想要获得的项目数量)。 在本例中,您可以看到五个最小的项目已被排序到前半部分,而后半部分的项目最大。
名称奇怪的nth_element
函数的作用类似于partial_sort
。 向第n元素提供迭代器,该函数确保范围内的前n项是最小的。 nth_element
函数比partial_sort
快,虽然可以保证第n元素之前的项小于或等于第n元素,但分区内的排序顺序没有其他保证。
partial_sort
和nth_element
函数是分区排序函数的版本。 partition
函数是一个更通用的版本。 您向此函数传递一个范围和一个谓词,用于确定项将放置在两个分区中的哪个分区中。 满足谓词的项将被放入范围的第一个分区中,其他项将被放入第一个分区之后的范围中。 第二个分区的第一项称为分割点,它从partition
函数返回,但您可以稍后通过将迭代器传递给分区范围并将谓词传递给partition_point
函数来计算它。 partition_copy
函数还将对值进行分区,但它将保持原始范围不变,并将值放入已分配的范围中。 这些分区函数不能保证等价项的顺序,如果这个顺序很重要,那么您应该调用stable_partitian
函数。 最后,您可以通过调用is_partitioned
函数来确定容器是否被分区。
函数shuffle
将容器中的项目重新排列为随机顺序。 此函数需要来自<random>
库的统一随机数生成器。 例如,下面的代码将用十个整数填充一个容器,然后按随机顺序放置它们:
vector<int> vec;
for (int i = 0; i < 10; ++ i) vec.push_back(i);
random_device rd;
shuffle(vec.begin(), vec.end(), rd);
堆是部分排序的序列,其中第一个项始终是最大的,并且项在对数时间内添加到堆中或从堆中删除。 堆基于序列容器,奇怪的是,您必须在现有容器上使用函数调用,而不是由标准库提供适配器类。 要从现有容器创建堆,需要将范围迭代器传递给make_heap
函数,该函数将容器作为堆进行排序。 然后,您可以使用其push_back
方法将新项添加到容器中,但每次执行此操作时,都必须调用push_heap
来重新排序堆。 类似地,要从堆中获取项,您可以调用容器上的front
方法,然后通过调用pop_heap
函数删除该项,该函数可确保堆保持有序。 您可以通过调用is_heap
来测试容器是否安排为堆,如果容器没有完全安排为堆,则可以通过调用is_heap_until
获得不满足堆条件的第一个项的迭代器。 最后,可以使用sort_heap
将堆排序为排序序列。
对容器进行排序后,可以调用一些函数来获取有关序列的信息。 已经为容器描述了lower_bound
和upper_bound
方法,函数的行为方式相同:lower_bound
返回值大于或等于提供的值的第一个元素的位置,upper_bound
返回大于提供的值的下一项的位置。 函数的作用是:测试一个排序范围是否包含第二个排序范围内的项目。
以set_
开头的函数将把两个排序的序列组合成第三个容器。 set_difference
函数将复制第一个序列中的项目,但不复制第二个序列中的项目。 这不是对称操作,因为它不包括第二个序列中但不包括第一个序列中的项目。 如果您想要对称的差异,那么您应该调用set_symmetric_difference
函数。 set_intersection
将复制这两个序列中的项目。 set_union
函数将合并这两个序列。 还有一个函数可以组合两个序列,它是merge
函数。 这两个函数的不同之处在于,使用set_union
函数时,如果一个项同时位于两个序列中,则结果容器中将只有一个副本,而使用merge
时,结果容器中将有两个副本。
如果对某个范围进行了排序,则可以调用equal_range
函数来获取与传递给函数或谓词的值相等的元素范围。 此函数返回一对迭代器,它们表示容器中的值范围。
需要排序容器的最后一个方法是binary_search
。 此函数用于测试值是否在容器中。 传递给函数的迭代器指示要测试的范围和一个值,如果在该范围内有一个项目等于该值,它将返回true
(您可以提供一个谓词来执行此相等测试)。
标准库有几个用于执行数值操作的类库。 在本节中,我们将介绍两个:使用<ratio>
的编译时算术和使用<complex>
的复数。
分数是一个问题,因为对于某些分数,没有足够的有效数字来准确地表示它们,导致在进一步的算术中使用它们时会失去准确性。 此外,计算机是二进制的,仅将十进制小数部分转换为二进制将会失去准确性。 <ratio>
库提供的类允许您将小数表示为整数比率的对象,并以比率执行分数计算。 只有在执行完所有小数运算后,才能将数字转换为小数,这意味着精度的潜在损失将降至最低。 由<ratio>
库中的类执行的计算是在编译时执行的,因此编译器将捕获除以零和溢出等错误。
使用库很简单;您可以使用ratio
类,并提供分子和分母作为模板参数。 分子和分母将被分解存储,您可以通过对象的num
和den
成员访问这些值:
ratio<15, 20> ratio;
cout << ratio.num << "/" << ratio.den << endl;
这将打印出3/4
。
分数运算是使用模板执行的(实际上,这些模板是ratio
模板的专门化)。 乍一看可能有点奇怪,但你很快就会习惯的!
ratio_add<ratio<27, 11>, ratio<5, 17>> ratio;
cout << ratio.num << "/" << ratio.den << endl;
这将打印出514/187
(您可能需要一些纸张并进行分数计算以确认这一点)。 数据成员实际上是static
成员,因此创建变量意义不大。 此外,由于运算是使用类型而不是变量执行的,因此最好通过这些类型访问成员:
typedef ratio_add<ratio<27, 11>, ratio<5, 17>> sum;
cout << sum::num << "/" << sum::den << endl;
现在,您可以将 SUM 类型用作可以执行的任何其他操作的参数。 四个二进制算术运算使用ratio_add
、ratio_subtract
、ratio_multiply
和ratio_divide
执行。 通过ratio_equal
、ratio_not_equal
、ratio_greater
、ratio_greater_equal
、ratio_less
和ratio_less_equal
进行比较。
bool result = ratio_greater<sum, ratio<25, 19> >::value;
cout << boolalpha << result << endl;
此操作测试之前执行的计算(514/187
)是否大于分数25/19
(是)。 编译器将拾取被零除的错误和溢出,因此以下代码将不会编译:
typedef ratio<1, 0> invalid;
cout << invalid::num << "/" << invalid::den << endl;
但是,必须指出的是,当访问分母时,编译器将在第二行发出错误。 还有用于 SI 前缀的 Ratio 类型定义。 这意味着您可以以纳米为单位执行计算,当您需要以米为单位表示数据时,您可以使用nano
类型来获取比率:
double radius_nm = 10.0;
double volume_nm = pow(radius_nm, 3) * 3.1415 * 4.0 / 3.0;
cout << "for " << radius_nm << "nm "
"the volume is " << volume_nm << "nm3" << endl;
double factor = ((double)nano::num / nano::den);
double vol_factor = pow(factor, 3);
cout << "for " << radius_nm * factor << "m "
"the volume is " << volume_nm * vol_factor << "m3" << endl;
这里,我们在纳米(nm)的球体上进行计算。 球体的半径为 10 nm,因此第一次计算得出的体积为 4188.67 Nm3。 第二个计算将纳米转换为米;系数由nano
比率确定(请注意,对于体积,系数是立方体)。 您可以定义一个类来执行这样的转换:
template<typename units>
class dist_units
{
double data;
public:
dist_units(double d) : data(d) {}
template <class other>
dist_units(const dist_units<other>& len) : data(len.value() *
ratio_divide<units, other>::type::den /
ratio_divide<units, other>::type::num) {}
double value() const { return data; }
};
该类是为特定类型的单元定义的,它将通过ratio
模板的实例化来表示。 该类有一个构造函数用来为这些单位中的值初始化它,还有一个构造函数用来从其他单位转换,它只是将当前单位除以其他类型的单位。 此类可按如下方式使用:
dist_units<kilo> earth_diameter_km(12742);
cout << earth_diameter_km.value() << "km" << endl;
dist_units<ratio<1>> in_meters(earth_diameter_km);
cout << in_meters.value()<< "m" << endl;
dist_units<ratio<1609344, 1000>> in_miles(earth_diameter_km);
cout << in_miles.value()<< "miles" << endl;
第一个变量基于kilo
,因此单位是公里。 要将其转换为米,第二个变量类型基于与ratio<1,1>
相同的ratio<1>
。 结果是,将earth_diameter_km
中的值放入in_meters
中时,会将其乘以 1000。 转换为英里的过程稍微复杂一些。 一英里有 1609.344 米。 用于in_miles
变量的比率为 1609344/1,000 或 1609.344。 我们用earth_diameter_km
来初始化变量,那么这个值是不是太大了 1000 倍呢? 不,原因是earth_diameter_km
的类型是dist_units<kilo>
,所以公里和里程之间的换算将包含 1000 的因子。
复数不仅对数学感兴趣,在工程和科学中也是至关重要的,因此complex
类型是任何类型库的重要组成部分。 复数由两部分组成--实数部分和虚数部分。 顾名思义,虚数不是实数,不能视为实数。
在数学中,复数通常表示为二维空间中的坐标。 如果实数可以被认为是 x 轴上无限多个点中的一个,那么虚数就可以被认为是 y 轴上无限多个点中的一个。 这两个数字之间唯一的交点是原点,因为零是零,什么也不是,所以它可以是零实数,也可以是零虚数。 复数既有实数部分,也有虚数部分,因此可以将其视作笛卡儿点。 实际上,将复数可视化的另一种方式是将其表示为极数,其中该点表示为与 x 轴上的位置(正实数轴)成指定角度的指定长度的向量。
complex
类基于浮点类型,并且有针对float
、double
和long double
的专门化。 这个类很简单;它有一个构造函数,它有两个参数表示数字的实部和虚部,并且它定义了用于赋值、比较、+
、-
、/
和*
的运算符(成员方法和全局函数),作用于实部和虚部。
An operation like +
is simple for a complex number: you just add the real parts together and the imaginary parts together, and these two sums are the real and imaginary parts of the result. However, multiplication and division are a bit more, umm, complex. In multiplication, you get a quadratic: the aggregation of the two real parts multiplied, the two imaginary parts multiplied, the two values of the real part of the first multiplied with the imaginary part of the second, and the imaginary part of the first multiplied with the real part of the second. The complication is that two imaginary numbers multiplied is equivalent to the multiplication of two equivalent real numbers multiplied by -1. Furthermore, multiplying a real and an imaginary number results in an imaginary number that is equivalent in size to the multiplication of two equivalent real numbers.
还有对复数执行三角运算的函数:sin
、cos
、tan
、sinh
、cosh
和tanh
;以及基本的数学运算,如log
、exp
、log10
、pow
和sqrt
。 您还可以调用函数来创建复数并获取有关它们的信息。 因此,polar
函数将接受两个浮点数,表示矢量长度和角度的极坐标。 如果您有一个complex
Number 对象,您可以通过调用abs
(获取长度)和arg
(获取角度)来获得极坐标。
complex<double> a(1.0, 1.0);
complex<double> b(-0.5, 0.5);
complex<double> c = a + b;
cout << a << " + " << b << " = " << c << endl;
complex<double> d = polar(1.41421, -3.14152 / 4);
cout << d << endl;
要说明的第一点是,有一个为complex
数字定义的ostream
插入操作符,因此您可以将它们插入到cout
流对象中。 此代码的输出如下所示:
(1,1) + (-0.5,0.5) = (0.5,1.5)
(1.00002,-0.999979)
第二行显示了仅对 2 和-1/4pi 的平方根使用五位小数的限制,这个数字实际上是复数(1, -1)
。
在本例中,我们将为逗号分隔值(CSV)文件开发一个简单的解析器。 我们将遵循的规则如下:
- 每条记录将占用一行,换行符表示新记录
- 记录中的字段用逗号分隔,除非它们位于带引号的字符串内
- 字符串可以用单引号(
'
)或双引号("
)引起来,在这种情况下,字符串可以包含逗号作为字符串的一部分 - 立即重复的引号(
''
或""
)是文字,是字符串的一部分,而不是字符串的分隔符 - 如果字符串用引号引起来,则字符串外部的空格将被忽略
这是一个非常基本的实现,省略了引号字符串可以包含换行符的通常要求。
在本例中,大部分操作将使用string
对象作为单个字符的容器。
首先,在本书的文件夹中创建一个名为Chapter_08
的章节文件夹。 在该文件夹中,创建名为csv_parser.cpp
的文件。 由于应用将使用控制台输出和文件输入,因此请在文件顶部添加以下行:
#include <iostream>
#include <fstream>
using namespace std;
该应用还将接受一个命令行参数,该参数是要解析的 CSV 文件,因此在该文件的底部添加以下代码:
void usage()
{
cout << "usage: csv_parser file" << endl;
cout << "where file is the path to a csv file" << endl;
}
int main(int argc, const char* argv[])
{
if (argc <= 1)
{
usage();
return 1;
}
return 0;
}
应用会将文件逐行读取到string
个对象的vector
个中,因此将<vector>
添加到包含文件列表中。 为简化编码,请在usage
函数上面定义以下内容:
using namespace std;
using vec_str = vector<string>;
main
函数将逐行读取文件,最简单的方法是使用getline
函数,因此将<string>
头文件添加到包含文件列表中。 将以下行添加到main
函数的末尾:
ifstream stm;
stm.open(argv[1], ios_base::in);
if (!stm.is_open())
{
usage();
cout << "cannot open " << argv[1] << endl;
return 1;
}
vec_str lines;
for (string line; getline(stm, line); )
{
if (line.empty()) continue;
lines.push_back(move(line));
}
stm.close();
前几行使用ifstream
类打开文件。 如果找不到该文件,则打开该文件的操作将失败,并通过调用is_open
进行测试。 接下来,声明string
个对象的vector
个,并用从文件中读取的行填充这些对象。 函数getline
有两个参数:第一个是打开的文件流对象,第二个是包含字符数据的字符串。 此函数返回流对象,该对象具有bool
转换操作符,因此for
语句将循环,直到此流对象指示它无法读取更多数据。 当流到达文件末尾时,会设置一个内部文件结束标志,这会导致bool
转换操作符返回值false
。
如果getline
函数读取空行,那么将无法解析string
,因此需要对此进行测试,并且不会存储此类空行。 每个合法的行都被推入vector
,但是,由于在此操作之后将不会使用此string
变量,因此我们可以使用移动语义,因此通过调用move
函数来显式地实现这一点。
这段代码现在可以编译和运行(尽管它不会产生任何输出)。 您可以在满足前面给定条件的任何 CSV 文件上使用它,但作为测试文件,我们使用了以下文件:
George Washington,1789,1797
"John Adams, Federalist",1797,1801
"Thomas Jefferson, Democratic Republican",1801,1809
"James Madison, Democratic Republican",1809,1817
"James Monroe, Democratic Republican",1817,1825
"John Quincy Adams, Democratic Republican",1825,1829
"Andrew Jackson, Democratic",1829,1837
"Martin Van Buren, Democratic",1837,1841
"William Henry Harrison, Whig",1841,1841
"John Tyler, Whig",1841,1841
John Tyler,1841,1845
这些是 1845 年以前的美国总统;第一个字符串是总统的名字和他们的从属关系,但当总统没有从属关系时,它就会被漏掉(华盛顿和泰勒)。 然后在名字后面加上他们任期的开始和结束年份。
接下来,我们希望解析向量中的数据,并根据前面给出的规则将项目拆分成单独的字段(字段之间用逗号分隔,但使用引号)。 为此,我们将每行表示为list
个字段,每个字段是一个string
。 在文件顶部附近添加<list>
的 Include。 在进行using
声明的文件顶部,添加以下内容:
using namespace std;
using vec_str = vector<string>;
using list_str = list<string>;using vec_list = vector<list_str>;
现在,在main
函数的底部添加:
vec_list parsed;
for (string& line : lines)
{
parsed.push_back(parse_line(line));
}
第一行创建list
个对象的vector
个,for
循环遍历调用一个名为parse_line
的函数的每一行,该函数解析字符串并返回list
个string
个对象。 函数的返回值将是一个临时对象,因此是一个右值,因此这意味着将调用具有移动语义的push_back
版本。
在用法函数上方,添加parse_line
函数的开头:
list_str parse_line(const string& line)
{
list_str data;
string::const_iterator it = line.begin();
return data;
}
该函数将字符串视为字符容器,因此它将使用const_iterator
迭代 line 参数。 解析将在do
循环中执行,因此添加以下内容:
list_str data;
string::const_iterator it = line.begin();
string item; bool bQuote = false; bool bDQuote = false; do{++ it; } while (it != line.end()); data.push_back(move(item));
return data;
稍后将解释布尔变量。 do
循环递增迭代器,当达到end
值时,循环结束。 item
变量将保存解析的数据(此时为空),最后一行将把值放入list
;这是为了在函数结束之前将任何未保存的数据存储在list
中。 由于 Item 变量即将被销毁,因此调用move
可确保将其内容移动到list
中,而不是复制。 如果没有此调用,则在将项放入list
时将调用字符串复制构造函数。
接下来,您需要对数据进行解析。 为此,添加一个开关来测试以下三种情况:逗号(表示字段结尾)和引号或双引号表示带引号的字符串。 其思想是读取每个字段,并使用item
变量逐个字符构建其值。
do
{
switch (*it) { case ''': break; case '"': break; case ',': break; default: item.push_back(*it); };
++ it;
} while (it != line.end());
默认操作很简单:它将字符复制到临时字符串中。 如果字符是单引号,我们有两个选择。 要么引号在带双引号的字符串中(在这种情况下,我们希望将引号存储在item
中),要么引号是分隔符,在这种情况下,我们通过设置bQuote
值来存储它是开始引号还是结束引号。 对于单报价的情况,请添加以下内容:
case ''':
if (bDQuote) item.push_back(*it); else { bQuote = !bQuote; if (bQuote) item.clear(); }
break;
这很简单。 如果这是双引号字符串(设置了bDQuote
),则存储引号。 如果没有,那么我们翻转bQuote bool
,这样如果这是第一个引号,我们就注册该字符串是被引用的,否则我们注册它是一个字符串的末尾。 如果我们位于带引号的字符串的开头,则清除 Item 变量以忽略前一个逗号(如果有)和引号之间的任何空格。 但是,此代码没有考虑使用相邻的两个引号,这意味着引号是文字和字符串的一部分。 更改代码以添加针对此情况的检查:
if (bDQuote) item.push_back(*it);
else
{
if ((it + 1) != line.end() && *(it + 1) == ''') { item.push_back(*it); ++ it; } else
{
bQuote = !bQuote;
if (bQuote) item.clear();
}
}
if
语句进行检查,以确保如果我们递增迭代器,我们不会到达行尾(在本例中,短路将在这里开始,表达式的其余部分将不会被求值)。 我们可以测试下一项,然后查看下一项是否为单引号;如果是,则将其添加到item
变量并递增迭代器,以便在循环中使用两个引号。
双引号的代码类似,但切换了布尔变量并测试双引号:
case '"':
if (bQuote) item.push_back(*it); else { if ((it + 1) != line.end() && *(it + 1) == '"') { item.push_back(*it); ++ it; } else { bDQuote = !bDQuote; if (bDQuote) item.clear(); } }
break;
最后,我们需要代码来测试逗号。 同样,我们有两种情况:要么是带引号的字符串中的逗号,在这种情况下,我们需要存储字符;要么是字段的末尾,在这种情况下,我们需要完成对该字段的解析。 代码非常简单:
case ',':
if (bQuote || bDQuote) item.push_back(*it); else data.push_back(move(item));
break;
if
语句测试我们是否在带引号的字符串中(在这种情况下,bQuote
或bDQuote
将为真),如果是,则存储字符。 如果这是字段的末尾,我们将把string
推入list
,但我们使用move
,这样变量数据就会移动,而string
对象则处于未初始化状态。
此代码将编译并运行。 但是,仍然没有输出,所以在我们纠正这个问题之前,请检查一下您编写的代码。 在main
函数的末尾,您将拥有一个vector
,其中每个项目都有一个list
对象,表示 CSV 文件中的每一行,而list
中的每个项目都是一个字段。 现在您已经解析了该文件,可以相应地使用该数据了。 为了让您看到数据已被解析,请在main
函数的底部添加以下行:
int count = 0;
for (list_str row : parsed)
{
cout << ++ count << "> ";
for (string field : row)
{
cout << field << " ";
}
cout << endl;
}
现在可以编译代码(使用/EHsc
开关)并运行传递 CSV 文件名称的应用。
在本章中,您已经看到了 C++ 标准库中的一些主要类,并深入研究了容器类和迭代器类。 string
类就是这样一个容器;这是一个非常重要的类,我们将在下一章更深入地介绍它。**