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

目 录CONTENT

文章目录

第 9 章

9.1

【出题思路】

学习数据结构很重要的一点是清晰地掌握数据结构之上各种操作的时间复杂度,并据此分析求解问题不同算法的优劣。本题即是熟悉几种常见容器的插入、删除等基本操作的时间复杂度,并练习据此选择容器求解实际问题。

【解答】

(a)“按字典序插入到容器中”意味着进行插入排序操作,从而需要在容器内部频繁进行插入操作,vector 在尾部之外的位置插入和删除元素很慢,deque 在头尾之外的位置插入和删除元素很慢,而 list 在任何位置插入、删除速度都很快。因此,这个任务选择 list 更为合适。当然,如果不是必须边读取单词边插入到容器中,可以使用 vector,将读入的单词依次追加到尾部,读取完毕后,调用标准库到排序算法将单词重排为字典序。

(b)由于需要在头、尾分别进行插入、删除操作,因此将 vector 排除在外,deque 和 list 都可以达到很好的性能。如果还需要频繁进行随机访问,则 deque 更好。

(c)由于整数占用空间很小,且给数字排序时需要频繁随机访问元素,将 list 排除在外。由于无须在头部进行插入、删除操作,因此使用 vector 即可,无须使用 deque。

9.2

【出题思路】

本题练习容器的定义

【解答】

list<deque<int>> a;

9.3

【出题思路】

准确理解怎样的迭代器可以构成迭代器范围。

【解答】

两个迭代器 begin 和 end 必须指向同一个容器中的元素,或者是容器最后一个元素之后的位置;而且,对 begin 反复进行递增操作,可保证到达 end,即 end 不在 begin 之前。

9.4

【出题思路】

本题练习遍历迭代器范围的方法。

【解答】

#include <iostream>
#include <vector>
using namespace std;

bool search_vec(vector<int>::iterator beg,
        vector<int>::iterator end, int val) {
    for ( ; beg != end; ++beg)
        if (*beg == val)
            return true;
    return false;
}

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

    cout << search_vec(ilist.begin(), ilist.end(), 3) << endl;
    cout << search_vec(ilist.begin(), ilist.end(), 8) << endl;

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

Process finished with exit code 0

9.5

【出题思路】

练习如何用迭代器表示搜索成功和搜索失败。

【解答】

#include <iostream>
#include <vector>
using namespace std;

vector<int>::iterator search_vec(vector<int>::iterator beg,
        vector<int>::iterator end, int val) {
    for ( ; beg != end; ++beg)      // 遍历范围
        if (*beg == val)            // 检查是否与给定值相等
            return beg;             // 搜索成功,返回元素对应迭代器
    return end;                     // 搜索失败,返回尾后迭代器
}

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

    cout << search_vec(ilist.begin(), ilist.end(), 3) - ilist.begin() << endl;
    cout << search_vec(ilist.begin(), ilist.end(), 8) - ilist.begin() << endl;

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

Process finished with exit code 0

9.6

【出题思路】

理解不同类型容器的迭代器之间的差别。更深层次的,理解数据结构的实现如何导致迭代器的差别。

【解答】

与 vector 和 deque 不同,list 的迭代器不支持 < 运算,只支持递增、递减、== 以及 != 运算。

原因在于这几种数据结构实现上的不同。vector 和 deque 将元素在内存中连续保存,而 list 则是将元素以链表方式存储,因此前者可以方便地实现迭代器的大小比较(类似指针的大小比较)来体现元素的前后关系。而在 list 中,两个指针的大小关系与它们指向的元素的前后关系并不一定是吻合的,实现 < 运算将会非常困难和低效。

9.7

【出题思路】

标准库容器定义了若干类型成员,对应容器使用中可能涉及的类型,如迭代器、元素引用等。本题和下一题帮助读者理解这些类型。

【解答】

使用迭代器类型 vector<int>::iterator 来索引 int 的 vector 中的元素。

9.8

【出题思路】

理解容器的类型成员。

【解答】

为了读取 string 的 list 中的元素,应使用 list<string>::value_type ,因为 value_type 表示元素类型。

为了写入数据,需要(非常量)引用类型,因此应使用 list<string>::reference

9.9

【出题思路】

begin 和 end 都有多个版本,分别返回普通、反向和 const 迭代器。本题帮助读者理解不同版本间的差异。

【解答】

cbegin 是 C++ 新标准引入的,用来与 auto 结合使用。它返回指向容器第一个元素的 const 迭代器,可以用来只读地访问容器元素,但不能对容器元素进行修改。因此,当不需要写访问时,应该使用 cbegin。

begin 则是被重载过的,有两个版本:其中一个是 const 成员函数,也返回 const 迭代器;另一个则返回普通迭代器,可以对容器元素进行修改。

9.10

【出题思路】

继续熟悉 begin 不同版本的差异和不同类型迭代器的差异,特别是 begin 与 auto 配合使用时的细微差异。

【解答】

v1 是 int 的 vector 类型,我们可以修改 v1 的内容,包括添加、删除元素及修改元素值等操作。

v2 是 int 的常量 vector 类型,其内容不能修改,添加、删除元素及修改元素值等均不允许。

begin 与 auto 结合使用时,会根据调用对象的类型来决定迭代器的类型,因此 it1 是普通迭代器,可对容器元素进行读写访问,而 it2 是 const 迭代器,不能对容器元素进行写访问。

而 cbegin 则不管调用对象是什么类型,始终返回 const 迭代器,因此 it3 和 it4 都是 const 迭代器。

9.11

【出题思路】

C++11 提供了丰富的容器初始化方式,理解各种初始化方式之间的不同,有助于我们在求解实际问题时选择恰当的方式。

【解答】

1:

vector<int> ilist1;			// 默认初始化

默认初始化,vector 为空 —— size 返回 0,表明容器中尚未有元素;capacity 返回 0,意味着尚未分配存储空间。这种初始化方式适合于元素个数和值未知,需要在程序运行中动态添加的情况。

2:

vector<int> ilist2(ilist);			// ilist2 初始化为 ilist 的拷贝
vector<int> ilist2 = ilist;			// 等价方式

ilist2 初始化为 ilist 的拷贝,ilist 必须与 ilist2 类型相同,即也是 int 的 vector 类型,ilist2 将具有与 ilist 相同的容量和元素。

3:

vector<int> ilist = {1, 2, 3.0, 4, 5, 6, 7};
vector<int> ilist{1, 2, 3.0, 4, 5, 6, 7};				// 等价方式

ilist 初始化为列表中元素的拷贝,列表中元素类型必须与 ilist 的元素类型相容,在本例中必须是与整型相容的数值类型。对于整型,会直接拷贝其值,对于其他类型则需进行类型转换(如 3.0 转换为 3)。这种初始化方式适合元素数量和值预先可知的情况。

4:

vector<int> ilist3(ilist.begin() + 2, ilist.end() - 1);

ilist3 初始化为两个迭代器指定范围中的元素的拷贝,范围中的元素类型必须与 ilist3 的元素类型相容,在本例中 ilist3 被初始化 {3, 4, 5, 6}

注意,由于只要求范围中元素类型与待初始化的容器的元素类型相容,因此,迭代器来自于不同类型的容器是可能的,例如,用一个 double 的 list 的范围来初始化 ilist3 是可行的。另外,由于构造函数只是读取范围中的元素并进行拷贝,因此使用普通迭代器还是 const 迭代器来指出范围并无区别。这种初始化方法特别适合于获取一个序列的子序列。

5:

vector<int> ilist4(7);

默认值初始化,ilist4 中将包含 7 个元素,每个元素进行缺省的值初始化,对于 int,也就是被赋值为 0,因此 ilist4 被初始化为包含 7 个 0。当程序运行初期元素大致数量可预知,而元素的值需动态获取时,可采用这种初始化方式。

6:

vector<int> ilist5(7, 3);

指定值初始化,ilist5 被初始化为包含 7 个值为 3 的 int。

9.12

【出题思路】

继续熟悉不同的初始化方式。

【解答】

接受一个已有容器的构造函数会拷贝此容器中的所有元素,这样,初始化完成后,我们得到此容器的一个一模一样的拷贝。当我们确实需要一个容器的完整拷贝时这种初始化方式非常方便。

但当我们不需要已有容器中的全部元素,而只是想拷贝其中一部分元素时,可使用接受两个迭代器的构造函数。传递给它要拷贝的范围的起始和尾后位置的迭代器,即可令新容器对象包含所需范围中元素的拷贝。

9.13

【出题思路】

更深入地理解容器拷贝初始化和范围初始化这两种方式的差异。

为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。

当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型元素类型都必须相同。

【解答】

由于 list<int>vector<double> 是不同的容器类型,因此无法采用容器拷贝初始化方式。但前者的元素类型是 int,与后者的元素类型 double 是相容的,因此可以采用范围初始化方式来构造一个 vector<double> ,令它的元素值与 list<int> 完全相同。对 vector<int> 也是这样的思路。

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

using namespace std;

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

    // 容器类型不同,不能使用拷贝初始化
    // vector<double> ivec(ilist);
    // 元素类型相容,因此可采用范围初始化
    vector<double> dvec(ilist.begin(), ilist.end());
    cout << dvec.capacity() << " " << dvec.size() << " "
         << dvec[0] << " " << dvec[dvec.size() - 1] << endl;

    // 容器类型相同,容器的元素不同,不能使用拷贝初始化
    // vector<double> ivec1(ivec);
    // 元素类型相容,因此可采用范围初始化
    vector<double> dvec1(ivec.begin(), ivec.end());
    cout << dvec1.capacity() << " " << dvec1.size() << " "
         << dvec1[0] << " " << dvec1[dvec1.size() - 1] << endl;
  
    return 0;
}
// 运行结果
7 7 1 7
7 7 7 1

Process finished with exit code 0

size vs capacity of a vector?

9.14

【出题思路】

容器有多种赋值操作,本题帮助读者理解不同赋值方式的差异。

【解答】

由于 list<char *>vector<string> 是不同类型的容器,因此无法采用赋值运算符 = 来进行元素赋值。但 char * 可以转换为 string ,因此可以采用范围赋值方式来实现本题要求。

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

using namespace std;

int main() {
    list<char *> slist = {"hello", "world", "!"};
    vector<string> svec;

    // 容器类型不同,不能直接赋值
    // svec = slist;
    // 元素类型相容,可以采用范围赋值
    svec.assign(slist.begin(), slist.end());
    cout << svec.capacity() << " " << svec.size() << " "
         << svec[0] << " " << svec[svec.size() - 1] << endl;

    return 0;
}
3 3 hello !

Process finished with exit code 0

9.15

【出题思路】

练习容器的关系运算符的使用。

【解答】

标准库容器支持关系运算符,比较两个 vector 是否相等使用 == 运算符即可。当两个 vector 包含相同个数的元素(两容器各调用 size() 成员函数返回值相等),且对位元素都相等时,判定两个 vector 相等,否则不等。两个 vector 的 capacity 不会影响相等性的判定,因此,当下面程序中,ivec1 在添加、删除元素导致扩容后,仍然与 ivec 相等。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> ivec = {1, 2, 3, 4, 5, 6, 7};
    vector<int> ivec1 = {1, 2, 3, 4, 5, 6, 7};
    vector<int> ivec2 = {1, 2, 3, 4, 5};
    vector<int> ivec3 = {1, 2, 3, 4, 5, 6, 8};
    vector<int> ivec4 = {1, 2, 3, 4, 5, 7, 6};

    cout << (ivec == ivec1) << endl;                            // 1
    cout << (ivec == ivec2) << endl;                            // 0
    cout << (ivec == ivec3) << endl;                            // 0
    cout << (ivec == ivec4) << endl;                            // 0

    cout << ivec1.capacity() << " " << ivec1.size() << endl;    // 7 7
    ivec1.push_back(8);
    ivec1.pop_back();
    cout << ivec1.capacity() << " " << ivec1.size() << endl;    // 14 7
    cout << (ivec == ivec1) << endl;                            // 1

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

Process finished with exit code 0

9.16

【出题思路】

首先,理解容器关系运算符的限制 —— 关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。即,我们只能将一个 vector<int> 与另一个 vector<int> 进行比较,而不能将一个 vector<int> 与一个 list<int> 或一个 vector<double> 进行比较。其次,练习自己编写程序来实现容器内容的比较。

【解答】

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

using namespace std;

bool l_v_equal(vector<int> &ivec, list<int> &ilist) {
    // 比较 list 和 vector 元素个数
    if (ilist.size() != ivec.size())
        return false;

    // auto lb = ilist.cbegin(); // 也可
    list<int>::const_iterator lb = ilist.cbegin();      // list 首元素
    list<int>::const_iterator le = ilist.cend();        // list 尾后位置
    vector<int>::const_iterator vb = ivec.cbegin();     // vector 首元素
    for ( ; lb != le; ++lb, ++vb)
        if (*lb != *vb)
            return false;
    return true;
}

int main() {
    vector<int> ivec = {1, 2, 3 , 4, 5, 6, 7};
    list<int> ilist = {1, 2, 3 , 4, 5, 6, 7};
    list<int> ilist1 = {1, 2, 3 , 4, 5};
    list<int> ilist2 = {1, 2, 3 , 4, 5, 6, 8};
    list<int> ilist3 = {1, 2, 3 , 4, 5, 7, 6};

    cout << l_v_equal(ivec, ilist) << endl;
    cout << l_v_equal(ivec, ilist1) << endl;
    cout << l_v_equal(ivec, ilist2) << endl;
    cout << l_v_equal(ivec, ilist3) << endl;

    return 0;
}
// 运行结果
// 用时 195 ms
// ivec 与 ilist 只是两容器对应的元素相等,两容器并不相等
1
0
0
0

Process finished with exit code 0

【其它解题思路】

首先利用范围初始化(迭代器初始化)创建一个与 list<int> 内容相同(容器的元素相同)的 vector<int> ,然后用 == 运算符比较此 vector 和给定的 vector。与上一种方法相比,这种方法的优点是直接利用标准库的功能实现,比较简单,且不必为不同容器类型实现不同的版本,但创建容器的临时拷贝会占用额外的内存,且需要额外时间拷贝数据。

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

using namespace std;

bool l_v_equal(vector<int> &ivec, list<int> &ilist) {
    // auto lb = ilist.cbegin(); // 也可
    list<int>::const_iterator lb = ilist.cbegin();      // list 首元素
    list<int>::const_iterator le = ilist.cend();        // list 尾后位置
    vector<int> ivec1(lb, le);      // 将 list<int> 元素拷贝到 vector<int> 中

    return ivec == ivec1;
}

int main() {
    vector<int> ivec = {1, 2, 3 , 4, 5, 6, 7};
    list<int> ilist = {1, 2, 3 , 4, 5, 6, 7};
    list<int> ilist1 = {1, 2, 3 , 4, 5};
    list<int> ilist2 = {1, 2, 3 , 4, 5, 6, 8};
    list<int> ilist3 = {1, 2, 3 , 4, 5, 7, 6};

    cout << l_v_equal(ivec, ilist) << endl;
    cout << l_v_equal(ivec, ilist1) << endl;
    cout << l_v_equal(ivec, ilist2) << endl;
    cout << l_v_equal(ivec, ilist3) << endl;

    return 0;
}
// 运行结果
// 用时 659 ms
// ivec 与 ilist 只是两容器对应的元素相等,两容器并不相等
1
0
0
0

Process finished with exit code 0

9.17

【出题思路】

理解容器关系运算符对容器类型和元素类型的限制。

【解答】

首先,容器类型必须相同,元素类型也必须相同。

其次,元素类型必须支持 < 运算符。

9.18

【出题思路】

本题练习向容器中添加元素,继续练习遍历容器中的元素。重点:不同容器在不同位置添加元素的性能是有差异的。

【解答】

对 deque 来说,在首尾位置添加新元素性能最佳,在中间位置插入新元素性能会很差。对遍历操作,可高效完成。

#include <iostream>
#include <deque>

using namespace std;

int main() {
    deque<string> sd;       // string 的 deque

    string word;
    while (cin >> word && word != "Q")     // 读取字符串,直至遇到文件结束符
        sd.push_back(word);

    // 用 cbegin() 获取 deque 首元素迭代器,遍历 deque 中所有元素
    for (deque<string>::const_iterator db = sd.cbegin(); db != sd.cend(); ++db)
        cout << *db << endl;

    return 0;
}
// hello world
//  !
// Q
// 上边三行是从控制台输入的测试数据
// 字符串 "Q" 用于结束输入
// 运行结果
hello world
 !
Q
hello
world
!

Process finished with exit code 0

【其他解题思路】

由于在 deque 的首尾位置添加新元素性能很好,因此可以用 push_front 替换 push_back,性能不变,但元素在 deque 中的顺序将与输入顺序相反。若需输出与输入保持相同顺序,应使用 push_back。

#include <iostream>
#include <deque>

using namespace std;

int main() {
    deque<string> sd;       // string 的 deque

    string word;
    while (cin >> word && word != "Q")     // 读取字符串,直至遇到文件结束符
        sd.push_front(word);

    // 用 cbegin() 获取 deque 首元素迭代器,遍历 deque 中所有元素
    for (deque<string>::const_iterator db = sd.cbegin(); db != sd.cend(); ++db)
        cout << *db << endl;

    return 0;
}
// hello world
//  !
// Q
// 上边三行是从控制台输入的测试数据
// 字符串 "Q" 用于结束输入
// 运行结果
hello world
 !
Q
!
world
hello

Process finished with exit code 0

9.19

【出题思路】

练习不同容器的添加操作的异同。

【解答】

对 list 来说,在任何位置添加新元素都有很好的性能,每次遍历操作需要从头开始挨个找。程序的书写与上一题并无太大差异。

#include <iostream>
#include <list>

using namespace std;

int main() {
    list<string> sl;       // string 的 list

    string word;
    while (cin >> word && word != "Q")     // 读取字符串,直至遇到文件结束符
        sl.push_back(word);

    // 用 cbegin() 获取 list 首元素迭代器,遍历 list 中所有元素
    for (list<string>::const_iterator lb = sl.cbegin(); lb != sl.cend(); ++lb)
        cout << *lb << endl;

    return 0;
}
// hello world
//  !
// Q
// 上边三行是从控制台输入的测试数据
// 字符串 "Q" 用于结束输入
// 运行结果
hello world
 !
Q
hello
world
!

Process finished with exit code 0

【其他解题思路】

与上一题一样,可以用 push_front 替换 push_back。甚至可以用 insert 等更复杂的操作,可获得相同的性能,但对本题的简单要求来说,并无必要。

9.20

【出题思路】

这是一个很简单的数据处理问题的练习。读者可练习多个容器间数据的处理、拷贝。

【解答】

通过遍历 list<int> ,可检查其中每个元素的奇偶性,并用 push_back 分别添加到目的 deque 的末尾。程序中用位与运算检查元素最低位的值,若为 1,表明是奇数,否则即为偶数。

#include <iostream>
#include <list>
#include <deque>

using namespace std;

int main() {
    list<int> ilist = {1, 2, 3, 4, 5, 6, 7, 8};     // 初始化 int 的 list
    deque<int> odd_d, even_d;

    // 遍历整数 list
    for (auto lb = ilist.cbegin(); lb != ilist.cend(); ++lb)
        if (*lb & 1)        // 查看最低位,1:奇数;0:偶数
            odd_d.push_back(*lb);
        else
            even_d.push_back(*lb);

    cout << "奇数值有:";
    for (auto db = odd_d.cbegin(); db != odd_d.cend(); ++db)
        cout << *db << " ";
    cout << endl;

    cout << "偶数值有:";
    for (auto db = even_d.cbegin(); db != even_d.cend(); ++db)
        cout << *db << " ";
    cout << endl;

    return 0;
}
// 运行结果
奇数值有:1 3 5 7 
偶数值有:2 4 6 8 

Process finished with exit code 0

【其他解题思路】

出于简单和保持顺序考虑,不必用其他操作代替 push_back 。对于奇偶性的判定,可用模 2 运算 %2 代替位与运算,两者是等价的。

9.21

【出题思路】

本题练习用 insert 向容器中添加元素的方法。理解这是最通用的方法,可以实现 push_back 和 push_front 这些特殊插入操作的效果。

【解答】

在循环之前,vector 为空,此时将 iter 初始化为 vector 首位置,与初始化为尾后位置效果是一样的。循环中第一次调用 insert 会将读取的第一个 string 插入到 iter 指向位置之前的位置,即,令新元素成为 vector 的首元素。而 insert 的返回指向此元素的迭代器,我们将它赋予 iter,从而使得 iter 始终指向 vector 的首元素。接下来的每个循环步均是如此,将新 string 插入到 vector 首元素之前的位置,成为新的首元素,并使 iter 始终指向 vector 首。这样,string 在 vector 排列的顺序将与它们的输入顺序恰好相反。整个循环执行的过程和最后的结果都与 list 版本没有区别。但要注意,在 list 首元素之前插入新元素性能很好,但对于 vector ,这样的操作需要移动所有现有元素,导致性能很差。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<string> svec;        // string 的 vector

    string word;
    // vector<string>::iterator iter = svec.begin();
    auto iter = svec.begin();   // 获取 vector 首位置迭代器
    while (cin >> word && word != "Q")
        iter = svec.insert(iter, word);

    // 用 cbegin() 获取 vector 首元素迭代器,遍历 vector 中所有元素
    for (auto iter = svec.cbegin(); iter != svec.cend(); ++iter)
        cout << *iter << endl;

    return 0;
}
// hello world !!Q Q
// 该(第一行)行是从控制台输入的测试数据
// 字符串 "Q" 用于结束输入
// 运行结果
hello world !!Q Q
!!Q
world
hello

Process finished with exit code 0

9.22

【出题思路】

首先,理解容器插入操作的副作用 —— 向一个 vector、string 或 deque 插入元素会使现有指向容器的迭代器、引用和指针失效。其次,练习如何利用 insert 返回的迭代器,使得在向容器插入元素后,仍能正确在容器中进行遍历。

【解答】

循环中未对 iter 进行递增操作,iter 无法向中点推进。其次,即使加入了 iter++ 语句,由于向 iv 插入元素后,iter 已经失效,iter++ 也不能起到将迭代器向前推进一个元素的作用。修改方法如下:

首先,将 insert 返回的迭代器赋予 iter,这样,iter 将指向新插入的元素 y。我们知道,insert 将 y 插入到 iter 原来指向的元素 x 之前的位置,因此,接下来我们需要进行两次 iter++ 才能将 iter 推进到 x 之后的位置。

其次,insert() 也会使 mid 失效,因此,只正确设置 iter 仍不能令循环在正确的时候结束,我们还需要设置 mid 使之指向 iv 原来的中央位置的元素。在未插入任何新元素之前,此位置是 iv.begin() + iv.size() / 2 ,我们将此时的 iv.size() 的值记录在变量 org_size 中。然后在循环过程中统计新插入的元素的个数 new_ele ,则在任何时候,iv.begin() + org_size / 2 + new_ele 都能正确指向 iv 原来的中央位置的元素。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> iv = {1, 1, 2, 1};          // int 的 vector
    int some_val = 1;

    vector<int>::iterator iter = iv.begin();
    int org_size = iv.size(), new_ele = 0;  // 原大小和新元素个数

    // 每个循环步都重新计算 mid,保证正确指向 iv 原中央位置的元素
    while (iter != (iv.begin() + org_size / 2 + new_ele))
        if (*iter == some_val) {
            iter = iv.insert(iter, 2 * some_val);   // iter 指向新元素
            ++new_ele;
            iter += 2;                // 将 iter 推进到旧元素的下一个元素
        } else {
            ++iter;                         // 指向后推进一个位置
        }

    // 用 begin() 获取 vector 首元素迭代器,遍历 vector 中的所有元素
    for (iter = iv.begin(); iter != iv.end(); ++iter)
        cout << *iter << endl;

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

Process finished with exit code 0

【其他解题思路】

由于程序的意图是检查 iv 原来的前一半元素,也就是说,循环次数是预先可知的。因此,我们可以通过检测循环变量来控制循环执行次数,这要比比较 ”当前“ 迭代器和 “中央迭代器” 的方式简单一些:

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> iv = {1, 1, 1, 1, 1};          // int 的 vector
    int some_val = 1;

    vector<int>::iterator iter = iv.begin();
    int org_size = iv.size(), i = 0;  // 原大小

    // 用循环变量控制循环次数
    while (i <= org_size / 2) {
        if (*iter == some_val) {
            iter = iv.insert(iter, 2 * some_val);   // iter 指向新元素
            iter += 2;                // 将 iter 推进到旧元素的下一个元素
        } else {
            ++iter;                         // 指向后推进一个位置
        }
        ++i;
    }

    // 用 begin() 获取 vector 首元素迭代器,遍历 vector 中的所有元素
    for (iter = iv.begin(); iter != iv.end(); ++iter)
        cout << *iter << " ";
    cout << endl;

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

Process finished with exit code 0

注意:非连续存储的 list 和 forward_list 不支持迭代器加减元素(不支持 iter += 2 这样的代码),应多次调用 ++ 来实现与迭代器加法相同的效果( ++iter; ++iter; )。相关练习请参考 9.31

9.23

【出题思路】

理解获取容器首、尾元素的不同方法。

【解答】

4 个变量的值会一样,都等于容器中唯一一个元素的值。

9.24

【出题思路】

练习获取容器首元素的不同方法,以及如何安全访问容器元素。

【解答】

下面的程序会异常终止。因为 vector 为空,此时用 at 访问容器的第一个元素会抛出一个 out_of_range 异常,而此程序未捕获异常,因此程序会因异常退出。正确的编程方式是,捕获可能的 out_of_range 异常,进行相应的处理。

但对于后 3 种获取容器首元素的方法,当容器为空时,不会抛出 out_of_range 异常,而是导致程序直接退出(注释掉前几条语句即可看到后面语句的执行效果)。因此,正确的编程方式是,在采用这几种获取容器的方法时,检查下标的合法性(对 front 和 begin 只需检查容器是否为空),确定没有问题后再获取元素。当然这种方法对 at 也适用。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> iv;     // int 的 vector

    // libc++abi.dylib: terminating with uncaught exception of 
    // type std::out_of_range: vector
    cout << iv.at(0) << endl;
    cout << iv[0] << endl;              // 不报任何异常
    cout << iv.front() << endl;         // 不报任何异常
    cout << *(iv.begin()) << endl;      // 不报任何异常

    return 0;
}

9.25

【出题思路】

理解范围删除操作的两个迭代器参数如何决定删除操作的结果。

【解答】

如果两个迭代器 elem1 和 elem2 相等,则什么也不会发生,容器保持不变。哪怕两个迭代器是指向尾后位置(例如 end() + 1),也是如此,程序也不会出错。

因此 elem1 和 elem2 都是尾后迭代器时,容器保持不变。

如果 elem2 为尾后迭代器,elem1 指向之前的合法位置,则会删除从 elem1 开始直至容器末尾的所有元素。

9.26

【出题思路】

练习删除指定位置元素的操作,理解操作对迭代器的影响。

【解答】

当从 vector 中删除元素时,会导致删除点之后位置的迭代器、引用和指针失效。而 erase 返回的迭代器指向删除元素之后的位置。因此,将 erase 返回的迭代器赋予 iiv,使其正确向前推进。且尾后位置每个循环步中都用 end 重新获得,保证其有效。

对于 list,删除操作并不会令迭代器失效,但上述方法仍然是适用的。

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

using namespace std;

int main() {
    int ia[] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89};
    vector<int> iv;
    list<int> il;

    iv.assign(ia, ia + sizeof(ia) / sizeof(ia[0]));     // 将数据拷贝到 vector
    il.assign(ia, ia + sizeof(ia) / sizeof(ia[0]));     // 将数据拷贝到 list

    vector<int>::iterator iiv = iv.begin();
    while (iiv != iv.end())
        if (!(*iiv & 1))                // 偶数
            iiv = iv.erase(iiv);        // 删除偶数,返回下一位置迭代器
        else
            ++iiv;                      // 推进到下一位置

    list<int>::iterator iil = il.begin();
    while (iil != il.end())
        if (*iil & 1)                   // 奇数
            iil = il.erase(iil);        // 删除奇数,返回下一位置迭代器
        else
            ++iil;                      // 推进到下一位置

    for (iiv = iv.begin(); iiv != iv.end(); ++iiv)
        cout << *iiv << " ";
    cout << endl;
    for (iil = il.begin(); iil != il.end(); ++iil)
        cout << *iil << " ";
    cout << endl;

    return 0;
}
// 运行结果
1 1 3 5 13 21 55 89 
0 2 8 

Process finished with exit code 0

9.27

【出题思路】

练习 forward_list 特殊的删除操作。

【解答】

关键点是理解 forward_list 其实是单向链表数据结构,只有前驱节点指向后继节点的指针,而没有反向的指针。因此,在 forward_list 中可以高效地从前驱转到后继,但无法从后继转到前驱。而当我们删除一个元素后,应该调整被删元素的前驱指针指向被删元素的后继,起到将该元素从链表中删除的效果。因此,在 forward_list 中插入、删除元素既需要该元素的迭代器,也需要其前驱迭代器。为此,forward_list 提供了 before_begin 来获取首元素之前位置的迭代器,且插入、删除都是 _after 形式,即,删除(插入)给定迭代器的后继。

#include <iostream>
#include <forward_list>

using namespace std;

int main() {
    forward_list<int> iflst = {1, 2, 3, 4, 5, 6, 7, 8};

    auto prev = iflst.before_begin();       // 前驱元素
    auto curr = iflst.begin();              // 当前元素

    while (curr != iflst.end())
        if (*curr & 1)                      // 奇数
            curr = iflst.erase_after(prev); // 删除 curr 指向的元素,并移动 curr
        else {
            prev = curr;                    // 前驱和当前迭代器都向前推进
            ++curr;
        }

    for (curr = iflst.begin(); curr != iflst.end(); ++curr)
        cout << *curr << " ";
    cout << endl;

    return 0;
}
// 运行结果
2 4 6 8 

Process finished with exit code 0

9.28

【出题思路】

练习 forward_list 特殊的添加操作。

【解答】

与删除相同的是,forward_list 的插入操作也是在给定元素之后。不同的是,插入一个新元素后,只需将其(新元素)后继修改为给定元素的后继,然后修改给定元素的后继为新元素即可,不需要前驱迭代器参与。但对于本题,当第一个 string 不在链表中时,要将第二个 string 插入到链表末尾。因此仍然需要维护前驱迭代器,当遍历完链表时,“前驱” 指向尾元素,“当前” 指向尾后位置。若第一个 string 不在链表中,此时只需将第二个 string 插入到 “前驱” 之后即可。

总体来说,单向链表由于其数据结构上的局限,为实现正确插入、删除操作带来了困难。标准库的 forward_list 容器为我们提供了一些特性,虽然(与其他容器相比)我们仍需维护一些额外的迭代器,但已经比直接用指针来实现链表的插入、删除方便了许多。

#include <iostream>
#include <forward_list>

using namespace std;

void test_and_insert(forward_list<string> &sflst, const string &s1, const string &s2) {
    auto prev = sflst.before_begin();               // 前驱元素
    auto curr = sflst.begin();                      // 当前元素
    bool inserted = false;

    while (curr != sflst.end()) {
        if (*curr == s1) {                          // 找到给定字符串
            curr = sflst.insert_after(curr, s2);    // 插入新字符串,curr 指向它
            inserted = true;
            return;
        } else {
            prev = curr;                            // 前驱和当前迭代器都向前推进
            ++curr;
        }
    }
    if (!inserted)
        sflst.insert_after(prev, s2);               // 未找到给定字符串,插入尾后
}

int main() {
    forward_list<string> sflst = {"Hello", "!", "world", "!"};

    test_and_insert(sflst, "Hello", "你好");
    for (auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
        cout << *curr << " ";
    cout << endl;

    test_and_insert(sflst, "!", "?");
    for (auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
        cout << *curr << " ";
    cout << endl;

    test_and_insert(sflst, "Bye", "再见");
    for (auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
        cout << *curr << " ";
    cout << endl;

    return 0;
}
// 运行结果
Hello 你好 ! world ! 
Hello 你好 ! ? world ! ? 
Hello 你好 ! ? world ! ? 再见 

Process finished with exit code 0

9.29

【出题思路】

本题练习改变容器大小的操作。

【解答】

调用 vec.resize(100) 会向 vec 末尾添加 75 个元素,这些元素将进行值初始化。

接下来调用 vec.resize(10) 会将 vec 末尾的 90 个元素删除。

9.30

【出题思路】

更深入理解改变容器大小的操作。

【解答】

对于元素类型是类类型,则单参数 resize 版本要求该类型必须提供一个默认构造函数。

9.31

【出题思路】

本题继续练习 list 和 forward_list 的插入、删除操作,理解与其他容器的不同,理解对迭代器的影响。

【解答】

list 和 forward_list 与其他容器的一个不同是,迭代器不支持加减运算,究其原因,链表中元素并非在内存中连续存储,因此无法通过地址的加减在元素间远距离移动。因此,应多次调用 ++ 来实现与迭代器加法相同的效果。

#include <iostream>
#include <list>

using namespace std;

int main() {
    list<int> ilst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto curr = ilst.begin();                   // 首节点

    while (curr != ilst.end()) {
        if (*curr & 1) {                        // 奇数
            curr = ilst.insert(curr, *curr);    // 插入到当前元素之前
            ++curr; ++curr;                     // 移动到原容器下一元素
        } else {                                // 偶数
            curr = ilst.erase(curr);            // 删除,指向下一元素
        }
    }

    for (curr = ilst.begin(); curr != ilst.end(); ++curr)
        cout << *curr << " ";
    cout << endl;

    return 0;
}
// 运行结果
1 1 3 3 5 5 7 7 9 9 

Process finished with exit code 0

对于 forward_list,由于是单向链表结构,删除元素时,需将前驱指针调整为指向下一个节点,因此需维护 “前驱”、“后继” 两个迭代器。

#include <iostream>
#include <forward_list>

using namespace std;

int main() {
    forward_list<int> iflst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto prev = iflst.before_begin();               // 前驱节点
    auto curr = iflst.begin();                      // 首节点

    while (curr != iflst.end()) {
        if (*curr & 1) {                            // 奇数
            curr = iflst.insert_after(curr, *curr); // 插入到当前元素之后
            prev = curr;                            // prev 移动到新插入元素
            ++curr;                                 // 移动到原容器下一元素
        } else {                                    // 偶数
            curr = iflst.erase_after(prev);         // 删除,指向下一元素
        }
    }

    for (curr = iflst.begin(); curr != iflst.end(); ++curr)
        cout << *curr << " ";
    cout << endl;

    return 0;
}
// 运行结果
1 1 3 3 5 5 7 7 9 9 

Process finished with exit code 0

9.32

【出题思路】

本题复习实参与形参的关系,进一步熟悉迭代器的处理对容器操作的关键作用。

C++ 运算符优先级

【解答】

很多编译器对实参求值、向形参传递的处理顺序是由右至左。这意味着,编译器在编译上述代码时,首先对 *iter++ 求值,传递给 insert 第二个形参,此时 iter 已指向当前奇数的下一个元素,因此传递给 insert 的第一个参数的迭代器指向的是错误的位置,程序执行会发生混乱,最终崩溃。

因此,若将代码改为 iter = vi.insert(iter++, *iter); ,或是使用由左至右求值、传递参数的编译器,代码的运行结果是正确的。当然,这样的代码在逻辑上是毫无道理的。

9.33

【出题思路】

进一步理解容器插入、删除操作会使迭代器失效。

【解答】

向 vector 中插入新元素后,原有迭代器都会失效。因此,不将 insert() 返回的迭代器赋予 begin,会使 begin 失效。继续使用 begin 会导致程序崩溃。对此程序,保存尾后迭代器和不向 begin 赋值两个错误存在其一,程序都会崩溃。

不向 begin 赋值:

#include <iostream>
#include <vector>

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

int main() {
    vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
  
    auto begin = v.begin();
    while (begin != v.end()) {
        ++begin;
        /*begin = */ v.insert(begin, 42);
        ++begin;
    }

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

    return 0;
}
// 运行结果
test(8514,0x7fff9f0f0380) malloc: *** error for object 0x7fbf46c02818: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug

Process finished with exit code 6

保存尾后迭代器

#include <iostream>
#include <vector>

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

int main() {
    vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    // 灾难:此循环的行为是未定义的
    // 保存尾后迭代器的值是一个坏主意
    auto begin = v.begin(), end = v.end();
    while (begin != end) {
        ++begin;
        begin = v.insert(begin, 42);
        ++begin;
    }

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

    return 0;
}
// 运行结果

Process finished with exit code 11

正确的程序如下所示:

#include <iostream>
#include <vector>

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

int main() {
    vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    auto begin = v.begin();
    while (begin != v.end()) {
        ++begin;
        begin = v.insert(begin, 42);
        ++begin;
    }

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

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

9.34

【出题思路】

继续熟悉容器插入、删除操作与迭代器的关系,以及编程中容易出现的错误。

【解答】

此段代码的第一个错误是忘记使用花括号,使得 ++iter 变成循环结束后的第一条语句,而非所期望的循环体的最后一条语句。因此,除非容器为空,否则程序会陷入死循环:

  1. 若容器的第一个元素是偶数,布尔表示式为假,if 语句真分支不会被执行,iter 保持不变(因为 iter 不在循环作用域内)。循环继续执行,真分支仍然不会执行,iter 继续保持不变,如此陷入死循环。

  2. 若容器的第一个元素是奇数,insert 语句被调用,将该值插入到首元素之前,并将返回的迭代器(指向新插入元素)赋予 iter,因此 iter 指向新首元素。继续执行循环,会继续将首元素复制到容器首位置,并令 iter 指向它,如此陷入死循环。

示例(粗体代表迭代器位置):

初始:{1, 2, 3, 4, 5, 6, 7, 8, 9}

第一步:{1, 1, 2, 3, 4, 5, 6, 7, 8, 9}

第二步:{1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9}

… …

下面的程序可展示程序执行效果。其中,我们在循环体最后加入了一个 for 循环,打印容器中所有元素,即可观察程序执行效果。这是一种简单的程序调试方法。cout >> tmp; 是为了让程序暂停,程序员有时间观察输出,需要继续执行程序时,随意输入一个字符串即可。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> vi = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto iter = vi.begin();
    string tmp;
    while (iter != vi.end()) {
        if (*iter % 2)
            iter = vi.insert(iter, *iter);
        for (auto vb = vi.begin(); vb != vi.end(); ++vb)
            cout << *vb << " ";
        cout << endl;
        cin >> tmp;
    }
    ++iter;

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

当我们将 ++iter 放入循环体后,程序仍然是错误的,除非容器为空或仅包含偶数,否则程序仍然会陷入死循环。原因是,当遍历到奇数时,执行 insert 将该值插入到旧元素之前,将返回指向新元素的迭代器赋予 iter,再递增 iter,此时 iter 将指向旧元素。继续执行循环仍会重复这几个步骤,程序陷入死循环。

示例(粗体代表迭代器位置):

初始:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

第一步:{0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9}

第二步:{0, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9}

… …

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto iter = vi.begin();
    string tmp;
    while (iter != vi.end()) {
        if (*iter % 2)
            iter = vi.insert(iter, *iter);
        ++iter;
        for (auto vb = vi.begin(); vb != vi.end(); ++vb)
            cout << *vb << " ";
        cout << endl;
        cin >> tmp;
    }

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

正确的程序应该是将 ++iter 移入循环体,再增加一个 ++iter ,令 iter 指向奇数之后的元素。
该程序的目的是仅复制奇数元素。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto iter = vi.begin();
    string tmp;
    while (iter != vi.end()) {
        if (*iter % 2) {
            iter = vi.insert(iter, *iter);
            // ++iter; ++iter;
            iter += 2;
        } else ++iter;
    }

    for (auto vb = vi.begin(); vb != vi.end(); ++vb)
        cout << *vb << " ";
    cout << endl;

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

Process finished with exit code 0

9.35

【解答】

capacity 返回已经为 vector 分配了多大内存空间(单位是元素大小),也就是在不分配新空间的情况下,容器可以保存多少个元素。而 size 则返回容器当前已经保存了多少个元素。

9.36

【解答】

由上一题解答可知,这是不可能的。

9.37

【出题思路】

理解 list 和 array 与 vector 在数据结构上的差异导致内存分配方式的不同。

【解答】

list 是链表,当有新元素加入时,会从内存空间中分配一个新节点保存它;当从链表中删除元素时,该节点占用的内存空间会被立即释放。因此,一个链表占用的内存空间总是与它当前保存的的元素所需的空间相等。

而 array 是固定大小数组,内存一次性分配,大小不变,不会变化。

因此,它们均不需要 capacity。

9.38

【解答】

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> ivec;
    // size 应该为 0;capacity 的值依赖于具体实现
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 0 capacity: 0

    // 向 ivec 添加 24 个元素
    for (vector<int>::size_type ix = 0; ix != 24; ++ix)
        ivec.push_back(ix);
    // size 应该为 24;capacity 应该大于等于 24,具体值依赖于标准库实现
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 24 capacity: 32

    ivec.reserve(50);       // 将 capacity 至少设定为 50,可能会更大
    // size 应该为 24;capacity 应该大于等于 50,具体值依赖于标准库实现
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 24 capacity: 50

    // 添加元素用光多余容量
    while (ivec.size() != ivec.capacity())
        ivec.push_back(0);
    // capacity 应该未改变,size 和 capacity 相等
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 50 capacity: 50

    ivec.push_back(42);     // 在添加一个元素
    // size 应该为 51;capacity 应该大于等于 51,具体值依赖于标准库实现
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 51 capacity: 100

    ivec.shrink_to_fit();   // 要求归还内存
    // size 应该未改变(51);capacity 的值依赖于具体实现
    cout << "ivec: size: " << ivec.size()
         << " capacity: " << ivec.capacity() << endl;
    // ivec: size: 51 capacity: 51

    return 0;
}
// 运行结果
ivec: size: 0 capacity: 0
ivec: size: 24 capacity: 32
ivec: size: 24 capacity: 50
ivec: size: 50 capacity: 50
ivec: size: 51 capacity: 100
ivec: size: 51 capacity: 51

Process finished with exit code 0

注:在我自己的编译器上测试结果如上。vector 空间是成倍增长的。

9.39

【出题思路】

继续熟悉 vector 空间分配。

【解答】

首先,reserve 为 svec 分配了 1024 个元素(字符串)的空间。

随后,循环会不断读入字符串,添加到 svec 末尾,直至遇到文件结束符。这个过程中,如果读入的字符串数量不多于 1024,则 svec 的容量(capacity)保持不变,不会分配新的内存空间。否则,会按一定规则分配更大的内存空间,并进行字符串的移动。

接下来,resize 将向 svec 末尾添加当前字符串数量一半那么多的新字符串,它们的值都是空串。若空间不够,会分配足够容纳这些新字符串的内存空间。

9.40

【解答】

根据上题解答分析本程序。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<string> svec;
    svec.reserve(1024);
    // size 应该为 0;capacity 的值依赖于具体实现
    cout << "svec: size: " << svec.size()
         << " capacity: " << svec.capacity() << endl;

    // 读入 256 个词
    int count = 0;
    while (count != 256) {
        svec.push_back("C++");
        ++count;
    }
    // capacity 应该未改变,size 和 capacity 相等
    cout << "svec: size: " << svec.size()
         << " capacity: " << svec.capacity() << endl;
    
    // resize 向 svec 末尾添加当前字符串数量一半那么多
    // 的新字符串,它们的值都是空串。若空间不够,会分配
    // 足够容纳这些新字符串的内存空间
    svec.resize(svec.size() + svec.size() / 2);
    // capacity 应该未改变,size 和 capacity 相等
    cout << "svec: size: " << svec.size()
         << " capacity: " << svec.capacity() << endl;

    return 0;
}

当 while 条件判断语句为 count != 256 时,程序输出结果如下所示:

svec: size: 0 capacity: 1024
svec: size: 256 capacity: 1024
svec: size: 384 capacity: 1024

Process finished with exit code 0

当 while 条件判断语句为 count != 512 时,程序输出结果如下所示:

svec: size: 0 capacity: 1024
svec: size: 512 capacity: 1024
svec: size: 768 capacity: 1024

Process finished with exit code 0

当 while 条件判断语句为 count != 1000 时,程序输出结果如下所示:

svec: size: 0 capacity: 1024
svec: size: 1000 capacity: 1024
svec: size: 1500 capacity: 2048

Process finished with exit code 0

当 while 条件判断语句为 count != 1048 时,程序输出结果如下所示:

svec: size: 0 capacity: 1024
svec: size: 1048 capacity: 2048
svec: size: 1572 capacity: 2048

Process finished with exit code 0

9.41

【出题思路】

本题练习从字符数组初始化 string。

【解答】

vector 提供了 data 成员函数,返回其内存空间的首地址。将此返回值作为 string 的构造函数的第一个参数,将 vector 的 size 返回值作为第二个参数,即可获得 vector<char> 中的数据,将其看作一个字符数组来初始化 string。

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

using namespace std;

int main() {
    vector<char> cv = {'H', 'e', 'l', 'l', 'o'};
    string s(cv.data(), cv.size());
    cout << s << endl;

    return 0;
}
// 运行结果
Hello

Process finished with exit code 0

9.42

【出题思路】

本题练习高效地处理动态增长的 string。

【解答】

由于知道至少读取 100 个字符,因此可以用 reserve 先为 string 分配 100 个字符的空间,然后逐个读取字符,用 push_back 添加到 string 末尾。

#include <iostream>
#include <string>

using namespace std;

void input_string(string &s) {
    s.reserve(100);
    char c;
    while (cin >> c && c != 'Q')
        s.push_back(c);
}

int main() {
    string s;
    input_string(s);
    cout << s << endl;

    return 0;
}
// 运行结果
d
d
d
Q
ddd

Process finished with exit code 0

9.43

【出题思路】

本题练习较为复杂的 string 操作。

std::advance

std::distance

【解答】

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

using namespace std;

void replace_string(string &s, const string &oldVal, const string &newVal) {
    for (string::iterator beg = s.begin(); distance(beg, s.end()) >=
                               distance(oldVal.begin(), oldVal.end()); ) {
        if (string{beg, beg + oldVal.size()} == oldVal) {
            // beg = s.erase(beg, beg + oldVal.size());
            // beg = s.insert(beg, newVal.cbegin(), newVal.cend());
            s.replace(beg, beg + oldVal.size(), newVal);    // 该句也可用上边两句代替
            advance(beg, newVal.size());
        } else
            ++beg;
    }
}

int main() {
    string s = "tho thru tho!";
    replace_string(s, "thru", "through");
    cout << s << endl;

    replace_string(s, "tho", "though");
    cout << s << endl;

    replace_string(s, "through", "");
    cout << s << endl;

    return 0;
}
// 运行结果
tho through tho!
though through though!
though  though!

Process finished with exit code 0

9.44

【出题思路】

本题练习使用标准库提供的特性更简单地实现 string 操作。

std::string::npos

【解答】

由于可以使用下标和 replace,因此可以更为简单地实现上一题的目标。通过 find 成员函数(只支持下标参数)即可找到 s 中与 oldVal 相同的子串,接着用 replace 即可将找到的子串替换为新内容。可以看到,使用下标操作也可以简单地实现字符串操作。

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

using namespace std;

void replace_string(string &s, const string &oldVal, const string &newVal) {
    int p = 0;
    while ((p = s.find(oldVal, p)) != string::npos) {   // 在 s 中查找 oldVal
        s.replace(p, oldVal.size(), newVal);    // 将找到的子串替换为 newVal
        p += newVal.size();                     // 下表调整到新插入内容之后
    }
}

int main() {
    string s = "tho thru tho!";
    replace_string(s, "thru", "through");
    cout << s << endl;

    replace_string(s, "tho", "though");
    cout << s << endl;

    replace_string(s, "through", "");
    cout << s << endl;

    return 0;
}
// 运行结果
tho through tho!
though through though!
though  though!

Process finished with exit code 0

9.45

【出题思路】

本题练习 string 的追加操作。

【解答】

通过 insert 插入到首位置之前,即可实现前缀插入。通过 append 即可实现将后缀追加到字符串末尾。

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

using namespace std;

void name_string(string &name, const string &prefix, const string &suffix) {
    name.insert(name.begin(), 1, ' ');
    name.insert(name.begin(), prefix.begin(), prefix.end());    // 插入前缀
    name.append(" ");
    name.append(suffix.begin(), suffix.end());                  // 插入后缀
}

int main() {
    string s = "James Bond";
    name_string(s, "Mr", "II");
    cout << s << endl;

    s = "M";
    name_string(s, "Mrs.", "III");
    cout << s << endl;

    return 0;
}
// 运行结果
Mr James Bond II
Mrs. M III

Process finished with exit code 0

9.46

【出题思路】

本题继续练习基于位置的 string 操作。

【解答】

使用 insert,0 等价于 begin() ,都是在当前首字符之前插入新字符串;size() 等价于 end() ,都是在末尾追加新字符串。

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

using namespace std;

void name_string(string &name, const string &prefix, const string &suffix) {
    name.insert(0, " ");
    name.insert(0, prefix);                 // 插入前缀
    name.insert(name.size(), " ");
    name.insert(name.size(), suffix);       // 插入后缀
}

int main() {
    string s = "James Bond";
    name_string(s, "Mr", "II");
    cout << s << endl;

    s = "M";
    name_string(s, "Mrs.", "III");
    cout << s << endl;

    return 0;
}
// 运行结果
Mr James Bond II
Mrs. M III

Process finished with exit code 0

9.47

【出题思路】

本题练习 string 搜索操作的基本用法。

【解答】

find_first_of 在字符串中查找给定字符集合中任一字符首次出现的为位置。若查找数字字符,则 “给定字符集合” 应包含所有 10 个数字;若查找字母,则要包含所有大小写字母 —— abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ。

#include <iostream>
#include <string>

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

int main() {
    string numbers{"1234567890"};
    string alphabet{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
    string str{"ab2c3d7R4E6"};

    cout << "numeric characters: ";
    for (string::size_type pos = 0;
         (pos = str.find_first_of(numbers, pos)) != string::npos; ++pos)
        cout << str[pos] << " ";

    cout << "\nalphabetic characters: ";
    for (string::size_type pos = 0;
         (pos = str.find_first_of(alphabet, pos)) != string::npos; ++pos)
        cout << str[pos] << " ";
    cout << endl;

    return 0;
}
// 运行结果
numeric characters: 2 3 7 4 6 
alphabetic characters: a b c d R E 

Process finished with exit code 0

注:for 循环中的 pos = str.find_first_of(numbers, pos) 代码不要写成 pos = str.find_first_of(numbers) ,后者省略了可选参数 pos,这个可选的参数指出从哪个位置开始搜索。若没写 pos 参数,默认情况下,此位置被置为 0。即每次 for 循环总是从 str 字符串开头搜索,这样导致程序陷入死循环。

find_first_not_of 查找第一个不在给定字符集合中出现的字符,若用它查找某类字符首次出现的位置,则应使用补集。若查找数字字符,则 “给定字符集合” 应包含所有 10 个数字之外的所有字符 —— abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ;若查找字母,则要包含所有非字母字符。注意,这一设定仅对此问题要查找的字符串有效 —— 它只包含字母和数字。因此,字母和数字互为补集。若字符串包含任意 ASCII 字符,可以想见,正确的 “补集” 可能非常冗长。

#include <iostream>
#include <string>

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

int main() {
    string numbers{"1234567890"};
    string alphabet{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
    string str{"ab2c3d7R4E6"};

    cout << "numeric characters: ";
    for (string::size_type pos = 0;
         (pos = str.find_first_not_of(alphabet, pos)) != string::npos; ++pos)
        cout << str[pos] << " ";

    cout << "\nalphabetic characters: ";
    for (string::size_type pos = 0;
         (pos = str.find_first_not_of(numbers, pos)) != string::npos; ++pos)
        cout << str[pos] << " ";
    cout << endl;

    return 0;
}
// 运行结果
numeric characters: 2 3 7 4 6 
alphabetic characters: a b c d R E 

Process finished with exit code 0

9.48

【出题思路】

理解 find 与 find_first_of、find_first_not_of 的区别。

【解答】

s.find(args) 查找 s 中 args 第一次出现的位置,即第一个与 args 匹配的字符串的位置。args 是作为一个字符串整体在 s 中查找,而非一个字符集合在 s 中查找其中字符。因此,对 325 页给定的 name 和 numbers 值,在 numbers 中不存在与 name 匹配的字符串,find 会返回 npos。

9.49

【出题思路】

本题练习用搜索操作做一些更复杂的事情。

【解答】

查找既不包含上出头字母,也不包含下出头字母的单词,等价于 ”排除包含上出头字母或下出头字母的单词“ 。因此,用 find_first_of 在单词中查找上出头字母或下出头字母是否出现。若出现(find_first_of 函数会返回一个合法位置,而非 npos),则丢弃此单词,继续检查下一个单词。否则,表明单词符合要求,检查它是否比之前的最长合法单词更长,若是,记录其长度和内容。文件读取完毕后,输出最长的合乎要求的单词。

#include <iostream>
#include <fstream>
#include <string>

using namespace std;

void find_longest_word(ifstream &in) {
    string s, longest_word;
    int max_length = 0;

    while (in >> s) {
        if (s.find_first_of("bdfghjklpqty") != string::npos)
            continue;
        cout << s << " ";
        if (max_length < s.size()) {
            max_length = s.size();
            longest_word = s;
        }
    }
    cout << endl << "最长字符串:" << longest_word << endl;
}

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

    find_longest_word(in);

    return 0;
}

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

注:/Users/gemingdai/Desktop/data 即为文件 data 的文件名及其路径。

并在文件 data 中写入测试数据(我这里在网上找了一段英语 The unlikely rise of book fairs in the Gulf ):

published in 1959 by the Egyptian author Naguib Mahfouz

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

// 运行结果
/Users/macOS/CLionProjects/test/cmake-build-debug/test /Users/macOS/Desktop/data
in 1959 
最长字符串:1959

Process finished with exit code 0

9.50

【出题思路】

本题练习简单的字符串到数值的类型转换,这在开发实际应用程序时是非常常见的操作,是很有用的基本编程技巧。

【解答】

标准库提供了将字符串转换为各类数的函数。

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

using namespace std;

int main() {
    vector<string> isvec = {"123", "+456", "-789"};
    vector<string> fsvec{"12.3", "-4.56", "+7.8e-2"};
    int isum = 0;

    for (vector<string>::iterator iiter = isvec.begin();
    iiter != isvec.end(); ++iiter) {
        isum += stoi(*iiter);
    }
    cout << "sum(int) = " << isum << endl;

    float fsum = 0.0;
    for (auto fiter = fsvec.begin(); fiter != fsvec.end(); ++fiter) {
        fsum += stod(*fiter);
    }
    cout << "sum(float) = " << fsum << endl;

    return 0;
}
// 运行结果
sum(int) = -210
sum(float) = 7.818

Process finished with exit code 0

9.51

参考代码

// Exercise 9.51:
// Write a class that has three unsigned members representing year, month, and day.
// Write a constructor that takes a string representing a date. Your constructor should handle a
// variety of date formats, such as January 1, 1900, 1/1/1900, Jan 1, 1900, and so on.

#include <array>
#include <iostream>
#include <string>

class Date {
public:
    explicit Date(const std::string &str = "");

    void Print();

    unsigned year = 1970;
    unsigned month = 1;
    unsigned day = 1;

private:
    std::array<std::string, 12> month_names{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

    unsigned MonthFromName(const std::string &str);
};

Date::Date(const std::string &str) {
    if (str.empty()) return;
    std::string delimiters{" ,/"};      // 分隔符 ' ' ',' 和 '/'
    // month_day_delim_pos 表示月、日分割符在字符串中的下标索引,其它变量命名类似
    std::string::size_type month_day_delim_pos = str.find_first_of(delimiters);
    if (month_day_delim_pos == std::string::npos)
        throw std::invalid_argument("This format is not supported now.");
    month = MonthFromName(str.substr(0, month_day_delim_pos));
    // auto 自动推断字符串搜索函数返回类型,也就是 string::size_type 类型
    // 相关知识点可参考书 325 页 9.5.3 节开头部分
    auto day_year_delim_pos = str.find_first_of(delimiters, month_day_delim_pos + 1);
    auto day_len = day_year_delim_pos - month_day_delim_pos - 1;
    day = std::stoi(str.substr(month_day_delim_pos + 1, day_len));
    year = std::stoi(str.substr(day_year_delim_pos + 1));
}

void Date::Print() {
    std::cout << year << "-" << month << "-" << day << "\n";
}

unsigned Date::MonthFromName(const std::string &str) {
    if (str.empty()) return 0;
    if (std::isdigit(str[0])) return std::stoi(str);    // 若月份为数字
    for (size_t i = 0; i != 12; ++i) {                  // 若月份为英文
        if (str.find(month_names[i]) != std::string::npos) return i + 1;
    }
    return 0; //  not found
}

int main() {
    { //  default case
        auto date = Date();
        date.Print();
    }
    { //  case 0: January 1, 1900
        auto date = Date("January 1, 1900");
        date.Print();
    }
    { //  case 1: 1/1/1900
        auto date = Date("1/1/1900");
        date.Print();
    }
    { //  case 2: Jan 1, 1900
        auto date = Date("Jan 1, 1900");
        date.Print();
    }
}
// 运行结果
1970-1-1
1900-1-1
1900-1-1
1900-1-1

Process finished with exit code 0

9.52

参考代码

#include <stack>
using std::stack;

#include <string>
using std::string;

#include <iostream>
using std::cout;
using std::endl;

int main() {
    string expr = "This is (Mooophy(awesome)((((wooooooooo))))) and (ocxs) over";
    char repl = '#';
    int seen = 0;

    stack<char> stk;

    for (int i = 0; i < expr.length(); ++i) {   // 不包含字符串结束符 '\0'
        stk.push(expr[i]);
        if (expr[i] == '(')
            ++seen;                             // 左括号计数
        if (seen && expr[i] == ')') {           // 若左括号计数非零且当前字符是右括号
            while (stk.top() != '(')            // 判断栈顶字符是否是左括号
                stk.pop();                      // 若不是左括号字符,循环删除栈顶字符

            stk.pop();                          // 删除栈顶的左括号
            stk.push(repl);                     // 替换字符 '#' 入栈
            --seen;                             // 左括号计数减 1
        }
    }

    // 测试
    string str;                                 // 定义空串,用于存放栈中字符
    while (!stk.empty()) {
        // top 函数返回栈顶元素,但不将元素弹出栈
        str.insert(str.begin(), stk.top());
        // pop 函数删除栈顶元素,但不返回该元素值
        stk.pop();
    }
    cout << str << endl;

    return 0;
}
// 运行结果
This is # and # over

Process finished with exit code 0
29

评论区