C++智能指针shared_ptr讲解与使用

手动管理的弊端

在简单的程序中,我们不大可能忘记释放 new 出来的指针,但是随着程序规模的增大,我们忘了 delete 的概率也随之增大。在 C++ 中 new 出来的指针,赋值意味着引用的传递,当赋值运算符同时展现出“值拷贝”和“引用传递”两种截然不同的语义时,就很容易导致“内存泄漏”。

手动管理内存带来的更严重的问题是,内存究竟要由谁来分配和释放呢?指针的赋值将同一对象的引用散播到程序各处,但是该对象的释放却只能发生一次。当在代码中用完了一个资源指针,该不该释放 delete 掉它?这个资源极有可能同时被多个对象拥有着,而这些对象中的任何一个都有可能在之后使用该资源,其余指向这个对象的指针就变成了“野指针”;那如果不 delete 呢?也许你就是这个资源指针的唯一使用者,如果你用完不 delete,内存就泄漏了。

资源的拥有者是系统,当我们需要时便向系统申请资源,当我们不需要时就让系统自己收回去(Garbage Collection)。当我们自己处理的时候,就容易出现各种各样的问题。

C++中的智能指针

自C++11起,C++标准提供两大类型的智能指针:

1. Class shared_ptr实现共享式拥有(shared ownership)概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用(reference)被销毁”时候释放。为了在结构复杂的情境中执行上述工作,标准库提供了weak_ptr、bad_weak_ptr和enable_shared_from_this等辅助类。
2. Class unique_ptr实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(resourece leak)——例如“以new创建对象后因为发生异常而忘记调用delete”——特别有用。
注:C++98中的Class auto_ptr在C++11中已不再建议使用。

share_ptr

智能指针是(几乎总是)模板类,shared_ptr 同样是模板类,所以在创建 shared_ptr 时需要指定其指向的类型。shared_ptr 负责在不使用实例时释放由它管理的对象,同时它可以自由的共享它指向的对象。

shared_ptr 使用经典的 “引用计数” 的方法来管理对象资源。引用计数指的是,所有管理同一个裸指针( raw pointer )的 shared_ptr,都共享一个引用计数器,每当一个 shared_ptr 被赋值(或拷贝构造)给其它 shared_ptr 时,这个共享的引用计数器就加1,当一个 shared_ptr 析构或者被用于管理其它裸指针时,这个引用计数器就减1,如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个 shared_ptr 了,于是我们释放指针指向的资源。

在底层实现中,这个引用计数器保存在某个内部类型里(这个类型中还包含了 deleter,它控制了指针的释放策略,默认情况下就是普通的delete操作),而这个内部类型对象在 shared_ptr 第一次构造时以指针的形式保存在 shared_ptr 中(所以一个智能指针的析构会影响到其他指向同一位置的智能指针)。shared_ptr 重载了赋值运算符,在赋值和拷贝构造另一个 shared_ptr 时,这个指针被另一个 shared_ptr 共享。在引用计数归零时,这个内部类型指针与 shared_ptr 管理的资源一起被释放。此外,为了保证线程安全性,引用计数器的加1,减1操作都是 原子操作,它保证 shared_ptr 由多个线程共享时不会爆掉。

对于 shared_ptr 在拷贝和赋值时的行为,《C++Primer第五版》中有详细的描述:

每个 shared_ptr 都有一个关联的计数值,通常称为引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。
例如,当用一个 shared_ptr 初始化另一个 shred_ptr,或将它当做参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的 shared_ptr 离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象。


下面看一个常见用法,包括: 1. 创建 shared_ptr 实例 2. 访问所指对象 3. 拷贝和赋值操作 4. 检查引用计数。

#include <iostream>
#include <string>
#include <tr1/memory>

using namespace std;
using namespace std::tr1;
class Test
{
public:
    Test(string name)
    {
        name_ = name;
        cout << this->name_ << "  constructor" << endl;
    }
    ~Test()
    {
        cout << this->name_ << "  destructor" << endl;
    }

    string name_;
};


int main()
{
    /* 类对象 原生指针构造 */
    shared_ptr<Test> pStr1(new Test("object"));
    cout << (*pStr1).name_ << endl;
    /* use_count()检查引用计数 */
    cout << "pStr1 引用计数:" << pStr1.use_count() << endl;

    shared_ptr<Test> pStr2 = pStr1;
    cout << (*pStr2).name_ << endl;
    cout << "pStr1 引用计数:" << pStr1.use_count() << endl;
    cout << "pStr2 引用计数:" << pStr2.use_count() << endl;
    
    /* 先new 一个对象,把原始指针传递给shared_ptr的构造函数 */
    int *pInt1 = new int(11);
    shared_ptr<int> pInt2(pInt1);

    /* unique()来检查某个shared_ptr 是否是原始指针唯一拥有者 */
    cout << pInt2.unique() << endl; //true 1
    /* 用一个shared_ptr对象来初始化另一个shared_ptr实例 */
    shared_ptr<int> pInt3(pInt2);
    cout << pInt2.unique() << endl; //false 0
    
    cout << pInt3.use_count() << endl;
    cout << pInt2.use_count() << endl;
    return 0;
}

输出结果如下:
在这里插入图片描述

错误用法一:循环引用

循环引用可以说是引用计数策略最大的缺点,“循环引用”简单来说就是:两个对象互相使用一个 shared_ptr 成员变量指向对方(你中有我,我中有你)。突然想到一个问题:垃圾回收器是如何处理循环引用的? 下面看一个例子:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Children;
class Parent
{
public:
    ~Parent()
    {
        cout << "Parent    destructor" << endl;
    }

    shared_ptr<Children> children;
};

class Children
{
public:
    ~Children()
    {
        cout << "Children  destructor" << endl;
    }
    shared_ptr<Parent> parent;
};

void Test()
{
    shared_ptr<Parent> pParent(new Parent());
    shared_ptr<Children> pChildren(new Children());  
    if(pParent && pChildren)
    {
        pParent -> children = pChildren;
        pChildren -> parent = pParent;
    }

    cout << "pParent use_count: " << pParent.use_count() << endl;
    cout << "pChildren use_count: " << pChildren.use_count() << endl;
}

int main()
{
    Test();
    return 0;
}


输出结果如下:
在这里插入图片描述

退出之前,它们的 use_count() 都为2,退出了 Test() 后,由于 pParent 和 pChildren 对象互相引用,它们的引用计数都是 1,不能自动释放(可以看到没有调用析构函数),并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的“内存泄漏”。

那么如何解除循环引用呢?

weak_ptr

使用weak_ptr 来打破循环引用,它与一个 shared_ptr 绑定,但却不参与引用计数的计算,不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。weak_ptr 像是 shared_ptr 的一个助手。同时,在需要时,它还能摇身一变,生成一个与它绑定的 shared_ptr 共享引用计数的新 shared_ptr。

总而言之,weak_ptr 的作用就是:在需要时变出一个 shared_ptr,在其他时候不干扰 shared_ptr 的引用计数。

它没有重载 * 和 -> 运算符,因此不可以直接通过 weak_ptr 访问对象,典型的用法是通过 lock() 成员函数来获得 shared_ptr,进而使用对象。
下面是 weak_ptr 的一般用法:

std::shared_ptr<int> sh = std::make_shared<int>();
// 用一个shared_ptr初始化
std::weak_ptr<int> w(sh);
// 变出 shared_ptr
std::shared_ptr<int> another = w.lock();
// 判断weak_ptr所观察的shared_ptr的资源是否已经释放 
bool isDeleted = w.expired();
 

我们看看它如何来解决上面的循环引用。


#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Children;

class Parent
{
public:
    ~Parent()
    {
        cout << "Parent    destructor" << endl;
    }

    weak_ptr<Children> children; //注意这里
};

class Children
{
public:
    ~Children()
    {
        cout << "Children  destructor" << endl;
    }
    weak_ptr<Parent> parent;    //注意这里
};

void Test()
{
    shared_ptr<Parent> pParent(new Parent());
    shared_ptr<Children> pChildren(new Children());  
    if(pParent && pChildren)
    {
        pParent -> children = pChildren;
        pChildren -> parent = pParent;
    }

    // 看一下各自的引用计数
    cout << "pParent use_count: " << pParent.use_count() << endl;
    cout << "pChildren use_count: " << pChildren.use_count() << endl;
}

int main()
{
    Test();
    return 0;
}


输出结果:
在这里插入图片描述

可以看到各自的引用计数分别为1,而且函数执行完毕后,各自指向的对象得到析构,这样来解除了上面的循环引用。

std::shared_ptr大概总结有以下几点:

(1) 智能指针主要的用途就是方便资源的管理,自动释放没有指针引用的资源。

(2) 使用引用计数来标识是否有多余指针指向该资源。(注意,shart_ptr本身指针会占1个引用)

(3) 在赋值操作中, 原来资源的引用计数会减一,新指向的资源引用计数会加一。

 std::shared_ptr<Test> p1(new Test);
 std::shared_ptr<Test> p2(new Test);
 p1 = p2;

(4) 引用计数加一/减一操作是原子操作,所以线程安全的

(5) make_shared要优于使用new,make_shared可以一次将需要内存分配好。

std::shared_ptr<Test> p = std::make_shared<Test>();
std::shared_ptr<Test> p(new Test);

(6) std::shared_ptr的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。

(7) 引用计数是分配在动态分配的,std::shared_ptr支持拷贝,新的指针获可以获取前引用计数个数。

猜你喜欢

转载自blog.csdn.net/weixin_42205987/article/details/82946894