智能指针

本文介绍了智能指针的概念,如RAII,以及C++中的四种智能指针:auto_ptr、scoped_ptr、shared_ptr和weak_ptr。重点讨论了它们的设计思想、缺陷和适用场景,并解释了如何使用它们来防止内存泄漏。此外,文章还探讨了循环引用问题及其解决方案,强调了weak_ptr在打破循环引用中的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要用智能指针
智能指针是为了解决内存泄漏问题,在大型工程中,即时足够小心的保证new和delete 匹配出现。但无法避免throw的异常抛出导致一段程序的提前结束而产生的内存泄漏。
例如:

#include<iostream>
using namespace std;
void FunTest()
{
    int *p=new int[10];
    FILE* file=fopen("1.txt","r");
    if(file==NULL)
    {
        return;
    }
    if(p!=NULL)
    {
         delete[] p;
         p=NULL;
    }
}

上面代码中,如果文件打开失败,那么指针p开的空间就没进行释放,会造成内存泄漏。如果在每个返回语句前加delete[]进行释放,如果前面的申请空间很多时,就比较麻烦了。而智能指针可以自行内存释放。
1:智能指针的发展历史?
1)智能指针概念
RAII(Resource Acquisition Is Initialization,资源分配即初始化),定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
智能指针就是智能/自动化的管理指针所指向的动态资源的释放。通常是经由类模板来实现。借模板来达成泛型,通常由类的析构函数来达成自动释放指针所指向的内存或对象。
智能指针:1>RAII;2>像指针一样;
智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数等,控制权转移等)。
3)智能指针发展
C++四种智能指针auto_ ptr、scope _ ptr、shared_ ptr和weak_ ptr。其中auto_ ptr是C++98标准化引入的;unique_ ptr、shared_ ptr和weak_ ptr是C++11标准化才从boost引入的(unique_ ptr是由boost中的scope _ ptr改变的)。auto_ ptr虽简单,但缺陷很多,因此在C++11中已经明确被废弃了。shared_ ptr是引用计数的智能指针,被广泛使用。
这里写图片描述
2:auto_ ptr/scoped_ ptr/shared_ ptr/weak_ptr的设计思想、缺陷?
1)auto_ ptr
设计思想:
auto_ ptr 是C++98标准库里面一个轻量级的智能指针的实现,存在于头文件 memory中。其就是内部使用一个成员变量,指向一块内存资源(构造函数),并在析构函数中释放内存资源。从而达到RAII的目的。但是,auto_ptr 的拷贝物与被拷贝物之间是非等价的——这正是导致其到处是坑的根源所在。
auto_ptr的几点注意事项:
1、auto_ptr不能共享所有权
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员
4、不能通过复制操作来初始化auto_ptr
std::auto_ptr p(new int(42)); //OK
std::atuo_ptrp = new int(42);//Error
这是因为auto_ptr的构造函数被定义了explicit
5、不要把auto_ptr放入容器
缺陷:
只能由一个对象来管理空间;
当多个auto_ ptr指向同一块空间时,会由于多次释放而导致崩溃;(对象p1复制给另一个 auto_ ptr p2对象以后,p1则被置空,如果访问p1,则会出现非法访问, auto_ptr 的接口设计存在缺陷!)
auto_ ptr 的拷贝构造函数和拷贝赋值操作符函数所接受的参数类型都是非const 的引用类型(auto_ ptr<_Ty>& , 即我们可以且需要修改源对象),而不是我们一般应该使用的const引用类型。
2)scoped_ ptr
设计思想:
直接不允许进行拷贝以及赋值运算符重载,故而没有释放两次导致崩溃的事情发生。
scoped_ ptr 有着与 auto_ ptr 类似的特性。scoped_ ptr 与 auto_ ptr 间最大的区别主要在于对内存资源拥有权的处理。auto_ ptr 在拷贝(复制)时会从源 auto_ ptr 自动交出拥有权,而 scoped_ ptr 则不允许被复制(正是这一限制,使我们对scoped_ptr 的使用变得有信心且容易)。
缺陷:
不能满足需求,赋值运算符重载和拷贝构造就没什么存在的意义了。
3)shared_ ptr
设计思想:
shared_ ptr是一种通用实现技术,其使用引用计数(reference count)。将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。完美解决auto_ptr在对象所有权上的局限性。
缺陷:
循环引用出现内存泄漏问题。
4)weak_ptr
设计思想:
weak_ ptr 不更改引用计数,类似普通指针。weak_ ptr 是为配合 shared_ ptr 而引入的一种智能指针来协助shared_ ptr 工作,它可以从一个 shared_ ptr 或另一个 weak_ ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。weak_ptr 的一个重要用途是“打破循环引用”
缺陷:
虽然通过 weak_ptr 指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。
使用场景:
auto_ptr: 不提倡使用,即任何情况下都不要使用它。
scope_ ptr: 在局部作用域(例如函数内部或类内部),且不需要将指针作为参数来传递的情况下使用。scope_ ptr的开销较 shared_ptr 小一些。
shared_ ptr: 似乎大部分情况下都可以使用它。1)在scope_ ptr可以使用的情况如果使用shared_ ptr 的话将会带来较大的开销;2)如果需要讲指针作为参数或者函数的返回值传递的话,则只能使用shared_ptr。
weak_ ptr :打破循环引用,解除shared_ptr循环引用导致的内存泄漏问题。
3:模拟实现auto_ ptr/scoped_ ptr/shared_ ptr/weak_ptr
auto_ ptr:

template <class T>
class AutoPtr
{
    AutoPtr(T* sp = NULL)
        :_ptr(sp)
    {
         sp=NULL:
    }
    AutoPtr(const AutoPtr<T>& ap)
        : _ptr(ap._ptr)
    {
        ap._ptr=NULL:
    }

    AutoPtr<T>&operator=(AutoPtr<T> & ap)       
    {
        if (this != &ap)
        {
            if (_ptr)
            {
                delete _ptr;
                _ptr = ap._ptr;
                ap._ptr = NULL;
            }
        }
        return *this;
    }

    ~AutoPtr()
    {
        if (_ptr != NULL)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
    T& operator*()
    {
        return *_ptr;
    } 
    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
}

scoped_ ptr:

template <class T>
class ScopedPtr
{
public:
    ScopedPtr(T* sp = NULL)
    :_ptr(sp)
    {
        sp = NULL:
    }

    ~ScopedPtr()
    {
        if (_ptr != NULL)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
    //防拷贝
private:
    ScopedPtr(const ScopedPtr<T>& ap);
    ScopedPtr<T>&operator=(ScopedPtr<T> & ap);
private:
    T* _ptr;
}

shared_ ptr:

template <class T>
class SharedPtr
{
public:
    SharedPtr(T* sp = NULL)
        :_ptr(sp)
        , _refCount(new int(1))
    {

    }
    ~SharedPtr()
    {
        if (--(*_refCount)==0)
        {
            delete _ptr;
            _ptr = NULL;
            delete _refCount;
        }
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
    SharedPtr(const SharedPtr<T>& ap)
        :_ptr(ap._ptr)
        , _refCount(new int)
    {
        _refCount = ap._refCount;
        (*_refCount)++;
    }
    SharedPtr<T>&operator=(SharedPtr<T> & ap)
    {
        if (_ptr!= ap._ptr)
        {
            if (_ptr != NULL)
            {
                if (--(*_refCount) == 0)
                {
                    delete _ptr;
                    _ptr = NULL;
                    delete _refCount;
                }
            }           
            _ptr = ap._ptr;
            _refCount = ap._refCount;
            (*_refCount)++;

        }
        return *this;
    }
private:
    T* _ptr;
    int* _refCount;
}

weak_ptr :主要用于解决循环引用问题,见下文。
1)weak_ ptr不像其余三种,可以通过构造函数直接分配对象内存;他必须通过shared_ptr来共享内存。
2)没有重载opreator*和->操作符,也就意味着即使分配到对象,他也没法使用该对象
3)不主动参与引用计数,即,share_ ptr释放了,那么weak_ptr所存的对象也释放了。
4)使用成员函数expired()判断引用计数是否为空。
5)lock()函数,返回一个shared_ptr智能指针:
这里写图片描述
4:分析循环引用及解决方案
问题:

struct ListNode
{
    int data;
    std::shared_ptr<ListNode> _next;
    std::shared_ptr<ListNode> _prev;
}
void Test()
{
   std::shared_ptr<ListNode> cur(new ListNode);
   std::shared_ptr<ListNode> next(new ListNode);
   cur->_next=next;
   next->_prev=cur;
}

此时shared_ptr循环引用会出内存不释放,出现内存泄漏问题。分析如下图:
创建指针cur和next时:
这里写图片描述
执行代码后:
这里写图片描述
运行结束前析构:
这里写图片描述
cur->_next和next-> _prev的生命周期依赖于各自的结点,而各个结点的释放依赖于指向其的指针。(即cur-> _next的释放依赖于Node1,next-> _prev的释放依赖于Node2,而Node1的释放依赖于next-> _prev,Node2的释放依赖于cur-> _next。)
解决方案:
修改struct ListNode如下:

struct ListNode
{
    int data;
    std::weak_ptr<ListNode> _next;
    std::weak_ptr<ListNode> _prev;
}

之所以解决的原因是由于,weak_ptr没有增加引用计数,引用计数在析构前为1,故而析构时会释放空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值