C++智能指针

本文深入探讨C++中智能指针的使用,包括auto_ptr、unique_ptr、shared_ptr和weak_ptr的特点与区别,解决传统指针带来的内存管理问题,如内存泄露、野指针和重复释放。同时,文章提供了丰富的代码示例,帮助读者理解智能指针如何避免这些陷阱。
摘要由CSDN通过智能技术生成

一、为什么使用智能指针?智能指针的原理?

C/C++中的堆内存分配和释放的方式主要是: malloc/free 以及 new/delete 等。

使用new 和delete 管理内存存在三个常见问题:

1.忘记delete(释放) 内存,或者异常导致程序过早退出,没有执行 delete。忘记释放动态内存会导致内存泄露问题,长时间这样会导致系统内存越来越小。 (内存泄露问题往往很难查找到,内存耗尽时,才能检测出这种错误)

2.使用已经释放掉的对象。比如:我们使用delete释放掉申请的内存空间,但并未去除指向这片空间的指针,此时指针指向的就是“垃圾”内存。

3.同一块内存释放两次。当有两个指针指向相同的动态内存分配对象时,其中一个进行了delete操作,对象内存就还给了操作系统 ,如果我们要delete第二个指针,那么内存有可能遭到破坏。(浅拷贝问题)

使用智能指针可以很大程度上的避免这些问题。

智能指针就是一个类,类的构造函数中传入一个普通指针,当超出了类的作用域时,类会自动调用析构函数,释放资源。其核心思想是:栈上对象在离开作用范围时会自动析构。

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr,auto_ptr在C++11被弃用。

 

二、常用的智能指针

1、auto_ptr:自动指针,自动回收。在构造对象时赋予其管理空间的所有权,在拷贝或赋值中转移空间的所有权,拷贝和赋值后直接将_ptr赋为空,禁止其再次访问原来的内存空间。

构造函数:explicit关键字修饰的构造函数不能被隐式调用,只能显示调用

explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()   
        : _Myptr(_Ptr)     // 将指针交由auto_ptr托管
{   // construct from object pointer
}

析构函数:

// 释放了托管的对象所占用的内存空间
~auto_ptr()
{  
    delete _Myptr;
}

get方法:

// 返回保存的指针
_Ty *get() const _THROW0()
{   
    return (_Myptr);
}

release方法:

_Ty *release() _THROW0()
{    // return wrapped pointer and give up ownership  返回保存的指针,对象中不保留原来的指针,原来的指针直接赋值为0
    _Ty *_Tmp = _Myptr;
    _Myptr = 0;
    return (_Tmp);
}

reset方法:

void reset(_Ty* _Ptr = 0)
{   // 重置auto_ptr使之拥有另一个对象。先删除已经拥有的对象,然后新建一个并拥有一个新对象
    if (_Ptr != _Myptr)
        delete _Myptr;
    _Myptr = _Ptr;
}

拷贝构造函数:

// 明显可看出会发生托管权的转移
auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
        : _Myptr(_Right.release())
{    // construct by assuming pointer from _Right auto_ptr
}

赋值运算符:

// 很明显也发生了托管权的转移
template<class _Other> auto_ptr<_Ty>& operator=(auto_ptr<_Other>& _Right) _THROW0()
{   // assign compatible _Right (assume pointer)
    reset(_Right.release());
    return (*this);
}

使用:

void fun()
{
   T *pt = new T();

   // 将分配的堆内存指针交由auto_ptr托管
   std::auto_ptr apt(pt);      //显式调用构造函数
   
   // 像正常使用指针一样使用,相当于*pt= 10
   *apt = 10;
   
   // 相当于 pt->memFunc()
   apt->memFunc(); 
   
   // 使用get函数可获取它托管的指针
   T *pt2 = apt.get(); 

   // 可调用reset函数更改托管对象,这里删除了之前托管的 pt
   apt.reset(new T()); 

   // 可调用release函数放弃托管
   T *pt3 = apt.release(); 

   // 放弃托管意味着又需要自己手动释放内存了
   delete pt3;
   pt3 = NULL;
  
   return;
}

注意:

(1) auto_ptr没有使用引用计数,如果多个auto_ptr指向同一个对象,就会造成对象被删除一次以上的错误。因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。在赋值、参数传递的时候会转移所有权,不要轻易进行此类操作。

/* 1. 演示转移所有权 */
std::auto_ptr<int> aptr1(new int(3));  
// 执行后aptr1不再有效
std::auto_ptr<int> aptr2 = aptr1;  // or aptr2(aptr1)     
// 强行访问会发生不可预料的问题
*aptr1 = 4;    
// --------------------------------------------------------
/* 2. 演示参数传递的所有权转移 */ void lose(std::auto_ptr<int> a) {  // 空函数,仅仅为了演示参数传递 } std::auto_ptr<int> aptr3(new int(4)); // 所有权转移,aptr3不再有效 lose(aptr3); // 强行访问会发生不可预料的问题 *aptr3 = 10;

(2) auto_ptr的析构函数内部释放资源时调用的是delete而不是delete[],因此不要让auto_ptr托管数组。

(3) auto_ptr不能作为容器对象,因为容器中的元素经常要进行拷贝,赋值等操作,在这过程中auto_ptr会失去所有权。

(4) 判断auto_ptr是否为空不能使用if(aptr == NULL),应该使用if(aptr.get() == NULL)

 

2、unique_ptr:是指”唯一”地拥有其所指对象的智能指针,同一时刻只能有一个unique_ptr指向给定对象(使用移动语义来实现),与auto_ptr的不同点

(1) 可以通过间接的方式用于容器中

unique_ptr<int> sp(new int(10));
vector<unique_ptr<int> > vec;
vec.push_back(std::move(sp));    //通过这种移动语义来实现在容器中使用

vec.push_back(sp);               //这样直接使用不行,会报错
cout << *sp << end;              //这样也不行,因为sp添加到容器中后,会报错

(2) 无法直接进行复制构造与赋值操作,要使用move函数进行所有权的转移

unique_ptr<int> uq(new int(10));
unique_ptr<int> uq2 = uq;             //会报错,auto_ptr中可以
unique_ptr<int> uq3(uq);              //同样会报错,auto_ptr中可以
unique_ptr<int> uq4 = std::move(up); //使用move函数 直接显式的所有权转移是可以的

(3) 可以用于函数的返回值

// 函数定义
unique_ptr<int> myFunc()
{
    unique_ptr<int> up(new int(10));
    return up;
}

unique_ptr<int> upRet = myFunc();

(4) 可以直接用if(ptr == NULL)来判断是否空指针

使用:

unique_ptr<int> up(new int(3));       //托管一个对象

// 更改所有权
unique_ptr<int> up2 = std::move(up);  //所有权转移,转移后,up变为空指针
int *p = up.release();                //释放所有权
up.reset();                           //显式销毁所有权

 

3、shared_ptr:使用计数机制来表明资源被几个指针共享。与auto_ptr的不同点

(1) 使用一个引用计数shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块,可以通过成员函数use_count()来查看资源的所有者个数。

(2) 析构函数中对引用计数进行判断,如果 shared_count > 1,则不释放内存只是将引用计数减1,当shared_count == 1的时候释放内存。当调用release()时,当前指针会释放资源所有权,计数减1,当计数等于0时,资源会被释放。

(3) 复制构造与赋值操作符除了提供复制功能之外,还将引用计数加1。

 使用:

// 1. 构造方法
// 将指针交由shared_ptr托管  还有一种方式也可以创建shared_ptr对象,且比较常用,通过make_shared函数: shared_ptr<int> shPtr = make_shared<int>(10); 
shared_ptr<int> shPtr(new int(10)); 
int num = *shPtr;   // 像使用正常指针一样使用它,此时num == 10

// 2. 复制构造函数
shared_ptr<int> shPtr2(shPtr);    // 复制构造,此时引用计数会增加
// 两个shared_ptr相等,指向同一个对象,引用计数为2
assert(shPtr == shPtr2 && shPtr.use_count() == 2); 
// 原先的shPtr还可以继续使用,如果是auto_ptr,是不能使用的,因为有所有权的转移
num = *shPtr;      
*shPtr = 20;    
assert(*shPtr2 == 20);  // 在改一个shared_ptr的同时,另一个也会更改

// 3. 赋值运算符
shared_ptr<int> shPtr3 = shPtr2;  // 赋值操作符

// 4. 停止使用
shPtr.reset();
assert(!shPtr);   // shPtr停止使用后会变成空指针

注意:

(1) shared_ptr不能对循环引用的对象的内存进行自动管理,循环引用会导致堆内存无法正确释放,内存泄漏。循环引用在weak_ptr中介绍。

(2) 不要构造一个临时的shared_ptr作为函数的参数,存在内存泄漏的风险

void  f(shared_ptr<int>, int);
int g();
// 正确的使用方式
void OK()
{
    shared_ptr<int> p(new int(2));
    f(p, g());
}

// 错误的使用方式
void Bad()
{
    // 如果执行顺序是先new int(2), 然后g(), 最后将 new int(2)的指针给shared_ptr的构造函数的话,当g()中抛出异常的时候,第一个new int(2)就造成了内存泄漏了
    f(shared_ptr<int>(new int(2)), g());
}

 

4、weak_ptr:相对于shared_ptr这种强引用类型的智能指针, weak_ptr是一种弱引用型的指针。用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。可以看成是shared_ptr的助手而不是真正的智能指针,因为它不会托管资源,它的构造也不会引起引用计数的增加,且没有重载 operator * 和 operator ->,不具有普通指针的行为。和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

 循环引用:

class B;
class A
{
public:
    A()
    {
        cout << "Class A Constructor is called." << endl;
    }
    ~A()
    {
        cout << "Class A Deconstructor is called." << endl;
    }

    //tr1::shared_ptr<B> m_shB;
    tr1::weak_ptr<B> m_shB;
};

class B
{
public:
    B()
    {
        cout << "Class B Constructor is called." << endl;
    }
    ~B()
    {
        cout << "Class B Deconstructor is called." << endl;
    }

    tr1::shared_ptr<A> m_shA;
};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        // 测试重复引用
        tr1::shared_ptr<A> shA(new A());   //shA引用计数:1
        tr1::shared_ptr<B> shB(new B());   //shB引用计数:1
        
        if (shA && shB)
        {
            shA->m_shB = shB;   //因为m_shB是weak_ptr对象,不会引起计数增加,shB引用计数仍为1。若m_shB是shared_ptr对象,则shB引用计数变为2.
            shB->m_shA = shA;   //shA引用计数变为2
        }

        cout << "A的引用计数:" << shA.use_count() << " B的引用计数:" << shB.use_count() << endl;
        cout << "要离开shA和shB的作用域了,正常情况下在这之后会执行shA和shB的析构函数的" << endl;
            
        // 这里是要执行析构函数的
        // 首先,会执行shB这个B对象的析构函数,要析构B的话,得先去判断下托管B的shared_ptr的引用计数,(若这里是2,则不能析构B,B的成员对象A自然也不能析构,从而死锁)
        //       这里是1,所以去析构B,B析构后紧接着去析构其成员对象A,此时A的引用计数为2,所以会使A的引用计数减为1
        // 然后,会执行shA这个A对象的析构函数,要析构A的话,也得先去判断下托管的A的shared_ptr的引用计数,这里是1,它可以析构
    }
    cout << "已经离开shA和shB的作用域了,请观察shA和shB的析构函数有没有被执行" << endl;
}

 

参考:https://jocent.me/2017/05/31/cpp_smart_pointer.html

转载于:https://www.cnblogs.com/yapp/p/10133327.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值