欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++中的make_shared,shared_ptr与weak_ptr

程序员文章站 2024-03-14 12:12:04
...

C++中的make_shared,shared_ptr与weak_ptr

C++11中引入了智能指针,同时还有一个模板函数std::make_shared可以返回一个指定类型的std::shared_ptr

shared_ptr的基本操作

#include <memory>
#include <iostream>

struct Foo {
    Foo() { std::cout << "Foo...\n"; }
    ~Foo() { std::cout << "~Foo...\n"; }
};

struct D { 
    //删除p所指向的Foo对象
    void operator()(Foo* p) const {
        std::cout << "Call delete for Foo object...\n";
        delete p;
    }
};

int main()
{   
    // constructor with no managed object
    std::shared_ptr<Foo> sh1;

    // constructor with object
    std::shared_ptr<Foo> sh2(new Foo);
    std::shared_ptr<Foo> sh3(sh2);
    std::cout << sh2.use_count() << '\n';
    std::cout << sh3.use_count() << '\n';

    //constructor with object and deleter
    std::shared_ptr<Foo> sh4(new Foo, D());
}

构造方法

  1. 通过make_shared函数构造
    auto s_s = make_shared(“hello”);

  2. 通过原生指针构造

    int* pNode = new int(5);   
    shared_ptr s_int(pNode); 
    //获取原生指针 
    int* pOrg = s_int.get();
  3. 通过赋值函数构造shared_ptr

  4. 重载的operator->, operator*,以及其他辅助操作如unique()use_count(), get()等成员方法。

智能指针引用计数

实验的主要内容有:

  1. shared_ptr变量在生命周期中销毁后,引用计数是否减1?
  2. shared_ptr作为函数参数,分为传值和传引用,引用计数如何变化?
  3. 函数返回值为shared_ptr类型时,引用计数是否会变化?

带着这几个问题,我们来看下代码。

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

void Func1(shared_ptr<int> a)
{
    cout<<"Enter Func1"<<endl;
    cout<<"Ref count: "<<a.use_count()<<endl;
    cout<<"Leave Func1"<<endl;
}

shared_ptr<int> Func2(shared_ptr<int>& a)
{
    cout<<"Enter Func2"<<endl;
    cout<<"Ref count: "<<a.use_count()<<endl;
    cout<<"Leave Func2"<<endl;
    return a;
}

int main()
{
    //构造一个指向int类型对象的指针aObj1,引用计数+1
    shared_ptr<int> aObj1(new int(10));
    cout<<"Ref count: "<<aObj1.use_count()<<endl;

    {
        //同aObj1,不过由于生存周期在括号内,所以aObj2会被销毁
        shared_ptr<int> aObj2 = aObj1;
        cout<<"Ref count: "<<aObj2.use_count()<<endl;//引用计数-1
    }

    //在调用函数时,参数为shared_ptr类型,参数为传值类型,智能指针引用计数+1
    Func1(aObj1);

    //在调用函数时,参数为shared_ptr类型,参数为传引用类型,智能指针引用计数不变
    Func2(aObj1);

    shared_ptr<int> aObj3 = Func2(aObj1);//引用计数+1
    cout<<"Ref count:"<<aObj3.use_count()<<endl;

    return 0;
}

weak_ptr的目的

先来看一段代码:

class Parent
{
public:
    shared_ptr<Child> child;
};

class Child
{
public:
    shared_ptr<Parent> parent;
};

shared_ptr<Parent> pA(new Parent);
shared_ptr<Child> pB(new Child);
pA->child = pB;
pB->parent = pA;

在Parent类中存储了指向Child类对象的智能指针成员变量,而在Child类中也存储了指向Parent类对象的智能指针成员变量,如此就会造成环形引用,这个成因在C++中很好解释。

要解决环形引用的问题,没有特别好的办法,一般都是在可能出现环形引用的地方使用weak_ptr来代替shared_ptr

weak_ptr是一种不控制对象生命周期的智能指针, 指向shared_ptr指针指向的对象的内存,却并不拥有该内存。
但是,使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr)。

进行该对象的内存管理的是那个强引用的shared_ptrweak_ptr只是提供了对管理对象的一个访问手段。

weak_ptr设计的目的是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作, 它只可以从一个shared_ptr或另一个weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr

由于weak_ptr是指向shared_ptr所指向的内存的,所以,weak_ptr并不能独立存在。

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

void Check(weak_ptr<int> &wp)
{
    shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象
    if (sp != nullptr)
    {
        cout << "The value is " << *sp << endl;
    }
    else
    {
        cout << "Pointer is invalid." << endl;
    }
}

int main()
{
    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2 = sp1;
    weak_ptr<int> wp = sp1; // 指向sp1所指向的内存

    cout << *sp1 << endl;
    cout << *sp2 << endl;
    Check(wp);

    sp1.reset();
    cout << *sp2 << endl;
    Check(wp);

    sp2.reset();
    Check(wp);

    system("pause");
    return 0;
}

我们在使用weak_ptr时也要当心,时刻需要判断weak_ptr对应的shared_ptr是否为空,weak_ptr并不会增加shared_ptr的引用计数.

优点

效率更高

共享的对象会在最后一个强引用离开的时候销毁(也可能释放),这很好地消除了显式的delete调用。如果读者掌握了它的用法,可以肯定delete将会在你的编程字典中彻底消失。

所以在对象之间有“共享数据”,对象创建与销毁“分离” 时,尤其是容器中的动态对象,使用shared_ptr包装的能够减少代码的维护成本。

如果你通过使用原始的new表达式分配对象,然后传递给shared_ptr(也就是使用shared_ptr的构造函数) 的话,shared_ptr的实现没有办法选择,而只能单独的分配控制块:

auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 };


C++中的make_shared,shared_ptr与weak_ptr

虽然shared_ptr很好地包装了new表达式,但过多的显式new操作符也是个问题,它应该使用工厂模式来解决。

C++提供了的*工厂函数make_shared的话,情况就会变成下面这样:

auto sp1 = make_shared(), sp2{ sp1 };


C++中的make_shared,shared_ptr与weak_ptr

内存分配的动作, 可以一次性完成。这减少了内存分配的次数,而内存分配是代价很高的操作。

关于两种方式的性能测试可以看这里Experimenting with C++ std::make_shared

异常安全

看看下面的代码:

void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) 
{ 
    /* ... */ 
}

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

C++ 是不保证参数求值顺序,以及内部表达式的求值顺序的,所以可能的执行顺序如下:

new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr
std::shared_ptr

现在我们假设在第 2 步的时候,抛出了一个异常 (比如 out of memory,总之, Rhs 的构造函数异常了),那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于,shared_ptr没有立即获得裸指针。

我们可以用如下方式来修复这个问题。

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

当然, 推荐的做法是使用std::make_shared来代替:

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

线程安全

  1. 同一个shared_ptr被多个线程读,是线程安全的;
  2. 同一个shared_ptr被多个线程写,不是 线程安全的;
  3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。

对于第三点,我们一般采用:

对于线程中传入的外部shared_ptr对象,在线程内部进行一次新的构造,例如: sharedptr AObjTmp = outerSharedptrObj;

缺点

构造函数是保护或私有时,无法使用make_shared

当我想要创建的对象没有公有的构造函数时, make_shared就无法使用了,当然我们可以使用一些小技巧来解决这个问题,比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?

对象的内存可能无法及时回收

make_shared只分配一次内存,这看起来很好,减少了内存分配的开销。

问题来了,weak_ptr会保持控制块(强引用, 以及弱引用的信息)的生命周期,而因此连带着保持了对象分配的内存,只有最后一个weak_ptr离开作用域时,内存才会被释放。

原本强引用减为 0 时就可以释放的内存,现在需要强引用,弱引用都为 0 时才能释放,意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。关于这个问题可以看这里 make_shared, almost a silver bullet

参考资料

  1. Why Make_shared ? 主要是转载这篇的内容,之后会想着按照自己的理解改一改
  2. C++智能指针 weak_ptr
  3. shared_ptr与make_shared的用法