Skip to content

Latest commit

 

History

History
1457 lines (1058 loc) · 63 KB

动态内存.md

File metadata and controls

1457 lines (1058 loc) · 63 KB

动态内存

前面的程序只用过静态内存或者栈内存。静态内存用于保存局部static对象和类static数据成员以及定义在任何函数外的变量。栈内存用来保存在函数内的非static对象。分配在静态或者栈内存里的对象由编译器自动创建和销毁。栈对象只在其定义的程序块运行时候存在;static对象在使用前分配,程序结束时销毁。

除静态内存和栈内存,每个程序还有内存池。内存池的内存被称为自由空间(free store)或(heap)。程序用堆来存储动态分配(dynamically allocate)的对象,也就是那些在程序运行时候分配的对象。动态对象的生存期由程序控制,也就是说,当动态对象不再使用的时候,我们必须显式地销毁它们。

正确管理动态内存是件难事。

动态内存和智能指针

动态内存通过一对运算符来完成:

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

忘记释放内存会产生内存泄漏;还有指针引用内存时就释放会产生引用非法内存的指针。

C++11标准库为了更容易和更安全地使用动态内存,提供了两种智能指针(smart pointer)来管理动态对象,智能指针不同之处主要在于它负责自动释放所指向的对象。

两种智能指针的区别在于管理底层指针的方式:

  • shared_ptr允许多个指针指向同一对象
  • unique_ptr独占所指向地对象

标准库还定义了一个名为week_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

这三种类型都定义在头文件memory中。

shared_ptr类

智能指针也是模板。

创建智能指针:

shared_ptr<string> p1;				// shared_ptr 可以指向string
shared_ptr<list<int>> p2;			// shared_ptr 可以指向int的list

默认初始化的智能指针里保存一个空指针。

判断智能指针是否为空:

// 如果p1不是空的 检查它是否指向一个空string
if (p1 && p1->empty)
    *p1 = "hi";				// 如果p1指向一个空string 解引用p1 将hi赋给它

shared_ptr独有的操作:

shared_ptr独有的操作 含义
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。
使用args来初始化该对象
shared_ptr<T>p(q) pshared_ptr的拷贝;该操作会递增q中的计数器。
q中的指针必须要可以转换成T*
p = q pq都是shared_ptr,所保存的指针必须要可以互相转换。
该操作会递减p的引用计数,递增q的引用计数;
如果p的引用计数变成0,就会把p管理的原内存释放
p.unique() 如果p.use_count()1,返回true;否则false
p.use_count() 返回和p共享对象的智能指针数量;可能很慢,主要用于调试

shared_ptrunique_ptr都支持的操作:

shared_ptr和unique_ptr都支持的操作 含义
shared_ptr<T> sp
unique_ptr<T> up
空智能指针,可以指向类型是T的对象
p p作为一个条件判断,如果p指向一个对象,返回true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p.mem)
p.get() 返回p里保存的指针。谨慎使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q) 交换pq的指针
p.swap(q) 同上

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。

该函数在动态内存里分配一个对象并初始化它,返回指向该对象的shared_ptr。也是定义在头文件memory里面。

// 指向一个值为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>();

make_shared用参数来构造给定类型的对象。

通常用auto定义一个对象来保存make_shared的结果:

auto p6 = make_shared<vector<string>>();		// 指向动态分配的空vector<string>

shared_ptr的拷贝和赋值

auto p = make_shared<int>(42);					// p指向的对象只有p一个引用者
auto q(p);										// p和q指向相同对象 该对象由两个引用者

可以认为每个shared_ptr都有一个关联的计数器,通常会叫它引用计数(reference count)。

不管什么时候拷贝一个shared_ptr,计数器都会递增。比如用一个shared_ptr初始化另一个shared_ptr,或是作为参数传递给函数或是作为函数返回值。

shared_ptr赋新值或是shared_ptr被销毁(比如一个局部的string_ptr离开其作用域),计数器就会递减。

shared_ptr的计数器变成0的时候,就会自动释放自己所管理的对象:

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

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

shared_ptr类自动销毁对象是通过另一个特殊的成员函数,也就是析构函数(destructor)完成销毁工作的。

类似构造函数,每个类都有一个析构函数。构造函数控制初始化,而析构函数控制该类型的对象销毁时执行什么操作。

例如,string的构造函数会分配内存来保存string的字符。而string的析构函数就负责释放这些内存。

shared_ptr的析构函数会递减它所指向的对象的引用计数。若引用计数变成0shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

例如,我们可能有个函数,其返回一个shared_ptr,指向一个Foo类型的动态分配的对象,对象是通过T类型的参数进行初始化的:

// factory返回一个shared_ptr 指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
    // 恰当地处理arg
    // shared_ptr负责释放内存
    return make_shared<Foo>(arg);
}

因为factory返回一个shared_ptr,所以我们可以确保分配它地对象会在恰当地时刻被释放。

下面的函数把factory返回地shared_ptr保存在局部变量里面:

void use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    // 使用p
}		// p离开了作用域 它指向地内存会被自动释放掉

p被销毁的时候,会递减其引用计数并检查它是否为0。此例中p是唯一引用factory返回的内存的对象。因为p将要被销毁,所以p指向的对象也会被销毁,所占用的内存也会被释放。

但如果有其他的shared_ptr也指向这块内存,它就不会被释放掉:

shared_ptr<Foo> use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    // 使用p
    return p;	// 当我们返回p的时候 引用计数进行了递增操作
}	
// p离开了作用域 但是它指向的内存不会被释放掉

因为在最后一个shared_ptr销毁前内存都不会被释放,所以保证shared_ptr在没用之后不再保留就很重要了。不然占着茅坑不拉屎会导致别人不能用这块内存。

shared_ptr占着茅坑不拉屎的一种可能情况是:shared_ptr被放在一个容器里面,随后重拍了容器,从而不再需要某些元素。这种情况下应该用erase删除那些拉不出屎的shared_ptr元素。

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

程序使用动态内存处于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

容器是因为第一种原因而使用动态内存的典型例子。

本节中将定义一个类,它使用动态内存是为了让多个对象能够共享相同的底层数据。

有些类分配的资源具有和原来对象分别独立的生存期。我们可以定义一个类,用于保存一组元素,且该类不同的对象间拷贝元素时候共享相同的元素(也就是引用)。

Blob<string> b1;	// 空Blob
{
    // 新作用域
    Blob<string> b2{"a", "an", "the"};
    b1 = b2;		// b1和b2共享相同的元素
}
// b2被销毁了 但是b2中的元素不能销毁
// b1指向最初b2创建的元素 也就是b1还在用b2的元素 或者说那其实就是b1的元素 只是b2死了 也就是这些元素的一个主人死了

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

定义StrBlob类

我们定义一个管理string的类。该类名为StrBlob

因为现在还没学到模板的相关知识。所以暂借vector来替我们管理元素。

但我们不能直接在一个StrBlob对象里保存vector,因为一个对象的成员在对象销毁的时候也会被销毁。比如假定b1b2是两个Blob对象,共享相同的vector。如果vector保存在一个Blob里面,比如保存在b2里面,如果b2离开作用域的时候,vector也会被跟着销毁,也就是元素也会跟着没。为了保证vector里的元素继续存在,需要把vector保存在动态内存里面。

为实现数据共享,我们给每个StrBlob设置一个shared_ptr管理动态分配的vector。这里是用了shared_ptr的引用计数。

class StrBlob;		// 声明
#include<memory>
#include<string>
#include<vector>
#include<initializer_list>
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;
};

该类有个默认构造函数和一个构造函数,用于接受一个initializer_list<string>类型参数。该构造函数可以接受一个初始化器的花括号列表。此类中实现了sizeemptypush_back成员。这些成员通过指向底层vectordata成员来完成它们的工作。

StrBlob构造函数

两个构造函数都是用初始化列表来初始化其data成员,让它指向一个动态分配的vector。默认构造函数分配一个空vector

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

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

元素访问成员函数

pop_backfrontback操作访问vector里面的元素。这些操作在试图访问元素前必须检查元素是否存在:

#include<stdexcept>
void StrBlob::check(size_type i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}

checkprivate的工具函数,检查一个给定索引i是否在合法范围里。除了索引,还接受一个string参数,它会将此参数传递给异常处理程序,这个string描述了错误内容。

pop和元素访问成员函数先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:

std::string& StrBlob::front()
{
    // 若vector为空 check会抛出一个异常
    check(0, "front on empty StrBlob");
    return data->front();
}

std::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();
}

frontback应该对const进行重载

StrBlob的拷贝和赋值和销毁

我们的StrBlob类只有一个数据成员,它是shared_ptr类型。所以,在我们拷贝、赋值或者销毁一个StrBlob对象的时候,它的shared_ptr成员会相应地拷贝、赋值或者销毁。

所以,对于由StrBlob构造函数分配的vector,当最后一个指向它地StrBlob对象被销毁的时候,它会随之被自动销毁。

使用示例:

#include<iostream>
#include<strblob.h>
#include<initializer_list>
#include<string>
using namespace std;

int main()
{
    StrBlob a(initializer_list<string>{"a","b","c"});
    StrBlob b;
    b = a;          // b共享a的元素
    cout << &(b.front()) << endl;       // 打印b的首元素的地址
    cout << &(a.front()) << endl;       // 打印a的首元素的地址
    system("pause");    
    return 0;
}

直接管理内存

前面说过有两个东西可以分配和释放动态内存:

  • new运算符分配内存
  • delete表达式释放new分配的内存

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

在自由空间分配的内存是没有名字的,所以new没办法给其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;		// pi指向一个动态分配的、未初始化的int型无名对象

默认动态分配的对象是默认初始化的,也就是说会用到类类型的构造函数:

string *ps = new string;		// 初始化为空string
int *pi = new int;				// pi指向一个没有初始化的int

可以用直接初始化方式来初始化一个动态分配的对象。

int *pi = new int(1024);			// pi指向的对象的值为1024
string *ps = new string(3, '9');	// *ps为"999"
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

也可以给动态分配的对象进行值初始化:

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

和给变量初始化一样的原因,最好也给动态分配的对象进行值初始化。

也可以用单一的auto来推断我们想要分配的对象的类型:

auto p1 = new auto(obj);		// p1是个指向一个和obj类型相同的对象的指针
auto p2 = new auto{a,b,c};		// 错误 不支持多个初始化器

动态分配的const对象

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

常量肯定要初始化的不用多说,返回的也是常量指针

内存耗尽

一旦一个程序用光了他所有可用内存,new表达式就会失败。默认如果new不能分配所要求的内存空间,就会抛出一个bad_alloc类型的异常。可以改变使用new的方式来阻止它抛出异常:

// 若分配失败 new返回一个空指针
int *p1 = new int;		// 如果分配失败 new抛出std::bad_alloc
int *p2 = new (nothrow) int;		// 如果分配失败 new返回一个空指针

这种形式的new称为定位new(placement new),原因会在第十九章给出。

定位new表达式允许我们向new传递额外的参数。此例我们传递一个由标准库定义的名为nothrow的对象。若将nothrow传递给new,我们的意图是告诉它不能抛出异常。

bad_allocnothrow都定义在头文件new里面。

释放动态内存

可以用delete表达式(delete expression)来把动态内存归还给系统。

delete p;		// p必须指向一个动态分配的对象或者是个空指针

delete表达式执行两个动作:

  1. 销毁给定的指针指向的对象
  2. 释放对应的内存

指针值和delete

释放一个不是new分配的内存,或者是把同一个指针值释放多次,是有病的行为。

编译器不能分辨一个指针指向的是静态还是动态分配的对象,也不能分别一个指针所指的内存是否已经被释放了。

所以有时可能写错,但大多编译器还是会允许编译通过。

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

对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。

所以千万千万要记得释放内存!

// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
{
    // 视情况处理arg
    return new Foo(arg);		// 调用者负责释放这个内存
}

下面就是一个不会擦屁股的写法示例:

void use_factory(T arg)
{
    Foo *p = factory(arg);
    // 使用了p但是不delete它
}
// p离开了它的作用域 但它所指向的内存没有被释放!

而这个是个会擦屁股的写法示例:

void use_factory(T arg)
{
    Foo *p = factory(arg);
    // 使用p
    delete p;		// 释放内存
}

但是有时候可能也会有其它代码要用到use_facotry所分配的对象,就可以让它先别急着释放,而是作为返回值,以方便其它代码使用。

delete之后重置指针值

delete一个指针之后,指针值就变成无效的了。

但机器上指针还是保存着已经被释放掉的动态内存地址,这种指针人们称为空悬指针(dangling pointer)。即,指向一块曾经保存数据对象但现在已经无效的内存的指针。

避免空悬指针:在指针将要离开其作用域之前释放掉它所关联的内存。

这样在指针关联的内存被释放掉之后就没机会继续用指针了。

如果我们要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

但这也只是提供了有限地保护。

动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向已经释放内存的指针是没有用的:

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

程序执行完后,只是把p指定为一个空指针,但是q还是指着那个已经被销毁的内存。

shared_ptr和new结合使用

还可以用new返回的指针来初始化智能指针。

接受指针参数的智能指针构造函数是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>
}

shared_ptr<int> clone(int p) {
    return shared_ptr<int>(new int(p));		// 正确 将shared_ptr显式绑定到一个想要返回的指针上
}

默认一个用来初始化智能指针的普通指针必须要指向动态内存,因为智能指针默认用delete来释放它所关联的对象。也可以把智能指针绑定到指向其他类型的资源的指针上,但是这样就必须要定义自己的操作来替换掉delete。后面会说到怎么定义。

定义和改变shared_ptr的其他方法 含义
shared_ptr<T> p(q) p管理内置指针q所指向的对象
q必须指向new分配的内存,且能够转换成T*类型
shared_ptr<T> p(u) punique_ptr u那里接管了对象的所有权,将u置空
shared_ptr<T> p(q, d) p接管了内置指针q所指的对象的所有权。
q必须能够转换成T*类型。
p将使用可调用对象d来代替delete操作
shared_ptr<T> p(p2, d) pshared_ptr p2的拷贝,唯一的区别是p将调用d来代替delete
p.reset()
p.reset(q)
p.reset(q, d)
p是唯一指向其对象的shared_ptrreset会释放此对象。
若传递了可选的参数内置指针q,会让p指向q,否则会将p置为空。
若还传递了d,就会调用d而不是delete来释放q

不要混合使用普通指针和智能指针,也不要使用get初始化另一个智能指针或者为智能指针赋值

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝之间。这也是为什么推荐用make_shared而非new的原因。这样我们就可以在分配对象的同时就跟shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

// 在函数被调用的时候 ptr被创建并且初始化
void process(shared_ptr<int> ptr)
{
    // 使用ptr
}
// ptr离开作用域 被销毁

需要注意的是,process的参数是传值方式,所以实参肯定会拷贝到ptr里面。拷贝shared_ptr会递增其引用计数,所以在process运行过程中,引用计数值至少是2。当process结束时,ptr的引用计数会递减,但不会变成0。所以,局部变量ptr被销毁的时候,ptr指向的内存不会被释放。

使用process

shared_ptr<int> p(new int(42));		// 引用计数1
process(p);							// 拷贝p会递增它的引用计数 在process运行过程中引用计数是2
int i = *p;							// 正确 引用计数值是1

// 也可以传给它一个临时的shared_ptr 但是这样做很可能导致错误
int *x(new int(1024));				// 危险! x是个普通指针 而非智能指针
process(x);							// 错误 不能把int*转换成一个shared_ptr<int>
process(shared_ptr<int>(x));		// 合法 但是内存会被释放
int j = *x;							// 未定义的 x是个空悬指针

上例中,将一个临时shared_ptr传递给process。当这个调用所在的表达式结束的时候,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变成0了。所以当临时对象被销毁了,它所指向的内存会被释放掉。

x继续指向已经被释放掉的内存,x是指向动态内存的int*指针。它可不知道自己所指的内存已经没有东西了!你shared_ptr销毁内存和我int*有什么关系?如果再尝试使用x的值,行为的结果是未定义的。

当把一个shared_ptr绑定到一个普通指针的时候,我们就把内存的管理责任交给了shared_ptr。一旦这么做,就不应该再用内置指针去访问已经交给shared_ptr管理的内存了。

智能指针类型定义了一个名叫get的函数,它返回一个内置指针,只想智能指针管理的对象。

该函数是为了这种情况设计:我们要给不能使用智能指针的代码传递一个内置指针。但是get返回的指针的代码不能delete此指针。

把一个智能指针绑定到一个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指向的内存已经被释放了

要注意的是,因为之后p变成了一个空悬指针(因为它本来用的内存已经被q释放了),当p被销毁的时候,这块内存会被第二次delete,释放一个没有东西的内存?这不是脱裤子放屁?

其他shared_ptr操作

比如用reset可以把一个新的指针赋给一个shared_ptr

p = new int(1024);			// 错误 不能把一个指针赋予shared_ptr
p.reset(new int(1024));		// 正确 p指向一个新对象

reset也会更新引用计数,如果需要的话,会释放p指向的对象。reset常和unique一起用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if (!p.unique())
    p.reset(new string(*p));		// 我们不是唯一用户 所以分配新的拷贝
*p += newVal;		// 现在我们知道自己是唯一的用户 可以改变对象的值

智能指针和异常

第五章中提到了使用异常处理的程序可以在异常发生之后让程序流程继续,但这种程序需要确保在异常发生之后资源可以被正确地释放。

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

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

使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要的时候被释放掉。

函数可能是正常退出或者发生异常,但是不管哪种情况,局部对象都会被销毁。上例代码sp是个shared_ptr智能指针,所以sp销毁的时候会检查引用计数。此例中sp是只想着块内存的唯一指针,所以内存会被释放掉。

相对的,发生异常时候,我们直接管理的内存是不会被自动释放的。如果用内置指针管理内存,且在new之后在对应的delete之前发生异常,内存都不会被释放:

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

因为上述代码在delete之前抛出了异常,且我们没有针对异常的捕获代码,所以内存永远都不会被释放了。因为在函数f之外没有指针指向这个内存,我们不能对这块内存做任何的操作。

指针类型和哑类

所有标准库在内的很多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没有定义析构函数
}

使用我们自己的释放操作

为了用shared_ptr来管理一个connection,我们必须定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中把偶农村的指针进行释放的操作。

本例中,我们的删除器必须接受单个类型为connection*的参数:

void end_connection(connection *p)
{
    disconnect(*p);
}

使用:当我们创建一个shared_ptr的时候,==可以传递一个(可选的)指向删除器函数的参数==,以此来替代原先的delete操作:

void f(destination &d /* 其他参数 */)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 使用连接
    // 当f退出的时候 即使是异常退出 connection也会被正确关闭
}

end_connection会调用disconnect,从而确保连接被关闭。若是f正常退出,那么p的销毁会作为结束处理的一部分。若是发生异常,p同样会被销毁,从而连接被关闭。

也就是说,如果用智能指针管理的资源而不是new分配的内存,记住传递给它一个删除器。

unique_ptr

一个对象只能被一个unique_ptr指向。而不能像shared_ptr一样:可以同时多个shared_ptr指向一个对象。

unique_ptr支持的特有操作。和shared_ptr相同的操作前面已经说过了

操作 含义
unique_ptr<T> u1
unique_ptr<T, D> u2
nullptr,可以指向类型是T的对象。
u1会使用delete来释放它的指针。
u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u(d) unique_ptr,指向类型是T的对象,用类型D的对象d来代替delete
u = nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,并将u置空
u.reset()
u.reset(q)
u.reset(nullptr)
释放u指向的对象。
如果提供了内置指针q,让u指向这个对象
否则将u置为空
unique_ptr<doublt> p1;					// 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42));		// p2指向一个值为42的int

因为一个unique_ptr占有它指向的对象,所以unique_ptr不支持普通的拷贝或者赋值操作:

unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);				// 错误 unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2;								// 错误 unique_ptr不支持赋值

虽然不能拷贝或者赋值unique_ptr,但是可以通过调用release或者reset把指针的所有有权从一个unique_ptr转移给另一个unique_ptr

// 将所有权从p1转给p2
unique_ptr<string> p2(p1.release());	// release将p1置空

unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release());					// reset释放了p2原来指向的内存

调用release会切断unique_ptr和它原来管理的对象间的联系。如果我们不用另一个智能指针来接管release返回的指针,我们的程序就要负责资源的释放:

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

传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr有个例外:可以拷贝或者赋值一个即将被销毁的unique_ptr。比如从函数返回:

unique_ptr<int> clone(int p){
    // 正确 从int*创建一个unique_ptr<int>
    return unique_ptr<int>(new int(p));
}

还可以返回一个局部对象的拷贝:

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

对于两段代码,编译器都知道要返回的对象将要被销毁。在这种情况下,编译器执行一种特殊的"拷贝",将在后面介绍。

标准库早期版本包含了一个叫auto_ptr的类,它具有unique_ptr的部分特性,但不是全部。特别是我们不能在容器里保存auto_ptr,也不可以从函数里返回auto_ptr。虽然auto_ptr还是标准库的一部分,但是编写程序的时候应该用unique_ptr

向unique_ptr传递删除器

也可以重载unique_ptr里默认的删除器。但unique_ptr管理删除器的方式和shared_ptr不一样。

重载unique_ptr的删除器会影响到它的类型以及如何构造(或reset)该类型的对象。

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

示例:重写连接程序,用unique_ptr替代shared_ptr

void f(destination &d /* 其他需要的参数 */)
{
    connection c = connect(&d);		// 打开连接
    // 当p被销毁的时候 连接会关闭
    unique_ptr<connection, decltype(end_connection)*>		// 因为是decltype 所以末尾需要加个*表示在用指针
        p(&c, end_connection);
    // 使用连接
    // 当f退出的时候(就算是异常退出) connection也会被正确关闭
}

weak_ptr

weak_ptr是种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

weak_ptr绑定到shared_ptr的时候不会改变shared_ptr的引用计数。如果最后一个指向对象的shared_ptr被销毁,对象就会被释放。就算weak_ptr指向对象,对象也还是会被释放,所以weak_ptr的名字抓住了这种智能指针"弱"共享对象的特定。

weak_ptr 含义
weak_ptr<T> w weak_ptr可以指向类型是T的对象
weak_ptr<T> w(sp) shared_ptr sp指向相同对象的weak_ptr
T必须要可以转换成sp指向的类型
w = p p可以是个shared_ptrweak_ptr。赋值后wp共享对象
w.reset() w置空
w.use_count() w共享对象的shared_ptr数量
w.expired() 判断w.use_count()是否为0
w.lock() w.use_count()是空的,返回一个空shared_ptr
否则返回指向w的对象的shared_ptr
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);		// wp弱共享p p的引用计数未改变

因为wp指向的对象可能会被会释放掉。所以我们不能直接用weak_ptr访问对象,而必须调用lock。这个函数检查weak_ptr指向的对象是不是存在的。如果存在,lock返回一个指向共享对象的shared_ptr

if (shared_ptr<int> np = wp.lock()) {	// 如果np不是空的那么就条件成立
    // 在if里面 np和p共享对象
}

核查指针类

作为weak_ptr用途的一个展示,将给StrBlob类定义一个伴随指针类。该指针类命名未StrBlobStr,会保存一个weak_ptr,指向StrBlobdata成员,这是初始化时提供给它的。

// 对于访问一个不存在元素的尝试 StrBlobPtr抛出一个异常
class StrBlobPtr {
public:
    StrBlobPtr(): curr(0) { }
    StrBlobPtr(StrBlob &a, size_t sz = 0):
        wptr(a.data), curr(sz) { }		// 这一行会报出不可访问data 我们需要为它添加友元
    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;
};

该类有两个数据成员:

  • wptr:要么空的要么指向一个StrBlob中的vector
  • curr:保存当前对象所表示的元素的下标

类似伴随类StrBlob,我们的指针类也有个check成员来检查解引用StrBlobPtr是否安全。

需要注意到:

  • 我们不可以把StrBlobPtr绑定到一个const StrBlob对象。这个限制是因为构造函数接受一个非const StrBlob对象的引用而导致的。
  • 还有因为a.dataStrBlob的非公有成员,要访问它,必须要在StrBlob中添加友元

StrBlob中添加友元:

class StrBlobPtr;       		// 前置声明
class StrBlob {
friend class StrBlobPtr;		// 添加友元
public:
    typedef std::vector<std::string>::size_type size_type;
    StrBlob();
    ...
};

接下来完成check成员,用于检查指针指向的vector是否还存在:

std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
    auto ret = wptr.lock();     // vector是否还在
    if (!ret)                   // 如果不在
        throw std::runtime_error("unbound StrBlobPtr");
    // 判断指针指向的vector长度是否大于i 也就是判断用户想取的索引值是不是在vector对象当中
    if (i >= ret->size())       
        throw std::out_of_range(msg);
    return ret;
}

因为一个weak_ptr不参与其对于的shared_ptr的引用计数,StrBlobPtr指向的vector可能已经被释放了。如果vector已经被销毁了,lock返回一个空指针。本例里面,任何vector的引用都会失败,于是会抛出一个异常。否则check会检查给定索引,如果索引值合法,check返回从lock获得的shared_ptr

指针操作

将在第十四章学习定义自己的运算符。

现在定义derefincr的函数,分别用来解引用和递增StrBlobPtr

deref成员调用check,检查使用vector是否安全以及curr是否在合法范围里面:

std::string& StrBlobPtr::deref() const
{
    auto p = check(curr, "dereference past end");
    return (*p)[curr];      // (*p)是对象所指向的vector
}

如果check成功,p就是个shared_ptr,指向StrBlobPtr所指向的vector。表达式(*p)[curr]解引用shared_ptr来获得vector,然后使用下标运算符提取并返回curr位置上面的元素。

然后再来定义incr成员,该成员也会调用check

// 前缀递增 返回递增后的对象的引用
StrBlobPtr& StrBlobPtr::incr()
{
    // 如果curr已经指向容器的尾后位置 就不能递增它
    check(curr, "increment past end of StrBlobPtr");
    ++curr;     // 推进当前位置
    return *this;
}

完整代码见:strblob.h

动态数组

C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。

标准库里有一个叫allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

大多应用都没必要直接访问动态数组。如果一个应用要使用可变数组的对象,其实StrBlob里采用的方法大多时候更加简单、更加快速并且更加安全。

大多应用应该用标准库容器而非动态分配的数组。使用容器更简单、也不容易出现内存管理错误,而且可能有更好的性能。

使用容器的类可以用默认版本的拷贝、赋值和析构操作。但是分配动态数组的类就必须要定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。

new和数组

int *pia = new int[get_size()];		// pia指向第一个int 方括号里必须是整型 不必是常量

new分配要求数量的对象并返回指向第一个对象的指针。

也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式就不需要方括号了:

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

分配一个数组会得到一个(指向数组)元素类型的指针,而非返回一个数组类型的对象。所以不能对动态数组调用beginend。同样,也不能用范围for语句来处理动态数组的元素。

也就是说动态数组其实不是数组类型的,而是个指针。

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

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

C++11中,还可以提供元素初始化器的花括号列表:

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};

string *psa3 = new string[10]{"a", "an", "the", string(3,'x')};		// 前4个用给定的初始化器初始化,剩余的进行值初始化

若初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。本例中,new会抛出一个bad_array_new_length类型的异常。类似bad_alloc,该类型定义在头文件new里头。

虽然用空括号对数组的元素进行初始化,但是不能在括号里给出初始化器。

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

可以用任意表达式来确定要分配的对象的数目:

size_t n = get_size();		// get_size返回需要的元素的数目
int* p = new int[n];
for (int* q = p; q != p + n; ++q)
    // 处理数组

即使n0,代码也是合法的。new会返回一个合法的非空指针。该指针保证和new返回的其他任何指针都不同。对于零长度的数组来说,这个指针就像尾后指针一样,可以像使用尾后迭代器一样用这个指针。

释放动态数组

要用特殊的delete——在动态数组前加上一个空方括号对:

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

数组中的元素按逆序销毁,也就是最后一个元素最先死,然后倒数第二个...

方括号告诉编译器这个指针指向一个对象数组的第一个元素。

如果delete一个动态数组时候忽略方括号,或者删除一个动态内存,行为结果未定义。

智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique_ptr版本:

// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release();		// 自动用delete[]销毁其指针

(int[])指出up指向一个int数组而不是int。因为up指向数组,所以在up销毁它管理的指针的时候,会自动使用delete[]

指向数组的unique_ptr不支持成员访问运算符(点和箭头)除此之外的unique_ptr操作不变。

指向数组的unique_ptr的操作如下表所示:

操作 含义
unique_ptr<T[]> u u可以指向一个动态分配的数组,数组元素类型是T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。
p必须可以转换成类型T*
u[i] 返回u数组里位置i处的对象

如上表所示,当unique_ptr指向一个数组时,可以用下标运算符来访问数组里的元素:

for (size_t i = 0; i != 10; ++i)
    up[i] = i;		// 给每个元素赋一个新值

shared_ptr不直接支持管理动态数组,如果想用它来管理动态数组,必须提供自己定义的删除器:

shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset();	// 使用我们提供的lambda释放数组,它使用delete[]

如果不提供删除器,那么这段代码是未定义的。因为它会用delete销毁它指向的对象。造成的问题就跟释放的时候用delete而非delete[]一样。

shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组里的元素:

// shared_ptr未定义下标运算符 并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i;		// 使用get获取一个内置指针

因为shared_ptr不支持直接用动态数组,所以也就是说没有定义下标运算符,而且智能指针类型也没支持指针算术运算。所以为了访问数组的元素,必须要用get来获取一个内置指针,然后用它来访问数组元素。

allocator类

new的一个灵活性问题就是会把内存分配和对象构造一起做了。delete也差不多,把对象析构和内存释放一起做了。

但是如果有一大块内存的时候,我们会想自己计划什么时候干什么事,什么时候构造什么时候分配内存。也就是说我们可以先分给他一大块内存,但是只在真正需要的时候才去创建对象(这种行为会付出一定的额外开销,但是我们已经有那么大一块内存了)。

一般情况下,把内存分配和对象构造组合在一块可能会导致不必要的浪费,比如我们一次分配并且初始化一堆的int,但是我们其实不可能一下子就全部用了,甚至有些是永远也用不到的!甚至可能会在初始化之后因为不是要用初始化的值,所以导致我们又对它重新赋值,那就是两次赋值!

更重要的是:那些没有默认构造函数的类怎么办?

所以这里就用到标准库allocator类,它定义在头文件memory里,可以用来把内存分配和对象构造分离开来。

它提供一种类型感知的内存分配方式,分配的内存是原始的、未构造的。

allocator也是模板,模板就需要分配对象类型。

#include<iostream>
#include<memory>
#include<string>

using namespace std;

int main(void)
{
    // 使用allocator给n个string分配内存
    allocator<string> alloc;                // 可以分配string的allocator对象
    auto const p = alloc.allocate(5);       // 分配n个未初始化的string
    return 0;
}

allocator会根据给定的对象类型来确定恰当的内存大小和对齐位置。

allocator类及其算法 含义
allocator<T> a 定义allocator对象a,可以给类型T对象分配内存
a.allocate(n) 分配原始且未构造的内存,保存n个类型是T的对象
a.deallocate(p, n) 释放指向T*的指针p里地址开始的内存,该内存保存了n个类型是T的对象
p必须是个allocator返回的指针,且n必须是p创建时所要求的大小
调用deallocate之前,用户必须对每个在这块内存里创建的对象调用destroy
a.construct(p, args) p得是个T*类型的指针,指向一块原始内存
args被传递给类型T的构造函数,用来在p指向的内存里构造一个对象
a.destroy(p) pT*类型的指针,该算法对p指向的对象执行析构函数

allocator分配未构造的内存

allocator分配的内存是未构造的(unconstructed)。

C++11里,construct成员函数接受一个指针和零个或者多个额外参数,在给定位置构造元素。额外参数用来初始化构造的对象:

auto q = p;			// q指向最后构造的元素之后的位置
alloc.construct(q++);				// *q是空字符串
alloc.construct(q++, 10, 'c');		// *q是cccccccccc
alloc.construct(q++, "hi");			// *q是hi

早期版本的construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值。所以只能把一个元素拷贝到未构造的空间里,而不能用元素类型的任何其他构造函数来构造一个元素。

还未构造对象的情况下就用原始内存是错误的:

cout << *p << endl;			// 正确 使用string的输出运算符
cout << *q << endl;			// 未知 q指向未构造的函数 因为是后置版本

用完对象之后,必须要对每个构造的元素调用destroy来销毁:

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

循环开始处,q指向最后构造的元素之后的位置。调用destroy前对q进行了递减操作。所以也就是对最后一个元素调用destroy操作。在最后一次循环里面,我们destroy了第一个构造的元素,随后qp相等,循环结束。

我们只能对真正构造了的元素进行destroy操作。

元素被销毁后,就可以重新使用这块内存来保存其他的string(之所以是string,是因为该内存当前已经声明只能用来指向string),或是释放将其还给系统。释放内存就要调用deallocate来完成:

alloc.deallocate(p, n);

传递的deallocate的指针必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须和调用allocated分配内存时候提供的大小参数有一样的值。

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

allocator类的两个伴随算法,可以在未初始化内存里创建对象。它们都定义在头文件memory里。这些函数在给定目的位置创建元素,而不是由系统分配内存给他们:

allocator算法 含义
uninitialized_copy(b, e, b2) 从迭代器be指出的输入范围里拷贝元素到迭代器b2指定的未构造的原始内存里。
b2指向的内存必须要能够容纳输入序列里元素的拷贝
返回递增后的目的位置迭代器。也就是最后一个构造的元素之后的位置。
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存里
uninitialized_fill(b, e, t) 在迭代器be指定的原始内存范围里创建对象,对象的值都是t的拷贝
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。
b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
// 分配比vi里元素所占用的空间还大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);

// 通过拷贝vi里的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);

// 把剩余元素初始化为42
uninitialized_fill_n(q, vi.size(), 42);

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

使用已经学过的内容实现一个简单的文本查询程序:让用户在一个给定的文件里查找单词。

返回结果是个列表:

  • 单词在文件中出现的次数
  • 所在行

输出结果示例:

element occurs 112 times
	(line 36) A set element contains only a key;
	(line 158) operator creates a new element;

文本查询程序设计

  • 程序读入文件
    • 保存单词出现的每一行
  • 生成输出
    • 提取每个单词所关联的行号
    • 行号按升序且无重复
    • 打印给定行号中的文本

使用标准库设施:

  • vector<string>保存输入文件的拷贝。
    • 文件的每行就是vector的一个元素。
    • 打印一行时可以用下标提取行文本
  • istringstream将每行分解为单词
  • 用个set来保存每个单词在输入文本里出现的行号
  • 用个map把每个单词和它出现的行号set关联起来。这样就能方便地随便提取单词的set

数据结构

  • 定义保存输入文件的类TextQuery,包含:
    • vector:保存输入文件的文本
    • map:关联每个单词和它出现的行号的set
    • 用来读取给定输入文件的构造函数
    • 执行查询的操作:
      • 查找map成员,检查给定单词是否出现
  • 定义保存查询结果的类QueryResult
    • 有个print函数,完成结果打印工作

在类间共享数据

QueryResult类要表达的查询结果保存在TextQuery类里,其中包含:

  • 给定单词关联的行号的set
  • 这些行的对应文本

通过返回指向TextQuery对象内部的迭代器(或指针),可以避免拷贝操作。但为了避免TextQuery对象在对应的QueryResult对象前被销毁,所以使用智能指针shared_ptr

使用TextQuery类

// 使用TextQuery类
void runQueries(std::ifstream &infile)
{
    // infile是个ifstream 指向我们要处理的文件
    TextQuery tq(infile);                                                   // 保存文件并建立查询map 保存在tq里
    // 与用户交互:提示用户输入要查询的单词 完成查询并打印结果
    while (true) {
        std::cout << "enter word to look for, or q to quit: ";				// 输入关键字查找 或者 输入q退出
        std::string s;

        if (!(std::cin >> s) || s == "q") break;                            // 如果用户是否输入q 是就退出
        
        print(std::cout, tq.query(s)) << std::endl;                         // 指向查询并打印结果
    }
}

文本查询程序类的定义

先创建TextQuery类,它需要:

  • 接收用户输入的文件流
  • 提供一个query操作
    • 该操作接收一个string

返回QueryResult表示string出现的那些行。

设计该类需要考虑和QueryResult对象共享数据的需求:

  • 保存输入文件的vector,也就是指向vectorshared_ptr
  • 保存单词关联的行号的set,也就是指向setshared_ptr

为使代码更易读,再定义一个类型成员来引用行号,也就是保存文件的vector的下标。

代码如下:

// TextQuery类
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;       // 每个单词到其所在行号的集合(智能指针)的映射
};		// (单词:{行号13,行号25,行号31...})

TextQuery构造函数

构造函数:

  • 初始化器接受一个全新的vector来保存输入文件里的文本
  • while循环,循环条件是getline读入文件的当前行,直到读不出内容为止:
    • 将文件的当前行添加到代表文件每一行的file里。因为file是个shared_ptr,所以要先解引用再调用push_back添加
    • 通过获取当前file(类型是shared_ptr<vector<string>>)的元素数量(size),表示当前行的行号
    • 将当前行文本存进输入字符串流的line里。
    • 创建一个字符串类型的word用于保存单词
    • while循环,循环条件是word读入line
      • 在表示单词和对应行号的集合(wm)里创建(如果第二次碰到该关键字就不是创建了)当前单词的关键字,在上节已经了解过它的值是shared_ptr类型的,所以可以将它赋给一个引用
      • 判断该引用的内容是否为空,也就是判断是否第一次创建,若不是
        • 分配一个新的行号集合set<line_no>
      • 将其词的当前行号添加到单词的对应行号集合里(wm)

代码如下:

// TextQuery构造函数
TextQuery::TextQuery(std::ifstream &is): file(new std::vector<std::string>)		// 接收输入文件流 创建一个新的vector保存输入文件里的文本
{
    std::string text;								// 用于保存当前行的内容
    while (getline(is, text)) {                     // 循环遍历当前行的内容 并保存到text里
        file->push_back(text);                          // 将该行的内容添加到代表文件内容的容器file里
        int n = file->size() - 1;                       // 通过判断当前容器file的元素数量来获得当前的行号
        std::istringstream line(text);                  // 将当前行文本保存到字符串输入流里
        std::string word;								// 用于保存当前单词
        while (line >> word) {                          // 循环读入当前行的每个单词
            auto &lines = wm[word];                         // 获取代表当前单词(如果没有会自动创建)的所有行的引用
            if (!lines)                                     // 判断该引用是否为空
                lines.reset(new std::set<line_no>);             // 如果是就 分配一个新的行号集合给当前行
            lines->insert(n);                               // 将当前行号插入代表单词的行号集合里
        }
    }
}

QueryResult类

QueryResult类的数据成员:

  • 保存查询单词string
  • 指向保存输入文件的vectorshared_ptr
  • 指向保存单词出现行号的set

代码:

// QueryResult类
class QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);		// 友元允许print函数访问该类的私有成员
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;             	// 接收文件内容
};

query函数

  • 接收查询单词string
  • 用它来在已有的单词关联容器里查找对应出现的所有行号

返回:

  • 找到:构造QueryResult,构造函数的参数是:
    • 查询的单词
    • 单词出现的行号集合
    • 文件内容
  • 没找到:定义一个局部static对象,指向空行号的集合setshared_ptr,返回该对象的拷贝

代码:

// query函数
QueryResult TextQuery::query(const std::string &sought) const		// 接收查询单词 是常量引用
{	
    // 先定义一个指向空行号集合的shared_ptr对象 待会找不到就可以直接返回 以免忘记
    static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>);
    
    // 在已有的单词集合里查找输入的查询单词
    auto loc = wm.find(sought);
    if (loc == wm.end())		// 如果指向一个空的地址(也就是没找到会返回一个尾后迭代器)
        return QueryResult(sought, nodata, file);   // 构造一个QueryResult类 类内容是查询单词 指向空集合的shared_ptr 文件内容 并将其返回
    else
        return QueryResult(sought, loc->second, file);	// 构造一个QueryResult类 类内容是查询单词 指向查找到的单词所在行号集合的shared_ptr 以及文件内容 并将其返回
}

打印结果

在给定流上输出给定的QueryResult对象

// 打印结果
std::ostream &print(std::ostream & os, const QueryResult &qr)		// 接受一个输出流对象和一个QueryResult类引用
{
    // 如果找到了单词 打印出现次数和所有出现的位置
    os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << std::endl; 
    // 打印单词出现的每一行
    for (auto num : *qr.lines)          // 对set里的每个单词
        // 避免行号从0开始给用户带来的困惑
        os << "\t(line" << num + 1 << ") " << *(qr.file->begin() + num) << std::endl;
}


std::string make_plural(size_t ctr, const std::string &word, const std::string &ending)
{
    return (ctr > 1) ? word+ending : word;
}

完整代码示例

#include<iostream>
#include<sstream>
#include<fstream>

#include<string>
#include<vector>
#include<map>
#include<set>

#include<memory>


class QueryResult;
class TextQuery;
std::string make_plural(size_t ctr, const std::string &word, const std::string &ending);


// TextQuery类
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;
};


// TextQuery构造函数
TextQuery::TextQuery(std::ifstream &is): file(new std::vector<std::string>)
{
    std::string text;
    while (getline(is, text)) {
        file->push_back(text);
        int n = file->size() - 1;
        std::istringstream line(text);
        std::string word;
        while (line >> word) {
            auto &lines = wm[word];
            if (!lines)
                lines.reset(new std::set<line_no>);
            lines->insert(n);
        }
    }
}


// QueryResult类
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;
};


// query函数
QueryResult TextQuery::query(const std::string &sought) const
{
    static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>);
    auto loc = wm.find(sought);
    if (loc == wm.end())
        return QueryResult(sought, nodata, file);
    else
        return QueryResult(sought, loc->second, file);
}


// 打印结果
std::ostream &print(std::ostream & os, const QueryResult &qr)
{
    os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << std::endl; 
    for (auto num : *qr.lines)
        os << "\t(line" << num + 1 << ") " << *(qr.file->begin() + num) << std::endl;
}


std::string make_plural(size_t ctr, const std::string &word, const std::string &ending)
{
    return (ctr > 1) ? word+ending : word;
}


// 使用TextQuery类
void runQueries(std::ifstream &infile)
{
    TextQuery tq(infile);
    while (true) {
        std::cout << "enter word to look for, or q to quit: ";
        std::string s;

        if (!(std::cin >> s) || s == "q") break;
        
        print(std::cout, tq.query(s)) << std::endl;
    }
}

示例文件: