《C++Primer》第十二章——动态内存

本文详细介绍了C++中的动态内存管理,包括智能指针shared_ptr和unique_ptr的使用,以及动态数组的分配与释放。强调了智能指针在防止内存泄漏和空悬指针问题上的重要作用,同时讲解了动态数组的new运算符语法和释放规则。此外,还提到了allocator类在分离内存分配与对象构造中的作用。

第十二章:动态内存

静态内存区(全局区):保存局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量,分配在静态内存的对象由编译器自动创建和销毁,static 对象在使用之前分配,在程序结束时销毁
栈内存区:用来保存定义在函数内的非 static 对象,分配在栈内存的对象由编译器自动创建和销毁,对于栈对象,仅在其定义的程序块运行时才存在
堆内存区:程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象,动态对象的生存期由程序来控制,当动态对象不再使用时,我们的代码必须显式地销毁它们

12.1 动态内存与智能指针

为了更容易及安全的使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象
智能指针的行为类似常规指针,区别在于它负责自动释放所指向的对象
这两种智能指针的区别在于管理底层指针的方式:

  • shared_ptr 允许多个指针指向同一个对象
  • unique_ptr 独占所指向的对象
    标准库还定义了一个名为 weak_ptr 的伴随类,这是一种弱引用,指向 shared_ptr 所管理的对象
    注:这三种类型都定义在 memory 头文件中
    1. shared_ptr 类
    1)make_shared 函数:最安全的分配和使用动态内存的方式是调用一个名为 make_shared 的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
    make_shared(args)
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
  • 调用 make_shared 函数时,传递的参数必须与其给定类型的某个构造函数相匹配
  • 如果我们不传递任何参数,对象就会进行值初始化
    通常,我们使用 auto 定义一个对象来保存 make_shared 的结果,这样比较简单
auto p6 = make_shared<vector<string>>();

2)shared_ptr 的拷贝和赋值:每个 shared_ptr 都有一个关联的计数器,通常称之为引用计数
引用计数递增的情况:

  • 当用一个 shared_ptr 初始化另一个 shared_ptr
  • 将一个 shared_ptr 作为参数传递给一个函数
  • 将一个 shared_ptr 作为函数的返回值
    引用计数递减的情况:
  • 当给 shared_ptr 赋予一个新值
  • shared_ptr 被销毁(例如,一个局部的 shared_ptr 离开其作用域)
    当一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象
    3)shared_ptr 自动销毁所管理的对象并自动释放相关联的内存
  • shared_ptr 的析构函数递减它所指向的对象的引用计数,若引用计数变为 0,shared_ptr 的析构函数会销毁对象并释放它占用的内存
  • 若将 shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素
    4)使用动态生存期的资源的类
    程序使用动态内存主要有以下三个原因
  • 程序不知道自己需要使用多少对象(容器类)
  • 程序不知道所需对象的准确类型(第15章)
  • 程序需要在多个对象间共享数据,允许多个对象共享相同的状态(下面的例子)
    例:定义一个名为 Blob 的类,保存一组元素,并希望 Blob 对象的不同拷贝之间共享相同的元素
Blob<string> b1;	//空 Blob
{
	Blob<string> b2 = {"a", "an", "the"};
	b1 = b2;	//b1 和 b2共享相同的元素
}	//b2 被销毁了,但是 b2 中的内容没有销毁
	//b1 指向最初由 b2 创建的元素

2.直接管理内存
C++定义了两个运算符来分配和释放内存,运算符 new 分配内存,delete 释放 new 分配的内存
1)使用 new 动态分配和初始化对象

  • 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将使用默认构造函数进行初始化
int *pi = new int;	//pi指向一个未初始化的int
string *ps = new string;	//初始化为空string
  • 可以使用直接初始化方式来初始化一个动态分配的对象,也可以使用传统的构造方式(使用圆括号),也可以使用列表初始化(花括号)
int *pi = new int(1024);		//直接初始化
string *ps = new string(10, '9');	//传统的构造方式
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4};	//使用列表初始化
  • 也可以对动态分配的对象进行值初始化,只需在类名之后跟一对空括号
string *ps1 = new string;	//默认初始化为空string
string *ps = new string();	//值初始化为空string
int *pi1 = new int;			//默认初始化,*pi1的值未定义
int *pi2 = new int();		//值初始化为0,*pi2为0

2)动态分配的 const 对象
类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化,对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须进行显式初始化

//分配并初始化一个 const int
const int *pi = new const int(1024);	//显式初始化
//分配并默认初始化一个 const 的空 string
const string *pcs = new const string;	//隐式初始化

由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针
3)内存耗尽(p 408)
4)释放动态内存

  • 通过 delete 表达式来将动态内存归还给系统,delete 表达式接受一个指针指向我们想要释放的对象
  • delete 表达式执行两个动作:销毁给定的指针指向的对象、释放对应的内存
  • 由内置指针(而不是智能指针)管理的动态内存在被显示释放之前一直都会存在
    5)使用 new 和 delete 管理动态内存存在的三个常见问题
  • 忘记 delete 内存,这回导致内存泄露问题
  • 使用已经释放掉的对象
  • 同一块内存释放两次
    坚持只使用智能指针,可以避免所有这些问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它
    6)delete 之后重置指针值,但也只提供有限的保护
  • 空悬指针:当 delete 一个指针后,指针值就变为无效了,虽然指针无效但在很多容器上指针仍然保存着已经释放了的动态内存的地址,即指向一块曾经保存数据对象但现在已经无效的内存的指针
  • 为避免空悬指针问题,在指针即将离开其作用域之前释放掉它所关联的内存,这样在指针关联的内存被释放后就没有机会继续使用指针了
  • 若需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样该指针不指向任何对象
  • delete 内存之后重置指针的方法只对这个指针有效,对其他任何指向(已释放的)内存的指针无效,在实际系统中,查找指向相同内存的指针是很困难的
int *p(new int(42));	//p指向动态内存
auto q = p;		//p和q指向相同的内存
delete p;		//p和q均变为无效
p = nullptr;	//指出p不再绑定到任何对象

在我们释放 p 所指向的内存时,q 也变为无效了,但重置 p 对 q 没有任何作用
3. shared_ptr 和 new 结合使用

  • 如果不初始化一个智能指针,它就会被初始化为一个空指针;我们也可以用 new 返回的指针来初始化智能指针
shared_ptr<double> p1;	// shared_ptr 可以指向一个 double
shared_ptr<int> p2(new int(24));	//p2 指向一个值为42的int
  • 接受指针参数的智能指针构造函数是 explicit 的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024);	//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));	//正确:使用了直接初始化形式
  • 由于不能进行内置指针到智能指针间的隐式转换,一个返回 shared_ptr 的函数不能在其返回语句中隐式转换一个普通指针
shared_ptr<int> clone(int p){
	return new int(p);	//错误:尝试隐式转换为shared_ptr<int>,但接受指针参数的智能指针构造函数是 explicit 的
}

shared_ptr<int> clone(int p){
	return shared_ptr<int>(new int(p));	//正确:显式地用int*创建shared_ptr<int>
}

1)不要混合使用普通指针和智能指针
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁

void process(shared_ptr<int> ptr)
{
	//使用ptr
}//ptr离开作用域,被销毁

int *x(new int(1024));	
process(x);		//错误:不能将 int* 转换为一个 shared_ptr<int>
process(shared_ptr<int>(x));	//合法,但内存会被释放
int j = *x;		//未定义的,x是一个空悬指针

在上面调用中,我们将一个临时 shared_ptr 传递给 process,当这个调用结束后,这个临时对象就被销毁了,销毁该临时变量会递减引用计数,此时引用计数变为0,则所指向的内存也会被释放,x 变为空悬指针,而对一个空悬指针解引用是未定义的
2)不要使用 get 初始化另一个智能指针或为智能指针赋值

  • 智能指针类型定义了一个名为 get 的函数,它返回一个内置指针,指向智能指针管理的对象
  • 此函数的设计目的:向不能使用智能指针的代码传递一个内置指针,使用 get 返回的指针的代码不能 delete 此指针
  • get 用来将指针的访问权限传递给代码,只有在确定代码不会 delete 指针的情况下才能使用 get
  • 永远不要使用 get 初始化另一个智能指针或为另一个智能指针赋值
shared_ptr<int> p(new int(42));	//引用计数为1
int *q = p.get();	//正确,但使用q时要注意,不要让它管理的指针被释放
{//新程序块
//未定义:两个独立的 shared_ptr 指向相同的内存
shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;	//未定义:p指向的内存已经被释放了

4.智能指针和异常
1)如果使用智能指针,即使程序块结束过早,智能指针也难确保在内存不需要时将其释放
函数的退出有两种可能:正常处理结束 或 者发生了异常

void f()
{
	shared_ptr<int> sp(new int(42));	//分配一个新对象
	//这段代码抛出一个异常,且在f中未被捕获
}//在函数结束时,shared_ptr 自动释放内存

而与之相对的,如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则不会被释放

void f()
{
	int *ip = new int(42);	//动态分配一个新对象
	//这段代码抛出一个异常,且在f中未被捕获
	delete ip;	//在退出之前吃放内存
}

若在 new 和 delete 之间发生异常,且异常未在 f 中捕获,则内存就永远不会被释放了,在函数 f 之外没有指针指向这块内存,因此无法释放
2)使用我们自己的释放操作

  • 包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源,但是有些类比如那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源,对于这些分配了资源但没有定义析构函数来释放资源的类,可能会遇到与使用动态内存相同的错误——忘记释放资源,而如果在资源分配和释放之间发生了异常,程序也会发生资源泄露
struct destination;
struct connection;
connection connect(destination*);
void disconnect(connection);
void f(destination &d /*其他参数*/)
{
	//获得一个连接,记住使用完后要关闭它
	connection c = connect(&d);
	//使用连接
	//如果我们在f退出前忘记调用 disconnect,就无法关闭c了
}

若 connection 有一个析构函数,就可以在f结束时由析构函数自动关闭连接;但是,如果 connection 没有析构函数,会造成内存泄露
为解决这个问题,使用 shared_ptr 来管理一个 connection,我们首先必须定义一个函数来代替 delete,这个删除器(deleter)函数必须完成对 shared_ptr 中保存的指针进行释放操作

//自己定义的删除器deleter
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 分配的内存,记得传递一个删除器

5. unique_ptr(p 417)

  • 一个 unique_ptr 拥有它所指向的对象,与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定对象,当 unique_ptr 被销毁时,它所指向的对象也被销毁
  • 当定义一个 unique_ptr 时,需将其绑定在一个 new 返回的指针上,类似 shared_ptr,初始化 unique_ptr 必须采用直接初始化形式
unique_ptr<double> p1;		//可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42));	//p2指向一个值为42的int
  • 由于一个 unique_ptr 拥有它指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作
unique<string> p1(new string("Strgosaurus"));
unique_ptr<string> p2(p1);	//错误,unique_ptr不支持拷贝
unique_ptr<string> p3;	
p3 = p2;	//错误:unique_ptr不支持赋值
  • 通过调用成员函数 release 或 reset,可以将指针的所有权从一个(非 const)unique_ptr 转移到另一个 unique
  • release成员返回 unique_ptr 当前保存的指针并将其置空,切断 unique_ptr 和原来管理的对象间的联系,release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值,但如果我们不用另一个智能指针保存 release 返回的指针,我们的程序就要负责资源的释放
unique<string> p2(p1.release());	//release将p1置为空,并将所有权从p1转移给了p2
p2.release();	//错误,p2不会释放内存,而我们丢失了指针
auto p = p2.release();	//正确,但我们必须记得delete(p)
  • reset 成员接受一个可选的指针参数,令 unique_ptr 重新指向给定的指针。若 unique_ptr 不为空,它原来指向的对象被释放
unique_ptr<string> p3(new string("Trex"));
//将所有权从p3转移给了p2
p2.reset(p3.release);	//reset释放了p2原来指向的内存

1)传递 unique_ptr 参数和返回 unique_ptr
不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝和赋值一个将要被销毁的 unique_ptr,比如从函数返回一个 unique_ptr

unique_ptr<int> clone(int p){
	unique_ptr<int> ret(new int(p));
	//...
	return ret;
}

对于这段代码,编译器知道要返回的对象将要被销毁,在此情况,编译器执行一种特殊的“拷贝”(p 473)
2)向 unique_ptr 传递删除器
类似 shared_ptr,unique_ptr 默认情况下用 delete 释放它指向的对象,也可以重载一个 unique_ptr 中默认的删除器
但,unique_ptr 管理删除器的方式与 shared_ptr 不同(原因在16.1.6,p 599)
重载一个 unique_ptr 中的删除器会影响到 unique_ptr 类型以及如何构造该类型的对象
与重载关联容器的比较操作类似,我们必须在尖括号中 unique_ptr 指向类型之后提供删除器类型,在创建或 reset 一个这种 unique_ptr 类型的对象时,必须提供一个指定类型的可调用对象(删除器)

//p 指向一个类型为objT的对象,并使用一个类型为 delT 的对象释放 objT 对象
//它会调用一个名为 fcn 的 delT 类型对象
unique_ptr<objT, delT> p(new objT, fcn);

//一个例子
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);

6. weak_ptr

  • weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象
  • 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数
  • 一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放,即使有 weak_ptr 指向对象,对象依然会被释放

当创建一个 weak_ptr 时,要用一个 shared_ptr 来初始化

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);	//wp 弱共享 p; p的引用计数未改变

由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而是必须调用成员函数 lock,该函数检查 weak_ptr 指向的对象是否存在
如果存在,lock 返回一个指向共享对象的 shared_ptr;否则,返回一个空 shared_ptr

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

12.2 动态数组

new 和 delete 运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能
为支持这种需求,C++语言和标准库提供了两种一次分配一个对象数组的方法
1)C++定义了另一种 new 表达式语法,可以分配并初始化一个对象数组
2)标注库包含一个名为 allocator 的类,允许我们将分配和初始化分离,使用 allocator 通常会提供更好的性能和更灵活的内存管理能力

1. new 和数组

  • 为了让 new 分配一个对象数组,要在类型名之后跟一对方括号,并指明要分配的对象的数目,该数目的大小必须是整型,但不必是常量
  • 也可以使用一个表示数组类型的类型别名来分配一个数组,这样就不需要方括号了
//调用 get_size 确定分配多少个 int
int *pia = new int[get_size()];	//pia 指向第一个 int

typedef int arrT[42];	//arrT 表示42个 int 的数组类型
int *p = new arrT;		//分配一个42个 int 的数组,p 指向第一个 int

1)分配一个数组会得到一个元素类型的指针
使用 new T[ ] 分配的内存为“动态数组”,但是我们并没有得到一个数组类型的对象,而是得到一个数组元素类型的指针,即使我们使用类型别名定义一个数组类型,new 也不会分配一个数组类型的对象,new 返回的是一个元素类型的指针
由于分配的内存不是一个数组类型,就不能对动态数组调用 begin 或 end,出于相同的原因,也不能使用范围 for 语句处理动态数组中的元素
2)初始化动态分配对象的数组

  • 默认情况下,new 分配的对象,不管是单个分配的还是数组中的,都是默认初始化的
  • 通过在大小之后跟一对空括号,可以对数组中的元素进行值初始化
  • 在新标准中,我们还可以提供一个元素初始化器的花括号列表,如果初始化器数目小于元素数目,剩余元素将进行值初始化;如果初始化器数目大于元素数目,则 new 表达式失败,不会分配任何内存
int *pia = new int[10];		//10个未初始化的int
int *pia2 = new int[10]();	//10个值初始化为0的int
string *psa = new string[10];	//10个空string
string *psa2 = new string[10];	//10个空string

//10个int分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
//10个string,前4个用给定初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a", "an"};

3)动态分配一个空数组是合法的
4)释放动态数组
为了释放动态数组,我们使用一种特殊形式的 delete,在指针前加上一个空方括号对
当我们释放一个指向数组的指针时,方括号对是必须的,它只是编译器该指针指向一个对象数组的第一个元素
如果我们在 delete 一个数组的指针时忽略方括号,该行为是未定义的
当我们使用一个类型别名定义一个数组类型时,在 new 表达式中不使用 [ ],但在释放一个数组指针时,必须使用方括号

delete p;
delete [] pa;	//pa 必须指向一个动态分配的数组或为空

typedef int arrT[42];
int *p = new arrT;
delete [] p;

5)智能指针和动态数组

  • 标准库提供了一个管理 new 分配的数组的 unique_ptr 版本
    为了用一个 unique_ptr 管理动态数组,我们必须在对象类型后面跟一对空方括号
unique_ptr<int[]> up(new int[10]);
up.release();	//自动用delete[]销毁其指针

当一个 unique_ptr 指向一个数组时,我们可以使用下标运算符来访问数组中的元素

for(size_t i=0; i != 0; ++i)
	up[i] = i;	//为每个元素赋予一个新值
  • 与 unique_ptr 不同,shared_ptr 不直接支持管理动态数组,若希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器
shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});
sp.reset();	//使用我们提供的lambda释放数组,它使用delete[]

默认情况下,shared_ptr 使用 delete 销毁它指向的对象,若此对象是一个动态数组,对其使用 delete 所产生的的问题和释放一个动态数组指针时忘记 [ ] 产生的问题一样
由于 shared_ptr 未定义下标运算符怒,而且智能指针类型不支持指针算数运算,因此为了访问数组中的元素,必须用 get 获取一个内置指针,然后用它来访问数组元素

for(size_t i = 0; i != 10; ++i)
	*(sp.get() + i) = i;	//使用 get 获取一个内置指针

2. allocator 类
new 将内存分配和对象构造组合在了一起;
delete 将对象析构和内存释放组合在了一起;
分配单个对象时,我们一般都知道该对象是什么值,因此通常希望内存分配和对象初始化组合在一起
但是在分配一大块内存时,我们希望内存分配和对象构造进行分离,即我们可以分配大块内存,但只有在真正需要的时候才真正执行对象创建的操作
1)allocator 分配为构造的内存(p 428)

  • 标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来
  • allocator 分配的内存是 未构造的,我们需要在此内存中构造对象,即使用 construct 成员函数,还未构造对象的情况下就使用原始内存是错误的
  • 当我们用完对象后,必须对每个构造的元素调用 destroy 成员函数进行销毁,destroy 接受一个指针,对指向的对象执行析构函数,我们只能对真正构造了的元素进行 destroy 操作
  • 释放内存通过调用 deallocate 来完成,在调用 deallocate 之前,用户必须对每个在这块内存中创建的对象调用 destroy
    2)拷贝和填充未初始化内存的算法(p 429)

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

//TextQuery.h
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <memory>
#include <map>
#include <set>
#include <string>

class QueryResult;
class TextQuery
{
public:
	using line_no = std::vector<std::string>::size_type;
	TextQuery(std::ifstream&);
	QueryResult query(const std::string&) const;
private:
	std::shared_ptr<std::vector<std::string>> file;	//输入文件
	std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};

#endif
//TextQuery.cpp
#include "TextQuery.h"
#include "QueryResult.h"
using namespace std;

//读取输入文件并建立单词到行号的映射
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 中
		}
	}
}

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);
}
//QueryResult.h
#ifndef QUERYRESULT_H
#define QUERYRESULT_H

#include <string>
#include <set>
#include <memory>
#include <vector>
#include "TextQuery.h"
#include <iostream>

class QueryResult
{
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
	QueryResult(std::string s, std::shared_ptr<std::set<TextQuery::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<TextQuery::line_no>> lines;	//出现的行号
	std::shared_ptr<std::vector<std::string>> file;	//输入文件
};

#endif
//QueryResult.cpp
#include "QueryResult.h"
using namespace std;

//根据大小是否等于1打印 time 或 times
string make_plural(size_t ctr, const string& word, const string& ending)
{
	return (ctr > 1) ? word + ending : word;
}

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;
}
//12.3_text.cpp
#include "TextQuery.h"
#include "QueryResult.h"
using namespace std;

void runQueries(ifstream& infile)
{
	//infile 是一个 ifstream,指向我们要处理的文件
	TextQuery tq(infile);	//保存文件并建立 map
	while (true) {
		cout << "enter word to look for, or q to quit: ";
		string s;
		if (!(cin >> s) || s == "q")
			break;
		print(cout, tq.query(s)) << endl;
	}
}

int main()
{
	ifstream in("C:/Users/Lenovo/Desktop/text.txt");
	runQueries(in);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值