12.1
【出题思路】
理解智能指针的基本特点。
【解答】
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}
由于 StrBlob 的 data 成员是一个指向 string 的 vector 的 shared_ptr,因此 StrBlob 的赋值不会拷贝 vector 的内容,而是多个 StrBlob 对象共享同一个(创建于动态内存空间上)vector 对象。
代码第 3 行创建 b2 时提供了 3 个 string 的列表,因此会创建一个包含 3 个 string 的 vector 对象,并创建一个 shared_ptr 指向此对象(引用计数为 1)。
第 4 行将 b2 赋予 b1 时,创建一个 shared_ptr 也指向刚才创建的 vector 对象,引用计数变为 2。
因此,第 4 行向 b2 添加一个 string 时,会向两个 StrBlob 共享的 vector 中添加此 string。最终,在代码结尾,b1 和 b2 均包含 4 个 string。
右花括号结束,b2 销毁;b1 仍有效,包含 4 个 string。
拷贝一个 share_ptr 会递增其引用计数:将一个 share_ptr 赋予另一个 share_ptr 会递增赋值号右侧 share_ptr 的引用计数,而递减左侧 share_ptr 的引用计数。如果一个 share_ptr 的引用计数变为 0,它所指向的对象会被自动销毁。因此,对于由 StrBlob 构造函数分配的 vector,当最后一个指向它的 StrBlob 对象被销毁时,它会随之被自动销毁。
12.2
【出题思路】
本题练习智能指针的简单使用。
【解答】
参考书中代码,并补充 front 和 back 对 const 的重载,即可完成自己的 StrBlob 类:
StrBlob.h
#ifndef TEST_STRBLOB_H
#define TEST_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::string;
using std::vector;
using std::initializer_list;
using std::shared_ptr;
using std::make_shared;
using std::out_of_range;
class StrBlob {
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const string &t) { data->push_back(t); }
void pop_back();
// 元素访问
string& front();
const string& front() const;
string& back();
const string& back() const;
private:
shared_ptr<vector<string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const string &msg) const;
};
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list <string> il) :
data(make_shared<vector<string>>(il)) { }
void StrBlob::check(vector<string>::size_type i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front() {
// 如果 vector 为空,check 会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
// const 版本 front
const string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
// const 版本 back
const string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
#endif //TEST_STRBLOB_H
main.cpp(包含了对上一题的测试)
#include <iostream>
#include "StrBlob.h"
using namespace std;
int main() {
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
}
// b2 在花括号外失效,作用域仅限于花括号内
// cout << b2.size() << endl;
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const StrBlob b3 = b1;
cout << b3.front() << " " << b3.back() << endl;
return 0;
}
12.3
【出题思路】
理解 const 版本和非 const 版本的差别。
【解答】
push_back 和 pop_back 的语义分别是向 StrBlob 对象共享的 vector 对象添加元素和从其删除元素。因此,我们不应为其重载 const 版本,因为常量 StrBlob 对象是不应被允许修改共享 vector 对象内容的。也就是说程序中写为 const 版本程序也会正常执行,只是逻辑上不符。
此题没有理解,这里再贴一个链接。
基于练习 12.2,我们在 StrBlob.h 中增加了 push_back 和 pop_back 的 const 版本;在 main.cpp 利用 StrBlob 的常量对象 b4 进行了对 push_back 和 pop_back 的 const 版本的测试,可以实现对共享的 vector 添加和删除元素的操作。
StrBlob.h
#ifndef TEST_STRBLOB_H
#define TEST_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::string;
using std::vector;
using std::initializer_list;
using std::shared_ptr;
using std::make_shared;
using std::out_of_range;
class StrBlob {
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const string &t) { data->push_back(t); }
// const 版本 push_back
void push_back(const string &t) const { data->push_back(t); }
void pop_back();
// const 版本 pop_back
void pop_back() const;
// 元素访问
string& front();
const string& front() const;
string& back();
const string& back() const;
private:
shared_ptr<vector<string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const string &msg) const;
};
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list <string> il) :
data(make_shared<vector<string>>(il)) { }
void StrBlob::check(vector<string>::size_type i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front() {
// 如果 vector 为空,check 会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
// const 版本 front
const string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
// const 版本 back
const string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
// const 版本 pop_back
void StrBlob::pop_back() const {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
#endif //TEST_STRBLOB_H
main.cpp(b4 用于测试本题)
#include <iostream>
#include "StrBlob.h"
using namespace std;
int main() {
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
}
// b2 在花括号外失效,作用域仅限于花括号内
// cout << b2.size() << endl;
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const StrBlob b3 = b1;
cout << b3.front() << " " << b3.back() << endl;
const StrBlob b4 = b1;
b4.push_back("although");
cout << b4.size() << endl;
cout << b4.front() << " " << b4.back() << endl;
b4.pop_back();
cout << b4.size() << endl;
cout << b4.front() << " " << b4.back() << endl;
return 0;
}
// 运行结果
4
4
a about
a about
5
a although
4
a about
Process finished with exit code 0
12.4
Because the type of i
is std::vector<std::string>::size_type
which is an unsigned
.When any argument less than 0 is passed in, it will convert to a number greater than 0. In short std::vector<std::string>::size_type
will ensure it is a positive number or 0.
解答来自 Exercise 12.4
12.5
【出题思路】
复习隐式类类型转换和显式转换的区别。
【解答】
未编写接受一个初始化列表参数的显式构造函数,意味着可以进行列表向 StrBlob 的隐式类型转换,亦即在需要 StrBlob 的地方(如函数的参数),可以使用列表进行代替。而且,可以进行拷贝形式的初始化(如赋值)。这令程序编写更为简单方便。
但这种隐式转换并不总是好的。例如,列表中可能并非都是合法的值。再如,对于接受 StrBlob 的函数,传递给它一个列表,会创建一个临时的 StrBlob 对象,用列表对其初始化,然后将其传递给函数,当函数完成后,此对象将被丢弃,再也无法访问了。对于这些情况,我们可以定义显式的构造函数,禁止隐式类类型转换。
12.6
【出题思路】
本题练习用 new 和 delete 直接管理内存。
【解答】
直接内存管理的关键是谁分配了内存谁就要记得释放。在此程序中,主函数(main)调用分配函数(new_vector)在动态内存空间中创建 int 的 vector,因此在读入数据、打印数据之后,主函数(main)应负责释放 vector 对象。
#include <iostream>
#include <vector>
using namespace std;
vector<int> *new_vector() {
return new (nothrow) vector<int>;
}
void read_ints(vector<int> *pv) {
int v;
while (cin >> v) {
pv->push_back(v);
}
}
void print_ints(vector<int> *pv) {
for (const auto &i : *pv)
cout << i << " ";
cout << endl;
}
int main() {
vector<int> *ivecp = new_vector();
if (!ivecp) {
cout << "内存不足!" << endl;
return -1;
}
read_ints(ivecp);
print_ints(ivecp);
delete ivecp; // ivecp 变为无效
ivecp = nullptr; // 指出 ivecp 不再绑定到任何对象
return 0;
}
// 运行结果
10 9 8 7 6 5 4 3 2 1 0
^D
10 9 8 7 6 5 4 3 2 1 0
Process finished with exit code 0
12.7
【出题思路】
本题练习用智能指针管理内存。
【解答】
与上一题相比,程序差别不大,主要是将 vector<int> *
类型变为 shared_ptr<vector<int>>
类型,空间分配不再用 new 而改用 make_shared,在主函数(main)末尾不再需要主动释放内存。最后一点的意义对这个小程序还不明显,但对于大程序非常重要,它省去了程序员释放内存的工作,可以有效避免内存泄漏问题。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
shared_ptr<vector<int>> new_vector() {
// 返回一个值初始化的 vector<int> 的 shared_ptr
return make_shared<vector<int>>();
}
void read_ints(shared_ptr<vector<int>> spv) {
int v;
while (cin >> v) {
spv->push_back(v);
}
}
void print_ints(shared_ptr<vector<int>> spv) {
for (const auto &i : *spv)
cout << i << " ";
cout << endl;
}
int main() {
shared_ptr<vector<int>> ivecsp = new_vector();
read_ints(ivecsp);
print_ints(ivecsp);
return 0;
}
// 运行结果
10 9 8 7 6 5 4 3 2 1 0
^D
10 9 8 7 6 5 4 3 2 1 0
Process finished with exit code 0
12.8
【出题思路】
理解用 new 分配内存成功和失败的差别,以及复习类型转换。
【解答】
从程序片段看,可以猜测程序员的意图是通过 new 返回的指针值来区分内存分配成功或失败 —— 成功返回一个合法指针,转换为整型是一个非零值,可转换为 bool 值 true;分配失败,p 得到 nullptr,其整型值是 0,可转换为 bool 值 false。
但普通 new 调用在分配失败时抛出一个异常 bad_alloc,而不是返回 nullptr,因此程序不能达到预想的目的。
可将 new int
改为 new (nothrow) int
来令 new 在分配失败时不抛出异常,而是返回 nullptr。但这仍然不是一个好方法,应该通过捕获异常或是判断返回的指针来返回 true 或 false,而不是依赖类型转换。
12.9
【出题思路】
理解直接管理内存和智能指针的差别。
【解答】
这段代码非常好地展示了智能指针在管理内存上的优点。
对于普通指针部分,首先分配了两个 int 对象,指针分别保存在 p 和 r 中。接下来,将指针 q 的值赋予了 r,这带来了两个非常严重的内存管理问题:
- 首先是一个直接的内存泄漏问题,r 和 q 一样都指向 42 的内存地址,而 r 中原来保存的地址 —— 100 的内存再无指针管理,变成 “孤儿内存”,从而造成内存泄漏。
- 其次是一个 “空悬指针” 问题。由于 r 和 q 指向同一个动态对象,如果程序编写不当,很容易产生释放了其中一个指针,而继续使用另一个指针的问题。继续使用的指针指向的是一块已经释放的内存,是一个空悬指针,继续读写它指向的内存可能导致程序崩溃甚至系统崩溃的严重问题。
而 shared_ptr 则可很好地解决这些问题。首先,分配了两个共享的对象,分别由共享指针 p2 和 r2 指向,因此它们的引用计数均为 1。接下来,将 q2 赋予 r2。赋值操作会将 q2 指向的对象地址赋予 r2,并将 r2 原来指向的对象的引用计数减 1,将 q2 指向的对象的引用计数加 1。这样,共享对象 100 的引用计数变为 0,其占用的内存空间会被释放,不会造成内存泄漏。而共享对象 42 的引用计数变为 2,也不会因为 r2 和 q2 之一的销毁而释放它的内存空间,因此也不会造成空悬指针的问题。
12.10
【出题思路】
理解智能指针的使用。
【解答】
此调用是正确的,利用 p 创建一个临时的 shared_ptr 赋予 process 的参数 ptr,p 和 ptr 都指向相同的 int 对象,引用计数被正确地置为 2。process 执行完毕后,ptr 被销毁,int 对象 42 引用计数减 1,这是正确的 —— 只有 p 指向它。
12.11
【出题思路】
理解智能指针和普通指针不能混用。
【解答】
此调用是错误的。p.get()
获得一个普通指针,指向 p 所共享的 int 对象。利用此指针(普通指针)创建一个 shared_ptr 赋予 process 的参数 ptr,而不是利用 p 创建一个 shared_ptr 赋予 process 的参数 ptr,这样的话将不会形成正确的动态对象共享。编译器会认为 p 和 ptr 是使用两个地址(虽然它们相等)创建的两个不相干的 shared_ptr,而非共享同一个动态对象。这样,两者的引用计数均为 1。当 process 执行完毕后,ptr 的引用计数减为 0,所管理的内存地址被释放,而此内存就是 p 所管理的。p 成为一个管理空悬指针的 shared_ptr。
12.12
【出题思路】
理解智能指针和普通指针、new 混合使用应该注意的问题。
【解答】
(a)合法。sp 是一个共享指针,指向一个 int 对象。对 process 的调用会拷贝 sp,传递给 process 的参数 ptr,两者都指向相同的 int 对象,引用计数变为 2。当 process 执行完毕时,ptr 被销毁,引用计数变回 1。
(b)不合法。普通指针不能隐式转换为智能指针。
(c)不合法。原因同(b)。
(d)合法,但是是错误的程序。p 是一个指向 int 对象的普通指针,被用来创建一个临时 shared_ptr,传递给 process 的参数 ptr,引用计数为 1。当 process 执行完毕,ptr 被销毁,引用计数变为 0,int 对象被销毁。p 变为空悬指针。
12.13
【出题思路】
继续理解智能指针和普通指针使用上的问题。
【解答】
第二行用 get 获取了 sp 指向的 int 对象的地址,第三行用 delete 释放这个地址。这意味着 sp 的引用计数仍为 1,但其指向的 int 对象已经被释放了。sp 成为类似空悬指针的 shared_ptr。
12.14
【出题思路】
本题练习利用智能指针管理使用资源的类,避免内存泄漏等问题。
【解答】
参照本节内容设计函数即可。main 函数分别调用了未使用和使用了 shared_ptr 的版本,根据输出可以看出,前者未调用 disconnect,而后者调用了。注意观察 f1 的输出,很明显,disconnect 是在 f1 结束后,在销毁 p 时被调用的。
#include <iostream>
#include <memory>
using namespace std;
struct destination {};
struct connection {};
connection connect(destination *pd) {
cout << "打开连接" << endl;
return connection();
}
void disconnect(connection c) {
cout << "关闭连接";
}
// 未使用 shared_ptr 的版本
void f(destination &d) {
cout << "直接管理 connect" << endl;
connection c = connect(&d);
// 忘记调用 disconnect 关闭连接
cout << "f 结束" << endl;
}
void end_connection(connection *p) { disconnect(*p); }
// 使用 shared_ptr 的版本
void f1(destination &d) {
cout << "用 shared_ptr 管理 connect" << endl;
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
cout << "f1 结束" << endl;
}
int main() {
destination d;
f(d);
f1(d);
return 0;
}
// 运行结果
直接管理 connect
打开连接
f 结束
用 shared_ptr 管理 connect
打开连接
f1 结束
关闭连接
Process finished with exit code 0
12.15
【出题思路】
复习 lambda。
【解答】
根据 end_connection 的定义,lambda 不捕获局部变量,参数为 connection 指针,用该指针指向的对象调用 disconnect 即可:[] (connection *p) { disconnect(*p); }
程序如下所示:
#include <iostream>
#include <memory>
using namespace std;
struct destination { };
struct connection { };
connection connect(destination *pd) {
cout << "打开连接" << endl;
return connection();
}
void disconnect(connection c) {
cout << "关闭连接";
}
// 未使用 shared_ptr 的版本
void f(destination &d) {
cout << "直接管理 connect" << endl;
connection c = connect(&d);
// 忘记调用 disconnect 关闭连接
cout << "f 结束" << endl;
}
// void end_connection(connection *p) { disconnect(*p); }
// 使用 shared_ptr 的版本
void f1(destination &d) {
cout << "用 shared_ptr 管理 connect" << endl;
connection c = connect(&d);
shared_ptr<connection> p(&c, [] (connection *p) { disconnect(*p); });
cout << "f1 结束" << endl;
}
int main() {
destination d;
f(d);
f1(d);
return 0;
}
// 运行结果
直接管理 connect
打开连接
f 结束
用 shared_ptr 管理 connect
打开连接
f1 结束
关闭连接
Process finished with exit code 0
12.16
【出题思路】
深入地理解 unique_ptr 不能拷贝或赋值的限制。
【解答】
用 LLVM 10.0.1 (clang-1001.0.46.4) 编译本节开始的错误拷贝和赋值程序,会给出类似如下的错误信息:
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1(new int(42));
// error: call to implicitly-deleted copy
// constructor of 'unique_ptr<int>'
unique_ptr<int> p2(p1);
// error: object of type
// 'std::__1::unique_ptr<int, std::__1::default_delete<int> >'
// cannot be assigned because its copy assignment operator is
// implicitly deleted
unique_ptr<int> p3;
p3 = p1;
cout << *p1 << endl;
return 0;
}
即,程序调用了删除的函数。原因是,标准库为了禁止 unique_ptr 的拷贝和赋值,将其拷贝构造函数和赋值函数声明为了 delete 的:
unique_ptr(const unique_ptr &) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
查看 IDE 的编译器版本方法,在 IDE 编写下列程序即可:
#include <iostream>
using namespace std;
int main() {
cout << __VERSION__ << endl;
return 0;
}
// 运行结果
4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)
Process finished with exit code 0
12.17
【出题思路】
继续熟悉 unique_ptr 使用上应注意的问题。
【解答】
(a)不合法。unique_ptr 需要用一个指针初始化,无法将 int 转换为指针。
(b)The code below can compile, but will cause error at run time. The reason is that when the unique_ptr p1 is out of scope, delete will be called to free th object. But the object is not allocate using new
. Thus, an error would be thrown by operating system.
(c)This code can compile, but cause a dangling pointer at run time. The reason is that the unique_ptr will free the object the raw pointer is pointing to.
(d)存在与(b)相同的问题。
(e)合法。推荐使用的方法。
(f)error: double free or corruption at run time two unique_ptr are pointing to the same object. Thus, when both are out of scope, Operating system will throw double free or corruption.
编程测试各语句如下所示:
#include <iostream>
#include <memory>
using namespace std;
int main() {
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
/*
* error: no matching constructor for initialization
* of 'IntP' (aka 'unique_ptr<int>')
*/
// IntP p0(ix);
/*
* Process finished with exit code 134 (interrupted by
* signal 6: SIGABRT)
*/
// IntP p1(pi);
// IntP p2(pi2);
/*
* Process finished with exit code 134 (interrupted by
* signal 6: SIGABRT)
*/
// IntP p3(&ix);
// IntP p4(new int(2048));
/*
* Process finished with exit code 134 (interrupted by
* signal 6: SIGABRT)
*/
// IntP p5(p2.get());
return 0;
}
12.18
【出题思路】
理解 unique_ptr 和 shared_ptr 的差别。
【解答】
unique_ptr “独占” 对象的所有权,不能拷贝和赋值。release 操作是用来将对象的所有权转移给另一个 unique_ptr 的。
而多个 shared_ptr 可以 “共享” 对象的所有权。需要共享时,可以简单拷贝和赋值。因此,并不需要 release 这样的操作来转移所有权。
12.19
【出题思路】
熟悉 weak_ptr 的使用。
扩展练习 12.2
【解答】
参考本节内容实现所需程序即可。为了实现对 StrBlob 的 vector 中元素的遍历,还定义了 eq 和 neq 两个函数来比较两个 StrBlobPtr 是否指向相同 vector 的相同位置。
程序如下所示:
StrBlob.h
#ifndef TEST_STRBLOB_H
#define TEST_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::string;
using std::vector;
using std::initializer_list;
using std::shared_ptr;
using std::weak_ptr;
using std::make_shared;
using std::out_of_range;
using std::runtime_error;
// 对于 StrBlob 中的友元声明来说,此前置声明是必要的
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const string &t) { data->push_back(t); }
void pop_back();
// 元素访问
string& front();
const string& front() const;
string& back();
const string& back() const;
// 提供给 StrBlobPtr 的接口
// 返回指向首元素和尾后元素的 StrBlobPtr
StrBlobPtr begin(); // 定义 StrBlobPtr 后才能定义这两个函数
StrBlobPtr end();
private:
shared_ptr<vector<string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const string &msg) const;
};
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list <string> il) :
data(make_shared<vector<string>>(il)) { }
void StrBlob::check(vector<string>::size_type i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front() {
// 如果 vector 为空,check 会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
// const 版本 front
const string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
// const 版本 back
const string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
class StrBlobPtr {
friend bool eq(const StrBlobPtr &, const StrBlobPtr &);
public:
StrBlobPtr() : curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
string& deref() const;
StrBlobPtr& incr(); // 前缀递增
StrBlobPtr& decr(); // 后缀递减
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
// 保存一个 weak_ptr,意味着底层 vector 可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr; // 在数组中的当前位置
};
shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const {
auto ret = wptr.lock(); // vector 还存在吗?
if (!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret; // 否则,返回指向 vector 的 shared_ptr
}
string& StrBlobPtr::deref() const {
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*P) 是对象所指向的 vector
}
// 前缀递增:返回递增后的对象的引用
StrBlobPtr& StrBlobPtr::incr() {
// 如果 curr 已经指向容器的尾后位置,就不能递增它
check(curr, "increment past end of StrBlobPtr");
++curr; // 推进当前位置
return *this;
}
// 前缀递减:返回递减后的对象的引用
StrBlobPtr& StrBlobPtr::decr() {
// 如果 curr 已经为 0,递减它会产生一个非法下标
--curr; // 递减当前位置
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
// StrBlob 的 begin 和 end 成员的定义
StrBlobPtr StrBlob::begin() {
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end() {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
// StrBlobPtr 的比较操作
bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
// 若底层的 vector 是同一个
if (l == r)
// 则两个指针都是空,或者指向相同元素时,它们相等
return (!r || lhs.curr == rhs.curr);
else
return false; // 若指向不同 vector,则不可能相等
}
bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
return !eq(lhs, rhs);
}
#endif //TEST_STRBLOB_H
main.cpp
#include <iostream>
#include "StrBlob.h"
using namespace std;
int main() {
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
}
// b2 在花括号外失效,作用域仅限于花括号内
// cout << b2.size() << endl;
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const StrBlob b3 = b1;
cout << b3.front() << " " << b3.back() << endl;
for (auto iter = b1.begin(); neq(iter, b1.end()); iter.incr())
cout << iter.deref() << " ";
cout << endl;
return 0;
}
// 运行结果
4
4
a about
a about
a an the about
Process finished with exit code 0
12.20
【出题思路】
本题练习使用 StrBlob 和 StrBlobPtr。
【解答】
用 getline 逐行读取输入文件,存入 StrBlob 后,用 StrBlobPtr 从 StrBlob 的 begin 到 end,逐个打印每个字符串即可。
程序如下所示:
StrBlob.h 同上一题。
main.cpp
#include <iostream>
#include <fstream>
#include "StrBlob.h"
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;
}
StrBlob b;
string line;
while (getline(in, line))
b.push_back(line);
for (auto iter = b.begin(); neq(iter, b.end()); iter.incr())
// 打印元素,元素间用换行分割
cout << iter.deref() << endl;
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
并在文件 data 中写入如下内容:
c++ primer 5th
C++ Primer 5th
运行程序,程序执行结果如下所示:
c++ primer 5th
C++ Primer 5th
Process finished with exit code 0
【另外一种写法】
上边的程序是按行读取的,所以在 StrBlob 内是以行为一个单元存储的。我们也可以以单词为一个单元存储。
只要对上边的程序稍加改动即可。程序如下所示:
main.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include "StrBlob.h"
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;
}
StrBlob b;
string line; // 一行
string word; // 一个单词
while (getline(in, line)) { // 读取一行
istringstream l_in(line); // 构造字符串流,读取单词
while (l_in >> word)
b.push_back(word);
}
for (auto iter = b.begin(); neq(iter, b.end()); iter.incr())
// 打印元素,元素间用换行分割
cout << iter.deref() << endl;
return 0;
}
// 运行结果
c++
primer
5th
C++
Primer
5th
Process finished with exit code 0
12.21
【出题思路】
思考合法性检查不同方式的优缺点。
【解答】
书中的方式更好一些。将合法性检查与元素获取和返回语句分离开来,代码更清晰易读,当执行到第二条语句时,已确保 p 是存在的 vector,curr 是合法的位置,可安全地获取元素并返回。这种清晰的结构也更有利于修改不同的处理逻辑。
而本题中的版本将合法性检查和元素获取及返回合在一条语句中,不易读,也不易修改。
12.22
【出题思路】
本题练习设计 const 版本。
【解答】
首先,为 StrBlobPtr 定义能接受 const StrBlob &
参数的构造函数:
StrBlobPtr(const StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
其次,为 StrBlob 定义能操作 const 对象的 begin 和 end。
声明:
StrBlobPtr begin() const; // 定义 StrBlobPtr 后才能定义这两个函数
StrBlobPtr end() const;
定义:
// StrBlob 的 begin 和 end 成员的定义
StrBlobPtr StrBlob::begin() const {
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end() const {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
即可实现 StrBlobPtr 使用 const StrBlob:
#include <iostream>
#include "StrBlob.h"
using namespace std;
int main() {
const StrBlob b = {"Hello", "World", "!"};
for (auto iter = b.begin(); neq(iter, b.end()); iter.incr())
cout << iter.deref() << endl;
return 0;
}
// 运行结果
Hello
World
!
Process finished with exit code 0
12.23
【出题思路】
本题练习使用动态数组。
【解答】
用 new char[xx]
即可分配用来保存结果的 char 数组,其中 xx 应该足以保存结果字符串。由于 C 风格字符串以 \0
结尾,因此 xx 应不小于字符数加 1。
对字符串字面常量(即字符数组),可以使用 strcpy 将第一个字符串拷贝到动态数组中,然后用 strcat 将第二个字符串连接到动态数组中。
对两个 string 对象,使用 + 运算即可简单实现连接。然后用 c_str 获取连接结果(临时 string 对象)的内存地址,用 strcpy 拷贝到动态数组即可。
最终,不要忘记释放动态数组。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
const char *c1 = "Hello ";
const char *c2 = "World";
// 字符串所需空间等于字符数 + 1
char *r = new char[strlen(c1) + strlen(c2) + 1];
strcpy(r, c1); // 拷贝第一个字符串常量
strcat(r, c2); // 连接第二个字符串常量
cout << r << endl;
string s1 = "hello ";
string s2 = "world";
strcpy(r, (s1 + s2).c_str()); // 拷贝连接结果
cout << r << endl;
// 释放动态数组
delete [] r;
return 0;
}
// 运行结果
Hello World
hello world
Process finished with exit code 0
12.24
【出题思路】
本题继续练习使用动态数组。
【解答】
我们处理变长输入的方法是,根据动态分配的字符数组的大小确定字符串长度阀值,当读取的字符数超出阀值时,停止读取,即采取了截断的方式。还可以采取其他处理方式,如抛出异常。
另外,在动态分配字符数组的空间大小时,记得加上字符数组结束符 \0
所占的空间。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
cout << "How long do you want the string ? ";
int size{0};
cin >> size;
cin.ignore(); // 忽略输入长度 size 后的一个字符,如空格或换行符
char *r = new char[size + 1](); // size + 1 个值初始化为 0 的 char
cin.get(r, size + 1);
cout << "The input string is:\n\"" << r << "\"";
// 释放动态数组
delete [] r;
return 0;
}
// 测试数据1,运行结果
How long do you want the string ? 10
c++ primer 5th
The input string is:
"c++ primer"
Process finished with exit code 0
// 测试数据2,运行结果
How long do you want the string ? 10 c++ primer 5th
The input string is:
"c++ primer"
Process finished with exit code 0
注:cin.ignore();
用于忽略输入长度 size 后的一个字符。我们注释掉该行代码,再次运行程序:
// 运行结果
How long do you want the string ? 10 c++ primer 5th
The input string is:
" c++ prime"
Process finished with exit code 0
发现程序把两次输入之间的空格字符也当成字符数组的输入了。若输入完 size,直接 Enter 准备输入字符串,发现程序直接运行结束。如下所示:
// 运行结果
How long do you want the string ? 10
The input string is:
""
Process finished with exit code 0
我们根本无法输入我们的测试字符串(c++ primer 5th)。所以,这里的cin.ignore();
使程序忽略了 size 后的一个字符(这里是空格符或者换行符),从而确保程序能够读入我们的测试字符串。
12.25
【出题思路】
理解释放动态数组的特殊方式。
【解答】
delete [] p;
12.26
【出题思路】
本题练习使用 allocator。
【解答】
首先,定义一个 allocator<string> alloc
。
然后,用 alloc 的 allocate 而不是 new 操作来分配内存。这样,只会分配裸内存,而不会初始化 string 对象。
接下来,用 construct 操作从读取的 string 对象来初始化动态数组中的 string。随后动态数组的使用就和往常一样了。
使用完毕后,记住与内存分配和对象构造一样,对象析构(使用 destroy 操作)和内存释放(使用 deallocate 操作)也是分开的。
#include <iostream>
#include <string>
#include <memory>
#define LENGTH 100
using namespace std;
int main() {
allocator<string> alloc;
// 分配 100 个未初始化的 string
auto const p = alloc.allocate(LENGTH);
string s;
string *q = p; // q 指向第一个 string
while (cin >> s && q != p + LENGTH)
alloc.construct(q++, s); // 用 s 初始化 *q
const size_t size = q - p; // 记住读取了多少个 string
for (size_t i = 0; i < size; i++)
cout << p[i] << endl;
while (q != p)
alloc.destroy(--q); // 释放我们真正构造的 string
alloc.deallocate(p, LENGTH); // 释放内存
return 0;
}
// 运行结果
c++ primer 5th
^D
c++
primer
5th
Process finished with exit code 0
12.27
【出题思路】
本题综合练习已学过的知识实现文本查询程序。
【解答】
12.3.2 节中已经详细介绍了两个类的实现,配套网站上也有完整程序,这里就不再重复给出设计思路和代码。但读者不要直接查看设计思路和程序,应先尝试自己实现,然后再与书中内容和代码对照。
下面是我从书籍配套网站下载的代码调试运行结果。3 个 .h
头文件和 2 个 .cpp
实现文件:
QueryResult.h
#ifndef TEST_QUERYRESULT_H
#define TEST_QUERYRESULT_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <set>
using std::set;
#include <iostream>
using std::ostream;
class QueryResult {
friend ostream &print(ostream &, const QueryResult &);
public:
typedef vector<string>::size_type line_no;
typedef set<line_no>::const_iterator line_it;
QueryResult(string s,
shared_ptr<set<line_no>> p,
shared_ptr<vector<string>> f) :
sought(s), lines(p), file(f) { }
set<line_no>::size_type size( ) const { return lines->size(); }
line_it begin() const { return lines->begin(); }
line_it end() const { return lines->end(); }
shared_ptr<vector<string>> get_file() { return file; }
private:
string sought; // word this query represents
shared_ptr<set<line_no>> lines; // lines it's on
shared_ptr<vector<string>> file; // input file
};
ostream &print(ostream &, const QueryResult &);
#endif //TEST_QUERYRESULT_H
make_plural.h
#ifndef TEST_MAKE_PLURAL_H
#define TEST_MAKE_PLURAL_H
#include <cstddef>
using std::size_t;
#include <string>
using std::string;
#include <iostream>
using std::cout; using std::endl;
// return the plural version of word if ctr is greater than 1
inline string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
#endif //TEST_MAKE_PLURAL_H
TextQuery.h
#ifndef TEST_TEXTQUERY_H
#define TEST_TEXTQUERY_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <fstream>
using std::ifstream;
#include "QueryResult.h"
/* this version of the query classes includes two
* members not covered in the book:
* cleanup_str: which removes punctuation and
* convert all text to lowercase
* display_map: a debugging routine that will print the contents
* of the lookup map
*/
class QueryResult; // declaration needed for return type in the query function
class TextQuery {
public:
typedef vector<string>::size_type line_no;
TextQuery(ifstream&);
QueryResult query(const string&) const;
void display_map(); // debugging aid: print the map
private:
shared_ptr<vector<string>> file; // input file
// maps each word to the set of the lines in which that word appears
map<string, shared_ptr<set<line_no>>> wm;
// canonicalizes text: removes punctuation and makes everything lower case
static string cleanup_str(const string&);
};
#endif //TEST_TEXTQUERY_H
TextQuery.cpp
#include "TextQuery.h"
#include "make_plural.h"
#include <cstddef>
using std::size_t;
#include <memory>
using std::shared_ptr;
#include <sstream>
using std::istringstream;
#include <string>
using std::string;
using std::getline;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <iostream>
using std::cerr; using std::cout; using std::cin;
using std::endl; using std::ostream;
#include <fstream>
using std::ifstream;
#include <cctype>
using std::ispunct; using std::tolower;
#include <cstring>
using std::strlen;
#include <utility>
using std::pair;
// because we can't use auto, we'll define typedefs
// to simplify our code
// type of the lookup map in a TextQuery object
typedef map<string, shared_ptr<set<TextQuery::line_no>>> wmType;
typedef wmType::mapped_type lineType;
// the iterator type for the map
typedef wmType::const_iterator wmIter;
// type for the set that holds the line numbers
typedef set<TextQuery::line_no>::const_iterator lineIter;
// read the input file and build the map of lines to line numbers
TextQuery::TextQuery(ifstream &is) : file(new vector<string>) {
string text;
while (getline(is, text)) { // for each line in the file
file->push_back(text); // remember this line of text
int n = file->size() - 1; // the current line number
istringstream line(text); // separate the line into words
string word;
while (line >> word) { // for each word in that line
word = cleanup_str(word);
// if word isn't already in wm, subscripting adds a new entry
lineType &lines = wm[word]; // lines is a shared_ptr
if (!lines) // that pointer is null the first time we see word
lines.reset(new set<line_no>); // allocate a new set
lines->insert(n); // insert this line number
}
}
}
// not covered in the book -- cleanup_str removes
// punctuation and converts all text to lowercase so that
// the queries operate in a case insensitive manner
string TextQuery::cleanup_str(const string &word) {
string ret;
for (string::const_iterator it = word.begin(); it != word.end(); ++it) {
if (!ispunct(*it))
ret += tolower(*it);
}
return ret;
}
QueryResult TextQuery::query(const string &sought) const {
// we'll return a pointer to this set if we don't find sought
static lineType nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm!
// cleanup_str removes punctuation and convert sought to lowercase
wmIter loc = wm.find(cleanup_str(sought));
if (loc == wm.end())
return QueryResult(sought, nodata, file); // not found
else
return QueryResult(sought, loc->second, file);
}
ostream &print(ostream &os, const QueryResult &qr) {
// if the word was found, print the count and all occurrences
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") << endl;
// print each line in which the word appeared
// for every element in the set
for (lineIter num = qr.lines->begin(); num != qr.lines->end(); ++num)
// don't confound the user with text lines starting at 0
os << "\t(line " << *num + 1 << ")"
<< *(qr.file->begin() + *num) << endl;
return os;
}
// debugging routine, not covered in the book
void TextQuery::display_map() {
wmIter iter = wm.begin(), iter_end = wm.end();
// for each word in the map
for ( ; iter != iter_end; ++iter) {
cout << "word: " << iter->first << "{";
// fetch location vector as a const reference to avoid copying it
lineType text_locs = iter->second;
lineIter loc_iter = text_locs->begin(), loc_iter_end = text_locs->end();
// print all line numbers for this word
while (loc_iter != loc_iter_end) {
cout << *loc_iter;
if (++loc_iter != loc_iter_end)
cout << ", ";
}
cout << "}\n"; // end list of output this word
}
cout << endl; // finished printing entire map
}
querymain.cpp
#include <string>
using std::string;
#include <fstream>
using std::ifstream;
#include <iostream>
using std::cin; using std::cout; using std::cerr;
using std::endl;
#include <cstdlib> // for EXIT_FAILURE
#include "TextQuery.h"
void runQueries(ifstream &infile) {
// infile is an ifstream that is the file we want to query
TextQuery tq(infile); // store the file and build the query map
// iterate with the user: prompt for a word to find and print results
while (true) {
cout << "enter word to look for, or q to quit: ";
string s;
// stop if we hit end-of-file on the input or if a 'q' is entered
if (!(cin >> s) || s == "q")
break;
// run the query and print the results
print(cout, tq.query(s)) << endl;
}
}
// program takes single argument specifying the file to query
int main(int argc, char **argv) {
// open the file from which user will query words
ifstream infile;
// open returns void, so we use the comma operator XREF(commaOp)
// to check the state of infile after the open
if (argc < 2 || !(infile.open(argv[1]), infile)) {
cerr << "No input file!" << endl;
return EXIT_FAILURE;
}
runQueries(infile);
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
并在文件 data 中写入如下内容:
c++ primer 5th
C++ Primer 5th
example. Cplusplus
Primer example,
Example primer
运行程序,程序执行结果如下所示:
enter word to look for, or q to quit: example
example occurs 3 times
(line 3)example. Cplusplus
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: EXAMP.lE..
EXAMPlE.. occurs 3 times
(line 3)example. Cplusplus
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: pRimEr
pRimEr occurs 4 times
(line 1)c++ primer 5th
(line 2)C++ Primer 5th
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: q
Process finished with exit code 0
注:
我在自己的 IDE(CLion)编译程序时,报告如下错误:
Undefined symbols for architecture x86_64:
... ...
将 CLion 的 CMakeList.txt(这是我的内容,对应改最后一行即可):
cmake_minimum_required(VERSION 3.14)
project(test)
set(CMAKE_CXX_STANDARD 11)
add_executable(test querymain.cpp)
改为:
cmake_minimum_required(VERSION 3.14)
project(test)
set(CMAKE_CXX_STANDARD 11)
add_executable(test querymain.cpp TextQuery.cpp)
即,将 .cpp
实现文件都添加到最后一行。然后再编译就无错误了。
参考帖子Clion Undefined symbols for architecture x86_64
12.28
【出题思路】
采用过程式程序设计而非面向对象的程序设计来解决这个问题,并体会两者的差异。
【解答】
总体设计思路与面向对象的版本相似,但有一些差异:
- 由于不用类来管理数据,file 和 wm 都定义为全局变量,便于在函数间共享。当然也可以定义为局部变量,通过函数参数传递。
- 由于不必进行不同类对象间的数据共享,因此 file 和 wm 中的 set 都不必用 shared_ptr 管理,直接定义为 vector 和 set 即可。使用它们的代码也要相应修改。
- 由于不用类来保存查询结果,因此将 query 和 print 函数合二为一。
看起来代码较之面向对象的版本简单了不少,但读者应思考面向对象版本的诸多优势。
程序如下所示:
make_plural.h 文件代码与练习 12.27 的 make_plural.h 一样
main.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <cstdlib> // 要使用 EXIT_FAILURE
#include "make_plural.h"
using namespace std;
using line_no = vector<string>::size_type;
using wmType = map<string, set<line_no>>;
using wmIter = wmType::const_iterator;
using lineIter = wmType::mapped_type::const_iterator;
vector<string> file; // 文件每行内容
wmType wm; // 单词到行号 set 的映射
string cleanup_str(const string &word) {
string ret;
for (string::const_iterator it = word.begin(); it != word.end(); ++it) {
if (!ispunct(*it))
ret += tolower(*it);
}
return ret;
}
void input_text(ifstream &is) {
string text;
while (getline(is, text)) { // 对文件中每一行
file.push_back(text); // 保存此行文本
int n = file.size() - 1; // 当前行号
istringstream line(text); // 将行文本分解为单词
string word;
while (line >> word) { // 对行中每个单词
// 将当前行号插入到其行号 set 中
// 如果单词不在 wm 中,以之为下标在 wm 中添加一项
wm[cleanup_str(word)].insert(n);
}
}
}
ostream &query_and_print(const string &sought, ostream &os) {
// 使用 find 而不是下标运算符来查找单词,避免将单词添加到 wm 中!
wmIter loc = wm.find(cleanup_str(sought));
if (loc == wm.end()) { // 未找到
os << sought << " 出现了 0 次" << endl;
} else {
wmType::mapped_type lines = loc->second; // 行号 set
os << sought << " 出现了 " << lines.size() << " 次" << endl;
for (line_no num : lines)
os << "\t(第 " << num + 1 << " 行)"
<< *(file.begin() + num) << endl;
}
return os;
}
void runQueries(ifstream &infile) {
// infile 是一个 ifstream,指向我们要查询的文件
input_text(infile); // 读入文本并建立查询 map
// 与用户交互:提示用户输入要查询的单词,完成查询并打印结果
do {
cout << "enter word to look for, or q to quit: ";
string s;
// 若遇到文件尾或用户输入了 q 时循环终止
if (!(cin >> s) || s == "q")
break;
query_and_print(s, cout) << endl;
} while (true);
}
// 程序接受唯一的命令行参数,表示文本文件名
int main(int argc, char **argv) {
// 打开要查询的文件
ifstream infile;
// 打开文件失败,程序异常退出
if (argc < 2 || !(infile.open(argv[1]), infile)) {
cerr << "No input file!" << endl;
return EXIT_FAILURE;
}
runQueries(infile);
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
文件 data 中的测试数据同练习 12.27
运行程序,程序执行结果如下所示:
enter word to look for, or q to quit: example
example 出现了 3 次
(第 3 行)example. Cplusplus
(第 4 行) Primer example,
(第 5 行) Example primer
enter word to look for, or q to quit: EXAMP.lE..
EXAMP.lE.. 出现了 3 次
(第 3 行)example. Cplusplus
(第 4 行) Primer example,
(第 5 行) Example primer
enter word to look for, or q to quit: pRimEr
pRimEr 出现了 4 次
(第 1 行)c++ primer 5th
(第 2 行)C++ Primer 5th
(第 4 行) Primer example,
(第 5 行) Example primer
enter word to look for, or q to quit: q
Process finished with exit code 0
12.29
【出题思路】
采用过程式程序设计而非面向对象的程序设计来解决这个问题,并体会两者的差异。
【解答】
循环改成如下形式即可:
do {
cout << "enter word to look for, or q to quit: ";
string s;
// 若遇到文件尾或用户输入了 q 时循环终止
if (!(cin >> s) || s == "q")
break;
query_and_print(s, cout) << endl;
} while (true);
显然,由于循环中的执行步骤是 “输入 —— 检查循环条件 —— 执行查询”,检查循环条件是中间步骤,因此,while 和 do while 没有什么差异,不会比另一个更简洁。
针对本程序,我本人更倾向 do while 的写法,因为更符合逻辑。
12.30
【出题思路】
本题综合练习已学过的知识实现文本查询程序。
【解答】
解答同练习 12.27。
12.31
【出题思路】
理解 vector 和 set 的差异。
【解答】
对这个问题而言,vector 更好。因为,虽然 vector 不会维护元素值的序,set 会维护关键字的序,但注意到,我们是逐行读取输入文本的,因此每个单词出现的行号是自然按升序加入到容器中的,不必特意用关联容器来保证行号的升序。而从性能角度,set 是基于红黑树实现的,插入操作时间复杂性为 (n 为容器中元素数目),而 vector 的 push_back 可达到常量时间。
另外,一个单词在同一行中可能出现多次。set 自然可保证关键字不重复,但对 vector 这也不成为障碍 —— 每次添加行号前与最后一个行号比较一下即可。总体性能仍然是 vector 更优。
12.32
【出题思路】
本题练习在较大程序中配合使用 StrBlob 和 StrBlobPtr 来代替用 shared_ptr 管理的 vector<string>
及迭代器。
与本题相关联的练习 12.19、12.27
【解答】
对 QueryResult.h、TextQuery.h 和 StrBlob.h 相对于 12.19 和 12.27 的程序进行如下修改:
- 在 QueryResult.h 中包含头文件 StrBlob.h。
- QueryResult 类的 file 成员改为 StrBlob 类型,相应的,构造函数的第三个参数和成员函数 get_file 的返回类型也都改为 StrBlob 类型。
- 类似的 TextQuery 类的成员 file 也改为 StrBlob 类型。
- 由于 file 不再是 shared_ptr 而是 StrBlob,TextQuery 构造函数(TextQuery.cpp)中的
file->
均改为file.
。 - 在原来的代码中,TextQuery 构造函数动态分配了一个
vector<string>
,用其指针初始化 file 成员(shared_ptr)。但 StrBlob 类并未定义接受vector<string> *
的构造函数,因此我们在 StrBlob.h 文件中为其添加了这个构造函数,用指针参数直接初始化 data 成员(shared_ptr)。 - 在函数 print(TextQuery.cpp)中,用
file->begin()
获得了 vector 的首位置迭代器,对其进行加法操作获得了指向第 num 个 string 的迭代器,最后通过解引用获得了这个 string,将其打印出来。但 StrBlobPtr 只定义了递增和递减操作,并未定义加法运算。因此,我们为其增加了 StrBlob.h 接受一个整型参数 off 的 deref 操作,能解引用出距离当前位置 curr 偏移量为 off 的元素(但并不会修改 curr 的值)。
至此,所需要的修改进行完毕。
可以看到,我们对使用文本查询类的主程序(querymain.cpp)未进行任何修改!读者可好好体会面向对象程序设计将接口和实现分离的优点。
程序如下所示:
QueryResult.h
#ifndef TEST_QUERYRESULT_H
#define TEST_QUERYRESULT_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <set>
using std::set;
#include <iostream>
using std::ostream;
// new add code
#include "StrBlob.h"
class QueryResult {
friend ostream &print(ostream &, const QueryResult &);
public:
typedef vector<string>::size_type line_no;
typedef set<line_no>::const_iterator line_it;
// new add code
// QueryResult(string s,
// shared_ptr<set<line_no>> p,
// shared_ptr<vector<string>> f) :
// sought(s), lines(p), file(f) { }
QueryResult(string s,
shared_ptr<set<line_no>> p,
StrBlob f) :
sought(s), lines(p), file(f) { }
set<line_no>::size_type size( ) const { return lines->size(); }
line_it begin() const { return lines->begin(); }
line_it end() const { return lines->end(); }
// new add code
// shared_ptr<vector<string>> get_file() { return file; }
StrBlob get_file() { return file; }
private:
string sought; // word this query represents
shared_ptr<set<line_no>> lines; // lines it's on
// new add code
// shared_ptr<vector<string>> file; // input file
StrBlob file;
};
ostream &print(ostream &, const QueryResult &);
#endif //TEST_QUERYRESULT_H
make_plural.h
#ifndef TEST_MAKE_PLURAL_H
#define TEST_MAKE_PLURAL_H
#include <cstddef>
using std::size_t;
#include <string>
using std::string;
#include <iostream>
using std::cout; using std::endl;
// return the plural version of word if ctr is greater than 1
inline string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
#endif //TEST_MAKE_PLURAL_H
TextQuery.h
#ifndef TEST_TEXTQUERY_H
#define TEST_TEXTQUERY_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <fstream>
using std::ifstream;
#include "QueryResult.h"
/* this version of the query classes includes two
* members not covered in the book:
* cleanup_str: which removes punctuation and
* convert all text to lowercase
* display_map: a debugging routine that will print the contents
* of the lookup map
*/
class QueryResult; // declaration needed for return type in the query function
class TextQuery {
public:
typedef vector<string>::size_type line_no;
TextQuery(ifstream&);
QueryResult query(const string&) const;
void display_map(); // debugging aid: print the map
private:
// new add code
// shared_ptr<vector<string>> file; // input file
StrBlob file;
// maps each word to the set of the lines in which that word appears
map<string, shared_ptr<set<line_no>>> wm;
// canonicalizes text: removes punctuation and makes everything lower case
static string cleanup_str(const string&);
};
#endif //TEST_TEXTQUERY_H
TextQuery.cpp
#include "TextQuery.h"
#include "make_plural.h"
#include <cstddef>
using std::size_t;
#include <memory>
using std::shared_ptr;
#include <sstream>
using std::istringstream;
#include <string>
using std::string;
using std::getline;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <iostream>
using std::cerr; using std::cout; using std::cin;
using std::endl; using std::ostream;
#include <fstream>
using std::ifstream;
#include <cctype>
using std::ispunct; using std::tolower;
#include <cstring>
using std::strlen;
#include <utility>
using std::pair;
// read the input file and build the map of lines to line numbers
TextQuery::TextQuery(ifstream &is) : file(new vector<string>) {
string text;
while (getline(is, text)) { // for each line in the file
file.push_back(text); // remember this line of text
int n = file.size() - 1; // the current line number
istringstream line(text); // separate the line into words
string word;
while (line >> word) { // for each word in that line
word = cleanup_str(word);
// if word isn't already in wm, subscripting adds a new entry
auto &lines = wm[word]; // lines is a shared_ptr
if (!lines) // that pointer is null the first time we see word
lines.reset(new set<line_no>); // allocate a new set
lines->insert(n); // insert this line number
}
}
}
// not covered in the book -- cleanup_str removes
// punctuation and converts all text to lowercase so that
// the queries operate in a case insensitive manner
string TextQuery::cleanup_str(const string &word) {
string ret;
for (auto it = word.begin(); it != word.end(); ++it) {
if (!ispunct(*it))
ret += tolower(*it);
}
return ret;
}
QueryResult TextQuery::query(const string &sought) const {
// we'll return a pointer to this set if we don't find sought
static shared_ptr<set<line_no>> nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm!
// cleanup_str removes punctuation and convert sought to lowercase
auto loc = wm.find(cleanup_str(sought));
if (loc == wm.end())
return QueryResult(sought, nodata, file); // not found
else
return QueryResult(sought, loc->second, file);
}
ostream &print(ostream &os, const QueryResult &qr) {
// if the word was found, print the count and all occurrences
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") << endl;
// print each line in which the word appeared
for (auto num : *qr.lines)
// don't confound the user with text lines starting at 0
os << "\t(line " << num + 1 << ") "
<< qr.file.begin().deref(num) << endl;
return os;
}
// debugging routine, not covered in the book
void TextQuery::display_map() {
auto iter = wm.begin(), iter_end = wm.end();
// for each word in the map
for ( ; iter != iter_end; ++iter) {
cout << "word: " << iter->first << "{";
// fetch location vector as a const reference to avoid copying it
auto text_locs = iter->second;
auto loc_iter = text_locs->begin(), loc_iter_end = text_locs->end();
// print all line numbers for this word
while (loc_iter != loc_iter_end) {
cout << *loc_iter;
if (++loc_iter != loc_iter_end)
cout << ", ";
}
cout << "}\n"; // end list of output this word
}
cout << endl; // finished printing entire map
}
StrBlob.h
#ifndef TEST_STRBLOB_H
#define TEST_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::string;
using std::vector;
using std::initializer_list;
using std::shared_ptr;
using std::weak_ptr;
using std::make_shared;
using std::out_of_range;
using std::runtime_error;
// 对于 StrBlob 中的友元声明来说,此前置声明是必要的
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> il);
// new add code
StrBlob(vector<string> *p);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const string &t) { data->push_back(t); }
void pop_back();
// 元素访问
string& front();
const string& front() const;
string& back();
const string& back() const;
// 提供给 StrBlobPtr 的接口
// 返回指向首元素和尾后元素的 StrBlobPtr
StrBlobPtr begin() const; // 定义 StrBlobPtr 后才能定义这两个函数
StrBlobPtr end() const;
private:
shared_ptr<vector<string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const string &msg) const;
};
inline StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
inline StrBlob::StrBlob(initializer_list <string> il) :
data(make_shared<vector<string>>(il)) { }
// new add code
inline StrBlob::StrBlob(vector<string> *p) : data(p) { }
inline void StrBlob::check(vector<string>::size_type i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
inline string& StrBlob::front() {
// 如果 vector 为空,check 会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
// const 版本 front
inline const string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
inline string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
// const 版本 back
inline const string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
inline void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
class StrBlobPtr {
friend bool eq(const StrBlobPtr &, const StrBlobPtr &);
public:
StrBlobPtr() : curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
StrBlobPtr(const StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
string& deref() const;
// new add code
string& deref(int off) const;
StrBlobPtr& incr(); // 前缀递增
StrBlobPtr& decr(); // 后缀递减
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
// 保存一个 weak_ptr,意味着底层 vector 可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr; // 在数组中的当前位置
};
inline shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const {
auto ret = wptr.lock(); // vector 还存在吗?
if (!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret; // 否则,返回指向 vector 的 shared_ptr
}
inline string& StrBlobPtr::deref() const {
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*P) 是对象所指向的 vector
}
// new add code
inline string& StrBlobPtr::deref(int off) const {
auto p = check(curr + off, "dereference past end");
return (*p)[curr + off];// (*P) 是对象所指向的 vector
}
// 前缀递增:返回递增后的对象的引用
inline StrBlobPtr& StrBlobPtr::incr() {
// 如果 curr 已经指向容器的尾后位置,就不能递增它
check(curr, "increment past end of StrBlobPtr");
++curr; // 推进当前位置
return *this;
}
// 前缀递减:返回递减后的对象的引用
inline StrBlobPtr& StrBlobPtr::decr() {
// 如果 curr 已经为 0,递减它会产生一个非法下标
--curr; // 递减当前位置
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
// StrBlob 的 begin 和 end 成员的定义
inline StrBlobPtr StrBlob::begin() const {
return StrBlobPtr(*this);
}
inline StrBlobPtr StrBlob::end() const {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
// StrBlobPtr 的比较操作
inline bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
// 若底层的 vector 是同一个
if (l == r)
// 则两个指针都是空,或者指向相同元素时,它们相等
return (!r || lhs.curr == rhs.curr);
else
return false; // 若指向不同 vector,则不可能相等
}
inline bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
return !eq(lhs, rhs);
}
#endif //TEST_STRBLOB_H
querymain.cpp
#include <string>
using std::string;
#include <fstream>
using std::ifstream;
#include <iostream>
using std::cin; using std::cout; using std::cerr;
using std::endl;
#include <cstdlib> // for EXIT_FAILURE
#include "TextQuery.h"
void runQueries(ifstream &infile) {
// infile is an ifstream that is the file we want to query
TextQuery tq(infile); // store the file and build the query map
// iterate with the user: prompt for a word to find and print results
while (true) {
cout << "enter word to look for, or q to quit: ";
string s;
// stop if we hit end-of-file on the input or if a 'q' is entered
if (!(cin >> s) || s == "q")
break;
// run the query and print the results
print(cout, tq.query(s)) << endl;
}
}
// program takes single argument specifying the file to query
int main(int argc, char **argv) {
// open the file from which user will query words
ifstream infile;
// open returns void, so we use the comma operator XREF(commaOp)
// to check the state of infile after the open
if (argc < 2 || !(infile.open(argv[1]), infile)) {
cerr << "No input file!" << endl;
return EXIT_FAILURE;
}
runQueries(infile);
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
并在文件 data 中写入如下内容:
c++ primer 5th
C++ Primer 5th
example. Cplusplus
Primer example,
Example primer
运行程序,程序执行结果如下所示:
enter word to look for, or q to quit: example
example occurs 3 times
(line 3) example. Cplusplus
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: EXAMP.lE..
EXAMP.lE.. occurs 3 times
(line 3) example. Cplusplus
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: pRimEr
pRimEr occurs 4 times
(line 1) c++ primer 5th
(line 2) C++ Primer 5th
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: q
Process finished with exit code 0
注:
-
我在自己的 IDE(CLion)编译程序时,报告如下错误:
Undefined symbols for architecture x86_64: ... ...
将 CLion 的 CMakeList.txt(这是我的内容,对应改最后一行即可):
cmake_minimum_required(VERSION 3.14) project(test) set(CMAKE_CXX_STANDARD 11) add_executable(test querymain.cpp)
改为:
cmake_minimum_required(VERSION 3.14) project(test) set(CMAKE_CXX_STANDARD 11) add_executable(test querymain.cpp TextQuery.cpp)
即,将
.cpp
实现文件都添加到最后一行。然后再编译就无错误了。 -
若报如下所示的错误:
duplicate symbol xxxxxxx: ... ...
可参考此帖子解决,我这里采用的是 Solution 2,也就是将 StrBlob.h 文件中的函数都定义成内联函数(inline)。
12.33
【出题思路】
本题练习在较大的类中添加迭代器等功能。
【解答】
这些功能的实现非常简单。
对于 begin 和 end 成员,希望返回行号 set 中的位置,因此直接调用 lines 的 cbegin 和 cend 即可。
对于 get_file,直接返回 file 成员即可。
练习 12.27 的 QueryResult.h 已经实现了这些功能,这里不再重复列出代码。
评论区