C++ 智能指针

2022/2/23 20:53:27

本文主要是介绍C++ 智能指针,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

shared_ptr

智能指针也是模板类,因此当我们创建一个智能指针是要提供额外的信息——指针可以指向的类型。默认初始化的智能指针保存着一个空指针。shared_ptr允许多个指针指向同一对象。

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

在这里插入图片描述

make_shared

最安全的分配和使用动态内存的方法是调用一一个名为make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr与智能指针一样,make_shared也定义在头文件memory中。当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int> (42);
// p4指向一个值为"999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>() ;

类似顺序容器的emplace成员make_shared 用其参数来构造给定类型的对象。例如,调用make_shared时 传递的参数必须与string 的某个构造函数相匹配,调用make_shared< int>时传递的参数必须能用来初始化一个int,依此类推。如果我们不传递任何参数,对象就会进行值初始化当然,我们通常用auto定义一个对象来保存make_ shared的结果,这种方式较为简单:

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

拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ ptr都会记录有多少个其他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被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

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

自动销毁所管理的对象

shared_ ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。如果两个对象共享底层数据,当某个对象被销毁时,我们不能单方面的销毁数据。

和new使用

如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表所示,我们还可以用new返回的指针来初始化智能指针:

shared ptr<double> p1; // shared ptr可以指向一-个double
shared_ ptr<int> p2(new int(42)); // p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

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

p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

shared_ptr<int> clone(int p) {
	return new int(p); //错误:隐式转换为shared_ ptr<int>
}

我们必须将shared_ ptr 显式绑定到一个想要返回的指针上:

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

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用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
int i=*p;//正确:引用计数值为1

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

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

在上面的调用中,我们将一个临时 shared_ptr 传递给process.当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。当将一个shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr一旦这样做了 ,我们就不应该再使用内置指针来访问shared_ ptr 所指向的内存了。

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

智能指针类型定义了一个名为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指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

删除器

默认情况下,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接下来,end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。

陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

一个unique_ ptr “拥有”它所指向的对象。与shared_ ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当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> p1 (new string ("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误: unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2;//错误: unique_ptr不支持赋值

在这里插入图片描述
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const) unique_ptr 转移给另一个unique:

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

release成员返回unique_ ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。

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:

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;
}

对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”。

unique_ptr中的删除器

重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr 指向类型之后提供删除器类型。在创建或reset一个这种unique_ ptr 类型的对象时,必须提供一个指定类型的可调用对象(删除器):

// 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)*>
	P(&C, end_connection);
	//使用连接
	//当f退出时(即使是由于异常而退出), connection会被正确关闭
}

在本例中我们使用了decltype 来指明函数指针类型。由于decltype (end_connection) 返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个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。此函数检查weak_ptr 指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。 与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。 例如:

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

在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。



这篇关于C++ 智能指针的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程