Skip to content

Latest commit

 

History

History
1229 lines (887 loc) · 87.3 KB

05.md

File metadata and controls

1229 lines (887 loc) · 87.3 KB

五、使用标准库容器

标准库提供了几种类型的容器;每一个都是通过模板类提供的,因此容器的行为可以用于任何类型的项。有顺序容器的类,其中容器中项目的顺序取决于项目插入容器的顺序。还有排序和未排序的关联容器,它们将一个值与一个键相关联,随后使用该键访问该值。

虽然不是容器本身,但在本章中我们还将涉及两个相关的类:pair将两个值链接到一个对象中,以及tuple,可以在一个对象中保存一个或多个值。

使用对和元组

在许多情况下,您会希望将两个项目关联在一起;例如,关联容器允许您创建一种数组类型,其中除了数字之外的项用作索引。<utility>头文件包含一个名为pair的模板类,它有两个名为firstsecond的数据成员。

    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;

tie函数返回一个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)方法是容器中的物品数量。您很容易像遍历数组一样遍历容器:

    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; 
    }

所有容器都有一个名为iteratortypedef成员,它给出了从begin方法返回的迭代器的类型。迭代器对象的行为类似于指针,因此您可以使用取消引用操作符获取迭代器引用的项,并使用增量操作符移动到下一项。

对于除vector之外的所有容器,即使删除了其他元素,也可以保证迭代器保持有效。如果您插入项目,那么只有listsforward_lists和关联的容器保证迭代器保持有效。迭代器将在后面更深入地讨论。

所有容器都必须有一个名为swap的异常安全(nothrow)方法,并且(除了两个异常)它们必须有事务语义;也就是说,一个操作必须成功或失败。如果操作失败,容器将处于调用操作之前的状态。对于每个容器,当涉及到多元素插入时,这个规则是宽松的。例如,如果您使用迭代器区域一次插入许多项,并且该区域中的一项插入失败,则该方法将无法撤消以前的插入。

需要指出的是,对象被复制到容器中,所以放入容器中的对象类型必须有一个复制和复制赋值操作符。此外,请注意,如果您将派生类对象放入需要基类对象的容器中,那么复制将对该对象进行切片,这意味着与派生类相关的任何内容都将被移除(数据成员和虚拟方法指针)。

序列容器

序列容器存储一系列项目及其存储顺序,当您使用迭代器访问它们时,这些项目将按照它们放入容器的顺序进行检索。创建容器后,可以使用库函数更改排序顺序。

目录

顾名思义,一个list对象是通过一个双向链表来实现的,其中每一项都有一个到下一项和前一项的链接。这意味着可以快速插入项目(如第 2 章使用内存、数组和指针中的示例,用单链表显示),但是由于在链表中,一个项目只能访问它前面和后面的项目,所以不能使用[] indexoperator 进行随机访问。

类允许您通过构造函数提供值,或者您可以使用成员方法。例如,assign方法允许您使用初始化列表在一个动作中填充容器,或者使用迭代器填充另一个容器中的范围。您也可以使用push_backpush_front方法插入单个项目:

    list<int> primes{ 3,5,7 }; 
    primes.push_back(11); 
    primes.push_back(13); 
    primes.push_front(2); 
    primes.push_front(1);

第一行创建一个包含357list对象,然后将1113推到最后(按此顺序),这样list包含{3,5,7,11,13}。然后代码将数字21推到前面,这样最后的list就是{1,2,3,5,7,11,13}。不管名字如何,pop_frontpop_back方法只是删除列表前面或后面的项目,而不会返回项目。如果您想获得已移除的项目,您必须先通过frontback方法访问该项目:

    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中(通过右值语义)。该类还提供了几种定位的方法(emplaceemplace_frontemplace_back),这些方法将根据您提供的数据构造一个新的对象,并将该对象插入到list中。例如,如果您有一个可以从两个double值创建的point类,则可以通过提供两个double值来创建一个构造的point对象或一个emplace对象:

    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},如下图所示:

A 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_frontpop_frontemplace_front方法,但没有对应的_back方法。它还实现了其他一些方法,并且由于列表项只能向前访问,这意味着插入将发生在现有项之后,因此类实现了insert_afteremplace_after

同样,您可以移除列表开头(pop_front)或指定项目之后(erase_after)的项目,或者告诉类在列表中向前迭代并移除具有特定值的项目(removeremove_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值;也就是说,-2插入在-1的值之后。最后两行显示了如何删除项目;pop_front移除容器前面的项目,remove_if将移除满足谓词的项目(在这种情况下,当项目小于零时)。

矢量

vector类具有动态数组的行为;也就是说,存在对项目的索引随机访问,并且容器将随着更多的项目被插入其中而增长。您可以创建一个带有初始化列表的vector对象,以及一个项目的指定数量的副本。您也可以通过传递指示另一个容器中项目范围的迭代器,将vector基于该容器中的值。您可以通过提供容量作为构造函数参数来创建一个具有预定大小的向量,并且将在容器中创建指定数量的默认项。如果在稍后阶段,您需要指定容器大小,您可以调用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;

第一行创建一个有十个项目的vector,然后在循环中每次调用 C 运行时函数rand一千次,得到一个 0 到 32767 之间的伪随机数。模数运算符用于近似地得到一个介于 0 和 9 之间的随机数。该随机数随后被用作distrib对象的索引,以选择指定的项目,该项目随后被递增。最后,这个分布被打印出来,正如你所期望的,这给出了每个项目大约 100 的值。

该代码依赖于[]运算符返回对该项的引用,这就是该项可以以这种方式递增的原因。[]操作符可用于对容器中的项目进行读写。容器通过beginend方法以及(因为它们是容器适配器所需要的)frontback方法提供迭代器访问。

一个vector对象可以保存任何有复制构造函数和赋值运算符的类型,这意味着所有的内置类型。照目前的情况来看,bool项的vector会浪费内存,因为一个布尔值可以存储为一个位,编译器会将bool视为一个整数(32 位)。标准图书馆有一个专为更有效存储物品的bool而设的vector类。然而,虽然这个类乍一看是一个好主意,但问题是,由于容器将布尔值保存为位,这意味着[]运算符不会返回对bool的引用(相反,它会返回一个行为类似于 T7 的对象)。

如果你想保存布尔值,然后操纵它们,只要你在编译时知道有多少项,那么bitset类可能是一个更好的选择。

双端队列

deque这个名字的意思是双头队列,意思是可以从两头长,虽然中间可以插物品,但是比较贵。作为一个队列,它意味着项目是有序的,但是,因为项目可以从任何一端放入队列,所以顺序不一定与您将项目放入容器的顺序相同。

deque的界面类似于一个vector,所以你可以使用at函数和[]操作符进行迭代器访问和随机访问。与vector一样,您可以使用push_backpop_backback方法从deque容器的末端获取物品,但是与vector不同,您也可以使用push_frontpop_frontfront方法获取deque容器的前部。虽然deque类有方法允许您在容器中插入和删除项目,但是对于resize来说,这些都是昂贵的操作,如果您需要使用它们,那么您应该重新考虑使用这种容器类型。此外,deque类没有预分配内存的方法,因此,当您向该容器添加一个项目时,它可能会导致内存分配。

关联容器

使用类似 C 的arrayvector,每个项目都与其数字索引相关联。早先在vector一节的一个例子中利用了这一点,在这个例子中,指数提供了分布的十分位数,方便的是,分布被分成十个十分位数的数据。

关联容器允许您提供非数字的索引;这些是键,您可以将值与它们相关联。当您将键值对插入到容器中时,它们将被排序,以便容器随后可以通过其键有效地访问值。通常,这个顺序对您来说并不重要,因为您不会使用容器来顺序访问项目,而是通过它们的键来访问值。典型的实现将使用二叉树或哈希表,这意味着根据项的键查找项是一个快速的操作。

对于有序容器,如map,将使用<(较少的谓词)对容器中的键和现有键进行比较。默认谓词意味着比较键,如果这是一个智能指针,那么将被比较并用于排序的将是智能指针对象,而不是它们包装的对象。在这种情况下,您将希望编写自己的谓词来执行适当的比较,并将其作为模板参数传递。

这意味着插入或删除项目通常很昂贵,并且密钥被视为不可变的,因此您不能为项目更改它。对于所有关联容器,没有删除方法,但有擦除方法。但是,对于那些保持项目排序的容器,擦除项目可能会影响性能。

有几种类型的关联容器,主要区别在于它们如何处理重复的键以及出现的排序级别。map类有按唯一键排序的键值对,所以不允许重复键。如果要允许重复按键,那么可以使用multimap类。set类本质上是一个键与值相同的映射,同样不允许重复。multiset类不允许重复。

有一个键与值相同的关联类可能看起来很奇怪,但将该类包含在本节中的原因是,像map类一样,set类有一个类似的接口来查找值。同样类似于map班,set班能快速找到物品。

地图和多重地图

一个map容器存储两个不同的项目,一个键和值,它根据键以排序顺序维护项目。排序的map表示快速定位一个项目。该类具有与其他容器相同的接口来添加项目:您可以通过构造函数将它们放入容器中,或者您可以使用成员方法insertemplace。您还可以通过迭代器访问项目。当然,迭代器提供对单个值的访问,因此通过映射,这将是对同时具有键和值的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"。您也可以将项目作为pair对象插入,或者作为对象插入,或者使用insert方法通过迭代器插入到另一个容器中的pair对象。

大多数emplaceinsert方法将返回以下形式的pair对象,其中iterator类型与map相关:

    pair<iterator, bool>

你用这个对象来测试两件事。首先,bool表示插入是否成功(如果容器中已经有一个具有相同密钥的项目,则插入会失败)。其次,pairiterator部分要么指示新项目的位置,要么指示不会被替换的现有项目的位置(将导致插入失败)。

故障取决于等效而不是等效。如果有一个项的键与您试图插入的项相同,则插入将失败。等价的定义取决于与map对象一起使用的比较器谓词。因此,如果map使用谓词comp,那么两个项目ab之间的等价性通过测试!comp(a,b) && !comp(b,a)来确定。这和(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);

这里我们知道PolkMonroe之后,所以我们可以将迭代器传递给Monroe作为提示。该类通过迭代器提供对项目的访问,因此您可以使用 ranged for(基于迭代器访问):

    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函数,它将使用模板中指定的谓词(不像后面提到的全局find函数),它将为您提供一个作为pair对象的整个项目的迭代器
  • begin方法会给你第一项的迭代器,end方法会给你最后一项后的迭代器 ** lower_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;

这将按字母顺序打印出九个人,因为有两个项目叫做Adams,并且set类将拒绝重复。当项目被插入到集合中时,它将被排序,在这种情况下,顺序由比较两个string对象的词典排序来确定。如果要允许重复,让容器里放十个人,那就用multiset代替。

map一样,您不能更改容器中某个项目的键,因为该键用于确定订单。对于一个set来说,键和值是一样的,所以这意味着你根本不能改变项目。如果目的是执行查找,那么最好使用排序的vector来代替。一个set将比一个vector有更多的内存分配开销。如果搜索是连续的,在set容器上的查找可能会比在vector容器上更快,但是如果您使用对binary_search的调用(稍后在排序项目一节中解释),它可能会比关联容器更快。

set类的接口是map类的受限版本,因此您可以在容器中insertemplace项,将其分配给另一个容器中的值,并且您可以访问迭代器(beginend方法)。

由于没有明确的键,这意味着find方法寻找的是一个值,而不是一个键(边界方法也是如此;例如,equal_range)。没有at法,也没有[]符。

无序容器

mapset类允许您快速找到对象,这是通过这些类以排序顺序保存项目来实现的。如果你遍历这些条目(从beginend,那么你会得到那些按照排序顺序排列的条目。如果你想选择键值范围内的对象,你可以调用lower_boundupper_bound方法,让迭代器到达合适的键值范围。

这是这些关联容器的两个重要特性:查找和排序。在某些情况下,值的实际顺序并不重要,您需要的行为是高效的查找。在这种情况下,您可以使用mapset类的unordered_版本。因为顺序不重要,所以这些是使用哈希表实现的。

特殊用途容器

到目前为止描述的容器是灵活的,可以用于各种目的。标准库提供了具有特定目的的类,但是,因为它们是通过包装其他类来实现的,所以它们被称为容器适配器。例如,一个deque对象可以用作先进先出 ( FIFO )队列,方法是将对象推到deque的后面(用push_back),然后使用front方法从队列的前面访问对象(用pop_front移除它们)。标准库实现了一个名为queue的容器适配器,它具有这种先进先出行为,并且基于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_backpop_frontfront方法。也就是说,项目从一端放入容器,从另一端访问(和移除)。

一个后进先出 ( 后进先出)的容器将放入物品,并从同一端存取(和取出)物品。同样,通过使用push_back推送项目,使用front访问项目,并使用pop_back方法移除项目,可以使用deque对象来实现此行为。标准库提供了一个名为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数据项对任务进行了排序,toppop方法调用的组合按优先级顺序读取这些项,并将其从队列中移除。具有相同优先级的项目按照推入的顺序放入队列。

使用迭代器

到目前为止,在本章中,我们已经指出容器通过迭代器来访问项目。这意味着迭代器只是指针,这是故意的,因为迭代器的行为就像指针。然而,它们通常是迭代器类的对象(参见<iterator>头)。所有迭代器都有以下行为:

| 操作员 | 行为 | | * | 允许访问当前位置的元素 | | ++ | 向前移动到下一个元素(通常使用前缀运算符)(这仅在迭代器允许向前移动的情况下) | | - | 向后移动到前一个元素(通常使用前缀运算符)(这仅在迭代器允许向后移动的情况下) | | ==!= | 比较两个迭代器是否在同一位置 | | = | 分配一个迭代器 |

与 C++ 指针假设数据在内存中是连续的不同,迭代器可以用于更复杂的数据结构,例如链表,其中的项可能不是连续的。操作符++ --按预期工作,不考虑底层存储机制。

<iterator>头声明将迭代器递增的next全局函数和将迭代器改变指定数量位置的advance函数(向前或向后,取决于参数是否为负以及迭代器允许的方向)。还有一个prev函数可以将迭代器减少一个或多个位置。distance函数可以用来确定两个迭代器之间有多少项。

所有容器都有一个begin方法,返回第一项的迭代器,还有一个end方法,在最后一项之后返回迭代器*。这意味着您可以通过调用begin迭代容器中的所有项目,然后递增迭代器,直到它具有从end返回的值。迭代器上的*操作符提供了对容器中元素的访问,如果迭代器是读写的(如果从 begin 方法返回的话),这意味着该项可以被更改。*

容器也有cbegincend方法,它们将返回一个常量迭代器,该迭代器只提供对元素的只读访问:

    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迭代器,所以你不能改变它所引用的项目。

begincbegin方法返回向前迭代器,以便++ 操作符向前移动迭代器。容器也可以支持反向迭代器,其中rbegin是容器中的最后一项(即之前的项*end返回的位置)rend是第一项之前的位置。(还有crbegincrend,返回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();

允许正向迭代的容器将具有用于iteratorconst_iteratortypedef,而允许反向迭代的容器将具有用于reverse_iteratorconst_reverse_iteratortypedef

为了完整起见,容器还将具有用于返回指向元素的指针的方法的pointerconst_pointer,以及用于返回对元素的引用的方法的referenceconst_reference。这些类型定义使您能够在不知道容器中的类型的情况下编写泛型代码,但是代码仍然能够声明正确类型的变量。

尽管它们看起来像指针,迭代器通常由类实现。这些类型可能只允许一个方向的迭代:前向迭代器将只有++ 运算符,反向迭代器将有-运算符,或者该类型可能允许两个方向的迭代(双向迭代器),因此它们实现了++ --运算符。例如,listsetmultisetmapmultimap类上的迭代器是双向的。vectordequearraystring类都有允许随机访问的迭代器,所以这些迭代器类型有和双向迭代器一样的行为,但是也有像算术一样的指针,所以一次可以被多个项位置改变。

输入和输出迭代器

顾名思义,输入迭代器只会向前移动并具有读访问权限,输出迭代器只会向前移动但具有写访问权限。这些迭代器没有随机访问,也不允许向后移动。例如,输出流可以与输出迭代器一起使用:您将数据项分配给取消引用的迭代器,以便将该数据项写入流。类似地,输入流可以有一个输入迭代器,您可以取消对迭代器的引用来访问流中的下一项。这种行为意味着对于输出迭代器,取消引用操作符(*)的唯一有效用法是在赋值的左侧。用!=检查迭代器的值是没有意义的,你不能检查通过输出迭代器赋值是否成功。

例如,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; } );

这里beginend方法返回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

转换的结果被插入到results容器中,其中包含从back_inserter类创建的临时对象。使用一个back_inserter对象确保当transform函数通过迭代器写入时,该项目被插入到使用push_back包装的容器中。请注意,结果容器应该不同于源容器。

如果你想要相反顺序的值,那么如果容器支持push_front(例如deque),那么你可以使用front_insertervector类没有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改成rbeginend改成rend就可以颠倒结果的顺序。

流迭代器

这些是<iterators>中的适配器类,可用于从输入流中读取项目或将项目写入输出流。例如,到目前为止,我们已经使用迭代器通过范围for循环打印出容器的内容:

    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类将包装一个输入流对象并提供一个输入迭代器。这个类将使用流>>操作符提取指定类型的对象,这些对象可以通过流迭代器读取。然而,从一个流中读取数据比向一个流中写入数据更复杂,因为必须检测输入流中何时没有更多数据可供迭代器读取(文件结束的情况)。

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 标准库通常需要指向数据的指针。例如,当一个 C 函数需要一个字符串时,它将需要一个指向包含该字符串的字符数组的const char*指针。C++ 标准库被设计成允许您将它的类与 C 标准库一起使用;事实上,C 标准库是 C++ 标准库的一部分。对于string对象,解决方法很简单:当您需要一个const char*指针时,您只需在一个string对象上调用c_str方法。

将数据存储在连续内存中的容器(arraystringdata)有一个名为data的方法,该方法以 C 数组的形式访问容器的数据。此外,这些容器拥有[]运算符来访问它们的数据,因此您也可以将第一个项目的地址视为&container[0](其中container是容器对象),就像使用 C 数组一样。但是,如果容器是空的,这个地址将是无效的,所以在使用它之前,您应该调用empty方法。这些容器中的项数是从size方法返回的,所以对于任何指向 C 数组开始及其大小的 C 函数,都可以用&container[0]size方法的值来调用它。

您可能想通过调用其begin函数来获取具有连续内存的容器的开头,但这将返回一个迭代器(通常是一个对象)。所以,要得到指向第一项的 C 指针,应该调用&*begin;也就是说,取消引用从begin函数返回的迭代器来获取第一项,然后使用地址运算符来获取它的地址。坦率地说,&container[0]更简单,可读性更强。

如果容器没有将其数据存储在连续的内存中(例如dequelist,那么您可以通过简单地将数据复制到临时向量中来获得一个 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中的项目将被替换为这些项目的方块。如果使用 functor 或 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 3Using 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_eachtransform函数还允许您将迭代器传递给用于存储函数结果的容器。该函数必须有一个与输入迭代器引用的类型(或引用)相同类型的参数,并且它必须返回输出迭代器访问的类型。

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中指定的类型,在这种情况下将是intcount_if函数以类似的方式工作,但是您传递了一个谓词,该谓词接受一个参数(容器中的当前项目)并返回一个bool,指定这是否是正在计数的值。

count功能计算特定值的出现次数。如果你想聚合所有的值,那么你可以使用<numeric>中的accumulate功能。这将遍历该范围,访问每个项目,并保持所有项目的累计。

求和将使用该类型的+运算符进行,但也有一个版本采用二进制函数(容器类型的两个参数并返回相同的类型),该函数指定当您将两个这样的类型相加时会发生什么。

all_ofany_ofnone_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较少,所以v1v2大。第二种情况,v3的数值比v1大,但是数值少,所以v3比 T6 大。

您也可以使用equal功能比较范围。这被传递给两个范围(假设它们的大小相同,因此只需要一个迭代器来开始第二个范围),并且它使用迭代器访问的类型的==运算符或用户提供的谓词来比较两个范围中的对应项。只有当所有这些比较都是true时,函数才会返回true。类似地,mismatch功能比较两个范围内的相应项目。然而,这个函数为第一个不相同的项目返回一个pair对象,该对象在两个范围的每一个中都有迭代器。还可以提供比较功能。is_permutation is的相似之处在于它比较两个范围内的值,但是如果两个范围具有相同的值,但不一定是相同的顺序,则它返回true

时代的变化

反转功能作用于容器中的一个范围,并反转项目的顺序;这意味着迭代器必须是可写的。copycopy_n功能将每个项目从一个范围向前复制到另一个范围;对于copy,输入范围由两个输入迭代器给出,对于copy_n,范围是一个输入迭代器和一个项目计数。copy_backward功能将从范围的末尾开始复制项目,这样输出范围将具有与原始顺序相同的项目。这意味着输出迭代器将指示要复制到的范围的结束。只有当项目满足谓词指定的某些条件时,才可以复制项目。

  • reverse_copy功能将按照与输入范围相反的顺序创建一个副本;实际上,该函数在原始文件中向后迭代,并将项目向前复制到输出范围。
  • 不管名字如何,movemove_backward函数在语义上等同于copycopy_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_copyremove_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函数的行为类似,但被赋予了一个谓词而不是实际值。
  • removeremove_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}
  • replacereplace_if函数遍历单个范围,如果该值是指定值(replace)或从谓词(replace_if)返回true,则该项被替换为指定的新值。还有replace_copyreplace_copy_if两个功能,将原来的功能单独保留,改为另一个范围(类似于remove_copyremove_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;
  • iminimax值是迭代器,这就是为什么它们被解引用以获得值。如果你想一次得到最小元素和最大元素,你可以调用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后面是77后面是7do循环反复调用adjacent_find,直到返回end迭代器,表示已经搜索了所有项目。当找到重复对时,代码打印出该值,然后增加下一次搜索的开始位置。
  • find函数在容器中搜索单个值,如果找不到值,则返回该项的迭代器或end迭代器。find_if函数被传递一个谓词,它返回一个迭代器到它找到的第一个满足谓词的项;类似地,find_if_not函数查找第一个不满足谓词的项。
  • 有几个函数被赋予了两个范围,一个是要搜索的范围,另一个是要查找的值。不同的函数要么在搜索条件中查找一个项目,要么查找所有项目。这些函数将==操作符用于容器保存的类型或谓词。
  • find_first_of函数返回它在搜索列表中找到的第一个项目的位置。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

在这个例子中,有一个十个项目的向量,所以我们从一开始就把middle迭代器定义为五个项目(这只是一个选择,它可能是一些其他的值,取决于你想要获得多少个项目)。在这个例子中,您可以看到五个最小的项目已经被排序到前半部分,后半部分有最大的项目。

这个奇怪命名的nth_element函数就像partial_sort一样。您为第 n 个元素提供了一个迭代器,该函数确保范围中的第一个 n 项最小。nth_element功能比partial_sort快,虽然保证第 n 个元素之前的项目小于或等于第 n 个元素,但是分区内的排序顺序没有其他保证。

*partial_sortnth_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_boundupper_bound方法已经针对容器进行了描述,函数的行为方式相同:lower_bound返回第一个元素的位置,该元素的值大于或等于提供的值,upper_bound返回下一个项目的位置,该位置大于提供的值。includes功能测试一个排序范围是否包含第二个排序范围中的项目。

set_开头的函数将把两个排序的序列合并到第三个容器中。set_difference功能将复制第一序列中的项目,但不复制第二序列中的项目。这不是对称操作,因为它不包括第二个序列中的项目,但不包括第一个序列中的项目。如果你想要一个对称差,那么你应该调用set_symmetric_difference函数。set_intersection将复制两个序列中的项目。set_union功能将结合这两个序列。还有一个功能会把两个序列结合起来,就是merge功能。这两个函数的区别在于,使用set_union函数时,如果一个项目在两个序列中,结果容器中只放一个副本,而使用merge时,结果容器中将放两个副本。

如果对一个范围进行了排序,那么可以调用equal_range函数来获取元素的范围,这些元素相当于传递给函数或谓词的值。这个函数返回一对迭代器,代表容器中的值的范围。

最后一个需要排序容器的方法是binary_search。此函数用于测试容器中是否有值。向函数传递指示要测试的范围和一个值的迭代器,如果范围中有一个项目等于该值,它将返回true(您可以提供一个谓词来执行这个相等测试)。

使用数字库

标准库有几个类库来执行数字操作。在这一节中,我们将讨论两个:使用<ratio>的编译时算术和使用<complex>的复数。

编译时算术

分数是一个问题,因为有些分数没有足够的有效数字来准确地表示它们,导致当你在进一步的算术中使用它们时会失去准确性。此外,计算机是二进制的,仅仅将十进制小数部分转换成二进制将会失去准确性。<ratio>库提供了允许您将分数表示为整数比率的对象,并将分数计算作为比率执行的类。只有当你完成所有的小数运算后,你才会把数字转换成十进制,这意味着潜在的精度损失被最小化了。<ratio>库中的类执行的计算是在编译时进行的,所以编译器会捕捉到诸如被零除和溢出等错误。

使用库很简单;您使用ratio类,并提供分子和分母作为模板参数。分子和分母将被分解存储,您可以通过对象的numden成员访问这些值:

    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_addratio_subtractratio_multiplyratio_divide进行。通过ratio_equalratio_not_equalratio_greaterratio_greater_equalratio_lessratio_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;

然而,重要的是要指出,当分母被访问时,编译器将在第二行发出错误。国际单位制前缀也有不同的比例。这意味着您可以以纳米为单位进行计算,当您需要以米为单位显示数据时,您可以使用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;

在这里,我们在一个球体上进行计算,其单位为纳米 ( 纳米)。球体的半径为 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>,与ratio<1,1>相同。结果是当放置在in_meters中时,earth_diameter_km中的值乘以 1000。换算成英里数要复杂一点。一英里有 1609.344 米。in_miles变量使用的比值为 1609344/1000 或 1609.344。我们正在用earth_diameter_km初始化变量,那么这个值是不是大了 1000 倍?不是,原因是earth_diameter_km的类型是dist_units<kilo>,所以公里和英里之间的换算会包含 1000 这个因子。

复数

复数不仅仅是数学上的兴趣,它们在工程和科学中也是至关重要的,因此complex类型是任何类型库的重要组成部分。一个复数由两部分组成——实部和虚部。顾名思义,虚数不是实数,不能当作实数。

在数学中,复数通常表示为二维空间中的坐标。如果一个实数可以被认为是 x 轴上无穷多个点之一,那么一个虚数可以被认为是 y 轴上无穷多个点之一。这两者之间唯一的交集是原点,因为零是零,所以什么也不是,它可以是零实数或零虚数。一个复数既有实部也有虚部,因此这可以想象成一个笛卡尔点。事实上,将复数可视化的另一种方式是将其表示为一个极坐标数,其中该点表示为与 x 轴(正实数轴)上的位置成指定角度的指定长度的向量。

complex类基于浮点类型,有floatdoublelong 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.

还有对复数进行三角运算的函数:sincostansinhcoshtanh;以及logexplog10powsqrt等基本数学运算。您还可以调用函数来创建复数并获取有关它们的信息。所以,polar函数会取两个代表向量长度和角度极坐标的浮点数。如果你有一个complex数字对象,你可以通过调用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/4π的平方根只用 5 个小数位的局限性,这个数实际上就是复数(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>添加一个包含。在文件顶部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对象的vectorfor循环遍历每一行,调用一个名为parse_line的函数,该函数解析一个字符串并返回一个string对象的list。该函数的返回值将是一个临时对象,因此是一个右值,因此这意味着将调用带有移动语义的push_back版本。

在使用功能上面,增加parse_line功能的开始:

    list_str parse_line(const string& line) 
    { 
        list_str data; 
        string::const_iterator it = line.begin(); 

        return data; 
    }

该函数将字符串视为一个字符容器,因此它将使用const_iterator遍历行参数。解析将在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中。由于项目变量即将被销毁,对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语句测试我们是否在引用的字符串中(在这种情况下bQuotebDQuote将为真),如果是,则存储字符。如果这是字段的结尾,我们将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类;这是一门非常重要的课程,将在下一章中更深入地介绍。****