智能指针和普通指针的行为是类似的,都是用来动态管理对象的。shared_ptr允许多个指针指向同一个对象,而unique_ptr则独占所指向的对象。最后一个weak_ptr, 它是一种伴随类,是一种弱引用,指向shared_ptr所管理的对象。
一、shared_ptr类概述
shared_ptr是一种类模板,必须提供额外的信息——指针所指向的数据类型,定义在头文件memory当中。具体可见C++ primer 400页。
shared_ptr<string> ptr1;
这是定义一种智能指针的方式,默认初始化的智能指针保存着一个空指针。
最安全的分配和使用动态内存的方式就是使用make_shared的标准库函数,这个函数也定义在头文件memory当中。 此函数在内存中分配一个对象并初始化它,返回指向此对象的shared_ptr,如下所示:
shared_ptr<int> ptr1(make_shared<int>(100));
shared_ptr<string> ptr2(make_shared<string>(10,''a));
shared_ptr也可以和new结合起来使用,比如下面:
shared_ptr<int> ptr1(new int(100));
注意,下面这样写是不对的:
shared_ptr<int> ptr1 = new int(100);
这样是错误的,因为智能指针的构造函数是explicit的,不允许类类型之间的隐式转换。所以普通指针是不能直接赋值给智能指针的。必须使用直接初始化的方式才行。
shared_ptr可以协调对象的析构,但是这技能仅局限于自身的拷贝(shared_ptr之间),所以,我们应该避免将普通指针和智能指针混用。
看下面的例子:
void process(shared_ptr<int>ptr)
{
//使用ptr
}离开之后,ptr指向的内存不会被释放,继续访问其指向的内存是安全的
这是因为process的参数是传值的方式传输的,所以在复制的时候,引用计数会加一,在函数里面就不会释放内存。
shared_ptr<int> p(new int(100));
process(p);
int i = *p;//正确的,引用计数为1
但是,如果我们混用了普通指针和智能指针,比如下面:
int* i = new int(99);
process(i);//错误,普通指针不能直接赋值给智能指针
process(shared_ptr<int>(i));//这样是可以的,但是注意内存会被释放
int j = *i;//错误,i是一个悬空指针
上面的智能指针的参数是用以恶搞普通指针显示构造的,这样可行,但是会造成意想不到的麻烦。因为这样做就相当于把内存管理的责任交给了智能指针,一旦这么做了(混用普通指针和智能指针),我们就不能再访问那一段内存了, 否则很容易出错。如果我们后续想要释放那段内存的话,也会出错,就是经典的double free问题。 因为内存已经被释放了,再释放就会导致未定义的错误。
所以,记住,不要混用普通指针和智能指针。
shared_ptr原理:
shared_ptr内部实现了一个计数器,用于记录引用同一块动态内存的智能指针数量,一旦有新的智能指针指向了已被引用的动态内存,引用计数加一。一旦有智能指针生命周期到了调用析构函数销毁时,引用计数减一,如果检测到此时引用计数为零,则销毁智能指针的同时释放动态内存。
.use_count()方法可以返回智能指针的引用计数。
.unique()方法,如果use_count()为1,返回true,否则返回false
二、使用shared_ptr的陷阱
下面看一个例子:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Company;//前置声明,公司类
//创建worker类
class Worker {
public:
Worker() {
cout << "执行worker类的构造方法" << endl;
}
~BOY() {
cout << "执行worker类的析构方法" << endl;
}
void GetCompany(shared_ptr<Company>& company_ptr) { //BOY获取GRIL方法
company = company_ptr;
}
private:
shared_ptr<Company> company; //worker类里的shared_ptr指针
};
//创建Company类
class Company {
public:
Company() {
cout << "执行company类的构造方法" << endl;
}
~Company() {
cout << "执行company类的析构方法" << endl;
}
void GetWorker(shared_ptr<Worker>& worker_ptr) { //GRIL类获取BOY方法
worker = worker_ptr;
}
private:
shared_ptr<Worker> worker; //Company类里的shared_ptr指针
};
int main(void) {
{
shared_ptr<Company> ptr1(new Company); //分配一个boy类对象
shared_ptr<Worker> ptr2(new Worker); //分配一个gril类对象
ptr1->GetWorker(ptr2);
ptr2->GetCompany(ptr1);
}
system("pause");
return 0;
}
在这个例子中,有一个公司类,有一个员工类,然后他们的类里面的智能指针互相引用。
在动态创建完公司类和员工类的对象之后,它们各自的智能指针的引用计数为一,又因公司类与员工类类里各有一个指向对方的shared_ptr指针,执行完它们各自的GetCompany和GetWorker函数之后,他们的智能指针就会互相引用
当生命周期到了,都等着对方释放,但是又都不能释放,此时引用计数为一不为零,所以任何一个动态内存都无法释放,其中一个无法释放,就导致另一个无法释放,就造成了类似于死锁的情况。两个都不能释放。就造成了内存泄漏。
shared_ptr的double free问题
三、 “死锁”的解除——使用weak_ptr
weak_ptr指针——弱指针,它是为了解决shared_ptr的弊端而被设计出来辅助shared_ptr完成工作的指针。它不能使用 * 与 -> 运算符。但它可以通过lock 获得一个可用的 shared_ptr 对象。weak_ptr不会引起引用计数的变化。除非它通过lock 获得一个可用的 shared_ptr 对象,这时引用计数会加一,shared_ptr被销毁后,引用计数会减一。
weak_ptr从概念上,它是一个智能指针,相对于shared_ptr,它对于引用的对象是“弱引用”的关系。简单来说,它并不“拥有”对象本身。
有一个例子非常生动:weak_ptr就像是一个房地产中介。房地产中介并不拥有房子,但是我们有办法找到注册过的房产资源。在客户想要买房子的时候,它起初并不知道房子是否已经卖出了,它需要找到房主询问后再答复客户。
weak_ptr做的事情几乎和房产中介是一模一样的。weak_ptr并不拥有对象,在另外一个shared_ptr想要拥有对象的时候,它并不能做决定,需要转化到一个weak_ptr后才能使用对象。所以weak_ptr只是一个“引路人”而已。
那么weak_ptr除了解决相互引用的问题,还能做什么?答案是:一切应该不具有对象所有权,又想安全访问对象的情况。
就按照上面shared_ptr的冲突代码为例:公司和员工类互相引用对方的智能指针,通常的场景是:一个公司类可以拥有员工,那么这些员工就使用shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以也是用shared_ptr维护,这个时候问题就出来了。但是实际情况是,员工并不拥有公司,所以应该用weak_ptr来维护对公司的指针。 所以,只需要将其中一个对象的shared_ptr替换为weak_ptr,即可解决循环引用问题。
再举一个例子:我们要使用异步方式执行一系列的Task,并且Task执行完毕后获取最后的结果。所以发起Task的一方和异步执行Task的一方都需要拥有Task。但是有时候,我们还想去了解一个Task的执行状态,比如每10秒看看进度如何,这种时候也许我们会将Task放到一个链表中做监控。这里需要注意的是,这个监控链表并不应该拥有Task本身,放到链表中的Task的生命周期不应该被一个观察者修改。所以这个时候就需要用到weak_ptr来安全的访问Task对象了。
weak_ptr的操作
use_count():返回与weak_ptr共享对象的shared_ptr的数量。
expired():若use_count为0,则返回true,否则返回false;
lock():如果expired为true,返回一个空的共享指针,否则,返回一个指向共享对象的shared_ptr
由于对象可能不存在,我们不能直接访问weak_ptr指向的对象,必须要用lock检查对象是否还存在。
四、unique_ptr概述
unique_ptr具有与auto_ptr一样的排他所有权性,同一块动态内存也只允许一个智能指针对象绑定。
所以unique_ptr是不支持一般的拷贝和赋值的。
加一个move相当于向编译器声明你知道这样操作的风险和后果。实际上这样使用和auto_ptr的直接赋值也没有区别了,只是多了一个move提醒你自己智能指针赋值的特殊性。
虽然我们不能拷贝或者赋值unique_ptr,但是可以通过release或者reset函数调用来将指针的所有权从一个非const的unique_ptr转移到另一个unique_ptr上去:
u.release(): u放弃对指针的所有权,返回指针,并将u置为nullptr;
u.reset():u释放所指的对象
unique_ptr<int> p1(new int (11));
unique_ptr<int> p2(p1.release());
p1将指针所有权从p1转移到p2,并和p1被置空。 或者
unique_ptr<int> p1(new int (11));
unique_ptr<int> p2;
p2.reset(p1.release());
p2释放了原来指向的那个对象,然后p1对对象的所有权被转移给p2,p1被置空。