前面的程序只用过静态内存或者栈内存。静态内存用于保存局部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<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) |
p 是shared_ptr 的拷贝;该操作会递增q 中的计数器。q 中的指针必须要可以转换成T* |
p = q |
p 和q 都是shared_ptr ,所保存的指针必须要可以互相转换。该操作会递减 p 的引用计数,递增q 的引用计数;如果 p 的引用计数变成0 ,就会把p 管理的原内存释放 |
p.unique() |
如果p.use_count() 是1 ,返回true ;否则false |
p.use_count() |
返回和p 共享对象的智能指针数量;可能很慢,主要用于调试 |
shared_ptr
和unique_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) |
交换p 和q 的指针 |
p.swap(q) |
同上 |
最安全的分配和使用动态内存的方法是调用一个名为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>
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
类自动销毁对象是通过另一个特殊的成员函数,也就是析构函数(destructor)完成销毁工作的。
类似构造函数,每个类都有一个析构函数。构造函数控制初始化,而析构函数控制该类型的对象销毁时执行什么操作。
例如,string
的构造函数会分配内存来保存string
的字符。而string
的析构函数就负责释放这些内存。
shared_ptr
的析构函数会递减它所指向的对象的引用计数。若引用计数变成0
,shared_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
元素。
程序使用动态内存处于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
容器是因为第一种原因而使用动态内存的典型例子。
本节中将定义一个类,它使用动态内存是为了让多个对象能够共享相同的底层数据。
有些类分配的资源具有和原来对象分别独立的生存期。我们可以定义一个类,用于保存一组元素,且该类不同的对象间拷贝元素时候共享相同的元素(也就是引用)。
Blob<string> b1; // 空Blob
{
// 新作用域
Blob<string> b2{"a", "an", "the"};
b1 = b2; // b1和b2共享相同的元素
}
// b2被销毁了 但是b2中的元素不能销毁
// b1指向最初b2创建的元素 也就是b1还在用b2的元素 或者说那其实就是b1的元素 只是b2死了 也就是这些元素的一个主人死了
使用动态内存的一个常见原因是允许多个对象共享相同的状态。
我们定义一个管理string
的类。该类名为StrBlob
。
因为现在还没学到模板的相关知识。所以暂借vector
来替我们管理元素。
但我们不能直接在一个StrBlob
对象里保存vector
,因为一个对象的成员在对象销毁的时候也会被销毁。比如假定b1
和b2
是两个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>
类型参数。该构造函数可以接受一个初始化器的花括号列表。此类中实现了size
和empty
和push_back
成员。这些成员通过指向底层vector
的data
成员来完成它们的工作。
两个构造函数都是用初始化列表来初始化其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_back
和front
和back
操作访问vector
里面的元素。这些操作在试图访问元素前必须检查元素是否存在:
#include<stdexcept>
void StrBlob::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}
check
是private
的工具函数,检查一个给定索引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();
}
front
和back
应该对const
进行重载
我们的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
没办法给其分配的对象命名,而是返回一个指向该对象的指针:
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 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_alloc
和nothrow
都定义在头文件new里面。
可以用delete表达式(delete expression)来把动态内存归还给系统。
delete p; // p必须指向一个动态分配的对象或者是个空指针
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
一个指针之后,指针值就变成无效的了。
但机器上指针还是保存着已经被释放掉的动态内存地址,这种指针人们称为空悬指针(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
还是指着那个已经被销毁的内存。
还可以用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) |
p 从unique_ptr u 那里接管了对象的所有权,将u 置空 |
shared_ptr<T> p(q, d) |
p 接管了内置指针q 所指的对象的所有权。q 必须能够转换成T* 类型。p 将使用可调用对象d 来代替delete 操作 |
shared_ptr<T> p(p2, d) |
p 是shared_ptr p2 的拷贝,唯一的区别是p 将调用d 来代替delete |
p.reset() p.reset(q) p.reset(q, d) |
若p 是唯一指向其对象的shared_ptr ,reset 会释放此对象。若传递了可选的参数内置指针 q ,会让p 指向q ,否则会将p 置为空。若还传递了 d ,就会调用d 而不是delete 来释放q |
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
,释放一个没有东西的内存?这不是脱裤子放屁?
比如用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
指向。而不能像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<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
管理删除器的方式和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
是种不控制所指向对象生存期的智能指针,它指向由一个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_ptr 或weak_ptr 。赋值后w 和p 共享对象 |
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
,指向StrBlob
的data
成员,这是初始化时提供给它的。
// 对于访问一个不存在元素的尝试 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.data
是StrBlob
的非公有成员,要访问它,必须要在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
。
将在第十四章学习定义自己的运算符。
现在定义deref
和incr
的函数,分别用来解引用和递增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
里采用的方法大多时候更加简单、更加快速并且更加安全。
大多应用应该用标准库容器而非动态分配的数组。使用容器更简单、也不容易出现内存管理错误,而且可能有更好的性能。
使用容器的类可以用默认版本的拷贝、赋值和析构操作。但是分配动态数组的类就必须要定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
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];
分配一个数组会得到一个(指向数组)元素类型的指针,而非返回一个数组类型的对象。所以不能对动态数组调用begin
或end
。同样,也不能用范围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)
// 处理数组
即使n
是0
,代码也是合法的。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
来获取一个内置指针,然后用它来访问数组元素。
用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) |
p 是T* 类型的指针,该算法对p 指向的对象执行析构函数 |
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
了第一个构造的元素,随后q
和p
相等,循环结束。
我们只能对真正构造了的元素进行destroy
操作。
元素被销毁后,就可以重新使用这块内存来保存其他的string
(之所以是string
,是因为该内存当前已经声明只能用来指向string
),或是释放将其还给系统。释放内存就要调用deallocate
来完成:
alloc.deallocate(p, n);
传递的deallocate
的指针必须指向由allocate
分配的内存。而且,传递给deallocate
的大小参数必须和调用allocated
分配内存时候提供的大小参数有一样的值。
allocator
类的两个伴随算法,可以在未初始化内存里创建对象。它们都定义在头文件memory
里。这些函数在给定目的位置创建元素,而不是由系统分配内存给他们:
allocator算法 | 含义 |
---|---|
uninitialized_copy(b, e, b2) |
从迭代器b 和e 指出的输入范围里拷贝元素到迭代器b2 指定的未构造的原始内存里。b2 指向的内存必须要能够容纳输入序列里元素的拷贝返回递增后的目的位置迭代器。也就是最后一个构造的元素之后的位置。 |
uninitialized_copy_n(b, n, b2) |
从迭代器b 指向的元素开始,拷贝n 个元素到b2 开始的内存里 |
uninitialized_fill(b, e, t) |
在迭代器b 和e 指定的原始内存范围里创建对象,对象的值都是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类
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
,也就是指向vector
的shared_ptr
- 保存单词关联的行号的
set
,也就是指向set
的shared_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...})
构造函数:
- 初始化器接受一个全新的
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
类的数据成员:
- 保存查询单词
string
- 指向保存输入文件的
vector
的shared_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; // 接收文件内容
};
- 接收查询单词
string
- 用它来在已有的单词关联容器里查找对应出现的所有行号
返回:
- 找到:构造
QueryResult
,构造函数的参数是:- 查询的单词
- 单词出现的行号集合
- 文件内容
- 没找到:定义一个局部
static
对象,指向空行号的集合set
的shared_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;
}
}
示例文件: