C++primer 12章 动态内存和智能指针

C++引入智能指针的目的

  • 使用智能指针来管理动态分配的对象,当一个对象应该被释放的时候,指向他的智能指针确保自动释放它

内存分配

  • 静态内存:局部static对象、类static数据成员、定义在任何函数之外的变量
  • 栈内存:定义在函数内的非static对象
  • 分配在静态或者栈内存中的对象由编译器自动创建和销毁
  • 栈对象,仅在其定义的程序块运行时候才会存在
  • static对象在使用之前就会被分配,程序结束的时候会被销毁
  • 除了静态内存和栈内存,还有一个内存池,被称作自由空间或者叫堆,用来存储动态分配的对象,这些动态对象的生存周期由程序来控制,当动态独享不在使用的时候,代码必须显示的销毁他们

动态内存和智能指针

  • C++动态内存通过一对运算符号来完成,new创建 和 delete删除
  • new:在动态内存中为对象分配空间并且返回一个指向该对象的指针,我们可以选择对象并且初始化
  • delete:接受一个动态对象的指针,销毁该对象,并且释放与之相关的内存空间
  • 问题:内存释放的时间;忘记释放->内存泄露;提前释放->引用非法内存指针
  • C++引入两种智能指针来管理动态对象;
  • shared_ptr 允许多个指针指向同一个对象
  • unique_ptr 独占所指定的对象,还为此定义了一个weak_ptr的伴随类,这是一个弱的引用,指向unique_ptr管理的对象
  • 三者都定义在<memory>头文件中

shared_ptr类

  • 需要提供额外的信息-指针指向的类型,类似vector,之后是定义的这种智能指针的名字
  • shared_ptr<string> p1; //shared_ptr 指向string
  • shared_ptr<list<int>> p2; //shared_ptr 指向int的list
  • 智能指针和普通指针类似,解引用一个智能指针返回他所指向的对象,例如在条件判断中使用指正指针,效果就是检测他是否为空
if(p1 && p1->empty){// 如果p1不为空,检测他是否指向一个空的string
    *p1 = "Hello world";//如果p1指向一个空的string,将Hello world赋值给指针
}

make_shared函数

  • 最安全的分配和使用动态内存的方式是调用一个名为make_shared的标准库函数
  • 函数的目的是在动态内存中分配一个对象并且初始化他,返回的是指向这个对象的shared_ptr指针
  • 需要在<>括号内指定创建对象的类型
    shared_ptr<int>p3 = make_shared<int>(42);//指向一个数值为42的int的shared_ptr
    shared_ptr<string> p4 = make_shared<string>(10,'9');//指向一个数值为"9999999999"的string
  • 类似顺序容器的emplace成员,make_shared用其参数来构造指定类型的对象;比如上面的定义的指向string或者int的shared_ptr,需要满足string或者int的构造函数
  • 如果不传递任何参数,对象就会进行数值初始化
  • 通常使用auto来定义一个对象来保存make_shared的结果
  • auto p6 = make_shared<vector<string>>();

shared_ptr的拷贝和赋值

  • 当进行拷贝和赋值的时候,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象
    auto p = make_shared<int>(42);
    auto q(p);//p和q指向相同的对象,这个对象有两个引用者
  • 每一个shared_ptr都有一个关联的计数器,称作引用计数。无论何时拷贝一个shared_ptr都会使得计数器递增,比如使用shared_ptr初始化、将其作为函数返回值返回、将其作为一个参数传递给一个函数,都会使关联计数器递增
  • 当给shared_ptr赋一个新的数值或者销毁一个局部的shared_ptr(局部定义离开作用域),都会递减计数器
  • 当计数器的数值变成0的时候,就会自动释放所管理的对象 
    auto r = make_shared<int>(42);//r指向的int只有一个引用者
    r = q;
    //给r赋值,使得他指向另外一个地址
    //递增q指向的对象的引用计数
    //递减r指向的对象的引用次数
    //r先前的指向的对象,因为已经没有了引用者,因此会自动释放

Shard_ptr自动销毁所管理的对象

  • 使用析构函数实现销毁工作
  • 析构函数一般用来释放对象所分配的资源,例如string、vector的若干操作都会分配内存来保存元素,也会使用析构函数销毁元素,并且释放内存 
  • shared_ptr无用之后仍然保留的一种可能情况是将shared_ptr存放在了一个容器中,然后重排了容器,从而不再需要某些元素,这个时候需要注意,确保使用erase删除那些不再需要的shared_ptr元素

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

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

  • 程序不知道自己需要使用多少对象
  • 程序不知道对象的准确类型
  • 程序需要在多个对象之间共享数据

注意事项

  • 容器类是出于第一个原因而使用动态内存的典型例子
  • 第15章 出于第二个原因而使用共享内存的例子
  • 例子:使用共享内存使多个对象之间共享相同的底层的数据

使用动态内存的一个常见的原因是,允许多个对象之间共享相同的状态,vector v2 = v1;那么v2会具有和v1相同的元素,但是v1因为不在使用而被删除,与之相反,假设我们定义一个名为Blob的类,希望对Blob的不同拷贝之间共享相同的元素,这个底层是相当于,拷贝元素,不是将元素重新复制一遍然后开辟新的内存空间存储拷贝出来的数据,而是使用引用的方式,每一份拷贝的数据都会映射到同一个数据资源。

#include <memory>
class StrBlob{
public:
    typedef std::vector<std::string>::size_type size_type;
    StrBlob();
    StrBlob(std::initializer_list<std::string>i1);
    //size empty 和 push_back成员通过指向底层的vector成员来完成对应的工作
    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(){
        check(0,"pop_back on empty StrBlob");
        data->pop_back();
    };
    //元素访问
    std::string& front(){
        check(0,"front on empty StrBlob");
        return data->front();
    };
    std::string& back(){
        check(0,"back on empty STrBlob");
        return data->back();
    };

public:
    std::shared_ptr<std::vector<std::string>>data;
    //如果data[i]不合理,就会抛出一个异常
    void check(size_type i,const std::string &msg) const{
        if (i > data->size()){
            throw std::out_of_range(msg);
        }
    }

};
//两个函数都使用初始化列表来初始化其data成员,使其指向一个动态分配的vector
//默认构造函数分配一个空vector
StrBlob::StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
//接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数
//这个函数通过拷贝列表中的数值来初始化vector的元素
StrBlob::StrBlob(std::initializer_list<std::string> i1) : data (std::make_shared<std::vector<std::string>>(i1)) {};
//使用new和delete直接进行管理内存不能依赖类对象的拷贝、赋值和销毁操作的任何默认定义,因此使用智能指针更易于编写和调试
//使用new只能返回一个指向该对象的指针,而无法为其分配的对象命名
int *p1 = new int;//p1指向一个动态分配的、未初始化的无名对象
//这个new表达式在自由空间内构造了一个int型对象,并且返回指向这个对象的指针,
// 默认情况下,动态分配的对象是默认初始化的,内置类型或者组合类型的对象的数值将会是未定义的,类类型对象将使用默认构造函数进行初始化
std::string *ps = new std::string;//初始化为空的string
int *p2 = new int;//p1指向一个动态分配的、未初始化的无名对象
//可以使用直接初始化方式初始化一个动态分配的对象,可以使用传统的构造方式(使用圆括号),也可以使用列表初始化(使用花括号)
int *p3 = new int (1024);//p3指向的对象的数值为1024
std::string *ps1 = new std::string(10,'9');//*ps = "9999999999"
std::vector<int> *pv = new std::vector<int>{0,1,2,3,4,5,6,7,8,9};//vector里面有10个元素,数值依次从0到9
//对动态分配的对象进行值初始化 需要在类型名之后加上一对空括号即可
std::string *ps2 = new std::string();//值初始化为空string
int *p4 = new int;//默认初始化,*p4的数值没有定义
int *p5 = new int();//默认初始化,*p5的数值为0
//如果使用括号提供一个括号包围的初始化器,就可以使用auto从初始化器里面推断出想要分配给对象的类型
//但是括号里面仅有单一的初始化器才可以使用auto
//auto p6 = new auto{1,2,3}; //错误
auto p6 = new auto{1};//p6的类型是一个指针,指向从obj里面推断出来的类型
// 如果obj是一个int,p1就是int*;如果obj是一个string,那么p1是一个string*


//使用new分配const对象是合法的
const int *pci = new const int(1024);//分配并且初始化一个const int
const std::string *pcs = new const std::string;//分配并且初始化一个const的空string
//和其他任何const对象一样,动态分配的const对象必须进行初始化
//对于定义了默认构造函数的类类型可以进行隐式初始化,其他类型必须进行显示初始化
//由于分配的对象是const的,new返回的指针是一个指向const的指针

//内存空间耗尽
//如果new不能分配所要求的内存空间,就会抛出一个类型为bad_alloc的异常,可以通过改变使用new的方式来阻止它抛出异常
int *p7 = new (std::nothrow)int;//如果分配失败,new返回一个空的指针
//这种方式叫做定位new,其允许我们向new传递额外的参数,上例将nothrow传递给new,即告诉他不要抛出异常

//释放动态内存
//为了防止内存耗尽
// delete接受一个指针,指向要释放的内存

指针值和delete

  • 传递给delete的指针必须指向动态分配的内存,或者是一个空指针(参见2.3.2节,第48页)。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的
  • int i,*pil=&i,*pi2=nullptr;double*pd=new double(33),*pd2=pd;
  • delete i;//错误:i不是一个指针
  • delete pil;//未定义:pil指向一个局部变量
  • delete pd;//正确
  • delete pd2;//未定义:pd2指向的内存已经被释放了
  • delete pi2;//正确:释放一个空指针
  • 对于deletei的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。执行delete pil和pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。
  • 虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可:
  • const int *pci=new const int(1024);delete pci;//正确:释放一个const对象

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

  • 如12.1.1节(第402页)所述,由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
  • 返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担--调用者必须记得释放内存
// factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
    //视情况处理arg
return new Foo (arg) ; // 调用者负责释放此内存
)

void use_factory(T arg)
{
Foo *p = factory(arg);
// 使用p但不delete它
} //p离开了它的作用域,但它所指向的内存没有被释放!
  • 类似之前定义的factory函 数 (参 见 12.1.1节,第 403页),这个版本的factory分配一个对象,但并不delete它。factory的调用者负责在不需要此对象时释放它。此函数只负责申请创建空间,并不负责释放内存。不幸的是,调用者经常忘记释放对象:
  • 此处,use_factory函 数调用factory,后者分配一个类型 为 Foo 的新对象。当use_factory返问时,局部变量p 被销毁。此变量是一个内置指针,而不是一个智能指针。 与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
  • 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
在本例中,p 是指向factory分配的内存的唯一指针。一旦use_factory返回, 程序就没有办法释放这块内存了。根据整个程序的逻辑,修正这个错&的正确方法是在
use_factory中记得释放内存: void use_factory(T arg)
(
Foo *p = factory (arg);
// 使用p
delete p; / / 现在记得释放内存,我们已经不需要它了
)
还有一种可能,我们的系统中的其他代码要使用use_factory所分配的对象,我们就应该修改此函数,让它返回一个指针,指向它分配的内存:
Foo* use_factory(T arg)
(
、
Foo *p = factory(arg);
// 使 用 p
return p; // 调用者必须释放内存
}

使用new和delete管理动态内存存在三个常见问题

  • 1,忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
  • 2.使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
  • 3.同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。相对于查找和修正这些错误来说,制造出这些错误要简单得多"
  • 坚持只使用智能指针,就可以避免所有这些.问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。

delete之后重置指针值

  • 当delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。指针虽然存在,但是和内存空间脱离了关系。在delete之后,指针就变成了空悬指针(danglingpointer),即,指向一块曾经保存数据对象但现在己经无效的内存的指针。
  • 未初始化指针(参见2.3.2节,第49页)的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了如果需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

这只是提供了有限的保护

  • 动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用。
  • 多个指针指向同一个内存空间,仅仅对一个指针进行赋值为空的操作,其他指针仍然会出错
  • int *p(new int(42));//p指向动态内存
  • auto q = p;//p和q指向相同的内存
  • deletep;//p和q均变为无效
  • p=nullptr;//指出p不再绑定到任何对象
  • 本例中P和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。但是,重置p对q没有任何作用,在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常困难的。

12.1.3shared_ptr和new结合使用

  • 如前所述,如果不初始化一个智能指针,它就会被初始化为一个空指针。如表12.3所示,还可以用new返回的指针来初始化智能指针:
  • shared_ptr<double>pl;//shared_pt:r可以指向一个double
  • shared_ptr<int>p2(new int(42));//p2指向一个值为42的int
  • 接受指针参数的智能指针构造函数是explicit的(参见7.5.4节,第265页)(必须使用直接初始化的形式,不可以使用拷贝初始化的方式)。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式(参见3.2.1节,第76页)来初始化一个智能指针
  • shared_ptr<int>pl=new int(1024);//错误:必须使用直接初始化形式 不可以使用拷贝初始化的方式
  • shared_ptr<int>p2(new int(1024));//正确:使用了直接初始化形式
  • pl的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr,由于不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int>clone(int p)(
return new int(p);//错误:隐式转换为sha:red_ptrvint>
}
  • 我们必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr<int>clone(intp)(
//正确:显式地用int*创建shared_ptr<int>
return shared_ptr<int>(new int(p));
}
  • 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete

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

  • shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也赢什么推荐使用make_shared而不是new的原因。这样,在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
  • 考虑下面对shared_ptr进行操作的函数:
//在函教被调用时ptr被创建并初始化
void process(shared_ptr<int>ptr)
(
//使用ptr
}//ptr离开作用域,被销毁
  • process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。当process结束时,ptr的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
  •  使用此函数的正确方法是传递给它一个shared_ptr:
  • shared_ptr<int>p(new int(42));//引用计数为1
  • process(p);//拷贝p会递增它的引用计数;在process中引用计数值为2
  • inti=*p;//正确:引用计数值为1

虽然不能传递给process-个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

  • int *x(new int(1024));//危险:x是一个普通指针,不是一个智能指针
  • process(x);//错误:不能将int*转换为一个shared_ptr<int>
  • process(shared_ptr<int>(x));//合法的,但内存会被释放!
  • intj=*x;//未定义的:x是一个空悬指针!
  • 在上面的调用中,将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。
  • 但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。
  • 当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了
  • 使用内置指针来访问一个智能指针所负责的对象是很危险的,因为不知道对象何时会被销毁

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

  • 智能指针类型定义了一个名为get的函数(参见表12.1),它返回一个内置指针, 指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用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 指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都 是 1。当q所在的程序块结束时,q 被销毁,这会导致q 指向的内存被释放。从而p 变成 一个空悬指针,意味着当我们试图使用p 时,将发生未定义的行为。而且,当p 被销毁时, 这块内存会被第二次delete
  • get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

其他shared_ptr操作

  • shared_ptr还定义了其他一些操作,参见表12.2和表12.3所示。我们可以用reset来将一个新的指针赋予一个shared_ptr:
  • p=new int(1024);//错误:不能将一个指针赋予shared_ptr
  • p.reset(newint(1024));//正确:p指向一个新对象
  • 与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。“果不是,在改变之前要制作一份新的拷贝:
if(!p.unique())
p.reset(newstring(*p));//我们不是唯一用户;分配新的拷贝
*p+=newVal;//现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

  • 程序退出有两种可能:1,正常处理结束;2,发生了异常。局部对象都会被销毁
  • 使用new创建内存 到 delete删除异常这一段空间发生异常,内存不会被释放
  • 使用智能指针就可以

智能指针和哑类

  • 包括所有标准库类在内的很多C++类都定义了析构函数(参见12.1.1节,第402页),负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和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避免内存泄漏几乎是等价的。使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。

使用我们自己的释放操作

  • 默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr 被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来处理一个 connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:void end_connection(connection *p) ( disconnect(*p); )
  • 当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数(参 见 6.7节,第 221页):
void f (destination &d /* 其他参数 */)
{
connection c = connect(&d); 
shared_ptr<connection> p (&c, end_connection);
// 使用连接
// 当f退出时(即使是由于异常而退出), connection会被正确关闭
}
  • 当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connectiono接下来,end_connection会调用disconnect?从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,P同样会被销毁,从而连接被关闭。

智能指针使用规范

  • 智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
  • 不使用相同的内置指针值初始化(或reset)多个智能指针
  • 不 delete get()返回的指针
  • 不使用get()初始化或reset 另一个智能指针
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器 (参见 12.1.4节,第415页 和 12.1.5节,第419页

12.1.5unique_ptr

  • 一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。表12.4列出了unique_ptr特有的操作。与shared_ptr相同的操作列在表12.1(第401页)中。
  • 与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr.当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式
  • unique_ptr<double>pl;//可以指向一个double的unique_ptr
  • unique_ptr<int>p2(new int(42));//p2指向一个值为42的int
  • 由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作
  • unique_ptr<string>pl(new string("Stegosaurus"));
  • unique_ptr<string>p2(pl);//错误:unique_ptr不支持拷贝
  • unique_ptr<string>p3;
  • p3=p2;//错误:unique_ptr不支持赋值

  • 虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique
  • //将所有权从pl(指向stringStegosaurus)转移给p2
  • unique_ptr<string>p2(pl.release());//release将pl置为空
  • unique_ptr<string>p3(new string("Trex"));
  • //将所有权从p3转移给p2
  • p2.reset(p3.release());//reset释放了p2原来指向的内存
  • release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为pl原来保存的指针,而pl置为空。
  • reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。
  • 调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存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;
}
  • 于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊 的 “拷贝”,我们将在13.6.2节 (第473页)中介绍它。

向 unique_ptr传递删除器

// p 指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象 
// 它会调用一个名为fen的 delT类型对象
unique_ptr<objT, delT> p (new objT, fen);
作为一个更具体的例子,我们将重写连接程序,用 unique_ptr来代替shared_ptr, 如下所示:
void f (destination &d /* 其他需要的参数 */)
{
connection c = connect (&d) ; // 打开连接
// 当p 被销毁时,连接将会关闭
unique_ptr<connection, decltype(end_connection)*>
p (&c, end_connection);
// 使用连接
// 当f退出时(即使是由于异常而退出),connection 会被正确关闭
}
  • 在本例中我们使用了 decltype (参见2.5.3节,第 62页)来指明函数指针类型。由于decltype (end_connection)返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类形的一个指针(参见6.7节,第 223页)。

12.1.6weak_ptr

  • weak_ptr(见表12.5)是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。

  • 当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
  • auto p = make_shared<int>(42);
  • weak_ptr<int> wp (p) ; // wp弱共享p; p 的引用计数未改变
  • 本例中wp和 p 指向相同的对象。由于是弱共享,创建wp不会改变p 的引用计数;wp指 向的对象可能被释放掉。由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock来访问对象 lock函数检测 weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的 shared_ptro,与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向
    的底层对象也就会一直存在。
  • 例如:
    if (shared_ptr<int> np = wp. lock() ) ( // 如果 np 不为空则条件成立
    / / 在 if中,np与 p 共享对象
    }在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在 if中,使用 np访问共享对象是安全的。使用lock访问对象 

核查指针类

  • 作为weak_ptr用途的一个展示,我们将为StrBlob类定义一个伴随指针类。我们的指针类将命名为StrBlobPtr,会保存一个weak_ptr,指向StrBlob的 data成员,这是初始化时提供给它的。通过使用weak_ptr,不会影响一个给定的StrBlob所指向 的vector的生存期但是,可以阻止用户访问一个不再存在的vector的企图

12.2动态数组

  • 两种一次性分配一个对象数组的方法
  • new表达式语法
  • allocator的类,允许将分配和初始化分离
  • 当需要可变的数量的对象的时候,使用vector 或者其他标准库容器是快速并且安全的。大多数情况下,应该使用标准库容器而不是动态分配的数组,因为容器更为简单、不容易出现内存管理的错误,并且有很强的性能
  • 如前所述,使用容器的类可以使用默认版本的拷贝、赋值和析构操作(参见7.1.5节,第 239页)。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存

12.2.1 new和数组

  • 为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:
  • 调 用 get_size确定分配多少个int
  • int *pia = new int [get_size()] ; // pia 指向第一个 int
  • 方括号中的大小必须是整型,但不必是常量
  • 也可以用一个表示数组类型的类型别名(参见2.5.1节,第60页)来分配一个数组,这样,new表达式中就不需要方括号了:
  • typedef int arrT [42] ; // arrT 表示 42 个 int 的数组类型
  • int *p = new arrT; / / 分配一个42个 int的数组;p 指向第一个int
  • 在本例中,new分配一个int数组,并返回指向第一个int的指针。即使这段代码中没有方括号,编译器执行这个表达式时还是会用new[]。即,编译器执行如下形式:
    int *p = new int[42];

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

  • 虽然我们通常称new T[]分配的内存为“动态数组”,但这种叫法某种程度上有些误导。当 用 new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针(指针指向申请的内存空间,指针类型和数组类型一致)。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。在上例中,我们正在分配一个数组的事实甚至都是不可见的一一连[num]都 没有。new返回的是一个元素类型的指针。
  • 由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或 end (参 见 3.5.3节,第 106页)。这函数使用数组维度(回忆一下,维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围f o r 语句来处理 (所谓的)动态数组中的元素
  • 动态数组并不是数组类型

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

  • 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。 可以对数组中的元素进行值初始化(参见3.3.1节,第 88页),方法是在大小之后跟一对空括号
  • 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个用给定的初始化器初始化,剩余的进行值初始化 '\0'
  • string *psa3 = new string [10] ( "a', "an”, "the”, string (3,'x' )};
  • 与内置数组对象的列表初始化(参见3.5.1节,第 102页)一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。
  • 如果初始化器数目大于元素数目,则 new表达式失败,不会分配任何内存。在本例中,new会抛出一个类型为bad_array_new _length的异常。类似bad_alloc ,此类型定义在头文件new中。
  • 虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto 分配数组(参见12.1.2节,第 407页)。

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

  • 可以用任意表达式来确定要分配的对象的数目:
  • size_t n = get_size () ; // get_size返回需要的元素的数目
  • int* p = new int [n] ; / / 分配数组保存元素
  • for (int* q = p; q != p + n; ++q)
  • / * 处 理 数 组 */ ;
  • 这产生了一个有意思的问题:如果get_size返回0,会发生什么?答案是代码仍能正常工作。虽然我们不能创建一个大小为0曲静态数组对象,但当n等于0时,调用new[n]是合法的:
  • char arr[0];//错误:不能定义长度为0的数组
  • char*cp=new char[0];//正确:但cp不能解引用
  • 当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样(参见3.5.3节,第106页),我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用一一毕竟它不指向任何元素。在我们假想的循环中,若get_size返回0,则n也是0,new会分配0个对象。for循环中的条件会失败(p等于q+n「因为n为0)。因此,循环体不会被执行

释放动态数组

  • 为了释放动态数组,我们使用一种特殊形式的delete— 在指针前加上一个空方括号对
  • delete p; // p 必须指向一个动态分配的对象或为空
  • delete [] pa; // pa必须指向一个动态分配的数组或为空
  • 第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁, 即,最后一个元素首先被销毁,然后是倒数第二个,依此类推
  • 当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete 一个指向数组的指针时忽略了方括号 (或者在delete 一个指向单一对象的指针时使用了方括号),其行为是未定义的。 回忆一下,当我们使用一个类型别名来定义一个数组类型时,在 new表达式中不使用【】。即使是这样,在释放一个数组指针时也必须使用方括号:
  • typedef int arrT [42] ; // arrT是 42个 int的数组的类型别名
  • int *p = new arrT; / / 分配一个 42个 int的数组;p 指向第—个元素
  • delete [] p; / / 方括号是必需的,因为我们当初分配的是一个数组
  • 不管外表如何,p 指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p 时我们必须使用【】

智能指针和动态数组

  • 标准库提供了一个可以管理new分配的数组的unique_ptr 版本。为了用一个 unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
  • // up指向一个包含10个未初始化int的数组
  • unique_ptr<int[]> up (new int[10]);
  • up. release () ; / / 自动用delete []销毁其指针
  • 类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int
  • 由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete【】
  • 指向数组的unique_ptr提供的操作与我们在12.1.5节(第417页)中使用的那些操作有一些不同,我们在表12.6中描述了这些操作。当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素
  • for(size_t i=0;i!=10;++i)
  • up[i]=i;//为每个元素赋予一个新值

  • 与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
  • //为了使用shared_ptr,必须提供一个删除器
  • shared_ptr<int>sp(new int[10],[](int*p){delete []p;});
  • sp.reset();//使用我们提供的lambda释放数组,它使用delete[]
  • 本例中我们传递给shared_ptr一个lambda(参见10.3.2节,第346页)作为删除器,它使用delete[]释放数组。如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr使用delete销毁它指向的对象。如果此对象是一个动态数组,对其使用delete产生的问题与释放一个动态数组指针时忘记【】产生的问题一样(参见12.2.1节,第425页)。
  • shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
  • //shared_ptr未定义下标运算符,并且不支持指针的算术运算
  • for(size_ti=0;i!=10;++i)*(sp.get()+i)=i;//使用get获取一个内置指针
  • shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组面的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

12.2.2allocator类

  • new有一些灵活性上的局限,其中一方面表现在它将内存分配(申请空间)和对象构造(创建对象)组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。
  • 当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。例如:
  • string *const p=new string[n];//构造n个空string
  • string s;
  • string*q=p;//q指向第一个string
  • while(cin >> s&&q!=p+n)
  • *q++=s;//赋予*q一个新值
  • const size_t size=q-p;//记住我们读取了多少个string
  • //使用数组
  • delete[]  p;//p指向一个数组;记得用delete[]来释放
  • new表达式分配并初始化了n个string。但是,我们可能不需要n个string,少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。而且,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。
  • 更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

allocator类

  • 标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法它分配的内存是原始的、未构造的。表12.7概述了allocator支持的操作。在本节中,我们将介绍这些allocator操作。在13.5节(第464页),我们将看到如何使用这个类的典型例子。
  • 类似vector,allocator是一个模板(参见3.3节,第86页)。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置
  • allocator<string>alloc;//可以分配string的allocator对象
  • auto const p=alloc.allocate(n);//分配n个未初始化的string  这个allocate调用为n个string分配了内存。

allocator分配未构造的内存

  • allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数(参见12.1.1节,第401页),这些额外参数必须是与构造的对象的类型相无配的合法的初始化器:
  • auto q=p;//q指向最后构造的元素之后的位置
  • alloc.construct(q++);//*q为空字符串
  • alloc.construct(q++,10,zcr);//*q为cccccccccc
  • alloc.construct(q++,"hi”);//*q为hi!
  • 在早期版本的标准库中,construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任
  • 何其他构造函数来构造一个元素。
  • 还未构造对象的情况下就使用原始内存是错误的:
  • cout<<*p<<endl;//正确:使用string的输出运算符
  • cout<<*q<<endl;//灾难:q指向未构造的内存
  • 当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数(参见12.1.1节,第402页):
  • while(q!=p) alloc.destroy(--q);//释放我们真正构造的string
  • 在循环开始处,q指向最后构造的元素之后的位置。我们在调用destroy之前对q进行了递减操作。因此,第一次调用destroy时,q指向最后一个构造的元素。最后一步循环中我们destroy了第一个构造的元素,随后q将与p相等,循环结束
  • 一旦元素被销毁后,就可以重新使用这部分内存来保存其他s tr in g ,也可以将其归还给系统。释放内存通过调用deallocate来完成:alloc.deallocate(p, n);
  • 我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且, 传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。

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

  • 标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。 表 12.8描述了这些函数,它们都定义在头文件memory中。

  • 作为一个例子,假定有一个int的vector,希望将其内容拷贝到动态内存中。我们将分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充:
  • //分配比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);
  • 类似拷贝算法(参见10.2.2节,第341页),uninitialized_copy接受三个迭代器参数。前两个表示输入序列,第三个表示这些元素将要拷贝到目的空间。传递给uninitialized_copy的目的位置迭代器必须指向未构造的内存。与copy不同,uninitialized_copy在给定目的位置构造元素。类似copy,uninitialized_copy返回(递增后的)目的位置迭代器。因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置
  • 在本例中'我们将此指针保存在q中,然后将q传递给uninitialized_fill_no此函数类似fill_n(参见1022节,第340页),接受一个指向目的位置的旨针、一个计数和一个值。它会在目的位置指针指向的内存中创建给定数目个对象,用给定值对它们进行初始化。

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

  1. 我们将实现一个简单的文本查询程序,作为标准库相关内容学习的总结。我们的程序允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次,此行只列出一次。行会按照升序输出—— 即,第7行会在第9行之前显示,依此类推。例如,我们可能读入一个包含本章内容(指英文版中的文本)的文件,在其中寻找单词 element。输出结果的前几行应该是这样的:

12.3.1文本查询程序设计

  • 开始一个程序的设计的一种好方法是列出程序的操作。了解需要哪些操作会帮助我们分析出需要什么样的数据结构。从需求入手,我们的文本查询程序需要完成如下任务:
  • 当程序读取输入文件时,它必须记住单词出现的每一行。因此,程序需要逐行读取输入文件,并将每一行分解为独立的单词
  • 当程序生成输出时,
  • 1,它必须能提取每个单词所关联的行号
  • 2,行号必须按升序出现且无重复
  • 3,它必须能打印给定行号中的文本。
  • 利用多种标准库设施,我们可以很漂亮地实现这些要求:
  • 我们将使用一个vector<string>来保存整个输入文件的一份拷贝。输入文件中的每行保存为vector中的一个元素。当需要打印一行时,可以用行号作为下标来提取行文本。
  • 我们使用一个istringstream(参见8.3节,第287页)来将每行分解为单词。我们使用一个set来保存每个单词在输入文本中出现的行号。这保证了每行只出现一次且行号按升序保存。
  • 我们使用一个map来将每个单词与它出现的行号set关联起来。这样我们就可以方便地提取任意单词的set。我们的解决方案还使用了shared_ptr,原因稍后进行解释。

数据结构

  • 虽然我们可以用vector,set和map来直接编写文本杳询程序,但如果定一个更为抽象的解决方案,会更为有效。我们将从定义一个保存输入文件的类开始,这会令文件查询更为容易。我们将这个类命名为TextQuery,它包含一个vector和一个map。vector用来保存输入文件的文本,map用来关联每个单词和它出现的行号的set。这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。
  • 查询操作要完成的任务非常简单:查找map成员,检查给定单词是否出现。设计这个函数的难点是确定应该返同什么内容。一旦找到了一个单词,我们需要知道它出现了多
    少次、它出现的行号以及每行的文本。
  • 返回所有这些内容的最简单的方法是定义另~个类,可以命名为QueryResult,来保存查询结果。这个类会有一个print函数,完成结果打印工作。

在类之间共享数据

  • 我们的QueryResult类要表达查询的结果。这些结果包括与给定单词关联的行号的set和这些行对应的文本。这些数据都保存在TextQuery类型的对象中。由于QueryResult所需要的数据都保存在-TextQuery对象中,我们就必须确定如何访问它们。我们可以拷贝行号的set,但这样做可能很耗时。而且,我们当然不希望拷贝vector,因为这可能会引起整个文件的拷贝,而目标只不过是为了打印文件的小部分而已(通常会是这样)。
  • 通过返回指向TextQuery对象内部的迭代器(或指针),我们可以避免拷贝操作。但是,这种方法开启了-个陷阱:如果TextQuery对象在对应的QueryResult对象之前被销毁,会发生什么?在此情况下,QueryResult就将引用一个不再存在的对象中的数据。
  • 对于QueryResult对象和对应的TextQuery对象的生存期应该同步这一观察结果,其实已经暗示了问题的解决方案。考虑到这两个类概念上“共享”了数据,可以使用shared_ptr(参见12.1.1节,第400页)来反映数据结构中的这种共享关系。

使用TextQuery类

  • 当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法。通过这种方法,可以看到类是否具有我们所需要的操作。例如,下面的程序使用TTextQuery和QueryResult类。这个函数接受一个指向要处理的文件的ifstream,并与用户交互,打印给定单词的查询结果
#include <vector>
#include <memory>
#include <map>
#include <set>

class QueryResult;//为了定义函数query返回的类型,这个定义是必须的
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的构造函数接受一个ifstream,逐行读取输入文件:
#include "TextQuery.h"

//读取文件并且建立单词到行号的映射
TextQuery::TextQuery(std::ifstream &is) : file(new std::vector<std::string>) {
    std::string text;
    while (std::getline(is,text)){         //对文件的每一行
        file->push_back(text);             //保存此行文本
        int n = file->size() - 1;          //当前行号
        std::istringstream line(text);     //将行文本分解为单词
        std::string word;
        while (line >> word){              //对行中的每个单词
            //如果单词不在wm中,以之为下标在vm中添加一项
            auto &lines = wm[word];        //lines是一个shared_ptr
            if (!line){                    //在第一次遇到这个单词的时候,指针为空
                lines.reset(new std::set<line_no>);//分配一个新的set
            }
            lines->insert(n);              //将此行号插入到set中
        }
        
    }
}
  • 构造函数的初始化器分配一个新的vector来保存输入文件中的文本。我们用getline逐行读取输入文件,并存入vector中。由于file是一个shared_ptr,我们用-〉运算符解引用file来提取file指向的vector对象的push_back成员。
  • 接下来我们用一个istringstream (参见8.3节,第 287页 )来处理刚刚读入的一行中的每个单词。内层while循环用istringstream的输入运算符来从当前行读取每个单词,存入word中。在 while循环内,我们用map下标运算符提取与word相关联的shared_ptr<set>,并将lines绑定到此指针。注意,lines是一个引用,因此改变 lines也会改变wm 中的元素。
  • 若 word不在map中,下标运算符会将word添加到w m 中 (参 见 11.3.4节,第 387页),与 word关联的值进行值初始化。这意味着,如果下标运算符将word添加到w m 中,lines将是一个空指针。如果lines为空,我们分配一个新的set,并调用reset更新 lines引用的shared_ptr,使其指向这个新分配的set。
  • 不管是否创建了一个新的set,我们都调用insert将当前行号添加到set中。由于 lines是一个引用,对 insert的调用会将新元素添加到w m 中的set中。如 果 一 个 给定单词在同一行中出现多次,对 insert的调用什么都不会做。

QueryResult 类

  • QueryResult类有三个数据成员:一个string,保存查询单词; -个shared_ptr, 指向保存输入文件的vector; 一个shared_ptr,指向保存单词出现行号的set。它唯一的一个成员函数是一个构造函数,初始化这三个数据成员

query函数

  • query函数接受一个string参数,即查询单词,query用它来在map中定位对应的行号set。如果找到了这个string, query函数构造一个 QueryResult.保存给定 string, TextQuery的 file成员以及从wm中提取的set。
  • 唯一的问题是:如果给定string未找到,我们应该返回什么?在这种情况下,没有 可返回的set。为了解决此问题,我们定义了一个局部static对象,它是一个指向空的 行号set的 shared_ptro当未找到给定单词时,我们返回此对象的一个拷贝:

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值