C++Primer 5th:第十二章 动态内存

本文最后更新于:2021年5月13日

十二章 动态内存

  1. 静态内存:保存局部static对象、类static数据成员和定义在任何函数之外的变量
  2. 栈内存:保存定义在函数内的非static对象
  3. 分配在静态内存栈内存中的对象由编译器自动创建和销毁
  4. 栈对象:仅在其定义的程序块运行时才存在
  5. static对象:在使用之前分配,程序结束时销毁
  6. 堆(heap):自由空间(free store),存储动态分配的对象(在程序运行时分配的对象)
  7. 当动态对象不再使用时,必须显式销毁

12.1 动态内存与智能指针

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

  • 智能指针

    • 行为类似常规指针,区别是它负责自动释放所指向的对象
    • 头文件:memory
    • shared_ptr:允许多个指针指向同一个对象
    • unique_ptr:独占所指的对象
    • weak_ptr:伴随类,弱引用,指向shared_ptr所管理的对象

12.1.1 shared_ptr类

  • 智能指针也是模板
  • 默认初始化的指针中保存着一个空指针
1
2
3
4
5
6
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
//如果p1不为空,检查它是否指向一个空string
if(p1 && p1->empty()){
*p1 = "hi";
}

1. make_shared 函数

  • 头文件:memory
  • 最安全的分配和使用动态内存的方法
1
2
3
4
5
6
7
8
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//p4指向一个“9999999999”的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>();
//p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vevtor<string>>();

2. shared_ptr的拷贝和赋值

  • 引用计数(reference count)
    • 每个shared_ptr都会记录有多少个shared_ptr指向相同的对象
  • 递增
    • 用一个shared_ptr初始化另一个shared_ptr
    • 将它作为参数传递给一个函数,作为函数的返回值
  • 递减
    • 给shared_ptr赋予一个新值
    • shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)
  • 一旦计数器变为0,自动释放所管理的对象
1
2
3
4
5
6
7
8
auto p = make_shared<int>(42);	//p 指向的对象只有 p 一个引用者
auto q(p); //p 和 q 指向相同对象,此对象有两个引用者

auto r = make_shared<int>(42); //r 指向的 int 只有一个引用者
r = q; //给 r 赋值,令它指向另一个地址
//递增 q 指向的对象的引用计数
//递减 r 原来指向的对象的引用计数
//r 原来指向的对象已没有引用者,会自动释放

3. shared_ptr自动销毁所管理的对象,自动释放相关联的内存

  • 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。

4. 使用了动态生存期的资源的类

程序使用动态内存的原因:

  • 程序不知道自己需要使用多少对象(比如容器类)
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据

当拷贝一个vector时,原vector和副本vector中的元素相互分离:

1
2
3
4
5
6
vector<string> v1;
{//新作用域
vector<string> v2 = {"a", "an", "the"};
v1 = v2;
}//v2被销毁,其中的元素也被销毁
//v1有三个元素,是原来v2中元素的拷贝

PS: 使用动态内存的一个常见原因:允许多个对象共享相同的状态。

5. 定义StrBlob类

  • 类成员函数声明为const 以表明它们不修改类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const std::string& t) { data->push_back(t); }
void pop_back();
//元素访问
std::string& front();
std::string& back();
private:
std::shared_ptr<std::vector<std::string>> data;
// 如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string& msg) const;
};

6. Strblob构造函数

1
2
StrBlob::StrBlob() : data(make_shared<vector<string>>()){}
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(make_shared<vector<string>>(il)){}

接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数,此构造函数通过拷贝列表中的值来初始化vector的元素。

7. 元素访问成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void StrBlob::check(size_type i, const std::string& msg) const {
if(i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front(){
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back(){
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back(){
check(0,"pop_back on empty StrBlob");
data->pop_back();
}

12.1.2 直接内存管理

1. 使用new动态分配和初始化对象

  • 值初始化的内置类型对象有着良好定义的值
  • 默认初始化的对象的值是未定义的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int *pi = new int;	//pi指向一个动态分配的、未初始化的无名对象
string *ps = new string; //初始化为空 string
int *pi = new int(1024); //pi指向的对象的值为1024
string *ps = new string(10, '9');

vector<int> *pv = new vector<int>{0,1,2,3,4,5}; //新标准,列表初始化

string *ps1 = new string; //默认初始化为空string
string *ps = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化,*pi1的值未定义
int *pi2 = new int(); //值初始化为0,*pi2的值为0

// 只有当括号中仅有单一初始化器时才可以使用 auto
auto p1 = new auto(obj);
//auto p2 = new auto(a,b,c); //错误

2. 动态分配的 const 对象

  • 一个动态分配内存的const对象必须进行初始化

  • 对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化

  • 其他类型的对象必须显示初始化
1
2
const int *pci = new const int(1024);
const string *pcs = new const string;

3. 内存耗尽

  • 默认情况,如果 new 不能分配所需要的内存空间,会抛出一个类型为bad_alloc的异常
  • bad_allocnothrow定义在头文件new
1
2
3
int *p1 = new int;	//如果分配失败,new抛出std::bad_alloc
//定位new (placement new)
int *p2 = new (nothrow) int; //如果分配失败,new 返回一个空指针

4. 释放动态内存

  • delete
  • 销毁给定的指针指向的对象;释放对应的内存
1
delete p;	//p 必须指向一个动态分配的对象或是一个空指针

5. 指针值和 delete

  • 传递给delete的指针必须指向动态分配的内存或一个空指针
1
2
3
4
5
6
7
8
9
10
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; //错误:i不是一个指针
delete pi1; //未定义:pi1指向一个局部变量
delete pd; //正确
delete pd2; //未定义:pd2指向的内存已经被释放
delete pi2; //正确

const int *pci = new const int(1024);
delete pci; //正确:释放一个const对象

6. 动态对象的生存期直到被释放时为止

  • 有内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
1
2
3
4
5
6
7
8
9
// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg){
//视情况处理arg
return new Foo(arg);//调用者负责释放此内存
}
void use_factory(T arg){
Foo *p = factory(arg);
//使用p但是不delete它
}//p离开了它的作用域,但它所指向的内存没有被释放
  • 正确方法:在 use_factory 中释放内存
1
2
3
4
void use_factory(T arg){
Foo *p = factory(arg);
delete p;
}
  • 当其他代码要使用use_factory所分配的对象,修改函数让它返回一个指针,指向它分配的内存
1
2
3
4
Foo* use_factory(T arg){
Foo *p = factory(arg);
return p; //调用者必须释放内存
}

6. 使用newdelete管理动态内存存在的三个常见问题:

  • 忘记delete内存,导致内存泄露
  • 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误
  • 同一块内存释放两次

PS: 坚持只是用`智能指针`可以避免所有这些问题。对于一块内存,只有在设有任何智能指针指向它的情况下,智能指针才会自动释放他。

7. delete之后重置指针值,但这只提供了有限的保护

1
2
3
4
int *p(new int(42));	//p 指向动态内存
auto q = p; //p 和 q 指向相同的内存
delete p; //p 和 q 均变为无效
p = nullptr; //指出 p 不再绑定到任何对象

注意:此时 q 变为了空悬指针(dangling pointer),指向一块曾经保存数据对象但现在已经无效的内存的指针。

12.1.3 shared_ptr 和new 结合使用

  • 接受指针参数的智能指针构造函数是 explicit
  • explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
  • 不能将一个内置指针隐式转换为一个智能指针
1
2
//shared_ptr<int> p1 = new int(1024);	//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); //正确:使用了直接初始化形式
  • 一个返回shared_ptr的函数不能在其返回语句中隐式转换一个智能指针
1
2
3
4
5
6
7
8
//错误:隐式转换为 shared_ptr<int>
//shared_ptr<int> clone(int p){
// return new int(p);
//}

shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p));
}

1. 不要混合使用普通指针和智能指针

  • 使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁

2. 不要使用get初始化另一个智能指针或为智能指针赋值

  • get函数:返回一个内置指针,指向智能指针管理的对象
  • 使用的情况:需要向不能使用智能指针的代码传递一个内置指针
  • 使用get返回的指针的代码不能delete此指针
  • get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get
  • 永远不要用get初始化另一个智能指针或者为另一个智能指针赋值

  • 将另一个智能指针也绑定到get返回的指针上是错误的

1
2
3
4
5
6
7
shared_ptr<int> p(new int(42));
int *q = p.get();
{
//未定义:两个独立的shared_ptr指向相同的内存
shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;//未定义:p指向的内存已经被释放了

3. 其他shared_ptr操作

  • reset来将一个新的指针赋予一个shared_ptr
1
2
p = new int(1024);	//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024)); //p 指向一个新对象
  • unique一起使用,控制多个shared_ptr共享的对象
1
2
3
if(!p.unique())
p.reset(new string(*p)); //我们不是唯一用户;分配新的拷贝
*p += newVal; //现在知道我们是唯一的用户,可以改变对象的值

12.1.4 智能指针和异常

  • 使用异常处理的程序能在异常发生后令程序流程继续,这种程序需要确保在异常发生后资源被正确的释放

  • 一个简单的确保资源被释放的方法是使用智能指针

1. 智能指针和哑类

  • 为 C 和 C++ 两种语言设计的类,通常都要求用户显式地释放所使用地任何资源
1
2
3
4
5
6
7
8
9
10
struct destination;	//表示我们正在连接什么
struct connection; //使用连接所需的信息
connection connect(destination*);//打开连接
void disconnect(connection);//关闭给定连接
void f(destination &d){
//获得一个连接,记住使用完要关闭它
connection c = connect(&d);
//使用连接
//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}

2. 使用我们自己的释放操作

  • 定义一个删除器(deleter)函数完成对shared_ptr中保存地指针进行释放操作
1
2
3
4
5
6
7
8
void end_connection(connection *p) {disconnect(*p);}
void f(destination &d /* 其他参数 */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//使用连接
//当 f 退出时(即使由于异常退出),connection 会被正确关闭
}

3. 正确使用智能指针的基本规范

  • 不使用相同的内置指针初始化(或 reset)多个智能指针
  • 不 delete get()返回的指针
  • 不使用 get() 初始化或 reset 另一个智能指针
  • 如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变成无效了。
  • 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器

12.1.5 unique_ptr

  • 某个时刻只能有一个unique_ptr指向一个给定的对象

  • unique_ptr不支持普通的拷贝或赋值操作

  • 可以通过releasereset将指针的所有权从一个(非cosnt)unique_ptr 转移给另一个 unique

  • release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
unique_ptr<string> p1(new string("Stegosaurus"));	//p2指向一个值为42的int
//unique_ptr<string> p2(p1); //错误
unique_ptr<string> p3;
//p3 = p2; //错误

//将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); //release 将 p1 置空
unique_ptr<string> p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); //reset 释放了p2原来指向的内存

p2.release(); //错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); //正确,但必须记得delete(p)

1. 传递unique_ptr参数和返回unique_ptr

例外:可以拷贝或赋值一个将要被销毁的unique_ptr,例如从函数返回一个unique_ptr

1
2
3
4
5
6
7
8
9
10
//从函数返回一个unique_ptr
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
//返回一个局部对象的拷贝
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
//...
return ret;
}

2. 向unique_ptr传递删除器

  • dectype返回一个函数类型,添加一个*来指出我们正在使用该类型的一个指针
1
2
3
4
5
6
7
8
9
10
11
12
//p 指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT对象
unique_ptr<objT, delT> p(new objT, fcn);

void f(destination &d /* 其他需要的参数 */)
{
connection c = connect(&d); //打开连接
//当p被销毁时,连接将会关闭
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
//使用连接
//当 f 退出时(即使由于异常退出),connection 会被正确关闭
}

12.1.6 weak_ptr

  • 指向由一个shared_ptr管理的对象
  • 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
  • 要用一个shared_ptr来初始化一个weak_ptr
  • 由于对象可能不存在,不能使用weak_ptr直接访问对象,必须调用lock函数
1
2
3
4
5
6
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);

if(shared_ptr<int> np = wp.lock()){ //如果 np 不为空则条件成立
//在 if 中,np 与 p 共享对象
}

1. 检查指针类

  • weak_ptr的用途:阻止用户访问一个不再存在的vector的企图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//对于访问一个不存在元素的尝试,StrBlobPtr 抛出一个异常
class StrBlobPtr{
public:
StrBlobPtr(): curr(0) { }
StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
std::string& deref() const;
StrBlobPtr& incr(); //前缀递增
private:
//若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
//保存一个weak_ptr,意味着底层vector可能会被销毁
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; //在数组中的当前位置
}

std::shared_ptr<std::vector<std::string>> check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock(); //vector还存在吗
if(!ret)
throw std::runtime_error("unbound StrBlobPtr");
if(i >= ret->size())
throw std::out_of_range(msg);
return ret; //否则,返回指向vector的share_ptr
}

2. 指针操作

  • 定义函数deref 用来解引用StrBlobPtr
  • incur递增StrBlobPtr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::string& StrBlobPtr::deref() cosnt
{
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;
}

class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;

StrBlobPtr begin() { return StrBlobPtr(*this);}
StrBlobPtr end()
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
}

12.2 动态数组

  • 大多数应用应该使用标准库容器而不是动态分配的数组。
  • 使用容器更为简单、更不容器出现内存管理错误并且有可能有更好的性能。
  • 参考:StrBlob类

12.2.1 new 和数组

  • 方括号中的大小必须是整型,但不必是常量
1
2
3
4
5
6
int *pia = new int[get_size()];	//pia 指向第一个 int

typedef int arrT[42]; //arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组;p指向第一个 int
//编译器执行如下形式
int *p = new int[42];

1. 分配一个数组会得到一个元素类型的指针

  • 通常称new T[]分配的内存为“动态数组”

  • 动态数组并不是数组类型

  • 不能对动态数组调用begin或end

2. 初始化动态分配对象的数组

  • new 表达式失败会抛出一个类型为bad_array_new_length的异常
  • 头文件:new
1
2
3
4
int *pia = new int[10];		//10个未初始化的int
int *pia2 = new int[10](); //值初始化:10个值初始化为0的int
//新标准
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

3. 动态分配一个空数组是合法的

1
2
3
4
5
6
7
8
size_t n = get_size();
int* p = new int[n];
for(int* q = p; q != p+n; ++q){
//处理数组
}

//char arr[0]; //错误:不能定义长度为0的数组
char *cp = new char[0]; //正确:但cp不能解引用

4. 释放动态数组

  • 数组中的元素按逆序销毁
1
2
3
4
5
6
delete p;		//p必须指向一个动态分配的对象或为空
delete [] pa; //pa必须指向一个动态分配的数组或为空

typedef int arrT[42]; //arrT是42个int的数组的类型别名
int *p = new arrT; //分配一个42个int的数组,p指向第一个元素
delete [] p; //方括号是必须的

5. 智能指针和动态数组

  • unique_ptr
    • 不能使用点和箭头成员运算符
    • 可以使用下标运算符来访问数组中的元素
1
2
3
4
5
unique_ptr<int[]> up(new int[10]);	//up指向一个包含10个未初始化int的数组
up.release(); //自动用 delete[] 销毁其指针

for(size_t i = 0; i != 10; ++i)
up[i] = i;
  • shared_ptr
    • 必须提供自己定义的删除器
1
2
3
4
5
shared_ptr<int> sp(new int[10], [](int *p){delete [] p; });
sp.reset();
//shared_ptr未定义下标运算符,并且不支持指针的算数运算
for(size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; //使用get获取一个内置指针

12.2.2 allocator类

  • 分配单个对象时,希望将内存分配和对象初始化组合在一起
  • 分配一大块内存时,通常计划在这块内存上按需构造对象,此时希望将内存分配和对象构造分开

将内存分配和对象构造组合在一起可能会导致不必要的浪费

1
2
3
4
5
6
7
string *const p = new string[n];
string s;
string *q = p;
while(cin >> s && q != p+n)
*q++ = s;
const size_t size = q-p;
delete [] p;
  • 没有默认构造函数的类不能动态分配数组

1. allocator类

  • 头文件:memory
  • 帮助我们将内存分配和对象构造分离开
  • 提供一种类型感知的内存分配方法
  • 分配的内存是原始的、未构造的
1
2
allocator<string> alloc;			//可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n); //分配 n 个未初始化的 string

2. allocator 分配未构造的内存

  • 为了使用 allocate 返回的内存,必须用construct构造对象
  • 使用未构造的内存,其行为是未定义的
  • 函数destory接受一个指针,对指向的对象执行析构函数
  • 释放内存通过调用deallocate来完成
1
2
3
4
5
6
7
8
9
10
11
12
auto q = p;	//q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++, 10, 'c'); //*q为 cccccccccc
alloc.construct(q++, "hi"); //*q为hi

cout << *p << endl; //正确:使用 string 的输出运算符
//cout << *q << endl; //灾难:q 指向未构造的内存!

while(q != p)
alloc.destory(--q); //释放真正构造的string

alloc.deallocate(p, n);

3. 拷贝和填充未初始化内存的算法

1
2
3
4
5
6
//分配比 vi 中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
//通过拷贝 vi 中的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//将剩余元素初始化为42
uninitialzed_fill_n(q, vi.size(), 42);

12.3 使用标准库:文本查询程序

  • 允许用户在一个给定文件中查询单词
  • 查询结果是单词在文件中出现的次数及其所在行的列表
  • 如果一个单词在一行中多次出现,此行只列一次
  • 行按照升序输出

12.3.1 文本查询程序设计

  1. 数据结构
    • 保存输入文件的类TextQuery
    • vector保存输入文件的文本
    • map关联每个单词和它出现的行号的set
    • QueryResult:保存查询结果,完成结果打印工作
  2. 在类之间共享数据
    • shared_ptr
  3. 使用TextQuery
    • 当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void runQueries(ifstream &infile)
{
// infile 是一个 ifstream,指向我们要处理的文件
TextQuery tq(infile); //保存文件并建立查询map
//与用户交互:提示用户输入要查询的单词,完成查询并打印结果
while(true){
cout << "enter word to look for ,or q to quit:";
string s;
//若遇到文件尾或用户输入了'q'时循环终止
if(!(cin >> s) || s == "q") break;
//指向查询并打印结果
print(cout, tq.query(s)) << endl;
}
}

12.3.2 文本查询类的定义

1
2
3
4
5
6
7
8
9
10
11
class QueryResult;	//为了定义函数 query 的返回类型,这个定义是必需的
class TextQuery {
public:

TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
std::shared_ptr<std::vector<std::string>> file; //输入文件
//每个单词到它所在的行号的集合的映射
std::map < std::string, shared_ptr<std::set<line_no>>> wm;
};
  1. TextQuery 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TextQuery::TextQuery(ifstream& is) : file(new vector<string>)
{
string text;
while (getline(is, text)) //对文件中的每一行
{
file->push_back(text); //保存此行文本
int n = file->size() - 1; //当前行号
istringstream line(text); //将行文本分解为单词
string word;
while (line >> word) { //对行中每个单词
//如果单词不再wm中,以之为下标在wm中添加一项
auto& lines = wm[word]; //lines是一个shared_ptr
if (!lines) //在我们第一次遇到这个单词时,此指针为空
lines.reset(new set<line_no>); //分配一个新的set
lines->insert(n); //将此行号插入set中
}
}
}
  1. QueryResult类
1
2
3
4
5
6
7
8
9
10
class QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string s, std::shared_ptr<std::set<line_no>>p, std::shared_ptr<std::vector<std::string>> f):
sought(s), lines(p),file(f){}
private:
std::string sought; //查询单词
std::shared_ptr<std::set<line_no>> lines; //出现的行号
std::shared_ptr<std::vector<std::string>> file; //输入文件
};
  1. query函数
1
2
3
4
5
6
7
8
9
10
11
QueryResult TextQuery::query(const string& sought) const
{
//如果未找到sought,返回一个指向此 set 的指针
static shared_ptr<set<line_no>> nodata(new set<line_no>);
//使用find而不是下标运算来查找单词,避免将单词添加到wm中
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file); //未找到
else
return QueryResult(sought, loc->second, file);
}
  1. 打印结果
1
2
3
4
5
6
7
8
9
10
11
ostream& print(ostream& os, const QueryResult& qr)
{
//如果找到了单词,打印出现次数和所有出现的位置
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "times", "s") << endl;
//打印单词出现的每一行
for (auto num : *qr.lines) //对 set 中每个单词
//避免行号从0开始给用户带来困惑
os << "\t(line " << num + 1 << " )" << *(qr.file->begin() + num) << endl;
return os;
}