深入应用C++11 笔记---智能指针 (五)

深入应用C++11 笔记 (五)

第二章 C++11 解决内存泄漏的问题

​ 在C++中解决内存泄漏的有效方法是使用智能指针(Smart Pointer)。智能指针和普通指针的用法类似,只是不需要手动释放内存,而是通过智能指针自己管理内存释放。

​ 智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确的销毁动态分配的对象,防止内存泄漏。它的一种通用实现技术是使用引用计数,每使用它一次,内部的引用计数加1,每析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。

​ C++11 提供了3种智能指针:std::shared_ptr、std::uniq_ptr和std::weak_ptr,头文件为<memory >。

2.1 shared_ptr 共享的智能指针

​ std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

2.1.1 shared_ptr的基本用法
  1. 初始化

    通过构造函数、std::make_shared<T>辅助函数和reset方法来初始化shared_ptr:

    //智能指针的初始化
    std::shared_ptr<int> p(new int(1));
    std::shared_ptr<int> p2=p;
    std::shared_ptr<int> ptr;
    ptr.reset(new int(1));
    if(ptr)
    {
       cout<<"ptr is not null";
    }

    优先使用make_shared来构造智能指针,它更加高效

    不能将一个原始指针直接赋值给一个智能指针,例如,下面这种是错误的:

    std::shared_ptr<int> p=new int(1);//编译报错,不允许直接赋值

    对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。智能指针可以通过重载bool类型操作符来判断智能指针中是否为空(未初始化)。

  2. 获取原始指针

    当需要获取原始指针时,可以通过get方法来返回原始指针:

    std::shared_ptr<int> ptr(new int(1));
    int *p=ptr.get();
  3. 指定删除器

    智能指针初始化可以指定删除器:

    void DeleteIntPtr(int* p)
    {
       delete p;
    }
    std::shared_ptr<int> p(new int,DeleteIntPtr);

    当p的引用计数为0时,自动调用删除器DeleteIntPtr 来释放对象的内存。删除器可以是一个lambda表达式:

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

    用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象

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

    也可以将std::default_delete 作为删除器。default_delete的内部是通过调用delete来实现功能的:

    std::shared_ptr<int> p(new int[10],std::default_delete<int[]>);

    还可以通过封装一个make_shared_array方法来让shared_ptr支持数组:

    template<typename T>
    shared_ptr<T> make_shared_array(size_t size)
    {
       return shared_ptr<T>(new T[size],default_delete<T[]>());
    }
    //测试如下:
    std::shared_ptr<int> p=make_shared_array<int>(10);
    std::shared_ptr<char> p=make_shared_array<char>(10);
2.1.2 使用shared_ptr需要注意的问题

智能指针使用的时候需要注意:

  1. 不要用一个原始指针初始化多个shared_ptr,例如下面这些事错误的

    int *ptr=new int;
    shared_ptr<int> p1(ptr);
    shared_ptr<int> p2(ptr);//logic error
  2. 不要在函数实参中创建shared_ptr

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

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

    shared_ptr<int> p(new int());
    f(p,g());
  3. 通过shared_from_this()返回this指针。不要讲this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构。

    struct A
    {
       shared_ptr<A> GetSelf()
       {
           return shared_ptr<S>(this);//不要这样做!!!
       }
    };
    int main()
    {
       shared_ptr<A> sp1(new A);
       shared_ptr<A> sp2=sp1->GetSelf();
       return 0;
    }

    ​ 在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而它们之间是没有任何关系的,在离开作用域后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

    ​ 正确返回this的shared_ptr的做法是:让目标类通过派生std::enable_shared_from_this<T>l;类,然后使用基类的成员函数shared_from_this来返回shared_ptr:

    class A:public std::enable_shared_from_this<A>
    {
    public:
    std::shared_ptr<A> GetSelf()
       {
        return shared_from_this();
       }
    };
    std::shared_ptr<A> spy(new A);
    std::shared_ptr<A> p=spy->GetSelf().//ok
  4. 要避免循环引用。智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄露:

    struct A;
    struct B;
    struct A
    {
       std::shared_ptr<B> bptr;
       ~A(){cout<<"A is deleted!"<<endl;}
    };
    struct B
    {
       std::shared_ptr<A> aptr;
       ~B(){cout<<"B is deleted!"<<endl;}
    };
    void TestPtr()
    {
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }   //对象应该被销毁
    }

    ​ 测试结果是两个指针A和B都不会被删除,存在内存泄露。循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不会减少为0,导致两个指针都不会被析构,产生内存泄露。解决方法是将A和B任何一个成员变量改为weak_ptr;

2.2 unique_ptr 独占的智能指针

​ unique_ptr是一个独占型的智能指针,它不允许其他智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个unique_ptr。

unique_ptr<T> myPtr(new T);
unique_ptr<T> myOtherPtr=myPtr;//错误!! 不能复制

​ unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不在拥有原来指针的所有权:

unique_ptr<T> myPtr(new T); //OK
unique_ptr<T> myOtherPtr=std::move(myPtr);//OK
unique_ptr<T> ptr=myPtr;//错误!!只能移动 不可复制

​ unique_ptr不像shared_ptr可以通过make_shared方法来创建智能指针,C++11目前还没有提供make_unique方法。在**C++14中会提供**make_shared类似的make_unique来创建unique_ptr。

​ unique_ptr和shared_ptr相比,unique_ptr除了独占性这个特点之外,还可以指向一个数组,代码如下:

std::unique_ptr<int []> ptr(new int[10]);
ptr[9]=9;//设置最后一个元素为9

而std::shared_ptr<int []> ptr(new int[10]);是不合法的。

u**nique_ptr指定删除器**和std::shared_ptr是有差别的,比如:

std::shared_ptr<int> ptr(new int(1),[](int* p){delete p});//正确
std::unique_ptr<int> ptr(new int(1),[](int* p){delete p});//错误!

std::unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样:

std::unique_ptr<int,void(*)(int*)> ptr(new int(1),[](int* p){delete p;});

上面这种写法在lambda没有捕获变量的情况下是正确的,如果捕获了变量,则会编译报错:

std::unique_ptr<int,void(*)(int*)> ptr(new int(1),[&](int* p){delete p;});//错误! 捕获了变量

这是因为lambda在没有捕获变量的情况下是可以直接转换为函数指针的,一旦捕获了就无法转换了。

如果希望unique_ptr的删除器支持lambda,可以这样写

std::unique_ptr<int,std::function<void(int*)>> ptr(new int(1),[&](int* p){delete p;});

还可以自定义unique_ptr的删除器,比如:

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

struct MyDeleter
{
    void operator()(int *p)
    {
        cout<<"delete"<<endl;
        delete p;
    }
};

int main()
{
    std::unique_ptr<int,MyDeleter> p(new int(1));
}

如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

2.3 weak_ptr 弱引用的智能指针

​ 弱引用指针weak_ptr是用来监视shared_ptr的,不会使引用计数加1,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。weak_ptr没有重载操作符*和-> ,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的检测权,它的构造和析构不会影响引用计数,纯粹作为一个旁观者监视shared_ptr中管理的资源是否存在。weak_ptr还可以用来返回this指针来解决循环引用的问题。

2.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())
    {
       std::cout<<"weak_ptr 无效,所监视的智能指针已被释放"<<endl;
    }
    else
    {
       std::cout<<"weak_ptr有效"<<endl;
    }
    //结果输出:weak_ptr有效
  3. 通过lock方法来获取所监视的shared_ptr:

    std::weak_ptr<int> gw;
    void f()
    {
       if(gw.expired())//所监视的shared_ptr是否释放
       {
           std::cout<<"gw is expired"<<endl;
       }
       else
       {
           auto spt=gw.lock();
           std::cout<<*spt<<endl;
       }
    }
    
    int main()
    {
       {
           auto sp=std::make_shared<int>(42);
           gw=sp;
           f();
       }
       f();
    }
    //输出:42  gw is expired
2.3.2 weak_ptr返回this指针

​ 上节提到不能直接将this指针返回为shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其他方法shared_form_this来返回智能指针,原因是std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观测this智能指针,调用shared_from_this()方法时,会调用内部这个weak_ptr的lock方法,将所观测的shared_ptr返回。

struct A:public std::enable_shared_from_this<A>
{
    std::shared_ptr<A> GetSelf()
    {
        return shared_from_this();
    }
    ~A()
    {
        cout<<"A is deleted"<<endl;
    }
};
std::shared_ptr<A> spy(new A);
std::shared_ptr<A> p=spy->GetSelf();//OK
//输出结果: A is deleted

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

​ 需要注意的是,获取自身智能指针的函数仅在shared_ptr<T>的构造函数之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

2.3.3 weak_ptr 解决循环引用问题

在2.1.2提到智能指针要注意循环引用问题,在看前面例子:

struct A;
struct B;
struct A
{
    std::shared_ptr<B> bptr;
    ~A(){cout<<"A is deleted!"<<endl;}
};
struct B
{
    std::shared_ptr<A> aptr;
    ~B(){cout<<"B is deleted!"<<endl;}
};
void TestPtr()
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }   //对象应该被销毁
}

在这个例子中,由于循环引用导致ap和bp的引用计数都为2,离开作用域之后引用计数减为1,不会删除指针,导致内存泄漏。通过weak_ptr就可以解决这个问题,只要将A或B的任意一个成员变量改为weak_ptr即可:

struct A;
struct B;
struct A
{
    std::shared_ptr<B> bptr;
    ~A(){cout<<"A is deleted!"<<endl;}
};
struct B
{
    std::weak_ptr<A> aptr;//改为weak_ptr
    ~B(){cout<<"B is deleted!"<<endl;}
};
void TestPtr()
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }   //对象应该被销毁
}
//输入如下: A is deleted!   B is deleted!

这样对B的成员在赋值时,既执行bp->aptr=ap时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然是1,在离开作用域之后,ap的引用计数减为0,A指针会被析构,析构后其内部的bptr的引用计数会减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也将被析构,不会发生内存泄漏。

2.4通过智能指针管理第三方库分配

当我们使用第三方库时,通过其分配的内存一般也需要调用对应的释放接口才能释放,由于第三方库返回的指针一般都是原始指针,在用完后如果没有调用第三方库的释放接口,就很容易造成内存泄露。比如

void *p=GetHandle()->Create();
//do something
GetHandle()->Release(P);//如果程序异常返回或者没有调用Release,则会造成内存泄露

使用智能指针管理:

void *p=GetHandle()->Create();
std::shared_ptr<void> sp(p,[this](void* p){GetHandle()->Release(p);});

我们可以将其提炼为一个公共函数,简化调用:

std::shared_ptr<void> Guard(void* p)
{
    return std::shared_ptr<void> sp(p,[this](void* p){GetHandle()->Release(p);});
}
void *p=GetHandle()->Create();
auto sp=Guard(p);
//do something...

上面的Guard函数仍然不够安全,因为使用者可能这样使用

void *p=GetHandle()->Create();
Guard(p);//危险,这句结束后P就被释放了
//do something   导致访问野指针

因为Guard(p)是一个右值,如果不将这个右值赋值给一个指针,Guard(p)这句结束后,就会释放,导致P提前释放了,后面就会访问野指针的内容。我们可以使用一个宏来解决这个问题,代码如下:

#define GUARD(p) std::shared_ptr<void> p##p(p,[](void* p){GetHandle()->Release(p);});
void *p=GetHandle()->Create();
GUARD(p);//安全

也可以使用unique_ptr来管理第三方的内存,如下:

#define GUARD(p) std::unique_ptr<void,void(*)(int*)> p##p(p,[](void* p){GetHandle()->Release(p);});

声明:以上主要来源于深入应用C++11 这本书,强烈推荐

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值