C++智能指针 shared_ptr、weak_ptr和unique_ptr总结

1,078 阅读8分钟

智能指针和普通指针的行为是类似的,都是用来动态管理对象的。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问题

image.png

三、 “死锁”的解除——使用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一样的排他所有权性,同一块动态内存也只允许一个智能指针对象绑定。

image.png

所以unique_ptr是不支持一般的拷贝和赋值的。

image.png 加一个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被置空