C++动态内存管理之智能指针

前言

        动态内存管理引起的所谓内存泄漏的问题是编程领域的一大顽疾,它的成因非常直截了当---只拿不还”,但它产生的缘由有时却非常隐晦需要非常仔细布局你的每一行代码才能抵抗它们对你的系统的侵蚀。而智能指针就能解决上面的问题,它的主要的功能就是自动帮我们管理内存。

一、智能指针

1、智能指针主要解决以下问题

  • 内存泄漏:内存手动释放,使用智能指针可以自动释放
  • 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题

2、 智能指针的类型(其中后三个是C++11支持,并且第一个auto_ptr已经被C++11弃用
  • auto_ptr 
  • shared_ptr  共享对象的所有权,但性能略差。
  • unique_ptr   独占对象的所有权,由于没有引用计数,因此性能较好。
  • weak_ptr     配合shared_ptr,解决循环引用的问题

3、智能指针的使用

3.1 shared_ptr

3.1.1 内存模型:

shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块 (control block) ,控制块中包含一个引用计数(reference count), 一个弱计数 (weak count)和其它一些数据,内存模型如下图1。

  • std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每拷贝一次引用计数reference count就加1,每一次析构就减1。再最后一个shared_ptr析构的时候,内存才会被释放。
  • shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个 shared_ptr对象销毁时,被管理对象自动销毁。
C++动态内存管理之智能指针_第1张图片 图 1

3.1.1 基本用法和函数

shared_ptr的初始化

// 初始化的三种方式,优选第1 和第 2种,因为 make_shared是 构造函数,更高效
auto s = make_shared < int > ( 100 );
shared_ptr < int > s = make_shared < int > ( 100 );
shared_ptr < int > sp1 ( new int ( 100 ));
//不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:
std::shared_ptr < int > p = new int ( 1 );

shared_ptr的常用函数 

  • s.get():              功能:返回shared_ptr中保存的裸指针;
  • s.use_count() :功能:返回shared_ptr 的强引用计数;
  • s.unique() :      功能:若use_count() 1 ,返回 true ,否则返回 false
  • s.reset(…):       功能:重置shared_ptr
    • reset( )不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯一指向该对象的指针,则引用计数减少1,同时将P置空。
    • reset( )带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指针,则只减少引用计数,并指向新的对象。如:

auto s = make_shared < int > ( 100 );
s . reset ( new int ( 200 ));
获取原始指针 get
std::shared_ptr < int > ptr ( new int ( 1 ));
int * p = ptr . get (); //
delete p ;                         // 不小心释放  就会造成double free错误
p.get() 的返回值就相当于一个裸指针的值​​​​​​​,谨慎使用p.get() 的返回值
  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
  • 保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。

#include 
#include 

using namespace std;

void DeleteIntPtr(int *p) {
    cout << "call DeleteIntPtr" << endl;
    delete p;
}

int main()
{
    std::shared_ptr p(new int(1), DeleteIntPtr);
    return 0;
}
p 的引用计数为 0 时,自动调用删除器 DeleteIntPtr 来释放对象的内存。删除器可以是一个 lambda 表达式,上面的写法可以改为:
std::shared_ptr < int > p ( new int ( 1 ), []( int * p ) {
        cout << "call lambda delete p" << endl ;
        delete p ;
});
当我们用 shared_ptr 管理动态数组时,需要指定删除器, 因为 shared_ptr 的默认删除器不支持数组对 ,代码如下所示:

 std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});

3.1.2  使用shared_ptr要注意的问题

不要用一个原始指针初始化多个shared_ptr

例如下面错误范例: ​​​​​​​

int * ptr = new int ;
shared_ptr < int > p1 ( ptr );
shared_ptr < int > p2 ( ptr ); // 逻辑错误

不要在函数实参中创建shared_ptr

 function(shared_ptr<int>(new int), g()); //有缺陷

因为 C++ 的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的 ,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int ,然后调用 g() ,如果恰好 g() 发生异常,而 shared_ptr 还没有创建, 则int 内存泄漏了.
正确的写法应该是先创建智能指针,代码如下:
shared_ptr < int > p ( new int );
function ( p , g ());

通过shared_from_this()返回this指针

 不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。​​​​​​​

/*
    错误范例:
    由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系
    的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
*/
#include 
#include 
using namespace std;
class A
{
public:
    shared_ptr GetSelf()
    {
        return shared_ptr(this); // 不要这么做
    }
    ~A()
    {
        cout << "Destructor A" << endl;
    }
};

int main()
{
    shared_ptr sp1(new A);
    shared_ptr sp2 = sp1->GetSelf();
    return 0;
}



/*
    正确范例,正确返回this的shared_ptr的做法是:
    让目标类通过std::enable_shared_from_this基类的方法shared_from_this()来返回一个新的                 
     std::shared_ptr 对象来作为this的shared_ptr,如下所示。
    
*/
#include 
#include 
using namespace std;
class A: public std::enable_shared_from_this
{
    public:
    shared_ptrGetSelf()
    {
        return shared_from_this(); //正确的做法
    }
    ~A()
    {
        cout << "Destructor A" << endl;
    }
};

int main()
{
    shared_ptr sp1(new A);
    shared_ptr sp2 = sp1->GetSelf(); // ok
    return 0;
}
避免循环引用
循环引用会导致内存泄漏,比如下面错误范例:
        循环引用导致ap bp 的引用计数为 2 ,在离开作用域之后, ap bp 的引用计数减为 1 ,并不回减为 0 ,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把 A B 任何一个成员变量改为 weak_ptr ,具体方法见 weak_ptr 章节。
#include 
#include 

using namespace std;

class A;
class B;
class A {
public:
    std::shared_ptr bptr;
    ~A() {
        cout << "A is deleted" << endl;
    }
};

class B {
public:
    std::shared_ptr aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};

int main()
{
    {
        std::shared_ptr ap(new A);
        std::shared_ptr bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
    return 0;
}

3.2 unique_ptr独占的智能指针

  • unique_ptr是一个独占型的智能指针,不能将其赋值给另一个unique_ptr
  • unique_ptr可以指向一个数组
  • unique_ptr需要确定删除器的类型

3.2.1 unique_ptr使用特点

1、unique_ptr 不允许复制,但可以通过函数返回给其他的 unique_ptr ,还可以通过 std::move 来转移到其他的unique_ptr ,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr < T > my_ptr ( new T ); // 正确
unique_ptr < T > my_other_ptr = std::move ( my_ptr ); // 正确
unique_ptr < T > ptr = my_ptr ; // 报错,不能复制 ​​​​​​​
2、 unique_ptr可以指向一个数组,而 shared_ptr不可以, 代码如下所示
std::unique_ptr < int [] > ptr ( new int [ 10 ]);
ptr [ 9 ] = 9 ;
std::shared_ptr < int [] > ptr2 ( new int [ 10 ]); // 这个是不合法的
3、 unique_ptr 指定删除器和 shared_ptr 有区别
std::shared_ptr < int > ptr3 ( new int ( 1 ), []( int * p ){ delete p ;}); // 正确
std::unique_ptr < int > ptr4 ( new int ( 1 ), []( int * p ){ delete p ;}); // 错误
unique_ptr 需要确定删除器的类型,所以不能像 shared_ptr 那样直接指定删除器,可以这样写:
std::unique_ptr < int , void ( * )( int* ) > ptr5 ( new int ( 1 ), []( int * p ){ delete p ;}); //正确

3.3 weak_ptr弱引用的智能指针

        share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

        weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr weak_ptr只是提供了对管理对象的一个访问手段。

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

3.3.1 weak_ptr的基本用法

1. 通过use_count()方法获取当前观察资源的引用计数,如下所示:

shared_ptr < int > sp ( new int ( 10 ));
weak_ptr < int > wp ( sp );
cout << wp . use_count () << endl ; // 结果讲输出 1
2. 通过 expired() 方法判断所观察资源是否已经释放,如下所示:
shared_ptr < int > sp ( new int ( 10 ));
weak_ptr < int > wp ( sp );
if ( wp . expired ())
        cout << "weak_ptr 无效 , 资源已释放 " ;
else
        cout << "weak_ptr 有效 " ;
3. 通过 lock 方法获取监视的 shared_ptr ,如下所示:
std::weak_ptr < int > gw ;
void f ()
{
        auto spt = gw . lock ();
        if ( gw . expired ()) {
                cout << "gw 无效 , 资源已释放 " ;
        }
        else {
                cout << "gw 有效 , *spt = " << * spt << endl ;
        }
}
int main ()
{
        {
                auto sp = std::make_shared < int > ( 42 );
                gw = sp ;
                f ();
        }
        f ();
        return 0 ;
}
3.3.2 weak_ptr 返回 this 指针
shared_ptr 章节中提到不能直接将 this 指针返回 shared_ptr ,需要通过派生
std::enable_shared_from_this 类,并通过其方法 shared_from_this 来返回指针,原因是
std::enable_shared_from_this 类中有一个 weak_ptr ,这个 weak_ptr 用来观察 this 智能指针,调用
shared_from_this() 方法是,会调用内部这个 weak_ptr lock() 方法,将所观察的 shared_ptr返回,
再看前面的范例
#include 
#include 
using namespace std;

class A: public std::enable_shared_from_this
{
public:
    shared_ptrGetSelf()
    {
        return shared_from_this(); //
    }
    ~A()
    {
        cout << "Destructor A" << endl;
    }
};

int main()
{
    shared_ptr sp1(new A);
    shared_ptr sp2 = sp1->GetSelf(); // ok
    return 0;
}

在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0A对象会被析构,不会出现A对象被析构两次的问题。

需要注意的是,获取自身智能指针的函数尽在 shared_ptr 的构造函数被调用之后才能使用,因为
enable_shared_from_this 内部的 weak_ptr 只有通过 shared_ptr 才能构造。
3.3.3 weak_ptr解决循环引用问题
这样在对 B 的成员赋值时,即执行 bp->aptr=ap; 时,由于 aptr weak_ptr ,它并不会增加引用计数,所以ap 的引用计数仍然会是 1 ,在离开作用域之后, ap 的引用计数为减为 0 A 指针会被析构,析构后其内部的bptr 的引用计数会被减为 1 ,然后在离开作用域后 bp 引用计数又从 1 减为 0 B 对象也被析构,不会发生内存泄漏。

 

3.3.4 weak_ptr 使用注意事项
1. weak_ptr 在使用前需要检查合法性。
weak_ptr < int > wp ;
{
        shared_ptr < int > sp ( new int ( 1 )); //sp.use_count()==1
        wp = sp ; //wp 不会改变引用计数,所以 sp.use_count()==1
        shared_ptr < int > sp_ok = wp . lock (); //wp 没有重载 -> 操作符。只能这样取所指向的对象
}
shared_ptr < int > sp_null = wp . lock (); //sp_null .use_count()==0;
因为上述代码中 sp sp_ok 离开了作用域,其容纳的 K 对象已经被释放了。得到了一个容纳NULL 指针的 sp_null 对象。 在使用 wp 前需要调用 wp.expired() 函数判断一下。 因为wp 还仍旧存在,虽然引用计数等于 0 ,仍有某处 全局 性的存储块保存着这个计数信息。直到最后一个weak_ptr 对象被析构,这块 存储块才能被回收。否则 weak_ptr 无法直到自己所容纳的那个指针资源的当前状态。​​​​​​​​​​​​​​

3.4 智能指针安全性问题 

引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:
情况 1 :多线程代码操作的是同一个 shared_ptr 的对象,此时是不安全的。
比如 std::thread 的回调函数,是一个 lambda 表达式,其中引用捕获了一个 shared_ptr
std::thread td ([ & sp1 ]()){....});
又或者通过回调函数的参数传入的 shared_ptr 对象,参数类型引用
void fn ( shared_ptr < A >& sp ) {
...
}
..
std::thread td ( fn , sp1 );
这时候必然不是线程安全的。
情况 2 :多线程代码操作的不是同一个 shared_ptr 的对象
这里指的是管理的数据是同一份,而 shared_ptr 不是同一个对象。比如多线程回调的 lambda的是按值捕获的对象。
std::thread td ([ sp1 ]()){....});
另一个线程传递的 shared_ptr 是值传递,而非引用:
void fn ( shared_ptr < A > sp ) {
...
}
..
std::thread td ( fn , sp1 );
这时候每个线程内看到的 sp ,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp 指向的操作的时候,是不会出现非预期的异常行为的。也就是说,如下操作是安全的。
void fn ( shared_ptr < A > sp ) {
        ...
        if (..){
                sp = other_sp ;
        } else {
                sp = other_sp2 ;
        }
}
需要注意:所管理数据的线程安全性问题 。显而易见,所管理的对象必然不是线程安全的,必然 sp1 、sp2、 sp3 智能指针实际都是指向对象 A 三个线程同时操作对象 A ,那对象的数据安全必然是需要对象A自己去保证。

你可能感兴趣的:(c++,开发语言)