智能指针
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准提供的两种智能指针的区别在于管理底层指针的方式。shared_ptr
允许多个指针指向同一个对象;unique_ptr
则“独占”所指向的对象。标准库还定义了一个名为weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr
所管理的对象。 这三个类型都定义在memory
头文件中。
文章目录
shared_ptr
类似vector
,智能指针是模板。当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2; //shared_ptr,可以指向Int的list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
shared_ptr和unique_ptr都支持的操作
shared_ptr<T> sp; //空智能指针,可以指向类型为T的对象
unique_ptr<T> up;
p; //将p用作一个条件判断,若p指向一个对象,则为true
*p; //解引用p,获得它所指向的对象
p->mem; //等价于(*p).mem
p.get(); //返回p中保存的指针。要小心使用,若智能指针释放了其对象,
//返回的指针所指向的对象也就消失了
swap(p, q); //交换p和q中的指针
p.swap(q);
shared_ptr独有的操作
make_shared<T>(args); //返回一个shared_ptr,指向一个动态分配的类型为T的对象
//使用args初始化此对象
shared_ptr<T>p(q); //p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针
//必须能够转换到T*;
p = q; //p和q都是shared_ptr,所保存的指针必须能够互相转换。
//此操作会递减p的引用次数,递增q的引用次数;若p的引用次数变为0
//则将其管理的原内存释放
p.unique(); //若p.use_count()为1,返回true;否则返回false
p.use_count(); //返回与p共享对象的智能指针数量;
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared
的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。
当要用make_shared
时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
类似顺序容器的emplace
成员,make_shared
用其参数来构造给定类型的对象。例如,调用make_shared<string>
时传递的参数必须与string
的某个构造函数相匹配,调用make_shared<int>
时传递的参数必须能用来初始化int
,依此类推,如果我们不传递任何参数,对象就会进行值初始化。
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象。
auto p = make_shared<int>(42); //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
可以认为每个shared_ptr
都有一个关联的计数器。通常称为引用计数,无论何时拷贝一个shared_ptr
,计数器都会递增。无论何时我们拷贝一个shared_ptr
,计数器都会递增。例如,当用一个shared_ptr
初始化另一个shared_ptr
,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增,当我们给shared_ptr
赋予一个新值或者是shared_ptr
被销毁(例如一个局部的shared_ptr
离开作用域)时,计数器就会递减。
一旦一个shared_ptr
的计数器变为0,它就会自动释放自己所管理的对象。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr
被销毁时,shared_ptr
类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。
shared_ptr
的析构函数会递减它所指向的对象的引用次数。如果引用计数变为0,shared_ptr
的析构函数就会销毁对象,并释放它所占用的内存。
shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。
shared_ptr<double> p1; //shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42)); //p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit
的(拷贝赋值也是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<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
p.reset(); //若p是唯一指向其对象的shared_ptr,reset会释放此对象
p.reset(q); //若传递了可选的参数内置指针q,会令p指向q,否则将p置为空
p.reset(q, d); //若还传递了参数d,将会调用d而不是delete来释放q
不要混合使用普通指针和智能指针
shared_ptr
可以协调对象的析构,但这仅限于自身的拷贝(也是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
指向的内存不会被释放。
使用此函数的正确方法是传递给它一个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)); //危险: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<int>(q); //两个独立的shared_ptr指向相同的内存
} //程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义:p指向的内存已经被释放了
p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这回导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete
。
智能指针陷阱
- 不使用相同的内置指针值初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果你使用了get()返回的指针,记住当最后一个对应的智能指针销毁时,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
unique_ptr
一个unique_ptr
拥有它所指向的对象。与shared_ptr
不同,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。
与shared_ptr
不同,没有类似make_shared
的标准库函数返回一个unique_ptr
。当我们定义一个unique_ptr
时,需要将其绑定到一个new
返回的指针上,初始化unique_ptr
必须采用直接初始化形式。
unique_ptr<double> 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操作
unique_ptr<T> u1; //空unique_ptr,可以指向类型为T的对象。u1会使用delete
unique_ptr<T, D> u2; //来释放它的指针;u2会使用一个类型为D的可调用对象来释放
//它的指针
unique_ptr<T, D> u(d); //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替
//delete
u.release(); //u放弃对指针的控制权,返回指针,并将u置为空
u.reset(); //释放u指向的对象
u.reset(q); //如果提供了内置指针q,令u指向这个对象,否则将u置为空
u.reset(nullptr);
虽然我们不能拷贝或赋值unique_ptr
,但是可以通过调用release
或reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个unique
:
unique_ptr<string> p2(p1.release()); //将所有权从p1转给p2
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
释放了初始化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<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;
}
借助std::move()
可以实现将一个unique_ptr
对象赋值给另一个unique_ptr
对象,其目的是实现所有权的转移。
//A作为一个类
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针。它指向由一个shared_ptr
管理的对象。将一个weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用次数。一旦最后一个指向对象的shared_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,返回true,否则返回false
w.lock(); //如果expired为true,返回一个空shared_ptr;否则返回一个
//指向w的对象的shared_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
。
由于weak_ptr
并没有重载operator->
和operator *
操作符,因此不可以直接通过weak_ptr
使用对象,同时也没有提供get()
函数直接获取裸指针。典型的用法是调用其lock
函数来获得shared_ptr
示例,进而访问原始对象。
初始化方式
- 通过
shared_ptr
直接构造,也可以通过隐式转换来构造; - 允许移动构造,也允许拷贝构造
#include <iostream>
#include <memory>
class Frame {};
int main()
{
std::shared_ptr<Frame> f(new Frame());
std::weak_ptr<Frame> f1(f); //shared_ptr直接构造
std::weak_ptr<Frame> f2 = f; //隐式转换
std::weak_ptr<Frame> f3(f1); //拷贝构造函数
std::weak_ptr<Frame> f4 = f1; //拷贝构造函数
std::weak_ptr<Frame> f5;
f5 = f; //拷贝赋值函数
f5 = f2; //拷贝赋值函数
std::cout << f.use_count() << std::endl; //1
}