C++内存管理:shared_ptr


智能指针shared_ptr,与unique_ptrweak_ptr不同:

  • 不像unique_ptr那么独断专制;
  • 也不像weak_ptr那么不负责任;

而是可以同时多个shared_ptr指向同一块内存地址,这种特性使得可以使用shared_ptr进行数据的共享。

初始化

  • 当未显式初始化时,默认情况下使用nullptr进行初始化。

    // define a nullptr pointing to s string, default initialization
    shared_ptr<string> p1;
    // nullptr
    cout << (s == nullptr) << endl;
    
  • 可以对shared_ptr进行赋值操作,类似于普通的指针类型。

    shared_ptr<string> p1(new string("hello, world"));
    cout << *p1 << endl;
    // assignment
    if(p1) {
        *p1 = "hi";
    }
    // use * access the object pointer points to
    cout << *p1 << endl;
    /**
    hello, world
    hi
    */
    
  • 可以使用make_shared进行初始化,是推荐使用方式,安全且不易出错;

    This function allocates and initializes an object in dynamic memory and returns a shared_ptr that points to that object.

    shared_ptr<int> p3 = make_shared<int>(42);
    shared_ptr<string> p4 = make_shared<string>(10, '9');
    // use auto for simplification
    auto p6 = make_shared<vector<string>>();
    
    • make_shared中,使用该指针所指向类型的构造函数来初始化。
    • 无参的情况下,则为value initialization
  • 与new结合使用

    类似于unique_ptrshared_ptr也可以使用new来支持初始化。

    // error, 类型转换不支持
    shared_ptr<int> p1 = new int(1024);
    // ok, direct initialization
    shared_ptr<int> p2(new int(42))
    

一些初始化的方式总结如下:

一些shared_ptr初始化的方式

copy & assign

When we copy or assign a shared_ptr, each shared_ptr keeps track of how many other shared_ptrs point to the same object.

每个shared_ptr都有一个绑定的counter,即reference count,用来表示有多少个shared_ptr共享指向的对象。当shared_ptr的reference count变为0后,则会导致其指向的object被free,此时会调用该obejct的析构函数完成。

// int to which r points has one user 
auto r = make_shared<int>(42); 
// assign to r, making it point to a different address
// increase the use count for the object to which q points 
// reduce the use count of the object to which r had pointed 
// the object r had pointed to has no users; that object is automatically freed
r = q; 

reference count的变化受到以下事件的影响:

  • 增加reference count;

    • shared_ptr被copy;
    • 用该shared_ptr初始化另一个shared_ptr;
    shared_ptr<string> p1(new string("hello, world"));
    cout << p1.use_count() << endl;
    shared_ptr<string> p2(p1);
    cout << p1.use_count() << endl;
    shared_ptr<string> p3 = p1;
    cout << p1.use_count() << endl;
    /**
    output:
    1
    2
    3
    */
    
  • 减少reference count;

    • shared_ptr被赋予新值;
    shared_ptr<string> p1(new string("hello, world"));
    cout << p1.use_count() << endl;
    p1 = nullptr;
    cout << p1.use_count() << endl;
    /**
    output: 不会崩溃
    1
    0
    */
    
    • 该shared_ptr超出其定义的scope后,其被自动destroyed;
    auto func() -> shared_ptr<string> {
    shared_ptr<string> p1(new string("hello, world"));
    cout << p1.use_count() << endl;
    shared_ptr<string> p2(p1);
    cout << p1.use_count() << endl;
    return p1;
    }
    
    int main()
    {   
        // p1 and p1 are destroyed, p is generated.
        auto p = func();
        cout << p.use_count() << endl;
        return 0;
    }
    /**
    output:
    1
    2
    1
    */
    

函数中使用

函数返回值

当作为函数返回值时,可以直接返回shared_ptr

// factory returns a shared_ptr pointing to a dynamically allocated object 
shared_ptr<Foo> factory(T arg) {
    // shared_ptr will take care of deleting this memory
    return make_shared<Foo>(arg); 
}

当作为函数返回值返回时,函数内部的指针会被destroyed,当外部的函数返回值没有“接盘侠”时,这块内存因为reference count变为0,就会被系统释放。

但是当返回值被赋值给新变量时,则这块内存的引用计数为1,就不会被释放,destroy的只有函数内部指向这块内存的shared_ptr

作为函数参数

unique_ptr作为函数参数进行传递时,有一些限制,因为它不能copy和assign,所以只能按照引用传递参数,或者释放控制权,或者重新使用make_unique赋值,如C++内存管理:智能指针与unique_ptr所述。

shared_ptr可以记进行copy和assign,因此限制会少一些。

一些关于将智能指针作为参数传递的推荐使用方式如下:

  1. C++ Core Guidelines
  2. C++ Core Guidelines: Passing Smart Pointers
  3. Move smart pointers in and out functions in modern C++
  4. Arguments and Smart Pointers

pass by value

这种方式中,与普通的对象类似,当传入shared_ptr时,进行了copy,生成了一个新的智能指针,并且两个指针指向同一块内存,但是两个指针被存在不同的地址,且此时的reference count变成了2。

void func(shared_ptr<string> sp){
    cout << *sp << endl;
    cout << &sp << endl;
    cout << sp.use_count() << endl;
}

int main()
{
    shared_ptr<string> sp(new string("hello, world"));
    // ok, this kind of form is accepted.
    func(shared_ptr<string>(new string("hello, world")));
    cout << &sp << endl;
    func(sp);

    return 0;
}
/**
output:
0x7fffc9825e90
hello, world
0x7fffc9825ea0
2
/

pass by reference

此时,只需要将func函数的参数改为引用即可,得到的输出完全不一样了,引用计数并不会增加,且智能指针是同一个。

void func(shared_ptr<string> &sp){
    cout << *sp << endl;
    cout << &sp << endl;
    cout << sp.use_count() << endl;
}

int main()
{
    shared_ptr<string> sp(new string("hello, world"));
    cout << &sp << endl;
    func(sp);

    return 0;
}
/**
output:
0x7ffe147b9db0
hello, world
0x7ffe147b9db0
1
*/

shared_ptr的缺点

The program will execute correctly but may waste memory if you neglect to destroy shared_ptrs that the program does not need.

当程序不能及时将最后一个shared_ptr销毁,则其指向的内存会一直存在,导致浪费。这种问题在容器中可能更容易忽略。

换句话说,因为shared_ptr及其对应内存的销毁,与reference count有紧密的关系,而且一般不建议显式操作,但是如果想要shared_ptr一直存在呢,可以将它们放到vector等容器中,但是对于容器中不需要的shared_ptr,也要及时erase掉,相关问题可以参考这里

一些使用技巧和注意事项

自己死亡不牵连别人

vector<string> s1;
{
    vector<string> s2 = {"hello", "world"};
    s1 = s2;
    s1.push_back("haha")

    for(auto si: s2) {
        cout << si << endl;
    }
}
for(auto si: s1) {
    cout << si << endl;
}
/**
output:
hello
world
hello
world
haha
*/

当在block scope中对s1赋值时,当该scope结束后,s2就被回收了,但是不影响s1,因为s2中的元素已经被copy到s1中,两者没有任何关系,因此对其中任意一个修改,不会影响另一个。

当scope结束后,s2被回收,其中对应的元素也会被释放,对于vector来说,其指针存储在stack中,而对应的元素是动态分配在heap中的。此时s1中被赋值的元素仍然存在。

除了上述这种方式,s2的死亡不影响s1,但是s1和s2之间的元素是不能共享的,有时需要进行共享,这里可以使用shared_ptr实现。

class sharedObject
{
public:
    sharedObject() = default;
    sharedObject(shared_ptr<string> name, shared_ptr<string> address) : m_name(name), m_address(address) {}

    shared_ptr<string> m_name;
    shared_ptr<string> m_address;
};

int main()
{
    sharedObject so1;
    {
        shared_ptr<string> name(new string("john"));
        shared_ptr<string> address(new string("sh"));
        sharedObject so2(name, address);
        so1 = so2;
        *so2.m_name = "Ram";
    }
    cout << *so1.m_name << endl;
    cout << *so1.m_address << endl;

    return 0;
}
/**
output:
Ram
sh
*/

其中,so1和so2共享其中的两个成员变量,这并没有通过类的静态成员实现,而且类的静态成员变量在所有对象之间共享,但是这种使用shared_ptr的方式可以指定进行共享的对象,在更细粒度上控制共享的范围。

谨慎使用普通指针访问智能指针指向的内存

It is dangerous to use a built-in pointer to access an object owned by a smart pointer, because we may not know when that object is destroyed.

用智能指针得到的内存对象,就一直用智能指针来访问,因为这些不会因为scope的结束导致对象被destroyed,但是普通的指针会。看一个例子:

void func(shared_ptr<string> sp){
    cout << sp.use_count() << endl;
}

int main()
{
    string *p1 = new string("hello, world");
    cout << *p1 << endl;
    func(shared_ptr<string>(p1));
    cout << *p1 << endl;

    return 0;
}
/**
output:
hello, world
1

*/

当完成了func函数的调用,再使用*p1访问内存中的数据时已经不起作用了,此时p1成为dangling pointer,因为其指向的内存,在func中,当参数sp超过func的作用域后,连同智能指针sp都被销毁了,因为无法访问了。

谨慎使用get

Use get only to pass access to the pointer to code that you know will not delete the pointer. In particular, never use get to initialize or assign to another smart pointer.

这个函数又将智能指针与普通指针混用了,因为get返回的是智能指针管理的普通指针,如果返回了该普通指针,就会脱离智能指针为其建立的一系列安全机制,比如自动删除内存等,会导致内存误删、重复删除等问题。

看如下代码:

int main()
{
    shared_ptr<string> sp1(new string("hello, world"));
    string *p = sp1.get();
    {
        shared_ptr<string> sp2(p);
    }
    cout << *sp1 << endl;
    return 0;
}

运行时出现了重复使用内存的问题,如下:

重复释放内存

写数据时要验证

因为shared_ptr指向的内存对象是在多个指针之间共享,因此当其中一个指针更改其中的值,会影响到其他指针的使用,为了避免这种情况,需要在写入新数据之前进行验证,判断自己是不是唯一的使用者。

share_ptr<string> sp(new string("hello, world"));
// equal to sp.use_count() == 1.
if(!sp.unique()){
    sp.reset(new string(*p));
}
*sp += " haha";
cout << *sp << endl;
/**
output:
hello, world haha
*

这里,当sp是唯一的使用者时,sp可以直接操作数据;但是如果有其他的使用者时,sp需要释放其原来的指向的对象,并重新申请一块内存,并将原来内存中的数据复制过来,然后将新生成的shared_ptr指向这块新内存,这就避免了对数据的负面影响。

其他相关操作

对于智能指针的操作,有些是共有的API,shared_ptr和unique_ptr都可以使用,如下:

共有的API

还有一些是shared_ptr独有的API,如下所示:

独有的API

参考资料

  1. C++ Primer - 12.1.1 The shared_ptr class
  2. C++ Primer - 12.1.3 Using shared_ptrs with new

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
 上一篇
C++内存管理:智能指针、异常与自定义deleter的关系 C++内存管理:智能指针、异常与自定义deleter的关系
介绍C++中内存管理遭遇异常时的处理方法,以及通过自定义deleter预先管理资源的策略。
2022-11-24
下一篇 
C++内存管理:基本知识与new-delete的使用 C++内存管理:基本知识与new-delete的使用
介绍C++中动态内存面临的问题,各种变量的内存分布方式,以及传统上使用new、delete进行内存管理的使用。
2022-11-16
  目录