第12章动态内存——动态内存与智能指针

所有对象都有严格定义的生存期

全局对象在程序启动时分配,在程序结束时销毁;

局部对象,当进入其定义所在的程序块时呗创建,离开块时被销毁;

局部static对象在第一次使用前分配,在程序结束时销毁。

除了自动和static对象外,c++还支持动态分配对象。动态分配的对象生存期和他们在哪里被创建无关,只有显示的被释放,这些对象才会被销毁。

静态内存保存局部static对象、类static数据成员,以及定义在任何函数之外的变量。

栈内存用来定义在函数内的非static对象。

分配在静态或者栈内存的对象由编译器自动创建或销毁,对于栈对象,在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池,被称为自由空间(free store)堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用,代码必须显示销毁它们。

12.1动态内存与智能指针

动态内存的管理通过一对运算符来完成:new和delete。

new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;

delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态对象的使用很容易出问题

1、有时我们忘记释放内存,会产生内存泄漏。

2、尚且还有指针引用内存的情况下我们就释放它,产生引用非法内存的指针。

智能指针(smart pointer):

  • shared_ptr允许多个指针指向同一个对象;
  • unique_ptr则“独占”所指向的对象;

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

头文件:memory

12.1.1shared_ptr类

类似vector,智能指针也是模板。所以在创建一个智能指针的时候,必须提供指针可以指向的类型。默认初始化的智能指针保存一个空指针。

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared标准库函数。此函数在动态内存中分配一个对象并且初始化它,返回指向该对象的shared_ptr。

使用方式:当做模板类来使用就好,类后面加<>尖括号,其中给定类型,若使用string类型,则传递的参数必须和string的某个构造函数相匹配;当然也可以使用auto来进行定义。

shared_ptr<int> p1 = make_shared<int>(6);

shared_ptr<string> p2 = make_shared<string>("aaqqzz");
shared_ptr<string> p2 = make_shared<string>(6,'y');

auto p2 = make_shared<string>("qweq");

shared_ptr的拷贝和赋值

当进行拷贝或者赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

shared_ptr<int> r = make_shared<int>(3);
r = p;//q所指向的对象引用计数递增
      //r所指向的对象引用计数递减
      //r原来所指向的对象已没有引用者,会自动释放

NOTE:到底是使用一个计数器还是其它数据结构来记录有多少指针共享对象,由标准库的具体实现来决定。关键在于智能指针类能记录有多少个shared_ptre指向相同的对象,并且在恰当的时候自动释放对象

shared_ptr自动销毁所管理的对象

shared_ptr析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr的析构函数会销毁对象,并且释放它使用的内存。

share_ptr还会自动释放相关联的内存

当动态对象不再被使用时,shared_ptr类会自动释放动态对象。

比如:定义一个返回Foo类型的shared_ptr函数

shared_ptr<Foo> factory(T arg){
    return make_shared<Foo>(arg);
}

由于factory返回一个shared_ptr,所以我们可以确保它分配的对象在恰当的时候被释放。例如:下面的函数将factory返回的shared_ptr保存在局部变量之中

void use_factory(T arg){
    shared_ptr<Foo> p = factory(arg);
    //在use_factory函数内部使用p
    //p是局部变量,p离开了作用域将会销毁
    //由于p将销毁,p指向的对象也会被销毁,指向的内存会被自动释放掉
}

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

void use_factory(T arg){
    shared_ptr<Foo> p = factory(arg);
    return p;
    //return语句向函数的调用者返回一个p的拷贝,p的引用计数进行递增操作
    //它所指向的内存就不会被释放掉
}

NOTE:如果你将shared_ptr存放在一个容器中,而后来不再需要全部元素,记得用erase删除不再需要的元素。

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

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

1、程序不知道自己需要使用多少对象 --- 容器类使用动态内存的原因

2、程序不知道所需对象的准确类型

3、程序需要再多个对象之间共享数据

我们使用的某些类,分配的资源和对应对象生存期一致

vector<string> v1;
{
	vector<string> v2{ "aaa","qqqq","z" };
	v1 = v2;//从v2中拷贝元素到v1中
    //v2离开作用域被销毁,其中的元素也被销毁
}

但是某些类分配的资源具有与原对象相独立的生存期,定义一个名为Blob的类,保存一组元素。和容器不同,希望Blob对象的不同拷贝之间共享相同元素。一般而言,如果两个对象共享低层的数据,当某个对象被销毁时,我们不能单方面地销毁低层数据。

Blob<string> b1;
{
    Blob<string> b2{"a","an","the"};
    b1 = b2;//b1和b2共享相同的元素
    //b2被销毁,b2中的元素不能销毁
}//b1指向最初由b2创建的元素

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

定义StrBlob类

定义一个管理string的类,此版本命名为StrBlob,实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素,借助标准库类型来管理元素所使用的内存空间本例中使用vector来保存元素。

首先,我们不能在一个Blob对象内直接保存vector,因为一个对象销毁时其成员也自动被销毁。为了保存vector中元素继续存在,我们将vector保存在动态内存之中。

然后,为了实现数据共享我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector。

最后,还要确定这个类应该提供什么操作:访问元素的操作(front 和 back);当试图访问不存在的元素,抛出异常。

我们的类有一个默认构造函数和一个构造函数接受单一的initializer_list<string>类型参数。可以接受一个初始化器的花括号列表。

StriBlob构造函数

StrBlob::StrBlob():data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string> il):data(make_shared<vector<string>>(il)){}
//接受一个列表初始化的构造函数将其参数传递给对应的vector构造函数
//此构造函数通过拷贝列表中的值来初始化vector中的元素

C++11允许构造函数和其他函数把初始化列表当做参数

元素访问成员函数

pop_back、front和back操作访问vector中的元素,试图访问元素之前必须检查元素是否存在定义一个check()函数检查一个给定的索引是否在合法范围内

StrBlob的拷贝、赋值和销毁

StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。

StrBlob类的具体实现

class StrBlob {
public:
	typedef vector<string>::size_type size_type;
	StrBlob();
    //接受一个列表初始化的构造函数将其参数传递给对应的vector构造函数
	StrBlob(initializer_list<string> il);
	//此构造函数通过拷贝列表中的值来初始化vector中的元素
	//C++11允许构造函数和其他函数把初始化列表当做参数
	size_type size() const { return data->size(); }
	bool empty()const { return data->empty(); }
	//添加和删除元素
	void push_back(const string&t) { data->push_back(t); }
	void pop_back();
	//元素访问
	string &front();
	string &back();
private:
	shared_ptr<vector<string>> data;
	void check(size_type i, const string &msg) const;
};
StrBlob::StrBlob():data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string> il):data(make_shared<vector<string>>(il)){}
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}
string& StrBlob::front() {
	check(0, "front on empty StrBlob");
	return data->front();
}
string& StrBlob::back() {
	check(0, "back on empty StrBlob");
	return data->back();
}
void StrBlob::check(size_type i, const string &msg) const
{
	if (i >= data->size())
		throw out_of_range(msg);
}

12.1.2直接管理内存 

c++定义两个运算符来分配和释放动态内存:new 和 delete

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

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针

在默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型的对象将用默认构造函数进行初始化:

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

初始化方式:

  1. 直接初始化   
  2. 传统构造方式初始化
  3. 列表初始化
  4. 值初始化---类型名之后加一个空括号
int *p = new int(1024);
string *ps = new string(10,'9');
vector<int> *pv = new vector<int>{1,2,3,4};
int *p2 = new int(); //值初始化为0,*p2 = 0
string *ps2 = new string();//值初始化为空string
//等价于 string *p2 = new string; //默认初始化也是空string

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的。不管采用什么形式,对象都会采用默认初始化的方式;

对于内置类型两者差别就很大,值初始化的内置对象由良好的定义,而默认初始化对象的值则是未定义的。

Best Practices : 处于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

提供了括号包围的初始化器,可以使用auto从此初始化器腿短我们想要分配的对象类型,但是括号中只能包含单一初始化器。

auto p1 = new auto(obj);//正确
auto p2 = =new auto(a,b,c) //错误

动态分配的const对象

用new分配const对象是合法的:

const int *p = new const int(1024);//分配并初始化一个const int
const string *ps = new const string;//分配并默认初始化一个const空string

类似于其他const对象,一个动态分配的const对象必须初始化,由于其分配的对象是const,所以new返回的指针是一个指向const的指针。

内存耗尽

如果new不能分配所要求的的内存空间,它会抛出一个类型为bad_alloc的异常,我们可以改变使用new的方式来组织它抛出异常:

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

我们称这种形式的new为定位new(placement new),定位new表达式允许我们向new传递额外的参数。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc 和 nothrow都定义在头文件new之中。

释放动态内存

通过delete表达式来将动态内存归还给系统,delete表达式接受一个指针,指向我们想要释放的对象。

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

delete表达式也是像new一样执行两个操作:销毁给定指针所指向的对象;释放对应的内存。

指针值和delete

传递给delete的指针必须指向动态分配的内存或是一个空指针,释放一块并非new分配的内存或者将相同指针值释放多次,其行为都是未定义的。

int i,*pi = &i;
double *pd = new double(3),*pd2 = pd;
delete pi;//未定义:pi指向一个局部变量
delete pd;//正确
delete pd2;//未定义:pd2指向的内存已经被释放了

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

虽然const对象的值不能改变,但是它本身是可以被销毁,想要释放一个const动态对象,只要delete指向它的指针即可。

const int *p1 = new const int(1024);
delete p1;

动态对象的生存期知道被释放时为止

内置指针管理的动态对象,知道被显示释放之前它都是存在的。返回指向动态内存的指针(而不是只能指针)的函数给其他调用者增加一个额外的负担:调用者必须释放内存

Foo* factory(T arg){
    return new Foo(arg); //调用者负责释放此内存
}

factory的调用者扶着在不需要此对象时释放它。不幸的是,调用者经常会忘记释放对象:

void use_factory(T arg){
    Foo *p = factory(arg);//使用了p但是忘记delete
}//p离开作用域,局部变量p被销毁,但是内存并没有释放

与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果指针指向动态内存,那么内存将不会被自动释放。

WARING:由内置指针(而不是只能指针)管理的动态内存在被显示释放前一直都会存在

正确的做法:在use_factory中记得释放内存

void use_factory(T arg){
    Foo *p = factory(arg); 
    delete p;
} 

小心:动态内存的管理非常容易出错

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

  • 1、忘记delete内存。

忘记释放动态内存会导致人们常说的“内存泄露”问题,值得注意的是查找内存泄露错误是非常困难的,因为通常程序运行了很久之后,真正耗尽内存时,才能检测到这种错误。忘记释放动态内存会导致人们常说的“内存泄露”问题,值得注意的是查找内存泄露错误是非常困难的,因为通常程序运行了很久之后,真正耗尽内存时,才能检测到这种错误。

  • 2、使用已经释放掉的对象。

释放内存后将指针置为空,有时可以检测出这种错误。

  • 3、同一块内存释放两次。

当有两个指针指向相同的动态分配对象时,可能发生这种错误,如果对其中一个指针进行了delete操作,对象的内存已经归还给自由空间了,如果再次delete第二个指针,自由空间就可能被破坏。当有两个指针指向相同的动态分配对象时,可能发生这种错误,如果对其中一个指针进行了delete操作,对象的内存已经归还给自由空间了,如果再次delete第二个指针,自由空间就可能被破坏。

相对于查找和修正这些错误,制造这些错误要简单的多。
Best Practice:坚持使用智能指针,就可以有效避免以上问题。对于一块内存,只有在没有任何只能指针指向它的情况下,只能指针才会自动释放。

delete之后重置指针值……

当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但是很多机器上指针仍然保存(已经释放了的)动态内存地址。在delete之后,指针就变成了人们所说的空悬指针(dangling pointer),即一块曾经保存数据对象但是现在已经无效的内存的指针
有一种方法可以避免空悬指针的问题,在指针即将要离开作用域之间释放掉它所关联的内存。这样指针关联的内存呗释放掉之后,就没有机会继续使用指针了。

……这只是提供了有限的保护

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

    int*p(new int(4));
    auto p = q;
    delete p ;
    p = nullptr;

p,q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,对q没有任何作用,当我们删除p所指向的内存时,q也变成无效的了。在实际系统中,查找指向相同内存的所有指针是异常困难的!

12.1.3shared_ptr和new结合使用

重点:接受指针参数的智能指针构造函数是explicit的,内置指针和智能指针不能隐式转换,必须直接初始化

shared_ptr<int> p1 = new int(111); //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(111));  //正确:使用了直接初始化形式

p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于内置指针和只能指针不能隐式转换,所以这个初始化语句是错的。同样,一个shared_ptr的函数不能在反悔的时候隐式转换成一个普通指针:

shared_ptr<int> clone(int p){
    return new int(p);                  //错误:隐式转换为shared_ptr<int>
    return shared_ptr<int>(new int(p)); //正确:显示绑定到一个想要返回的智能指针
}

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。可以将智能指针绑定到一个指向其他类资源的指针上。但是,必须提供自己的操作来替代delete

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

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

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

process的参数是传值方式传递的,因此实参会被拷贝到ptr中,拷贝一个shared_ptr会增加其引用次数;因此当局部变量ptr被销毁时,ptr指向的内存不会被释放。

如果传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显示构造的。但是这样很可能会导致错误:

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

销毁这个临时变量递减引用次数,此时引用次数变为0。因此,当临时对象被销毁时,它所指向的内存会被释放。但是x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。

一旦用shared_ptr绑定到一个普通指针,我们就把内存的管理责任交给这个shared_ptr。一旦这样做了!我们就不应该再使用内置指针来访问shared_ptr所指向的内存。

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

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

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

get()为这种情况而设计:我们需要向不适用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针

shared_ptr<int> p(new int(34));
int *q = p.get(); //正确:但是要注意q指针不能被释放
{
    shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放了
int foo = *p;//未定义:p指向的内存已经呗释放了。

 “p q指向想用的内存,由于它们是相互独立创建的,因此哥子的引用计数都是1.当q所在的程序块结束时,q被销毁,导致q指向的内存被释放。p变成一个空悬指针,意味着我们试图使用p时,会发生未定义的行为,而且当p被销毁时,这块内存会被第二次delete” ——书上这样写的,理论上应该是对的,但是实际操作发现好像不一样。

WARNING:get用来将指针的访问权限传递给代码,只有确定代码不会delete指针的情况下才可以使用。特别的是:永远不要用get初始化另一个智能指针或者另一个智能指针赋值。

其它的shared_ptr操作

我们可以用reset来将一个新的指针赋予一个shared_ptr:

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

与赋值类似,reset会更新引用计数。reset成员经常和unique一起使用,来控制多个shared_ptr共享的对象

在改变低层对象之前,要检查自己是否是当前对象仅有的用户;如果不是,就要在改变之前做一份拷贝。

if(!p.unique())
    p.reset(new string(*p)); //我们不是唯一用户;分配新的拷贝
*p += newVal; //我们确定是唯一用户,可以改变对象的值。

12.1.4智能指针和异常

重点:智能指针即使发生异常也可以释放内存

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

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

为什么即使发生异常也可以释放呢?原因在:函数退出有两种可能,正常结束或者发生了异常,无论哪种情况,局部对象都会被销毁。sp销毁时候引用计数递减为0,所以此块内存释放掉。

与之相对!当发生异常,直接管理的内存是不会自动释放。

void f(){
    int *ip = new int(4);
    //这段代码抛出一个异常,在f中未被捕获
    delete ip;    //在退出之前释放内存
}

如果在new和delete之间发生异常,且f中没有捕获异常,则内存就永远不会释放了。因为这个内存在f之外没有指针指向了,所以无法释放了。

智能指针和哑类

很多c++类都定义了析构函数,负责清理对象使用的资源。但是,并不是所有的类都有这样良好的定义。特别是那些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结束时由析构函数自动关闭连接。所以我们使用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时,可以传递一个(可选的)指向删除器函数的参数

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

当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。

错误的想法:

想直接在函数结束shared_ptr自动释放,后来想想觉得不对,可能在disconnect()函数里面还有别的操作吧,先不深究,记住如何正确操作就行了。

void f(destination &d /*其他参数*/){
  //修改使用shared_ptr
    shared_ptr<connection> c = connect(&d);
} 

注意:智能指针陷阱

  • 为了正确使用智能指针,我们必须坚持一些基本规范。
  • 不使用相同的内置指针初始化(或reset)多个智能指针
  • 不delete get() 返回的指针
  • 不使用get() 初始化 或reset另一个智能指针
  • 如果使用get()返回的指针,要记住当最后一个智能指针被销毁后,指针就变得无效了。
  • 如果使用的智能指针管理的资源不是new分配的,要传给它一个删除器

 12.1.5  unique_ptr

一个unique_ptr“拥有”它所指向的对象,某个时刻智能有一个unique_ptr指向一个给定对象。

当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:

unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作

unique_ptr<string> p1(new string("asx"));
unique_ptr<stirng> p2(p1);                 //错误:unique_ptr不支持拷贝

unique_ptr<string> p3;
p3 = p2;                                   //错误:unique_ptr不支持赋值

但是我们可以通过release或者reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique

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

reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放所以将p2调用reset释放了所使用的内存。

调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放

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

传递unique参数和返回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

12.1.6  weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象还是会被释放。

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

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

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

if(shared_ptr<int> np = wp.lock()){    //如果np不为空则条件成立
}

核查指针类

作为weak_ptr用途的一个展示,我们将为StrBlob类定义一个伴随指针类。我们的指针类将明明为StrBlobPtr,会保存一个weak_ptr,指向StrBlob的data成员。通过使用weak_ptr,可以组织用户访问一个不再存在的vector的企图。

StrBlobPtr会有两个数据成员:

wptr:指向一个StrBlob中的vector(或者为空)

curr:保存当前对象所表示的元素的下标

指针类的check()来检查解引用StrBlobPtr是否安全:

//对于访问一个不存在的元素的尝试,StrBlobPtr抛出一个异常
class StrBlobPtr {
public:
	StrBlobPtr():curr(0){}
	StrBlobPtr(StrBlob& a,size_t sz =0):wptr(a.data),curr(sz){}
	string& deref() const;
	StrBlobPtr& incr();
private:
	//若检查成功,check返回一个指向vector的shared_ptr
	shared_ptr<vector<string>> check(size_t, const string&) const;
	//保存一个weak_ptr,意味着低层vector可能会被销毁
	weak_ptr<vector<string>> wptr;
	size_t curr;//数组中当前的位置
};

 StrBlobPtr的check()检查指针指向的vector是否还存在

shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string& msg) const {
	auto ret = wptr.lock();
	if(!ret)
		throw runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw out_of_range(msg);
	return ret;
}

指针操作

定义名为deref和incr的函数,分别用来解引用和递增StrBlobPtr

deref()调用check(),检查vector是否以及curr是否在合法范围内:

string& StrBlobPtr::deref() const{
    auto p = check(curr,"derefernce past end");
    return (*p)[curr];
}

incr()也调用check()

//前缀递增:返回递增后对象的引用
StrBlobPtr& StrBlobPtr::incr(){
    check(curr,"increment past end of StrBlobPtr");
    ++curr;
    return *this;
}

为了访问data成员,把指针类StrBlobPtr声明为StrBlob的friend,还要为StrBlob类定义为begin和end操作,返回一个指向它自身的StrBlobPtr

class StrBlobPtr;
class StrBlob{
    friend class StrBlobPtr;
    //返回指向首元素和尾后元素的StrBlobPtr
    StrBlobPtr begin(){return StrBlobPtr(*this);}
    StrBlobPtr end(){
        auto ret = StrBlobPtr(*this,data->size());
        return ret;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_34269988/article/details/88613928