侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计创建 0 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

第 10 章

10.1

【出题思路】

本题练习泛型算法的简单使用。

【解答】

泛型算法的使用其实很简单,记住关键一点:泛型算法不会直接调用容器的操作,而是通过迭代器来访问、修改、移动元素。对于本题,将 vector 的 begin 和 end 传递给 count,并将要查找的值作为第三个参数传递给它即可。

#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <algorithm>

int main() {
    // 10.1
    std::vector<int> ivec = {1, 2, 3, 4, 5, 6, 6, 6, 2};
    std::cout << "ex 10.1: " << std::count(ivec.cbegin(), ivec.cend(), 6)
              << std::endl;

    // 10.2
    std::list<std::string> slst = {"aa", "aaa", "aa", "cc"};
    std::cout << "ex 10.2: " << std::count(slst.cbegin(), slst.cend(), "aa")
              << std::endl;

    return 0;
}
// 运行结果
ex 10.1: 3
ex 10.2: 2

Process finished with exit code 0

10.2

【出题思路】

理解 “泛型” 的优点。

【解答】

(代码已在 10.1 中实现)可以看到,与上一题对比,程序的变化只在不同类型变量的声明上,而算法的使用部分几乎没有任何差异。

10.3

【出题思路】

练习泛型算法的使用。

【解答】

只读算法 accumulate 定义在头文件 numeric 中。accumulate 函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。

#include <iostream>
#include <vector>
#include <numeric>

int main() {
    std::vector<int> ivec;
    int i = 1;
    while (i != 101) {
        ivec.push_back(i);
        ++i;
    }

    // 对 ivec 中的元素求和,和的初始值是 0
    int sum = std::accumulate(ivec.cbegin(), ivec.cend(), 0);
    std::cout << sum << std::endl;

    return 0;
}
// 运行结果
5050

Process finished with exit code 0

10.4

【出题思路】

理解 accumulate 的第三个参数。

【解答】

accumulate 的第三个参数是和的初值,它还决定了函数的返回类型,以及函数中使用哪个加法运算符。

因此,本题中的调用是错误的,第三个参数 0 告知 accumulate,和是整型的,使用整型加法运算符。下面我们尝试输入带小数的值,函数返回的是一个整数。

正确的调用方法是将 0.0 作为第三个参数传递给 accumulate。

#include <iostream>
#include <vector>
#include <numeric>

int main() {
    std::vector<double> v = {-9.21, 3.14};

    // 错误的调用方法
    std::cout << std::accumulate(v.cbegin(), v.cend(), 0) << std::endl;
    // 正确的调用方法
    std::cout << std::accumulate(v.cbegin(), v.cend(), 0.0) << std::endl;

    return 0;
}
-5
-6.07

Process finished with exit code 0

10.5

【出题思路】

理解 equal 如何比较元素。

Difference between <string.h> and ?

【解答】

equal 使用 == 运算符比较两个序列中的元素。string 类重载了 ==,可比较两个字符串是否长度相等且其中元素对应位相同。而 C 风格字符串本质是 char * 类型,用 == 比较两个 char * 对象,只是检查两个**指针值(地址)**是否相等,即地址是否相等,而不会比较其中字符是否相同。所以,只有当两个序列中的指针都指向相同的地址时,equal 才会返回 true。否则,即使字符串内容完全相同,也会返回 false。

代码来自

/*
 * @Brief  In the call to equal on rosters, what
 * would happen if both rosters held C-style strings,
 * rather than library strings?
 * @Answer It's not quite the same as `std::string`
 * Maybe the function 'equal' return true when you
 * make a comparison between two c-style strings containers.
 * Nonetheless, we need to keep in mind that when it comes
 * to comparison of c-style strings, we need to use 'strcmp'
 * but not simply relational operators, for using relational
 * operators is just comparison between the address of two
 * c-style strings but not their values.
*/

#include <iostream>
#include <algorithm>
#include <vector>
#include <list>

int main() {
    char c1[] = "c++ primer";
    char c2[] = "c++ primer";
    std::vector<char *> roster1{c1};
    std::list<char *> roster2{c2};
    std::cout << std::equal(roster1.cbegin(), roster1.cend(), roster2.cbegin()) << std::endl;

    return 0;
}
// 运行结果
0

Process finished with exit code 0

注:只是地址的比较,地址不同 equal 返回 0.

10.6

【出题思路】

练习使用 fill_n。

【解答】

fill_n 接受一个迭代器,指出起始位置,还接受一个整数,指出设置的元素数目,第三个参数则是要设置的值。

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> ivec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    fill_n(ivec.begin(), ivec.size(), 0);

    for (int i : ivec)
        std::cout << i << " ";
    std::cout << std::endl;

    return 0;
}
// 运行结果
0 0 0 0 0 0 0 0 0 0 

Process finished with exit code 0

10.7

【出题思路】

进一步理解泛型算法的特点。

【解答】

(a)是错误的。因为泛型算法的一个基本特点是:算法总是通过迭代器操作容器,因此不能直接向/从容器添加、删除元素(但可以读、写),无法改变容器大小。因此,对于 copy 算法,要求目标序列至少要包含与源序列一样多的元素。而此程序中,vec 进行缺省初始化,它是空的,copy 无法进行。如需改变容器大小,需要使用一类特殊的称为插入器的迭代器。我们可以将第三个参数改为 back_inserter(vec) ,通过它,copy 算法即可将 lst 中元素的拷贝插入到 vec 的末尾。

copy(lst.begin(), lst.end(), back_inserter(vec));

(b)这段程序仍然是错误的。粗看起来,reserve 为 vec 分配了至少能容纳 10 个 int 的内存空间,调用 fill_n 时,vec 已有足够空间。但泛型算法对于容器的要求并不是有足够的空间。而是足够的元素。此时 vec 仍然为空,没有任何元素。而算法又不具备向容器添加元素的能力,因此 fill_n 仍然失败。这里,我们还是需要使用 back_inserter 来让 fill_n 有能力向 vec 添加元素。

copy(back_inserter(vec), 10, 0);

10.8

【出题思路】

深入理解泛型算法的这一特点。

【解答】

严格来说,标准库算法根本不知道有 “容器” 这个东西。它们只接受迭代器参数,运行于这些迭代器之上,通过这些迭代器来访问元素。

因此,当你传递给算法普通迭代器时,这些迭代器只能顺序或随机访问容器中的元素,造成的效果就是算法只能读取元素、改变元素的值、移动元素,但无法添加或删除元素。

但当我们传递给算法插入器,例如 back_inserter 时,由于这类迭代器能调用下层容器的操作来向容器插入元素,造成的算法执行的效果就是向容器中添加了元素。

因此,关键要理解:标准库算法从来不直接操作容器,它们只操作迭代器,从而间接访问容器。能不能插入和删除元素,不在于算法,而在于传递给它们的迭代器是否具有这样的能力。

10.9

【出题思路】

本题练习重排元素的算法。

【解答】

unique “消除” 重复值的方式并不是删除值重复的元素,执行 unique 后,容器的元素数目并未改变。不重复元素之后的位置上的元素的值是未定义的。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

void elimDups(std::vector<std::string> &words) {
    for (std::string &s : words)    // 无须拷贝字符串
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    std::sort(words.begin(), words.end());
    for (std::string &s : words)
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    std::vector<std::string>::iterator end_unique = std::unique(words.begin(), words.end());
    for (auto iter = words.begin(); iter != words.end(); ++iter)
        std::cout << *iter << " ";
    std::cout << words.size() << std::endl;

    words.erase(end_unique, words.end());
    for (std::string &s : words)
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    return;
}

int main() {
    std::vector<std::string> svec = {"cc", "bbbb", "zz", "aa", "aa", "ccc"};
    elimDups(svec);

    return 0;
}
// 运行结果
cc bbbb zz aa aa ccc 6
aa aa bbbb cc ccc zz 6
aa bbbb cc ccc zz  6
aa bbbb cc ccc zz 5

Process finished with exit code 0

10.10

【出题思路】

让读者对语言的设计有自己的思考。

【解答】

泛型算法的一大优点是 “泛型”,也就是一个算法可用于多种不同的数据类型,算法与所操作的数据结构分离。这对编程效率的提升是非常巨大的。

要做到算法与数据结构分离,重要的技术手段就是使用迭代器作为两者的桥梁。算法从不操作具体的容器,从而也就不存在与特定容器绑定,不适用于其他容器的问题。算法只操作迭代器,由迭代器真正实现对容器的访问。不同容器实现自己特定的迭代器(但不同迭代器是相容的),算法操作不同迭代器就实现了对不同容器的访问。

因此,并不是算法应该改变或不改变容器的问题。为了实现与数据结构的分离,为了实现通用性,算法根本就不知道容器的存在。算法访问数据的唯一通道是迭代器。是否改变容器大小,完全是迭代器的选择和责任。当我们向 fill_n 传递 back_inserter 时,虽然最终效果是向容器添加了新元素,但对 fill_n 来说,根本不知道这回事儿。它仍然像往常一样(通过迭代器)向元素赋予新值,只不过这次是通过 back_inserter 来赋值,而 back_inserter 选择将新值添加到了容器而已。

10.11

【出题思路】

练习向算法传递谓词来定制操作,理解稳定排序的概念。

【解答】

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

bool isShorter(const std::string &s1, const std::string &s2) {
    return s1.size() < s2.size();
}

void elimDups(std::vector<std::string> &words) {
    // 输出原本单词的顺序,单词间使用空格分割
    for (std::string &s : words)    // 无须拷贝字符串
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    // 按字典序排序 words,以便查找重复单词
    std::sort(words.begin(), words.end());
    for (std::string &s : words)
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    std::vector<std::string>::iterator end_unique =
            std::unique(words.begin(), words.end());
    for (auto iter = words.begin(); iter != words.end(); ++iter)
        std::cout << *iter << " ";
    std::cout << words.size() << std::endl;

    // 删除重复单词
    words.erase(end_unique, words.end());
    for (std::string &s : words)
        std::cout << s << " ";
    std::cout << words.size() << std::endl;

    // 按长度排序,长度相同的单词维持字典序
    std::stable_sort(words.begin(), words.end(), isShorter);
    for (std::string &s : words)
        std::cout << s << " ";
    std::cout << words.size() << std::endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    // 将 svec 中的单词按字典序排序,删除重复单词
    // 然后再按长度排序,长度相同的单词维持字典序
    elimDups(svec);

    return 0;
}
// 运行结果
the quick red fox jumps over the slow red turtle 10
fox jumps over quick red red slow the the turtle 10
fox jumps over quick red slow the turtle the  10
fox jumps over quick red slow the turtle 8
fox red the over slow jumps quick turtle 8

Process finished with exit code 0

10.12

【出题思路】

练习定义和使用谓词。

【解答】

我们将 compareIsbn 定义为一个二元为此,接受两个 Sales_data 对象,通过 isbn 成员函数获取 ISBN 编号,若前者小于后者返回真,否则返回假。

inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
    return lhs.isbn() < rhs.isbn();
}

主程序中,将 compareIsbn 作为第三个参数传递给 sort,即可实现销售数据按 ISBN 号排序。

程序实现如下所示:

Sales_data.h

#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H

// Definition of Sales_data class and related functions goes here
#include <iostream>

// 头文件不应包含 using 声明
// using namespace std;

class Sales_data {
public:
    // 4 个构造函数
    Sales_data() = default;
    Sales_data(const std::string &book) : bookNo(book) {}
    Sales_data(const std::string &book, const unsigned num,
               const double sellp, const double salep);
    Sales_data(std::istream &is);

    std::string isbn() const { return bookNo; }
    std::istream &read(std::istream &is, Sales_data &item);

private:                            // 定义私有数据成员
    std::string bookNo;             // 书籍编号,隐士初始化为空串
    unsigned units_sold = 0;        // 销售量,显示初始化为 0
    double sellingprice = 0.0;      // 原始售价,显示初始化为 0.0
    double saleprice = 0.0;         // 实售价格,显示初始化为 0.0
    double discount = 0.0;          // 折扣,显示初始化为 0.0
};

inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
    return lhs.isbn() < rhs.isbn();
}

Sales_data::Sales_data(const std::string &book, const unsigned num,
                       const double sellp, const double salep) {
    bookNo = book;
    units_sold = num;
    sellingprice = sellp;
    saleprice = salep;
    if (sellingprice != 0)
        discount = saleprice / sellingprice;    // 计算实际折扣
}

Sales_data::Sales_data(std::istream &is) {
    read(is, *this);
}

std::istream& Sales_data::read(std::istream &is, Sales_data &item) {
    is >> item.bookNo >> item.units_sold >> item.sellingprice
       >> item.saleprice;
    return is;
}

#endif //TEST_SALES_DATA_H

main.cpp

#include <vector>
#include "Sales_data.h"

int main() {
    Sales_data d1("0-201-78345-X");
    Sales_data d2("0-201-78345-X");
    Sales_data d3("0-201-78345-B", 100, 128, 109);
    Sales_data d4("0-201-78345-A");
    Sales_data d5("0-201-78345-C");
    Sales_data d6("0-201-78345-A");
    std::vector<Sales_data> v = {d1, d2, d3, d4, d5, d6};

    std::sort(v.begin(), v.end(), compareIsbn);

    for (const auto &element : v)
        std::cout << element.isbn() << std::endl;

    return 0;
}
// 运行结果
0-201-78345-A
0-201-78345-A
0-201-78345-B
0-201-78345-C
0-201-78345-X
0-201-78345-X

Process finished with exit code 0

10.13

【出题思路】

练习定义和使用一元谓词。

【解答】

本题要求谓词判断一个 string 对象的长度是否大于等于 5,而不是比较两个 string 对象,因此它应是一个一元谓词。其他与上一题基本类似。但需要注意,我们应该保存 partition 返回的迭代器 pivot,打印范围 [svec.begin(), pivot) 中的字符串。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

bool predicate(const std::string &s) {
    return s.size() >= 5;
}

int main() {
    std::vector<std::string> svec = {"cccccc", "iii", "zz", "bbbbb", "c"};
    for (auto &s : svec)        // 无须拷贝字符串
        std::cout << s << " ";
    std::cout << std::endl;

    std::vector<std::string>::iterator pivot = std::partition(svec.begin(),
            svec.end(), predicate);

    for (auto &s : svec)
        std::cout << s << " ";
    std::cout << std::endl;
    for (std::vector<std::string>::iterator iter = svec.begin();
    iter != pivot; ++iter) {
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

    return 0;
}
// 运行结果
cccccc iii zz bbbbb c 
cccccc bbbbb zz iii c 
cccccc bbbbb 

Process finished with exit code 0

10.14

【出题思路】

练习定义和使用简单的 lambda。

【解答】

由于此 lambda 无须使用所在函数中定义的局部变量,所以捕获列表为空。参数列表为两个整型。返回类型由函数体唯一的语句 —— 返回语句推断即可。

#include <iostream>

using std::cout;
using std::endl;

int main() {
    auto sum = [] (int a, int b) { return a + b; };
    cout << sum(1, 2) << endl;
}
// 运行结果
3

Process finished with exit code 0

10.15

【出题思路】

练习定义和使用简单的 lambda。

【解答】

由于需要计算所在函数的局部 int 和自己的 int 参数的和,该 lambda 需捕获所在函数的局部 int。参数列表为一个整型(int b)。返回类型仍由返回语句推断。

#include <iostream>

using std::cout;
using std::endl;

void add(int a) {
    // lambda 捕获它所在 add 函数中的变量 a
    auto sum = [a] (int b) { return a + b; };
    cout << sum(4) << endl;		// b = 4

}
int main() {
    add(1);		// 调用函数 add,形参 a = 1
    add(2);		// 调用函数 add,形参 a = 2

    return 0;
}
// 运行结果
5
6

Process finished with exit code 0

下边我们再写一个 10.14 和 10.15 练习的另一个例子(可能更有助理解 lambda):

#include <iostream>

using std::cout;
using std::endl;

int main() {
    int i = 2;
    
    auto add1 = [] (int lhs, int rhs) { return lhs + rhs; };
    // lambda 捕获它所在 main 函数中的变量 i
    auto add2 = [i] (int num) { return i + num; };


    cout << "add1(1, 2): " << add1(1, 2) << endl;
    cout << "add2(3): " << add2(3) << endl;

    return 0;
}
// 运行结果
add1(1, 2): 3
add2(3): 5

Process finished with exit code 0

捕获列表只用于局部非 static 变量。lambda 可以直接使用局部 static 变量和在它所在函数之外声明的名字。

10.16

【出题思路】

继续练习 lambda。

【解答】

本题与练习 10.11 不同的地方是,使用了 lambda 而不是谓词。而且函数在功能上也稍加扩展。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

using std::string;
using std::vector;
using std::cout;
using std::endl;

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word, const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void elimDups(std::vector<std::string> &words) {
    // 按字典序排序 words,以便查找重复单词
    sort(words.begin(), words.end());
    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    // 删除重复单词
    words.erase(end_unique, words.end());
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    // 将 words 按字典序排序,删除重复单词
    elimDups(words);
    // 按长度排序,长度相同的单词维持字典序
    stable_sort(words.begin(), words.end(),
            [] (const string &a, const string &b) {
        return a.size() < b.size();
    }
    );
    // 获取一个迭代器,指向第一个满足 size() >= sz 的元素
    auto wc = find_if(words.begin(), words.end(),
            [sz] (const string &a) {
        return a.size() >= sz;
    }
    );
    // 计算满足 size() >= sz 的元素的数目
    auto count = words.end() - wc;
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(wc, words.end(),
            [] (const string &s) {
        cout << s << " ";
    }
    );
    cout << endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    // 按字典序打印 svec 中长度不小于 4 的单词
    biggies(svec, 4);

    return 0;
}
// 运行结果
5 words of length 4 or longer
over slow jumps quick turtle 

Process finished with exit code 0

10.17

【出题思路】

继续练习 lambda,体会其与谓词的区别。

【解答】

此 lambda 比较两个给定的 Sales_data 对象,因此捕获列表为空;有两个 Sales_data 对象引用的参数;函数体部分则与 compareIsbn 相同。

Sales_data.h 头文件与 10.12 完全一致。主函数所在文件 main.cpp 代码改动如下(程序输出结果与 10.12 一样):

#include <vector>
#include "Sales_data.h"

//inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
//    return lhs.isbn() < rhs.isbn();
//}

int main() {
    Sales_data d1("978-7-121-15535-3");
    Sales_data d2("978-7-121-15535-212");
    Sales_data d3("978-7-121-15535-2", 100, 128, 109);
    Sales_data d4("978-7-121-15535-1");
    Sales_data d5("978-7-121-15535-210");
    Sales_data d6("978-7-121-15535-12");
    std::vector<Sales_data> v = {d1, d2, d3, d4, d5, d6};

    // std::sort(v.begin(), v.end(), compareIsbn);
    std::sort(v.begin(), v.end(),
            [] (const Sales_data &lhs, const Sales_data &rhs) {
        return lhs.isbn() < rhs.isbn();
    }
    );

    for (const auto &element : v)
        std::cout << element.isbn() << std::endl;

    return 0;
}

10.18

【出题思路】

理解 find_if 和 partition 的不同。

【解答】

对于本题而言,若使用 find_if,要求序列已按字符串长度递增顺序排列好序。find_if 返回第一个长度 >= sz 的字符串的位置 wc,则所有满足长度 >= sz 的字符串位于范围 [wc, end) 之间。

而 partition 不要求序列已排序,它对所有字符串检查长度是否 >= sz,将满足条件的字符串移动到序列前端,不满足条件的字符串移动到满足条件的字符串之后,返回满足条件的范围的尾后迭代器。因此满足条件的字符串位于范围 [begin, wc) 之间。

因此,在 partition 之前不再需要 stable_sort。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

using std::string;
using std::vector;
using std::cout;
using std::endl;

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void elimDups(std::vector<std::string> &words) {
    // 按字典序排序 words,以便查找重复单词
    sort(words.begin(), words.end());
    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    // 删除重复单词
    words.erase(end_unique, words.end());
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    // 将 words 按字典序排序,删除重复单词
    elimDups(words);
    // 获取一个迭代器,指向最后一个满足 size() >= sz 的元素之后的位置
    auto wc = partition(words.begin(), words.end(),
                        [sz] (const string &a) {
                            return a.size() >= sz;
                        }
    );
    // 计算满足 size() >= sz 的元素的数目
    auto count = wc - words.begin();
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(words.begin(), wc,
             [] (const string &s) {
                 cout << s << " ";
             }
    );
    cout << endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    // 按字典序打印 svec 中长度不小于 4 的单词
    biggies(svec, 4);

    return 0;
}
// 运行结果
5 words of length 4 or longer
turtle jumps over quick slow 

Process finished with exit code 0

下面放一个自己调试的时候,打印程序中间执行过程中,容器中单词的变化状态。相比上边的程序,只是增加了打印语句而已(纯粹是为了观察程序执行状态)。程序如下所示:

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

using std::string;
using std::vector;
using std::cout;
using std::endl;

void output_words(vector<string> &words) {
    // 打印容器中的内容,方便观察程序执行状态
    for (auto &s : words)
        std::cout << s << " ";
    cout << words.size() << endl;
}

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void elimDups(std::vector<std::string> &words) {
    output_words(words);

    // 按字典序排序 words,以便查找重复单词
    sort(words.begin(), words.end());
    output_words(words);

    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    output_words(words);

    // 删除重复单词
    words.erase(end_unique, words.end());
    output_words(words);
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    // 将 words 按字典序排序,删除重复单词
    elimDups(words);

    // 获取一个迭代器,指向最后一个满足 size() >= sz 的元素之后的位置
    auto wc = partition(words.begin(), words.end(),
                        [sz] (const string &a) {
                            return a.size() >= sz;
                        }
    );
    output_words(words);
    // 计算满足 size() >= sz 的元素的数目
    auto count = wc - words.begin();
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(words.begin(), wc,
             [] (const string &s) {
                 cout << s << " ";
             }
    );
    cout << endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    biggies(svec, 4);
    return 0;
}
// 运行结果
the quick red fox jumps over the slow red turtle 10
fox jumps over quick red red slow the the turtle 10
fox jumps over quick red slow the turtle the  10
fox jumps over quick red slow the turtle 8
turtle jumps over quick slow red the fox 8
5 words of length 4 or longer
turtle jumps over quick slow 

Process finished with exit code 0

10.19

【出题思路】

理解 stable_partition 和 partition 的不同。

【解答】

将上一题程序中的 partition 换为 stable_partition 即可。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

using std::string;
using std::vector;
using std::cout;
using std::endl;

void output_words(vector<string> &words) {
    // 打印容器中的内容,方便观察程序执行状态
    for (auto &s : words)
        std::cout << s << " ";
    cout << words.size() << endl;
}

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void elimDups(std::vector<std::string> &words) {
    output_words(words);

    // 按字典序排序 words,以便查找重复单词
    sort(words.begin(), words.end());
    output_words(words);

    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    output_words(words);

    // 删除重复单词
    words.erase(end_unique, words.end());
    output_words(words);
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    // 将 words 按字典序排序,删除重复单词
    elimDups(words);

    // 获取一个迭代器,指向最后一个满足 size() >= sz 的元素之后的位置
    auto wc = stable_partition(words.begin(), words.end(),
                               [sz] (const string &a) {
                                   return a.size() >= sz;
                               }
    );
    output_words(words);
    // 计算满足 size() >= sz 的元素的数目
    auto count = wc - words.begin();
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(words.begin(), wc,
             [] (const string &s) {
                 cout << s << " ";
             }
    );
    cout << endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    biggies(svec, 4);
    return 0;
}
// 运行结果
// stable_partition 算法执行前、后容器中内容分别对应 4、5 行
the quick red fox jumps over the slow red turtle 10
fox jumps over quick red red slow the the turtle 10
fox jumps over quick red slow the turtle the  10
fox jumps over quick red slow the turtle 8
jumps over quick slow turtle fox red the 8
5 words of length 4 or longer
jumps over quick slow turtle 

Process finished with exit code 0

注:

上一题 partition 算法执行前、后容器中内容为:

fox jumps over quick red slow the turtle 
turtle jumps over quick slow red the fox 

本题 stable_partition 算法执行前、后容器中内容为:

fox jumps over quick red slow the turtle 
jumps over quick slow turtle fox red the 

10.20

【出题思路】

练习 count_if 算法的使用。

【解答】

若只统计容器中满足一定条件的元素的个数,而不打印或者获得这些元素的话。直接使用 count_if 即可,无须进行 unique、sort 等操作。

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

using std::string;
using std::vector;
using std::cout;
using std::endl;

void output_words(vector<string> &words) {
    // 打印容器中的内容,方便观察程序执行状态
    for (auto &s : words)
        std::cout << s << " ";
    cout << words.size() << endl;
}

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    output_words(words);

    // 统计满足 size() > sz 的元素的个数
    auto count = count_if(words.begin(), words.end(),
                          [sz] (const string &a) {
                              return a.size() > sz;
                          }
    );
    output_words(words);
    cout << count << " " << make_plural(count, "word", "s")
         << " of length longer than " << sz << endl;
}

int main() {
    std::vector<std::string> svec = {"determined", "quick", "nuclear", "fox",
                                     "negotiations", "Things", "accelerating"};
    biggies(svec, 6);
    return 0;
}
// 运行结果
determined quick nuclear fox negotiations Things accelerating 7
determined quick nuclear fox negotiations Things accelerating 7
4 words of length longer than 6

Process finished with exit code 0

10.21

【出题思路】

练习 lambda 改变捕获变量值的方法。

【解答】

若 lambda 需要改变捕获的局部变量的值,需要在参数列表之后、函数体之前使用 mutable 关键字。对于本题,由于 lambda 有两个返回语句(i 大于 0 时返回 false,等于 0 时返回 true),还需要显示指定 lambda 的返回类型 —— 使用尾置返回类型,在参数列表后使用 -> bool 。注意,正确的顺序是 mutable -> bool 。由于 i 的初始值为 5,程序执行后会打印 5 个 0 和 1 个 1.

#include <iostream>
#include <algorithm>

using namespace std;

void mutable_lambda() {
    int i = 5;
    auto f = [i] () mutable -> bool {
        if (i > 0) {
            --i;
            return false;
        } else
            return true;
    };

    for (int j = 0; j < 6; ++j)
        cout << f() << " ";
    cout << endl;
}

int main() {
    mutable_lambda();

    return 0;
}
// 运行结果
0 0 0 0 0 1 

Process finished with exit code 0

10.22

【出题思路】

本题练习用函数代替 lambda 的方法。

【解答】

当 lambda 不捕获局部变量时,用函数代替它是很容易的。但当 lambda 捕获局部变量时就不那么简单了。因为在这种情况下,通常是算法要求可调用对象(lambda)接受的参数个数少于函数所需的参数个数,lambda 通过捕获的局部变量来弥补这个差距,而普通函数是无法办到的。

解决方法是使用标准库中的 bind 函数在实际工作函数外做一层 “包装” —— 它接受一个可调用对象 A,即实际的工作函数,返回一个新的可调用对象 B,供算法使用。A 后面是一个参数列表,即传递给它的参数列表。其中有一些名字形如 _n 的参数,表示程序调用 B 时传递给它的第 n 个参数。也就是说,算法调用 B 时传递较少的参数(_n),B 再补充其他一些值,形成更长的参数列表,从而解决算法要求的参数个数比实际工作函数所需参数个数少的问题。

注意:_n 定义在命名空间 std::placeholders 中。

#include <vector>
#include <iostream>
#include <string>
#include <algorithm>
#include <functional>


using std::string;
using std::vector;
using std::cout;
using std::endl;
using std::placeholders::_1;

void output_words(vector<string> &words) {
    // 打印容器中的内容,方便观察程序执行状态
    for (auto &s : words)
        std::cout << s << " ";
    cout << words.size() << endl;
}

bool check_size(const string &s, string::size_type sz) {
    return s.size() <= sz;
}

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    output_words(words);

    // 统计满足 size() <= sz 的元素的个数
    auto count = count_if(words.begin(), words.end(), bind(check_size, _1, 6));
    output_words(words);
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or smaller" << endl;
}

int main() {
    std::vector<std::string> svec = {"determined", "quick", "nuclear", "fox",
                                     "negotiations", "Things", "accelerating"};
    biggies(svec, 6);
    return 0;
}
// 运行结果
determined quick nuclear fox negotiations Things accelerating 7
determined quick nuclear fox negotiations Things accelerating 7
3 words of length 6 or smaller

Process finished with exit code 0

10.23

【出题思路】

理解 bind 函数的使用。

【解答】

我们拿上题来说明:

bool check_size(const string &s, string::size_type sz);
auto f = bind(check_size, _1, 6)

注:A 对应 check_size;B 对应 f

bind 是可变参数的。它接受的第一个参数是一个可调用对象,即实际工作函数 A,返回供算法使用的新的可调用对象 B。若 A 接受 x 个参数,则 bind 的参数个数应该是 x+1,即除了 A 外,其他参数应一一对应 A 所接受的参数。这些参数中有一部分来自于 B(_n) ,另外一些来自于所处函数的局部变量。

10.24

【出题思路】

本题继续练习 bind 的使用。

【解答】

解题思路与练习 10.22 类似。

对于 bind 返回的可调用对象,其唯一参数是 vector 中的元素,因此 _1 应该是 bind 的第三个参数,即 check_size 的第二个参数,而给定 string 应作为 bind 的第二个即 check_size 的第一个参数。

#include <iostream>
#include <vector>
#include <string>
#include <functional>
#include <algorithm>

using namespace std;
using std::placeholders::_1;

bool check_size(const string &s, int sz) {
    return s.size() < sz;
}

void biggies(vector<int> &ivec, const string &s) {
    // 查找第一个大于等于 s 长度的数值
    auto p = find_if(ivec.begin(), ivec.end(),
            bind(check_size, s, _1));

    // 打印结果
    cout << "第" << p - ivec.begin() + 1 << "个数" << *p
         << "大于" << s << "的长度" << endl;
}

int main() {
    vector<int> iv = {3, 2, 4, 5, 7};

    biggies(iv, "C++");
    biggies(iv, "Primer");

    return 0;
}
// 运行结果
第3个数4大于C++的长度
第5个数7大于Primer的长度

Process finished with exit code 0

10.25

【出题思路】

本题继续练习 bind 的使用。

【解答】

把习题 10.18 稍加改动即可。改动处:

  1. 添加 #include <functional>
  2. 添加 using std::placeholders::_1;
  3. 添加 check_size 函数
  4. lambda 替换为 bind

程序如下所示:

#include <vector>
#include <algorithm>
#include <iostream>
#include <string>
#include <functional>

using std::string;
using std::vector;
using std::cout;
using std::endl;
using std::placeholders::_1;

bool check_size(const string &s, string::size_type sz) {
    return s.size() >= sz;
}

// 如果 ctr 的值大于 1,返回 word 的复数形式
string make_plural(size_t ctr, const string &word,
                   const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

void elimDups(std::vector<std::string> &words) {
    // 按字典序排序 words,以便查找重复单词
    sort(words.begin(), words.end());
    // unique 重排输入范围,使得每个单词只出现一次
    // 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    // 删除重复单词
    words.erase(end_unique, words.end());
}

void biggies(vector<string> &words, vector<string>::size_type sz) {
    // 将 words 按字典序排序,删除重复单词
    elimDups(words);
    // 获取一个迭代器,指向最后一个满足 size() >= sz 的元素之后的位置
    auto wc = partition(words.begin(), words.end(),
            bind(check_size, _1, sz));
    // 计算满足 size() >= sz 的元素的数目
    auto count = wc - words.begin();
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(words.begin(), wc,
             [] (const string &s) {
                 cout << s << " ";
             }
    );
    cout << endl;
}

int main() {
    std::vector<std::string> svec = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    // 按字典序打印 svec 中长度不小于 4 的单词
    biggies(svec, 4);

    return 0;
}
// 运行结果
5 words of length 4 or longer
turtle jumps over quick slow 

Process finished with exit code 0

10.26

【出题思路】

理解插入迭代器的概念,以及几种插入迭代器的不同。

【解答】

在书中前文,我们已经学习了一种插入迭代器 back_inserter(书中 P341P_{341},查看书后边索引很容易找到相关知识点)。插入迭代器又称插入器,本质上是一种迭代器适配器。如前所述,标准库算法为了保证通用性,并不直接操作容器,而是通过迭代器来访问容器元素。因此,算法不具备直接向容器插入元素的能力。而插入器正是帮助算法实现向容器插入元素的机制。

除了 back_inserter,标准库还提供了另外两种插入器:front_inserter 和 inserter。三者的差异在于如何向容器插入元素:back_inserter 调用 push_back;front_inserter 调用 push_front;inserter 则调用 insert。显然,这也决定了它们插入元素位置的不同。back_inserter 总是插入到容器尾元素之后;front_inserter 总是插入到容器首元素之前;而 inserter 则是插入到给定位置(作为 inserter 的第二个参数传递给它)之前。因此,需要注意这些特点带来的元素插入效果的差异。例如,使用 front_inserter 向容器插入一些元素,元素最终在容器中的顺序与插入顺序相反,但 back_inserter 和 inserter 则不会有这个问题。

10.27

【出题思路】

本题练习 unique_copy 的使用,以及使用插入迭代器帮助算法(unique_copy)实现向容器插入新元素。

【解答】

本题要求将 vector 中不重复元素按原有顺序拷贝到空的 list 中,因此使用 back_inserter 即可。需要注意的是,与 unique 一样,unique_copy 也要求在源容器中重复元素是相邻存放的。因此,若 vector 重复元素未相邻存放,unique_copy 就会失败。稳妥的方法是先对 vector 排序。

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

int main() {
    vector<int> ivec{1, 2, 2, 3, 4, 5, 5, 6};
    list<int> ilst;

    unique_copy(ivec.begin(), ivec.end(), back_inserter(ilst));

    for (auto v : ilst)
        cout << v << " ";

    return 0;
}
// 运行结果
1 2 3 4 5 6 
Process finished with exit code 0

【其他解题思路】

由于要保持原顺序,显然使用 inserter 也是可以的,将 back_inserter(ilst) 替换为 inserter(ilst, ilst.begin()) 即可。

10.28

【出题思路】

进一步理解三种插入迭代器的差异,并练习使用它们。

【解答】

若三个目的容器均为空,则显然 inserter 和 back_inserter 的输出结果是:“1 2 3 4 5 6 7 8 9”,而 front_inserter 的结果是 “9 8 7 6 5 4 3 2 1”。但如果目的容器不空,则 inserter 的结果取决于传递给它的第二个参数(一个迭代器)指向什么位置。

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

int main() {
    vector<int> ivec{1, 2, 3, 4, 5, 6, 7, 8, 9};
    list<int> ilst1, ilst2, ilst3;

    unique_copy(ivec.begin(), ivec.end(), inserter(ilst1, ilst1.begin()));
    for (auto v : ilst1)
        cout << v << " ";
    cout << endl;

    unique_copy(ivec.begin(), ivec.end(), back_inserter(ilst2));
    for (auto v : ilst2)
        cout << v << " ";
    cout << endl;

    unique_copy(ivec.begin(), ivec.end(), front_inserter(ilst3));
    for (auto v : ilst3)
        cout << v << " ";
    cout << endl;

    return 0;
}
// 运行结果
1 2 3 4 5 6 7 8 9 
1 2 3 4 5 6 7 8 9 
9 8 7 6 5 4 3 2 1 

Process finished with exit code 0

10.29

【出题思路】

本题练习流迭代器的简单使用。

【解答】

虽然流不是容器,但标准库提供了通过迭代器访问流的方法。声明一个流迭代器时,需要指出所绑定的流。对于本题,首先打开一个文本文件,将此文件的流作为参数提供给流迭代器的构造函数即可。当默认构造流迭代器时,得到一个尾后迭代器,对应文件结束。

#include <iostream>
#include <vector>
#include <fstream>
#include <iterator>

using namespace std;

int main(int argc, char *argv[]) {
    if (argc != 2) {
        cerr << "请给出文件名" << endl;
        return -1;
    }
    ifstream in(argv[1]);
    if (!in) {
        cerr << "无法打开输入文件" << endl;
        return -1;
    }

    // 创建流迭代器从文件读入字符串
    istream_iterator<string> in_iter(in);
    // 尾后迭代器
    istream_iterator<string> eof;
    vector<string> words;
    while (in_iter != eof)
        words.push_back(*in_iter++);    // 存入 vector 并递增迭代器

    for (auto word : words)
        cout << word << " ";
    cout << endl;

    return 0;
}

运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data

注:../data 即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。

并在文件 data 中写入如下内容:

It’s hard to recall a newly elected freshman representative to
Congress who has made a bigger impact than Alexandria Ocasio-Cortez.

运行程序,程序执行结果如下所示:

It’s hard to recall a newly elected freshman representative to Congress who has made a bigger impact than Alexandria Ocasio-Cortez. 

Process finished with exit code 0

10.30

【出题思路】

本题练习输入流迭代器的使用。

【解答】

使用流迭代器从标准输入读取整数序列的程序与上一题类似,创建流迭代器时指出是 int,并用 cin 代替文件流对象即可。

用 copy 将整数写到标准输出,需要声明一个输出流迭代器,作为第三个参数传递给 copy。将 cout 传递给输出流迭代器的构造函数,copy 即可将整数写到标准输出。将 " " 作为第二个参数传递给输出流迭代器的构造函数,表示在每个输出之后写一个空格,从而将整数分隔开来输出。

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

using namespace std;

int main() {
    // 创建流迭代器从标准输入读入整数
    istream_iterator<int> in_iter(cin);
    // 尾后迭代器
    istream_iterator<int> eof;
    vector<int> ivec;
    while (in_iter != eof)
        ivec.push_back(*in_iter++);     // 存入 vector 并递增迭代器

    sort(ivec.begin(), ivec.end());

    ostream_iterator<int> out_iter(cout, " ");
    copy(ivec.begin(), ivec.end(), out_iter);

    return 0;
}
// 运行结果
// 第一行为我在控制台的输入
3 2 6 4 8 6 5 9 0 1 7
^D
0 1 2 3 4 5 6 6 7 8 9 
Process finished with exit code 0

注:可能你会遇到按组合键 command + d 不管用?!请参考下边帖子:

clion控制台如何终止输入,而不终止程序

为什么如果我从键盘输入EOF Clion不要在Run窗口上打印程序的输出?

如果还不好使,在按组合键 command + d 前,对输入的内容换行(Enter)。即,输入完测试数据 -> 按 Enter -> 按组合键 command + d

10.31

【出题思路】

继续练习输出流迭代器的使用,并复习 unique_copy 算法的使用。

【解答】

用 unique_copy 替代上题的 copy 即可。

#include <iostream>
#include <vector>
#include <iterator>

using namespace std;

int main() {
    // 创建流迭代器从标准输入读入整数
    istream_iterator<int> in_iter(cin);
    // 尾后迭代器
    istream_iterator<int> eof;
    vector<int> ivec;
    while (in_iter != eof)
        ivec.push_back(*in_iter++);     // 存入 vector 并递增迭代器

    sort(ivec.begin(), ivec.end());

    ostream_iterator<int> out_iter(cout, " ");
    unique_copy(ivec.begin(), ivec.end(), out_iter);

    return 0;
}
// 运行结果
3 2 6 4 8 6 5 9 0 1 7
^D
0 1 2 3 4 5 6 7 8 9 
Process finished with exit code 0

10.32

【出题思路】

继续练习流迭代器的使用,并复习算法的使用。

【解答】

  1. 读取交易记录存入 vector 的代码与前两题类似。

  2. 修改 练习 1.20 的 Sales_item.h 头文件中的函数 compareIsbn,将 return lhs.isbn() == rhs.isbn(); 改为 return lhs.isbn() < rhs.isbn();

  3. 将 compareIsbn 作为第三个参数传递给 sort,即可实现将交易记录按 ISBN 排序。

  4. 本题要求将 ISBN 相同的交易记录累加,而 find 算法查找与给定值相同的容器元素,需要逐个查找需要累加的元素,显然性能太差。我们使用 find_if ,并构造如下 lambda:

    [item] (const Sales_item &item1) { return item1.isbn() != item.isbn(); }

    作为第三个参数传递给 find_if,从而查找到第一个 ISBN 与当前交易 l 记录的不同的记录 r(ISBN 相同的范围的尾后位置)。

  5. 接下来调用 accumulate 即可实现范围 [l, r) 间交易记录的累加。

  6. 最后将 l 移动到 r,继续循环,计算下一段交易记录的查找和累加。

Sales_item.h

/*
 * This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
 * Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
 * copyright and warranty notices given in that book:
 * 
 * "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
 * 
 * 
 * "The authors and publisher have taken care in the preparation of this book,
 * but make no expressed or implied warranty of any kind and assume no
 * responsibility for errors or omissions. No liability is assumed for
 * incidental or consequential damages in connection with or arising out of the
 * use of the information or programs contained herein."
 * 
 * Permission is granted for this code to be used for educational purposes in
 * association with the book, given proper citation if and when posted or
 * reproduced.Any commercial use of this code requires the explicit written
 * permission of the publisher, Addison-Wesley Professional, a division of
 * Pearson Education, Inc. Send your request for permission, stating clearly
 * what code you would like to use, and in what specific way, to the following
 * address: 
 * 
 *     Pearson Education, Inc.
 *     Rights and Permissions Department
 *     One Lake Street
 *     Upper Saddle River, NJ  07458
 *     Fax: (201) 236-3290
*/

/* This file defines the Sales_item class used in chapter 1.
 * The code used in this file will be explained in
 * Chapter 7 (Classes) and Chapter 14 (Overloaded Operators)
 * Readers shouldn't try to understand the code in this file
 * until they have read those chapters.
*/

#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined 
#define SALESITEM_H

// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>

class Sales_item {
// these declarations are explained section 7.2.1, p. 270 
// and in chapter 14, pages 557, 558, 561
    friend std::istream& operator>>(std::istream&, Sales_item&);
    friend std::ostream& operator<<(std::ostream&, const Sales_item&);
    friend bool operator<(const Sales_item&, const Sales_item&);
    friend bool
    operator==(const Sales_item&, const Sales_item&);
public:
    // constructors are explained in section 7.1.4, pages 262 - 265
    // default constructor needed to initialize members of built-in type
    Sales_item(): units_sold(0), revenue(0.0) { }
    Sales_item(const std::string &book):
            bookNo(book), units_sold(0), revenue(0.0) { }
    Sales_item(std::istream &is) { is >> *this; }
public:
    // operations on Sales_item objects
    // member binary operator: left-hand operand bound to implicit this pointer
    Sales_item& operator+=(const Sales_item&);

    // operations on Sales_item objects
    std::string isbn() const { return bookNo; }
    double avg_price() const;
// private members as before
private:
    std::string bookNo;      // implicitly initialized to the empty string
    unsigned units_sold;
    double revenue;
};

// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{ return lhs.isbn() < rhs.isbn(); }

// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);

inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
    // must be made a friend of Sales_item
    return lhs.units_sold == rhs.units_sold &&
           lhs.revenue == rhs.revenue &&
           lhs.isbn() == rhs.isbn();
}

inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
    return !(lhs == rhs); // != defined in terms of operator==
}

// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
    Sales_item ret(lhs);  // copy (|lhs|) into a local object that we'll return
    ret += rhs;           // add in the contents of (|rhs|) 
    return ret;           // return (|ret|) by value
}

std::istream&
operator>>(std::istream& in, Sales_item& s)
{
    double price;
    in >> s.bookNo >> s.units_sold >> price;
    // check that the inputs succeeded
    if (in)
        s.revenue = s.units_sold * price;
    else
        s = Sales_item();  // input failed: reset object to default state
    return in;
}

std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
    out << s.isbn() << " " << s.units_sold << " "
        << s.revenue << " " << s.avg_price();
    return out;
}

double Sales_item::avg_price() const
{
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}
#endif

main.cpp

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>    // sort
#include <numeric>      // accumulate
#include "Sales_item.h"

int main() {
    std::vector<Sales_item> v;
    std::istream_iterator<Sales_item> in_iter(std::cin);
    std::istream_iterator<Sales_item> eof;

    // 读入 ISBN 号、售出的册数以及销售价格,存入 vector
    while (in_iter != eof)
        v.push_back(*in_iter++);

    if (v.empty()) {
        // 没有输入!敬告读者
        std::cerr << "No data?!" << std::endl;
        return -1;      // 表示失败
    }

    // 将交易记录按 ISBN 排序
    sort(v.begin(), v.end(), compareIsbn);

    auto l = v.begin();
    while (l != v.end()) {
        auto item = *l;      // 相同 ISBN 的交易记录中的第一个
        // 在后续记录中查找第一个 ISBN 与 item 不同者
        auto r = find_if(l + 1, v.end(),
                [item] (const Sales_item &item1) {
            return item1.isbn() != item.isbn();
        });
        // 将范围 [l, r) 间的交易记录累加并输出
        // 输出格式:ISBN、售出的册数、总销售额和平均价格
        std::cout << std::accumulate(l, r, Sales_item(l -> isbn())) << std::endl;
        // l 指向下一段交易记录中的第一个
        l = r;
    }

    return 0;
}
// 0-201-78345-X 3 20.00
// 0-201-78345-X 2 22.00
// 0-201-78345-B 2 25.00
// 0-201-78345-A 5 24.00
// 0-201-78345-A 7 22.00
// 上边这几行为测试数据
// 运行结果
0-201-78345-X 3 20.00
0-201-78345-X 2 22.00
0-201-78345-B 2 25.00
0-201-78345-A 5 24.00
0-201-78345-A 7 22.00^D
0-201-78345-A 12 274 22.8333
0-201-78345-B 2 50 25
0-201-78345-X 5 104 20.8

Process finished with exit code 0

10.33

【出题思路】

本题通过一个稍大的例子巩固输入和输出流迭代器的使用。

【解答】

程序从命令行接受三个文件名参数,因此程序首先判断参数数目是否为 4(包括程序名)。

然后依次打开输入文件和两个输出文件,再用这三个流初始化一个输入流迭代器和两个输出流迭代器。注意,第一个流迭代器输出以空格间隔的奇数,将 " " 作为第二个参数传递给构造函数;第二个流迭代器输出以换行间隔的偶数,将 "\n" 作为第二个参数传递给构造函数。

随后在循环中通过输入流迭代器读取文件中的整数,直至到达文件末尾。读入每个整数后,判断它是奇数还是偶数,分别写入两个输出文件。

#include <iostream>
#include <fstream>
#include <iterator>

using namespace std;

int main(int argc, char *argv[]) {
    if (argc != 4) {
        cout << "用法:10_33.exe in_file "
                "out_file1 out_file2" << endl;
        return -1;
    }

    ifstream in(argv[1]);
    if (!in) {
        cout << "打开输入文件失败!" << endl;
        exit(1);
    }

    ofstream out1(argv[2]);
    if (!out1) {
        cout << "打开输出文件 1 失败!" << endl;
        exit(1);
    }

    ofstream out2(argv[3]);
    if (!out2) {
        cout << "打开输出文件 2 失败!" << endl;
        exit(1);
    }

    // 创建流迭代器从文件读入整数
    istream_iterator<int> in_iter(in);
    // 尾后迭代器
    istream_iterator<int> eof;
    // 第一个输出文件以空格间隔整数
    ostream_iterator<int> out_iter1(out1, " ");
    // 第二个输出文件以换行间隔整数
    ostream_iterator<int> out_iter2(out2, "\n");
    while (in_iter != eof) {
        if (*in_iter & 1)
            *out_iter1++ = *in_iter;        // 奇数写入第一个输出文件
        else
            *out_iter2++ = *in_iter;        // 偶数写入第二个输出文件
        ++in_iter;
    }
    return 0;
}

运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../in_file ../out_file1 ../out_file2

注:../in_file ../out_file1 ../out_file2 即为文件 in_file,out_file1 和 out_file2 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。

并在文件 in_file 中写入如下测试数据(out_file1 和 out_file2 为空文件):

4 5 7 3 2 1 0 9 6 8

运行程序后,输出文件 out_file1 和 out_file2 内容分别为:

out_file1 :

5 7 3 1 9 

out_file2 :

4
2
0
6
8

10.34

【出题思路】

本题练习反向迭代器的简单使用。

【解答】

我们可以用 (c)rbegin 获取反向遍历的起始位置(其实是容器的末尾元素位置),用 (c)rend 获取尾后迭代器(首元素之前的位置)。通过这两个迭代器控制循环,即可实现对容器的反向遍历。注意,循环中向前移动迭代器仍然用 ++,而非 --。

#include <iostream>
#include <iterator>
#include <vector>

using namespace std;

int main() {
    vector<string> svec{"Welcome", "To", "C++"};

    for (auto r_iter = svec.crbegin(); r_iter != svec.crend(); ++r_iter)
        cout << *r_iter;
    cout << endl;

    return 0;
}
// 运行结果
C++ToWelcome

Process finished with exit code 0

10.35

【出题思路】

体会反向迭代器和普通迭代器的差异。

【解答】

若使用普通迭代器反向遍历容器,首先通过 (c)end 获得容器的尾后迭代器,循环中递减该迭代器,直到它与 (c)begin 相等为止。但需要注意的是,遍历所用迭代器的初值为尾后位置,终值为 (c)begin 之后的位置(首元素之前的位置)。也就是说,在每个循环步中,它指向的都是我们要访问的元素之后的位置。因此,我们在循环中首先将其递减,然后通过它访问容器元素,而在循环语句的第三个表达式中就不再递减迭代器了。

显然,对于反向遍历容器,使用反向迭代器比普通迭代器更清晰易懂。

#include <iostream>
#include <iterator>
#include <vector>

using namespace std;

int main() {
    vector<string> svec{"Welcome", "To", "C++"};

    for (auto iter = svec.cend(); iter != svec.cbegin(); )
        cout << *(--iter);
    cout << endl;

    return 0;
}
// 运行结果
C++ToWelcome

Process finished with exit code 0

10.36

【出题思路】

练习反向迭代器和算法的结合。

【解答】

借助反向迭代器,可以扩展算法的能力。例如,使用普通迭代器,find 能查找给定值在容器中第一次出现的位置。如果要查找最后一次出现的位置,还使用普通迭代器的话,代码会很复杂。但借助反向迭代器,find 可以逆序遍历容器中的元素,从而 “第一次出现位置” 实际上也就是正常顺序的最后一次出现位置了。

注意:

  1. 由于 list 是链表结构,元素不连续存储,其迭代器不支持算术运算。因此,程序中用一个循环来计数位置编号(count)。
  2. 由于程序计数的是正向位置编号,因此,需要将 find 找到的反向迭代器 pos 转换为普通迭代器(使用 base 成员函数)。但要注意,反向迭代器与普通迭代器的转换是左闭合区间的转换,而非精确位置的转换。pos.base() 指向的并非最后一个 0,而是它靠近容器尾方向的邻居。因此,首先将 pos 向容器首方向靠近一个位置(++),然后再调用 base,得到的就是指向最后一个 0 的普通迭代器了。读者可以尝试对本例画出类似图 10.2 所示的迭代器位置关系图。
#include <iostream>
#include <iterator>
#include <list>
#include <algorithm>

using namespace std;

int main() {
    list<int> ilst = {0, 3, 7, 1, 0, 0, 4};
    // 利用反向迭代器查找最后一个 0
    auto pos = find(ilst.crbegin(), ilst.crend(), 0);
    // 将反向迭代器 pos 向链表头方向靠近一个位置,pos 转换为普通迭代
    // 器 pos.base() 时,将回到最后一个 0 的位置
    ++pos;
    int count = 1;      // 计数最后一个 0 在链表中的位置,从 1 开始
    for (auto iter = ilst.cbegin(); iter != pos.base(); ++iter, ++count) {}
    cout << "最后一个0在第" << count << "个位置" << endl;

    return 0;
}
// 运行结果
最后一个0在第6个位置

Process finished with exit code 0

10.37

【出题思路】

深入理解反向迭代器和普通迭代器间的差异及相互转换。

【解答】

反向迭代器和普通迭代器的转换是左闭合区间的转换。

对 10 个元素的 vector ivec,包含位置 3~7 之间元素的迭代器区间如下所示:

0		1		i1->2		3		4		5		6		i2->7		8		9

第一个迭代器是 ivec.begin() + 2,第二个迭代器指向位置 8,即 ivec.begin() + 7

当将这两个迭代器转换为反向迭代器时,位置如下:

0		re->1		2		3		4		5		rb->6		7		8		9

虽然与正向迭代器的位置不同,但左闭合区间 [rb, re) 仍然对应位置 3~7 之间的元素。显然,普通迭代器和反向迭代器间的这种错位,恰恰是因为标准库的范围概念是左闭合区间造成的。

另外,注意 back_inserter 和流迭代器的使用。

#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <algorithm>

using namespace std;

int main() {
    ostream_iterator<int> out_iter(cout, " ");
    vector<int> ivec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 用流迭代器和 copy 输出 int 序列
    copy(ivec.cbegin(), ivec.cend(), out_iter);
    cout << endl;

    list<int> ilst;
    // 将 ivec[2],也就是第 3 个元素的位置转换为反向迭代器
    // vector 是连续存储的,可以进行迭代器的加减
    vector<int>::reverse_iterator re(ivec.begin() + 2);
    // 将 ivec[7],也就是第 8 个元素的位置转换为反向迭代器
    vector<int>::reverse_iterator rb(ivec.begin() + 7);
    // 用反向迭代器将元素逆序拷贝到 list
    copy(rb, re, back_inserter(ilst));
    copy(ilst.cbegin(), ilst.cend(), out_iter);
    cout << endl;

    return 0;
}
// 运行结果
0 1 2 3 4 5 6 7 8 9 
6 5 4 3 2 

Process finished with exit code 0

【其他解题思路】

也可以通过 (c)rbegin() 获得反向迭代器的首位置(正向的尾元素),然后正确计算偏移量,来获得正确的反向范围,但计算上需要非常小心。

#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <algorithm>

using namespace std;

int main() {
    ostream_iterator<int> out_iter(cout, " ");
    vector<int> ivec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 用流迭代器和 copy 输出 int 序列
    copy(ivec.cbegin(), ivec.cend(), out_iter);
    cout << endl;

    list<int> ilst;
    copy(ivec.crbegin() + ivec.size() - 7, ivec.crend() - 3 + 1,
            back_inserter(ilst));
    copy(ilst.cbegin(), ilst.cend(), out_iter);
    cout << endl;

    return 0;
}
// 运行结果
0 1 2 3 4 5 6 7 8 9 
6 5 4 3 2 

Process finished with exit code 0

10.38

【出题思路】

理解 5 种迭代器及它们的差异。

【解答】

  • 输入迭代器:只读,不写;单遍扫描,只能递增;还支持相等性判定运算(==!=)、解引用运算符(*)(只出现在赋值运算符右侧)和箭头运算符(->)。
  • 输出迭代器:只写,不读;单遍扫描,只能递增;支持解引用运算符(*)(只出现在赋值运算符左侧)。
  • 前向迭代器:可读、写;多遍扫描;只能递增;支持所有输入、输出迭代器的操作。
  • 双向迭代器:可读、写;多遍扫描;可递增、递减;支持所有向前迭代器操作。
  • 随机访问迭代器:可读、写;多遍扫描;支持全部迭代器运算,除了上述迭代器类别支持的操作外,还有:
    1. 比较两个迭代器相对位置的关系运算符(<<=>>=
    2. 迭代器和一个整数值的加减运算(++=--=)令迭代器在序列中前进或后退给定整数个元素
    3. 两个迭代器上的减法运算符(-)得到其距离
    4. 下标运算符

10.39

【出题思路】

理解常用容器的迭代器类型。

【解答】

list 上的迭代器是双向迭代器,vector 上的迭代器是随机访问迭代器。

注:vector 在内存中是连续存储的,所以才可以用随机访问迭代器。list 链表在内存中不是连续存储的。

10.40

【出题思路】

理解算法对迭代器类型的要求。

【解答】

  • copy 要求前两个参数至少是输入迭代器,表示一个输入范围。它读取这个范围中的元素,写入到第三个参数表示的输出序列中,因此第三个参数至少是输出迭代器。
  • reverse 要反向处理序列,因此它要求两个参数至少是双向迭代器。
  • unique 顺序扫描元素,覆盖重复元素,因此要求两个参数至少是前向迭代器。“至少” 意味着能力更强的迭代器是可接受的。

10.41

【出题思路】

理解标准库算法的命名规范。

【解答】

  1. 将范围 [beg, end) 值等于 old_val 的元素替换为 new_val。
  2. 将范围 [beg, end) 满足谓词 pred 的元素替换为 new_val。
  3. 将范围 [beg, end) 的元素拷贝到目的序列 dest 中,将其中值等于 old_val 的元素替换为 new_val。
  4. 将范围 [beg, end) 的元素拷贝到目的序列 dest 中,将其中满足谓词 pred 的元素替换为 new_val。

10.42

【出题思路】

练习链表的特殊操作。

【解答】

本题要使用链表专用的 sort 和 unique 算法,与泛型算法的不同点有如下两点:

  1. 它们是以链表类的成员函数形式实现的(而非 algorithm 库),因此使用方式是在链表对象上调用它们,也并不需要迭代器参数指出处理的序列。
  2. 由于是以成员函数形式实现的,是直接操作容器而非通过迭代器访问容器元素,因此这些算法具有修改容器的能力(添加、删除元素)。例如,unique 会调用 erase 直接真正删除重复元素,容器的大小会变小,而不是像泛型 unique 算法那样只是覆盖重复元素,并不改变容器大小。因此程序已不再需要调用 erase 了。

建议读者好好体会**通用算法(泛型算法)专用算法(特定容器算法)**之间的差异,包括上述使用方式上的差异,以及从库的开发者的角度思考两种方式的差异。

#include <iostream>
#include <list>

using std::string;
using std::list;
using std::cout;
using std::endl;

void output_words(list<string> &words) {
    // 打印容器中的内容,方便观察程序执行状态
    for (auto &s : words)
        std::cout << s << " ";
    cout << words.size() << endl;
}

void elimDups(std::list<std::string> &words) {
    output_words(words);

    // 按字典序排序 words,以便查找重复单词(相同的单词紧挨着)
    words.sort();
    output_words(words);

    // unique 调用 erase 删除同一个值的连续拷贝,使得每个单词只出现一次
    words.unique();
    output_words(words);

    // 打印单词,每个单词后面接一个空格。功能和函数 output_words 一样
    for_each(words.begin(), words.end(),
             [] (const string &s) {
                 cout << s << " ";
             }
    );
    cout << endl;
}

int main() {
    std::list<std::string> slist = {"the", "quick", "red", "fox", "jumps",
                                     "over", "the", "slow", "red", "turtle"};
    elimDups(slist);
    return 0;
}
// 运行结果
the quick red fox jumps over the slow red turtle 10
fox jumps over quick red red slow the the turtle 10
fox jumps over quick red slow the turtle 8
fox jumps over quick red slow the turtle 

Process finished with exit code 0
30

评论区